diff --git a/components/heidisql/include/const.inc b/components/heidisql/include/const.inc index f2a5d27a..421903f0 100644 --- a/components/heidisql/include/const.inc +++ b/components/heidisql/include/const.inc @@ -160,6 +160,8 @@ const REGNAME_TOOLSWINWIDTH = 'TableTools_WindowWidth'; REGNAME_TOOLSWINHEIGHT = 'TableTools_WindowHeight'; REGNAME_TOOLSTREEWIDTH = 'TableTools_TreeWidth'; + REGNAME_TOOLSFINDTEXT = 'TableTools_FindText'; + REGNAME_TOOLSSKIPMB = 'TableTools_SkipMB'; REGNAME_USERMNGR_WINWIDTH = 'Usermanager_WindowWidth'; REGNAME_USERMNGR_WINHEIGHT = 'Usermanager_WindowHeight'; REGNAME_SELECTDBO_WINWIDTH = 'SelectDBO_WindowWidth'; diff --git a/source/helpers.pas b/source/helpers.pas index 13d21d14..f17778e0 100644 --- a/source/helpers.pas +++ b/source/helpers.pas @@ -196,6 +196,8 @@ type procedure SelectNode(VT: TVirtualStringTree; Node: PVirtualNode); overload; function DateBackFriendlyCaption(d: TDateTime): String; procedure InheritFont(AFont: TFont); + function FieldContent(ds: TDataSet; ColName: WideString): WideString; + function GetTableSize(ds: TDataSet): Int64; var MainReg : TRegistry; @@ -3077,6 +3079,26 @@ begin end; +// Fetch content from a row cell, avoiding NULLs to cause AVs +function FieldContent(ds: TDataSet; ColName: WideString): WideString; +begin + Result := ''; + if (ds.FindField(colName) <> nil) and (not ds.FindField(ColName).IsNull) then + Result := ds.FieldByName(ColName).AsWideString; +end; + + +function GetTableSize(ds: TDataSet): Int64; +var + d, i: String; +begin + d := FieldContent(ds, 'Data_length'); + i := FieldContent(ds, 'Index_length'); + if (d = '') or (i = '') then Result := -1 + else Result := MakeInt(d) + MakeInt(i); +end; + + end. diff --git a/source/main.dfm b/source/main.dfm index b05e45f7..67e1f8f9 100644 --- a/source/main.dfm +++ b/source/main.dfm @@ -1515,6 +1515,9 @@ object MainForm: TMainForm object menuMaintenance: TMenuItem Action = actMaintenance end + object actFindTextOnServer1: TMenuItem + Action = actFindTextOnServer + end object N7: TMenuItem Caption = '-' end @@ -1703,6 +1706,14 @@ object MainForm: TMainForm ImageIndex = 39 OnExecute = actMaintenanceExecute end + object actFindTextOnServer: TAction + Category = 'Tools' + Caption = 'Find text on server' + Hint = 'Searches selected tables for text occurences' + ImageIndex = 30 + ShortCut = 24646 + OnExecute = actFindTextOnServerExecute + end object actCopyAsHTML: TAction Tag = 49 Category = 'Export/Import' @@ -6070,6 +6081,9 @@ object MainForm: TMainForm object menuMaintenance2: TMenuItem Action = actMaintenance end + object Findtextonserver1: TMenuItem + Action = actFindTextOnServer + end object menuEmptyTables: TMenuItem Action = actEmptyTables end diff --git a/source/main.pas b/source/main.pas index c0c303ac..13270675 100644 --- a/source/main.pas +++ b/source/main.pas @@ -449,6 +449,9 @@ type actFilterPanel: TAction; actFindInVT1: TMenuItem; TimerFilterVT: TTimer; + actFindTextOnServer: TAction; + actFindTextOnServer1: TMenuItem; + Findtextonserver1: TMenuItem; procedure refreshMonitorConfig; procedure loadWindowConfig; procedure saveWindowConfig; @@ -545,7 +548,6 @@ type procedure ValidateControls(Sender: TObject); procedure ValidateQueryControls(Sender: TObject); procedure RefreshQueryHelpers; - function FieldContent(ds: TDataSet; ColName: WideString): WideString; procedure LoadDatabaseProperties(db: WideString); procedure ShowHost; procedure ShowDatabase(db: WideString); @@ -733,6 +735,7 @@ type procedure comboOnlyDBsKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure actFilterPanelExecute(Sender: TObject); procedure TimerFilterVTTimer(Sender: TObject); + procedure actFindTextOnServerExecute(Sender: TObject); private ReachedEOT : Boolean; FDelimiter: String; @@ -778,7 +781,6 @@ type function GetSelectedTable: TListNode; procedure SetSelectedDatabase(db: WideString); procedure SetVisibleListColumns( List: TVirtualStringTree; Columns: WideStrings.TWideStringList ); - function GetTableSize(ds: TDataSet): Int64; procedure ToggleFilterPanel(ForceVisible: Boolean = False); function GetSelectedTableColumns: TDataset; function GetSelectedTableKeys: TDataset; @@ -1972,6 +1974,16 @@ begin // optimize / repair... tables if TableToolsDialog = nil then TableToolsDialog := TfrmTableTools.Create(Self); + TableToolsDialog.PageControlTools.ActivePage := TableToolsDialog.tabMaintenance; + TableToolsDialog.ShowModal; +end; + +procedure TMainForm.actFindTextOnServerExecute(Sender: TObject); +begin + // Find text on server + if TableToolsDialog = nil then + TableToolsDialog := TfrmTableTools.Create(Self); + TableToolsDialog.PageControlTools.ActivePage := TableToolsDialog.tabFind; TableToolsDialog.ShowModal; end; @@ -4021,18 +4033,6 @@ begin end; -// Fetch content from a row cell, avoiding NULLs to cause AVs -function TMainForm.FieldContent(ds: TDataSet; ColName: WideString): WideString; -begin - Result := ''; - if - (ds.FindField(colName) <> nil) and - (not ds.FindField(ColName).IsNull) - then - Result := ds.FieldByName(ColName).AsWideString; -end; - - procedure TMainForm.LoadDatabaseProperties(db: WideString); var i, img : Integer; @@ -7171,16 +7171,6 @@ begin end; -function TMainForm.GetTableSize(ds: TDataSet): Int64; -var - d, i: String; -begin - d := FieldContent(ds, 'Data_length'); - i := FieldContent(ds, 'Index_length'); - if (d = '') or (i = '') then Result := -1 - else Result := MakeInt(d) + MakeInt(i); -end; - function TMainForm.DbTableListCachedAndValid(db: WideString): Boolean; var ds: TDataSet; diff --git a/source/tabletools.dfm b/source/tabletools.dfm index 22ca9983..6268d028 100644 --- a/source/tabletools.dfm +++ b/source/tabletools.dfm @@ -96,7 +96,7 @@ object frmTableTools: TfrmTableTools object lblResults: TLabel AlignWithMargins = True Left = 0 - Top = 164 + Top = 191 Width = 380 Height = 13 Margins.Left = 0 @@ -106,9 +106,9 @@ object frmTableTools: TfrmTableTools end object ResultGrid: TVirtualStringTree Left = 0 - Top = 180 + Top = 207 Width = 380 - Height = 156 + Height = 129 Align = alClient Header.AutoSizeIndex = -1 Header.DefaultHeight = 17 @@ -138,11 +138,11 @@ object frmTableTools: TfrmTableTools Top = 0 Width = 380 Height = 161 - ActivePage = Maintenance + ActivePage = tabMaintenance Align = alTop Images = MainForm.PngImageListMain TabOrder = 1 - object Maintenance: TTabSheet + object tabMaintenance: TTabSheet Caption = 'Maintenance' ImageIndex = 39 DesignSize = ( @@ -188,7 +188,7 @@ object frmTableTools: TfrmTableTools Height = 17 Caption = 'Quick' TabOrder = 1 - OnClick = MaintenanceOptionClick + OnClick = ValidateControls end object chkFast: TCheckBox Left = 81 @@ -197,7 +197,7 @@ object frmTableTools: TfrmTableTools Height = 17 Caption = 'Fast' TabOrder = 2 - OnClick = MaintenanceOptionClick + OnClick = ValidateControls end object chkMedium: TCheckBox Left = 81 @@ -206,7 +206,7 @@ object frmTableTools: TfrmTableTools Height = 17 Caption = 'Medium' TabOrder = 3 - OnClick = MaintenanceOptionClick + OnClick = ValidateControls end object chkExtended: TCheckBox Left = 184 @@ -215,7 +215,7 @@ object frmTableTools: TfrmTableTools Height = 17 Caption = 'Extended' TabOrder = 4 - OnClick = MaintenanceOptionClick + OnClick = ValidateControls end object chkChanged: TCheckBox Left = 184 @@ -224,7 +224,7 @@ object frmTableTools: TfrmTableTools Height = 17 Caption = 'Changed' TabOrder = 5 - OnClick = MaintenanceOptionClick + OnClick = ValidateControls end object btnExecuteMaintenance: TButton Left = 80 @@ -242,7 +242,7 @@ object frmTableTools: TfrmTableTools Height = 17 Caption = 'Use FRM file' TabOrder = 7 - OnClick = MaintenanceOptionClick + OnClick = ValidateControls end object btnHelp: TButton Left = 294 @@ -255,6 +255,97 @@ object frmTableTools: TfrmTableTools OnClick = btnHelpClick end end + object tabFindInDB: TTabSheet + Caption = 'Find text' + ImageIndex = 30 + DesignSize = ( + 372 + 132) + object lblFindText: TLabel + Left = 3 + Top = 14 + Width = 60 + Height = 13 + Caption = 'Text to find:' + end + object lblDataTypes: TLabel + Left = 80 + Top = 77 + Width = 151 + Height = 13 + Caption = 'Restrict search to column types' + end + object memoFindText: TTntMemo + Left = 80 + Top = 11 + Width = 289 + Height = 57 + Anchors = [akLeft, akTop, akRight, akBottom] + ScrollBars = ssVertical + TabOrder = 0 + OnChange = ValidateControls + end + object btnFindText: TButton + Left = 80 + Top = 99 + Width = 75 + Height = 25 + Anchors = [akLeft, akBottom] + Caption = 'Find' + TabOrder = 1 + OnClick = ExecuteOperation + end + object comboDataTypes: TComboBox + Left = 264 + Top = 74 + Width = 105 + Height = 21 + Style = csDropDownList + Anchors = [akLeft, akTop, akRight] + ItemHeight = 13 + TabOrder = 2 + end + end + end + object pnlSkipLargeTables: TPanel + Left = 0 + Top = 161 + Width = 380 + Height = 27 + Align = alTop + BevelOuter = bvNone + TabOrder = 2 + object lblSkipLargeTables: TLabel + Left = 0 + Top = 6 + Width = 107 + Height = 13 + Caption = 'Skip tables larger than' + end + object lblSkipLargeTablesMB: TLabel + Left = 184 + Top = 6 + Width = 87 + Height = 13 + Caption = 'MB (0 = unlimited)' + end + object editSkipLargeTables: TEdit + Left = 122 + Top = 3 + Width = 40 + Height = 21 + TabOrder = 0 + Text = '20' + end + object udSkipLargeTables: TUpDown + Left = 162 + Top = 3 + Width = 16 + Height = 21 + Associate = editSkipLargeTables + Position = 20 + TabOrder = 1 + end end end end diff --git a/source/tabletools.pas b/source/tabletools.pas index 3928a340..56668010 100644 --- a/source/tabletools.pas +++ b/source/tabletools.pas @@ -10,7 +10,7 @@ interface uses Windows, SysUtils, Classes, Controls, Forms, StdCtrls, ComCtrls, Buttons, - WideStrings, WideStrUtils, VirtualTrees, ExtCtrls, Db, Contnrs, Graphics; + WideStrings, WideStrUtils, VirtualTrees, ExtCtrls, Db, Contnrs, Graphics, TntStdCtrls; type TfrmTableTools = class(TForm) @@ -22,7 +22,7 @@ type ResultGrid: TVirtualStringTree; lblResults: TLabel; PageControlTools: TPageControl; - Maintenance: TTabSheet; + tabMaintenance: TTabSheet; comboOperation: TComboBox; lblOperation: TLabel; chkQuick: TCheckBox; @@ -34,6 +34,17 @@ type chkUseFrm: TCheckBox; lblOptions: TLabel; btnHelp: TButton; + tabFindInDB: TTabSheet; + lblFindText: TLabel; + memoFindText: TTntMemo; + btnFindText: TButton; + comboDataTypes: TComboBox; + lblDataTypes: TLabel; + pnlSkipLargeTables: TPanel; + lblSkipLargeTables: TLabel; + editSkipLargeTables: TEdit; + udSkipLargeTables: TUpDown; + lblSkipLargeTablesMB: TLabel; procedure FormDestroy(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); @@ -53,18 +64,20 @@ type procedure ResultGridGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString); procedure TreeObjectsChecked(Sender: TBaseVirtualTree; Node: PVirtualNode); - procedure MaintenanceOptionClick(Sender: TObject); procedure ResultGridHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo); procedure ResultGridCompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer); procedure ResultGridPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType); + procedure ValidateControls(Sender: TObject); private { Private declarations } FResults: TObjectList; - procedure ValidateControls(Sender: TObject); + FRealResultCounter: Integer; procedure ProcessTableNode(Sender: TObject; Node: PVirtualNode); procedure AddResults(SQL: WideString); + procedure AddNotes(Col1, Col2, Col3, Col4: WideString); + procedure UpdateResultGrid; public { Public declarations } end; @@ -72,17 +85,28 @@ type implementation -uses main, helpers; +uses main, helpers, mysql_structures; + +const + STRSKIPPED = 'Skipped - '; {$R *.DFM} procedure TfrmTableTools.FormCreate(Sender: TObject); +var + i: Integer; begin // Restore GUI setup Width := GetRegValue(REGNAME_TOOLSWINWIDTH, Width); Height := GetRegValue(REGNAME_TOOLSWINHEIGHT, Height); TreeObjects.Width := GetRegValue(REGNAME_TOOLSTREEWIDTH, TreeObjects.Width); + memoFindText.Text := Utf8Decode(GetRegValue(REGNAME_TOOLSFINDTEXT, '')); + comboDatatypes.Items.Add('All data types'); + for i:=Low(DatatypeCategories) to High(DatatypeCategories) do + comboDatatypes.Items.Add(DatatypeCategories[i].Name); + comboDatatypes.ItemIndex := GetRegValue(REGNAME_TOOLSDATATYPE, 0); + udSkipLargeTables.Position := GetRegValue(REGNAME_TOOLSSKIPMB, udSkipLargeTables.Position); SetWindowSizeGrip( Self.Handle, True ); InheritFont(Font); FixVT(TreeObjects); @@ -99,6 +123,9 @@ begin MainReg.WriteInteger( REGNAME_TOOLSWINWIDTH, Width ); MainReg.WriteInteger( REGNAME_TOOLSWINHEIGHT, Height ); MainReg.WriteInteger( REGNAME_TOOLSTREEWIDTH, TreeObjects.Width); + MainReg.WriteString( REGNAME_TOOLSFINDTEXT, Utf8Encode(memoFindText.Text)); + MainReg.WriteInteger( REGNAME_TOOLSSKIPMB, udSkipLargeTables.Position); + MainReg.WriteInteger( REGNAME_TOOLSDATATYPE, comboDatatypes.ItemIndex); end; @@ -130,9 +157,12 @@ end; procedure TfrmTableTools.ValidateControls(Sender: TObject); +var + SomeChecked: Boolean; begin - btnExecuteMaintenance.Enabled := (Pos(STR_NOTSUPPORTED, comboOperation.Text) = 0) and - (TreeObjects.CheckedCount > 0); + SomeChecked := TreeObjects.CheckedCount > 0; + btnExecuteMaintenance.Enabled := (Pos(STR_NOTSUPPORTED, comboOperation.Text) = 0) and SomeChecked; + btnFindText.Enabled := SomeChecked and (memoFindText.Text <> ''); // CHECKSUM's options are mutually exclusive if comboOperation.Text = 'Checksum' then begin if (Sender = chkExtended) and chkExtended.Checked then chkQuick.Checked := False @@ -205,11 +235,6 @@ begin end; -procedure TfrmTableTools.MaintenanceOptionClick(Sender: TObject); -begin - ValidateControls(Sender); -end; - procedure TfrmTableTools.ExecuteOperation(Sender: TObject); var DBNode, TableNode: PVirtualNode; @@ -217,6 +242,7 @@ begin Screen.Cursor := crHourGlass; ResultGrid.Clear; FResults.Clear; + FRealResultCounter := 0; TreeObjects.SetFocus; DBNode := TreeObjects.GetFirstChild(TreeObjects.GetFirst); while Assigned(DBNode) do begin @@ -235,20 +261,76 @@ end; procedure TfrmTableTools.ProcessTableNode(Sender: TObject; Node: PVirtualNode); var - SQL: WideString; + SQL, db, table, QuotedTable: WideString; + TableSize, RowsInTable: Int64; + ds: TDataset; + i: Integer; + HasSelectedDatatype: Boolean; begin // Prepare SQL for one table node - if (csCheckedNormal in [Node.CheckState, Node.Parent.CheckState]) and - (Node.CheckType <> ctNone) then begin - SQL := UpperCase(comboOperation.Text) + ' TABLE ' + - Mainform.mask(TreeObjects.Text[Node.Parent, 0])+'.'+Mainform.mask(TreeObjects.Text[Node, 0]); - if chkQuick.Enabled and chkQuick.Checked then SQL := SQL + ' QUICK'; - if chkFast.Enabled and chkFast.Checked then SQL := SQL + ' FAST'; - if chkMedium.Enabled and chkMedium.Checked then SQL := SQL + ' MEDIUM'; - if chkExtended.Enabled and chkExtended.Checked then SQL := SQL + ' EXTENDED'; - if chkChanged.Enabled and chkChanged.Checked then SQL := SQL + ' CHANGED'; - if chkUseFrm.Enabled and chkUseFrm.Checked then SQL := SQL + ' USE_FRM'; - AddResults(SQL); + if (csCheckedNormal in [Node.CheckState, Node.Parent.CheckState]) and (Node.CheckType <> ctNone) then begin + db := TreeObjects.Text[Node.Parent, 0]; + table := TreeObjects.Text[Node, 0]; + QuotedTable := Mainform.mask(db)+'.'+Mainform.mask(table); + // Find table in cashed dataset and check its size - perhaps it has to be skipped + TableSize := 0; + RowsInTable := 0; + ds := Mainform.FetchDbTableList(db); + while not ds.Eof do begin + if (ds.FieldByName(DBO_NAME).AsWideString = table) + and (GetDBObjectType(ds.Fields) in [lntTable, lntCrashedTable]) then begin + TableSize := GetTableSize(ds); + RowsInTable := MakeInt(ds.FieldByName(DBO_ROWS).AsString); + // Avoid division by zero in below SQL + if RowsInTable = 0 then + RowsInTable := 1; + break; + end; + ds.Next; + end; + if (udSkipLargeTables.Position = 0) or ((TableSize div SIZE_MB) < udSkipLargeTables.Position) then try + if Sender = btnExecuteMaintenance then begin + SQL := UpperCase(comboOperation.Text) + ' TABLE ' + QuotedTable; + if chkQuick.Enabled and chkQuick.Checked then SQL := SQL + ' QUICK'; + if chkFast.Enabled and chkFast.Checked then SQL := SQL + ' FAST'; + if chkMedium.Enabled and chkMedium.Checked then SQL := SQL + ' MEDIUM'; + if chkExtended.Enabled and chkExtended.Checked then SQL := SQL + ' EXTENDED'; + if chkChanged.Enabled and chkChanged.Checked then SQL := SQL + ' CHANGED'; + if chkUseFrm.Enabled and chkUseFrm.Checked then SQL := SQL + ' USE_FRM'; + end else if Sender = btnFindText then begin + ds := Mainform.GetResults('SHOW COLUMNS FROM '+QuotedTable); + SQL := ''; + while not ds.Eof do begin + HasSelectedDatatype := comboDatatypes.ItemIndex = 0; + if not HasSelectedDatatype then for i:=Low(Datatypes) to High(Datatypes) do begin + HasSelectedDatatype := (LowerCase(getFirstWord(ds.FieldByName('Type').AsString)) = LowerCase(Datatypes[i].Name)) + and (Integer(Datatypes[i].Category)+1 = comboDatatypes.ItemIndex); + if HasSelectedDatatype then + break; + end; + if HasSelectedDatatype then + SQL := SQL + Mainform.mask(ds.FieldByName('Field').AsWideString) + ' LIKE ' + esc('%'+memoFindText.Text+'%') + ' OR '; + ds.Next; + end; + if SQL <> '' then begin + Delete(SQL, Length(SQL)-3, 3); + SQL := 'SELECT '''+db+''' AS `Database`, '''+table+''' AS `Table`, COUNT(*) AS `Found rows`, ' + + 'CONCAT(ROUND(100 / '+IntToStr(RowsInTable)+' * COUNT(*), 1), ''%'') AS `Relevance` FROM '+QuotedTable+' WHERE ' + + SQL; + end; + end; + if SQL <> '' then + AddResults(SQL) + else + AddNotes(db, table, STRSKIPPED+'table doesn''t have columns of selected type ('+comboDatatypes.Text+').', ''); + except + // The above SQL can easily throw an exception, e.g. if a table is corrupted. + // In such cases we create a dummy row, including the error message + on E:Exception do + AddNotes(db, table, 'error', E.Message); + end else begin + AddNotes(db, table, STRSKIPPED+FormatByteNumber(TableSize), ''); + end; end; end; @@ -268,7 +350,7 @@ begin // Add missing columns for i:=ResultGrid.Header.Columns.Count to ds.FieldCount-1 do begin Col := ResultGrid.Header.Columns.Add; - Col.Width := 100; + Col.Width := 130; end; // Remove superfluous columns for i:=ResultGrid.Header.Columns.Count-1 downto ds.FieldCount do @@ -278,7 +360,9 @@ begin Col := ResultGrid.Header.Columns[i]; Col.Text := ds.Fields[i].FieldName; if ds.Fields[i].DataType in [ftSmallint, ftInteger, ftWord, ftLargeint, ftFloat] then - Col.Alignment := taRightJustify; + Col.Alignment := taRightJustify + else + Col.Alignment := taLeftJustify; end; ds.First; while not ds.Eof do begin @@ -290,13 +374,37 @@ begin ds.Next; end; + Inc(FRealResultCounter); + lblResults.Caption := IntToStr(FRealResultCounter)+' results:'; + lblResults.Repaint; + UpdateResultGrid; +end; + + +procedure TfrmTableTools.AddNotes(Col1, Col2, Col3, Col4: WideString); +var + Row: TWideStringlist; +begin + // Adds a row with non SQL results + Row := TWideStringlist.Create; + Row.Add(Col1); + Row.Add(Col2); + Row.Add(Col3); + Row.Add(Col4); + FResults.Add(Row); + UpdateResultGrid; +end; + + +procedure TfrmTableTools.UpdateResultGrid; +begin + // Refresh resultgrid ResultGrid.RootNodeCount := FResults.Count; ResultGrid.FocusedNode := ResultGrid.GetLast; ResultGrid.Selected[ResultGrid.FocusedNode] := True; ResultGrid.Repaint; end; - procedure TfrmTableTools.ResultGridCompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer); begin @@ -326,14 +434,16 @@ var VT: TVirtualStringTree; Msg: WideString; begin - // Red text color for errors, purple for nodes - VT := Sender as TVirtualStringTree; - if VT.Header.Columns.Count >= 3 then begin - Msg := LowerCase(VT.Text[Node, 2]); - if Msg = 'note' then + // Red text color for errors, purple for notes, grey for skipped tables + if not (vsSelected in Node.States) then begin + VT := Sender as TVirtualStringTree; + Msg := VT.Text[Node, 2]; + if LowerCase(Msg) = 'note' then TargetCanvas.Font.Color := clPurple - else if Msg = 'error' then - TargetCanvas.Font.Color := clRed; + else if LowerCase(Msg) = 'error' then + TargetCanvas.Font.Color := clRed + else if Pos(STRSKIPPED, Msg) > 0 then + TargetCanvas.Font.Color := clGray; end; end; @@ -344,7 +454,10 @@ var begin if Column > NoColumn then begin Data := Sender.GetNodeData(Node); - CellText := Data^[Column]; + if Data^.Count > Column then + CellText := Data^[Column] + else + CellText := ''; end; end;