// File for testing: boltsize.laz, use BoltLngth variable // NOTE: THE OUTPUT DOES NOT EXACTLY MATCH THAT OF THE PDF DOCUMENTATION !!!! unit SensUnit; {$mode objfpc}{$H+} interface uses Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, Buttons, ExtCtrls, ComCtrls, MainUnit, Globals, FunctionsLib, MatrixLib, ReportFrameUnit, ChartFrameUnit, BasicStatsReportFormUnit; type { TSensForm } TSensForm = class(TBasicStatsReportForm) AllBtn: TBitBtn; AlphaEdit: TEdit; AvgSlopeChk: TCheckBox; InBtn: TBitBtn; Label1: TLabel; Label2: TLabel; Label3: TLabel; PageControl: TPageControl; SelectedList: TListBox; OutBtn: TBitBtn; RankedSlopesChartPage: TTabSheet; DataPage: TTabSheet; SlopesMatrixPage: TTabSheet; RankedSlopesPage: TTabSheet; SlopesPlotPage: TTabSheet; RankedSlopesPlotPage: TTabSheet; ResultsPage: TTabSheet; StandardizeChk: TCheckBox; VarList: TListBox; PrtRanksChk: TCheckBox; PrtSlopesChk: TCheckBox; PrtDataChk: TCheckBox; GroupBox3: TGroupBox; PlotRankedSlopesChk: TCheckBox; PlotSlopesChk: TCheckBox; GroupBox2: TGroupBox; GroupBox1: TGroupBox; procedure AllBtnClick(Sender: TObject); procedure InBtnClick(Sender: TObject); procedure OutBtnClick(Sender: TObject); procedure SelectedListDblClick(Sender: TObject); procedure VarListDblClick(Sender: TObject); procedure VarListSelectionChange(Sender: TObject; {%H-}User: boolean); private FDataReportFrame: TReportFrame; FSlopesReportFrame: TReportFrame; FRankedSlopesReportFrame: TReportFrame; FSlopesChartFrame: TChartFrame; FRankedSlopesChartFrame: TChartFrame; procedure DisplayData(const AValues: DblDyneMat; ANumSelected: Integer; const ARowLabels, AColLabels: StrDyneVec); procedure GetData(out ARowLabels, AColLabels: StrDyneVec; out ANumSelected: Integer; out ASelected: IntDyneVec; out AValues: DblDyneMat); procedure GetMannKendall(AIndex: Integer; const AValues: DblDyneMat; out MannKendall: Double; out ANumTies: Integer); function GetMedianSlope(ARankedQ: DblDyneVec): Double; procedure PlotRankedSlopes(AIndex: Integer; const ARankedQ: DblDyneVec; ATitle: String); procedure PlotSlopes(AIndex: Integer; const AValues: DblDyneMat; ATitle: String); procedure PrepareChart(AChartFrame: TChartFrame; ATitle, XTitle, YTitle: String); procedure ProcessRankedQ(AReport: TStrings; const ASlopes: DblDyneMat; const ATitle: String; out ARankedQ: DblDyneVec); procedure ProcessSlopes(AReport: TStrings; AIndex: Integer; const AValues: DblDyneMat; const ARowLabels, AColLabels: StrDyneVec; const ATitle: String; out ASlopes: DblDyneMat); procedure StandardizeValuesAndDisplay(AValues: DblDyneMat; ASelected: IntDyneVec; ANumSelected: Integer; AReport: TStrings); protected procedure AdjustConstraints; override; procedure Compute; override; procedure UpdateBtnStates; override; function Validate(out AMsg: String; out AControl: TWinControl): Boolean; override; public constructor Create(AOwner: TComponent); override; procedure Reset; override; end; var SensForm: TSensForm; implementation {$R *.lfm} uses Math, TACustomSeries, Utils, MatrixUnit, GridProcs; { TSensForm } constructor TSensForm.Create(AOwner: TComponent); begin inherited; FReportFrame.Parent := ResultsPage; FReportFrame.BorderSpacing.Left := 0; FReportFrame.BorderSpacing.Top := 0; FReportFrame.BorderSpacing.Right := 0; FReportFrame.BorderSpacing.Bottom := 0; InitToolbar(FReportFrame.ReportToolbar, tpTop); FDataReportFrame := TReportFrame.Create(self); FDataReportFrame.Name := ''; FDataReportFrame.Parent := DataPage; FDataReportFrame.Align := alClient; FSlopesReportFrame := TReportFrame.Create(self); FSlopesReportFrame.Name := ''; FSlopesReportFrame.Parent := SlopesMatrixPage; FSlopesReportFrame.Align := alClient; FRankedSlopesReportFrame := TReportFrame.Create(self); FRankedSlopesReportFrame.Name := ''; FRankedSlopesReportFrame.Parent := RankedSlopesPage; FRankedSlopesReportFrame.Align := alClient; FSlopesChartFrame := TChartFrame.Create(self); FSlopesChartFrame.Parent := SlopesPlotPage; FSlopesChartFrame.Align := alClient; FSlopesChartFrame.Chart.BottomAxis.Intervals.MaxLength := 80; FSlopesChartFrame.Chart.BottomAxis.Intervals.MinLength := 30; FRankedSlopesChartFrame := TChartFrame.Create(self); FRankedSlopesChartFrame.Parent := RankedSlopesPlotPage; FRankedSlopesChartFrame.Align := alClient; FRankedSlopesChartFrame.Chart.BottomAxis.Intervals.MaxLength := 80; FRankedSlopesChartFrame.Chart.BottomAxis.Intervals.MinLength := 30; PageControl.ActivePageIndex := 0; end; procedure TSensForm.AdjustConstraints; begin inherited; ParamsPanel.Constraints.MinWidth := Max( 4*CloseBtn.Width + 3*CloseBtn.BorderSpacing.Left, GroupBox1.Width ); ParamsPanel.Constraints.MinHeight := AllBtn.Top + AllBtn.Height + VarList.BorderSpacing.Bottom + GroupBox1.Height + GroupBox1.BorderSpacing.Bottom + GroupBox3.Height + GroupBox3.BorderSpacing.Bottom + ButtonBevel.Height + CloseBtn.BorderSpacing.Top + CloseBtn.Height; end; procedure TSensForm.AllBtnClick(Sender: TObject); var i: integer; begin for i := 0 to VarList.Items.Count-1 do SelectedList.Items.Add(VarList.Items[i]); VarList.Clear; UpdateBtnStates; end; procedure TSensForm.Compute; var NoSelected, count, half, q, tp, low, hi: integer; Values: DblDyneMat = nil; Slopes: DblDyneMat = nil; AvgSlopes: DblDyneMat = nil; RankedQ: DblDyneVec = nil; RowLabels: StrDyneVec = nil; ColLabels: StrDyneVec = nil; Selected: IntDyneVec = nil; MedianSlope, MannKendall, Z, C, M1, M2, Alpha, mean, stddev: double; lTitle: String; i, j, k, no2do: integer; lReport: TStrings; lSlopesReport: TStrings; lRankedReport: TStrings; begin Alpha := 1.0 - StrToFloat(AlphaEdit.Text); // Prepare charts if PlotSlopesChk.Checked then PrepareChart(FSlopesChartFrame, 'Slopes', 'Time', 'Measure'); SlopesPlotPage.TabVisible := PlotSlopesChk.Checked; if PlotRankedSlopesChk.Checked then PrepareChart(FRankedSlopesChartFrame, 'Ranked Slopes', 'Rank', 'Slope'); RankedSlopesPlotPage.TabVisible := PlotRankedslopesChk.Checked; // Get the data values from the grid, calc averages if needed. GetData(RowLabels, ColLabels, NoSelected, selected, Values); // Print data values DisplayData(Values, NoSelected, RowLabels, ColLabels); lReport := TStringList.Create; lSlopesReport := TStringList.Create; lRankedReport := TStringList.Create; try lReport.Add('SENS DETECTION AND ESTIMATION OF TRENDS'); lReport.Add('Number of data points: %4d', [NoCases]); lReport.Add('Confidence Interval: %4.2f', [Alpha]); // Standardize if more than one variable and standardization are selected if (noSelected > 1) and StandardizeChk.Checked then StandardizeValuesAndDisplay(Values, selected, NoSelected, lReport); // Get interval slopes if AvgSlopeChk.Checked then no2do := NoSelected + 1 else no2do := NoSelected; for j := 0 to no2do-1 do begin if j < NoSelected then lTitle := OS3MainFrm.DataGrid.Cells[selected[j], 0] else lTitle := 'Combined scores'; // Calculate slopes ProcessSlopes(lSlopesReport, j, Values, RowLabels, ColLabels, lTitle, Slopes); // Get ranked slopes and median estimator ProcessRankedQ(lRankedReport, Slopes, lTitle, RankedQ); count := Length(RankedQ); // Get median slope MedianSlope := GetMedianSlope(RankedQ); // Get Mann-Kendall statistic based on tied values GetMannKendall(j, Values, MannKendall, q); Z := InverseZ(Alpha); if MannKendall >= 0 then begin C := Z * sqrt(MannKendall); M1 := (count - C) / 2.0; M2 := (count + C) / 2.0; low := round(M1 - 1.0); if ((M1-1) - low) > 0.5 then low := round(M1-1); hi := round(M2); if (M2 - hi) > 0.5 then hi := round(M2); // ??? wp: This is the same as in the line above end; // show results lReport.Add(''); lReport.Add(DIVIDER_SMALL_AUTO); lReport.Add(''); if j < noSelected then lReport.Add('RESULTS FOR %s', [lTitle]) else lReport.Add('RESULTS FOR AVERAGED VALUES'); lReport.Add(''); if (NoSelected > 1) and StandardizeChk.Checked then begin mean := 0.0; stddev := 0.0; for i := 0 to NoCases-1 do begin mean := mean + Values[i,j]; stddev := stddev + sqr(Values[i,j]); end; stddev := stddev - sqr(mean) / NoCases; stddev := stddev / (NoCases - 1); stddev := sqrt(stddev); mean := mean / NoCases; lReport.Add('Mean: %8.3f, Standard Deviation: %8.3f', [mean, stddev]); end; lReport.Add( 'Median Slope for %d values: %8.3f', [count, MedianSlope]); if MannKendall > 0 then begin lReport.Add('Mann-Kendall Variance statistic: %8.3f (%d ties)', [MannKendall, q]); lReport.Add('Ranks of the lower and upper confidence: %8.3f ... %.3f', [M1, M2+1]); if (low > 0) or (hi <= count) then lReport.Add('Corresponding lower and upper slopes: %8.3f and %.3f', [RankedQ[low], RankedQ[hi]]) else lReport.Add('INDEX ERROR: low rank = %d, hi rank = %d', [low, hi]); end else lReport.Add('ERROR: z = %.3f, Mann-Kendall = %.3f', [Z, MannKendall]); // plot slopes if elected if PlotSlopesChk.Checked then begin if j = NoSelected then lTitle := 'Average values'; PlotSlopes(j, Values, lTitle); end; // Plot ranked slopes if elected if PlotRankedSlopesChk.Checked then PlotRankedSlopes(j, RankedQ,lTitle); end; // next variable j // Average multiple measures if AvgSlopeChk.Checked then begin SetLength(AvgSlopes, NoCases, NoCases); for i := 0 to NoCases-2 do for k := i + 1 to NoCases-1 do AvgSlopes[i,k] := AvgSlopes[i,k] + Slopes[i,k]; for i := 0 to NoCases-2 do for k := i + 1 to NoCases-1 do AvgSlopes[i,k] := AvgSlopes[i,k] / noselected; // Get ranked slopes and median estimator count := 0; for i := 0 to NoCases-2 do begin for j := i + 1 to NoCases-1 do begin RankedQ[count] := AvgSlopes[i,j]; count := count + 1; end; end; SortOnX(RankedQ); // Get median slope half := count div 2; if odd(count) then MedianSlope := RankedQ[half + 1] else MedianSlope := (RankedQ[half] + RankedQ[half+1]) / 2.0; // Get Mann-Kendall statistic based on tied values MannKendall := 0.0; q := 0; i := -1; while (i < count-1) do begin i := i + 1; tp := 1; // no. of ties for pth (i) value for j := i + 1 to count-1 do begin if RankedQ[j] <> RankedQ[i] then begin i := j - 1; break; end else tp := tp + 1; end; if tp > 1 then begin q := q + 1; MannKendall := MannKendall + (tp * (tp-1) * (2 * tp + 5)); end; end; // end do while MannKendall := (NoCases * (NoCases-1) * (2 * NoCases + 5) - MannKendall) / 18.0; Z := inversez(Alpha); if MannKendall >= 0.0 then begin C := Z * sqrt(MannKendall); M1 := (count - C) / 2.0; M2 := (count + C) / 2.0; low := round(M1) - 1; if ((M1-1) - low) > 0.5 then low := round(M1 - 1); hi := round(M2); if (M2 - hi) > 0.5 then hi := round(M2); end; // Show results lReport.Add(''); lReport.Add(DIVIDER_AUTO); lReport.Add( ''); lReport.Add( 'RESULTS FOR AVERAGED SLOPES'); lReport.Add( ''); lReport.Add( 'Median Slope for %3d values: %8.3f for averaged measures', [count, MedianSlope]); if MannKendall >= 0 then begin lReport.Add('Mann-Kendall Variance statistic: %8.3f (%d ties observed)', [MannKendall, q]); lReport.Add('Ranks of the lower and upper confidence: %8.3f ... %.3f', [M1, M2]); lReport.Add('Corresponding lower and upper slopes: %8.3f and %.3f)', [RankedQ[low],RankedQ[hi]]); end else lReport.Add('ERROR in calculating Mann-Kendall: %.3f. Cannot be negative.', [MannKendall]); end; FReportFrame.DisplayReport(lReport); FSlopesReportFrame.DisplayReport(lSlopesReport); FRankedSlopesReportFrame.DisplayReport(lRankedReport); finally lReport.Free; lSlopesReport.Free; lRankedReport.Free; end; end; procedure TSensForm.DisplayData(const AValues: DblDyneMat; ANumSelected: Integer; const ARowLabels, AColLabels: StrDyneVec); var lReport: TStrings; begin DataPage.TabVisible := PrtDataChk.Checked; if PrtDataChk.Checked then begin lReport := TStringList.Create; try MatPrint(AValues, NoCases, ANumSelected, 'CASE', ARowLabels, AColLabels, NoCases, lReport); FDataReportFrame.DisplayReport(lReport); finally lReport.Free; end; end; end; procedure TSensForm.GetData(out ARowLabels, AColLabels: StrDyneVec; out ANumSelected: Integer; out ASelected: IntDyneVec; out AValues: DblDyneMat); var i, j, col: Integer; begin ARowLabels := nil; AColLabels := nil; SetLength(ARowLabels, NoCases); SetLength(AColLabels, NoCases); for i := 0 to NoCases-1 do begin ARowLabels[i] := OS3MainFrm.DataGrid.Cells[0 ,i+1]; AColLabels[i] := ARowLabels[i]; end; ANumSelected := SelectedList.Items.Count; // Get indices of selected variables ASelected := nil; SetLength(ASelected, ANumSelected); for i := 0 to ANumSelected-1 do ASelected[i] := GetVariableIndex(OS3MainFrm.DataGrid, SelectedList.Items[i]); // Get the data AValues := nil; SetLength(AValues, NoCases, ANumSelected+1); // +1 for AvgSlope for j := 0 to ANumSelected-1 do begin col := ASelected[j]; for i := 0 to NoCases-1 do begin AValues[i, j] := StrToFloat(Trim(OS3MainFrm.DataGrid.Cells[col, i+1])); if AvgSlopeChk.Checked then AValues[i, ANumSelected] := AValues[i, ANumSelected] + AValues[i, j]; end; end; // average the values if elected if AvgSlopeChk.Checked then for i := 0 to NoCases - 1 do AValues[i, ANumSelected] := AValues[i, ANumSelected] / ANumSelected; end; procedure TSensForm.GetMannKendall(AIndex: Integer; const AValues: DblDyneMat; out MannKendall: Double; out ANumTies: Integer); var i, k, q, tp: Integer; sorted: DblDyneVec = nil; begin SetLength(sorted, NoCases); for i := 0 to NoCases-1 do sorted[i] := AValues[i, AIndex]; SortOnX(sorted); MannKendall := 0.0; q := 0; i := -1; while (i < NoCases-2) do begin i := i + 1; tp := 1; // no. of ties for pth (i) value for k := i + 1 to NoCases-1 do begin if Sorted[k] <> Sorted[i] then begin i := k-1; break; end else tp := tp + 1; end; // next k if tp > 1 then begin q := q + 1; MannKendall := MannKendall + (tp * (tp-1) * (2 * tp + 5)); end; end; MannKendall := (NoCases * (NoCases-1) * (2 * NoCases + 5) - MannKendall) / 18.0; ANumTies := q; end; function TSensForm.GetMedianSlope(ARankedQ: DblDyneVec): Double; var half, count: Integer; begin count := Length(ARankedQ); half := count div 2; if odd(count) then Result := ARankedQ[half] else Result := (ARankedQ[half-1] + ARankedQ[half]) * 0.5; end; procedure TSensForm.InBtnClick(Sender: TObject); var i: integer; begin i := 0; while i < VarList.Items.Count do begin if VarList.Selected[i] then begin SelectedList.Items.Add(VarList.Items[i]); VarList.Items.Delete(i); i := 0; end else i := i + 1; end; UpdateBtnStates; end; procedure TSensForm.OutBtnClick(Sender: TObject); var i: integer; begin i := 0; while i < SelectedList.Items.Count do begin if SelectedList.Selected[i] then begin VarList.Items.Add(SelectedList.Items[i]); SelectedList.Items.Delete(i); i := 0; end else i := i + 1; end; UpdateBtnStates; end; // Plot slopes procedure TSensForm.PlotSlopes(AIndex: Integer; const AValues: DblDyneMat; ATitle: String); var ser: TChartSeries; i: Integer; begin ser := FSlopesChartFrame.PlotXY(ptLinesAndSymbols, nil, nil, nil, nil, ATitle, DATA_COLORS[AIndex mod Length(DATA_COLORS)]); for i := 0 to NoCases - 1 do ser.AddXY(i+1, AValues[i, AIndex]); end; procedure TSensForm.PlotRankedSlopes(AIndex: Integer; const ARankedQ: DblDyneVec; ATitle: String); var ser: TChartSeries; i, count: Integer; begin count := Length(ARankedQ); ser := FRankedSlopesChartFrame.PlotXY(ptLines, nil, nil, nil, nil, ATitle, DATA_COLORS[AIndex mod Length(DATA_COLORS)]); for i := 0 to count-1 do ser.AddXY(i+1, ARankedQ[i]); end; procedure TSensForm.PrepareChart(AChartFrame: TChartFrame; ATitle, XTitle, YTitle: String); begin AChartFrame.Clear; AChartFrame.SetTitle(ATitle); AChartFrame.SetXTitle(XTitle); AChartFrame.SetYTitle(YTitle); end; procedure TSensForm.ProcessRankedQ(AReport: TStrings; const ASlopes: DblDyneMat; const ATitle: String; out ARankedQ: DblDyneVec); var count: Integer; i, k: Integer; begin ARankedQ := nil; SetLength(ARankedQ, 500); // prelimiary dimension to some length, trim later. count := 0; for i := 0 to NoCases-2 do for k := i+1 to NoCases-1 do begin ARankedQ[count] := ASlopes[i, k]; count := count + 1; if count = Length(ARankedQ) then SetLength(ARankedQ, Length(ARankedQ) + 500); end; // Trim to length needed. SetLength(ARankedQ, count); // Sort into ascending order SortOnX(ARankedQ); if PrtRanksChk.Checked then begin AReport.Add('RANKED SLOPES FOR ' + ATitle); AReport.Add(''); AReport.Add(' Label Ranked Q'); AReport.Add('---------- ----------'); for i := 0 to count-1 do AReport.Add('%8d %9.4f', [i+1, ARankedQ[i]]); AReport.Add(''); AReport.Add(DIVIDER_SMALL_AUTO); AReport.Add(''); end; RankedSlopesPage.TabVisible := PrtRanksChk.Checked; end; procedure TSensForm.ProcessSlopes(AReport: TStrings; AIndex: Integer; const AValues: DblDyneMat; const ARowLabels, AColLabels: StrDyneVec; const ATitle: String; out ASlopes: DblDyneMat); var i, k: Integer; begin ASlopes := nil; SetLength(ASlopes, NoCases, NoCases); for i := 0 to NoCases-2 do for k := i + 1 to NoCases-1 do ASlopes[i,k] := (AValues[k, AIndex] - AValues[i, AIndex]) / (k-i); if PrtSlopesChk.Checked then begin MatPrint(ASlopes, NoCases, NoCases, ATitle, ARowLabels, AColLabels, NoCases, AReport); AReport.Add(''); AReport.Add(DIVIDER_SMALL_AUTO); AReport.Add(''); end; SlopesMatrixPage.TabVisible := PrtSlopesChk.Checked; end; procedure TSensForm.Reset; var i: integer; begin inherited; DataPage.TabVisible := false; SlopesMatrixPage.TabVisible := false; RankedSlopesPage.TabVisible := false; SlopesPlotPage.TabVisible := false; RankedSlopesPlotPage.TabVisible := false; if FDataReportFrame <> nil then FDataReportFrame.Clear; if FSlopesReportFrame <> nil then FSlopesReportFrame.Clear; if FRankedSlopesReportFrame <> nil then FRankedSlopesReportFrame.Clear; if FSlopesChartFrame <> nil then FSlopesChartFrame.Clear; if FRankedSlopesChartFrame <> nil then FRankedSlopesChartFrame.Clear; AlphaEdit.Text := FormatFloat('0.00', DEFAULT_ALPHA_LEVEL); StandardizeChk.Checked := false; PlotSlopesChk.Checked := false; PlotRankedSlopesChk.Checked := false; AvgSlopeChk.Checked := false; SelectedList.Clear; VarList.Clear; for i := 1 to NoVariables do VarList.Items.Add(OS3MainFrm.DataGrid.Cells[i,0]); UpdateBtnStates; end; procedure TSensForm.SelectedListDblClick(Sender: TObject); var index: Integer; begin index := SelectedList.ItemIndex; if index > -1 then begin VarList.Items.Add(SelectedList.Items[index]); SelectedList.Items.Delete(index); UpdateBtnStates; end; end; procedure TSensForm.StandardizeValuesAndDisplay(AValues: DblDyneMat; ASelected: IntDyneVec; ANumSelected: Integer; AReport: TStrings); var i, j: Integer; mean, stddev: Double; col: Integer; begin AReport.Add(''); AReport.Add(' Variable Mean Std.Dev. '); AReport.Add('------------ ---------- ----------'); for j := 0 to ANumSelected-1 do begin mean := 0.0; stddev := 0.0; for i := 0 to NoCases-1 do begin mean := mean + AValues[i, j]; stddev := stddev + sqr(AValues[i, j]); end; stddev := stddev - sqr(mean) / NoCases; stddev := stddev / (NoCases - 1); stddev := sqrt(stddev); mean := mean / NoCases; for i := 0 to NoCases-1 do AValues[i,j] := (AValues[i,j] - mean) / stddev; col := ASelected[j]; AReport.Add('%12s %10.3f %10.3f', [OS3MainFrm.DataGrid.Cells[col, 0], mean, stddev]); end; end; procedure TSensForm.UpdateBtnStates; begin inherited; if FDataReportFrame <> nil then FDataReportFrame.UpdateBtnStates; if FSlopesReportFrame <> nil then FSlopesReportFrame.UpdateBtnStates; if FRankedSlopesReportFrame <> nil then FRankedSlopesReportFrame.UpdateBtnStates; if FSlopesChartFrame <> nil then FSlopesChartFrame.UpdateBtnStates; if FRankedSlopesChartFrame <> nil then FRankedSlopesChartFrame.UpdateBtnStates; InBtn.Enabled := AnySelected(VarList); OutBtn.Enabled := AnySelected(SelectedList); AllBtn.Enabled := Varlist.Items.Count > 0; end; function TSensForm.Validate(out AMsg: String; out AControl: TWinControl): Boolean; var x: Double; begin Result := false; if SelectedList.Items.Count = 0 then begin AMsg := 'First select variables to analyze.'; AControl := SelectedList; exit; end; if AlphaEdit.Text = '' then begin AControl := AlphaEdit; AMsg := 'Input required.'; exit; end; if not TryStrToFloat(AlphaEdit.Text, x) or (x <= 0) or (x >= 1) then begin AControl := AlphaEdit; AMsg := 'Numeric value required in range > 0 and < 1.'; exit; end; Result := true; end; procedure TSensForm.VarListDblClick(Sender: TObject); var index: Integer; begin index := VarList.ItemIndex; if index > -1 then begin SelectedList.Items.Add(VarList.Items[index]); VarList.Items.Delete(index); UpdateBtnStates; end; end; procedure TSensForm.VarListSelectionChange(Sender: TObject; User: boolean); begin UpdateBtnStates; end; end.