Related
How to copy all columns between two VirtualTreeView? In my case only the first one is copied.
I use VirtualTreeView v6.6.
The code that I'm using:
type
PItemNode1 = ^TItemNode1;
TItemNode1 = record
Name: WideString;
Order: string;
Quantity:String;
end;
type
PItemNode2 = ^TItemNode2;
TItemNode2 = record
Name: WideString;
Order: string;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
Node: PVirtualNode;
begin
VT.NodeDataSize := SizeOf(TItemNode1);
VT2.NodeDataSize := SizeOf(TItemNode2);
VT.RootNodeCount := 2;
VT2.RootNodeCount := 10;
end;
Procedure for inserting OLE data
procedure TForm1.InsertData(Sender: TVirtualStringTree; DataObject: IDataObject;
Formats: TFormatArray; Effect: Integer; Mode: TVTNodeAttachMode);
var
FormatAccepted: Boolean;
i: Integer;
begin
FormatAccepted := false;
for i := 0 to High(Formats) do
begin
if Formats[i] = CF_VIRTUALTREE then
begin
if not FormatAccepted then
begin
Sender.ProcessDrop(DataObject, Sender.DropTargetNode, Effect, Mode);
FormatAccepted := True;
end;
end;
end;
end;
Drag and Drop precedures
procedure TForm1.VT2DragAllowed(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; var Allowed: Boolean);
begin
Allowed := True;
end;
procedure TForm1.VT2DragDrop(Sender: TBaseVirtualTree; Source: TObject;
DataObject: IDataObject; Formats: TFormatArray; Shift: TShiftState;
Pt: TPoint; var Effect: Integer; Mode: TDropMode);
procedure DetermineEffect;
begin
if Shift = [] then
begin
if Source = Sender then
Effect := DROPEFFECT_MOVE
else
Effect := DROPEFFECT_COPY;
end
else
begin
if (Shift = [ssAlt]) or (Shift = [ssCtrl, ssAlt]) then
Effect := DROPEFFECT_LINK
else if Shift = [ssCtrl] then
Effect := DROPEFFECT_COPY
else
end;
end;
var
Attachmode: TVTNodeAttachMode;
Nodes: TNodeArray;
i: Integer;
begin
Nodes := nil;
case Mode of
dmAbove:
Attachmode := amInsertBefore;
dmOnNode:
Attachmode := amAddChildLast;
dmBelow:
Attachmode := amInsertAfter;
else
Attachmode := amNowhere;
end;
if DataObject = nil then
begin
//VCL
if Source is TVirtualStringTree then
begin
DetermineEffect;
Nodes := VT2.GetSortedSelection(True);
if Effect = DROPEFFECT_COPY then
begin
for i := 0 to High(Nodes) do
VT2.CopyTo(Nodes[i], Sender.DropTargetNode, Attachmode, false);
end
else
for i := 0 to High(Nodes) do
VT2.MoveTo(Nodes[i], Sender.DropTargetNode, Attachmode, false);
end;
end
else
begin
// OLE drag&drop.
if Source is TBaseVirtualTree then
DetermineEffect
else
begin
if Boolean(Effect and DROPEFFECT_COPY) then
Effect := DROPEFFECT_COPY
else
Effect := DROPEFFECT_MOVE;
end;
InsertData(Sender as TVirtualStringTree, DataObject, Formats, Effect,
Attachmode);
end;
end;
procedure TForm1.VT2DragOver(Sender: TBaseVirtualTree; Source: TObject;
Shift: TShiftState; State: TDragState; Pt: TPoint; Mode: TDropMode;
var Effect: Integer; var Accept: Boolean);
// Return True, if AParent - child node of ANode.
function IsNodeParent(AParent, ANode: PVirtualNode): Boolean;
var
NextParent: PVirtualNode;
begin
NextParent := AParent;
repeat
NextParent := NextParent.Parent;
until (NextParent = Sender.RootNode) or (NextParent = nil) or
(NextParent = ANode);
Result := ANode = NextParent;
end;
var
i: Integer;
Nodes: TNodeArray;
begin
Accept := True;
if (Assigned(Sender.DropTargetNode)) and
(Sender.DropTargetNode <> Sender.RootNode) then
Nodes := (Sender as TVirtualStringTree).GetSortedSelection(True);
if Length(Nodes) > 0 then
begin
for i := 0 to Length(Nodes) - 1 do
begin
Accept :=
(not IsNodeParent(Sender.DropTargetNode, Nodes[i]))
and (not(Sender.DropTargetNode = Nodes[i]));
if not Accept then Exit;
end;
end;
end;
Initialize the nodes of VT2(right VT in the picture above )and get the text for them
procedure TForm1.VT2GetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
ItemNode: PItemNode2;
begin
ItemNode := Sender.GetNodeData(Node);
if Assigned(ItemNode) then
begin
case Column of
0:
CellText := ItemNode^.Name;
1:
CellText := ItemNode^.Order;
end;
end;
end;
procedure TForm1.VT2InitNode(Sender: TBaseVirtualTree;
ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
ItemNode: PItemNode2;
begin
ItemNode := Sender.GetNodeData(Node);
if Assigned(ItemNode) then
if Length(ItemNode^.Name) = 0 then
ItemNode^.Name := 'Node Index № ' + IntToStr(Node.Index);
ItemNode^.Order := IntToStr(Node.Index);
end;
Get a new text after the node is dropped
procedure TForm1.VT2NewText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; NewText: string);
var
ItemNode: PItemNode2;
begin
ItemNode := Sender.GetNodeData(Node);
if Assigned(ItemNode) then
begin
case Column of
0:
ItemNode^.Name := NewText;
1:
ItemNode^.Order := NewText; // I've checked this line in the debugger. It is skipped
end;
end;
end;
Initialize the nodes of VT(left VT in the picture above )and get the text for them
procedure TForm1.VTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
ItemNode: PItemNode1;
begin
ItemNode := Sender.GetNodeData(Node);
if Assigned(ItemNode) then
case Column of
0:
CellText := ItemNode^.Name;
1:
CellText := ItemNode^.Order;
end;
end;
procedure TForm1.VTInitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
ItemNode: PItemNode1;
begin
ItemNode := Sender.GetNodeData(Node);
if Assigned(ItemNode) then
if Length(ItemNode^.Name) = 0 then
ItemNode^.Name := 'VT1_Node № ' + IntToStr(Node.Index);
ItemNode^.Order := IntToStr(Node.Index);
end;
end.
I've try to implement an editor for a VirtualStringTree based on example of Lazarius
Can you tell me why did I get an Access Violation after TStringEditLink gets destroyed?
It's weired that error appear only when i press ESCAPE or ENTER. If i click from one cell to another there is no error.
Like an observation, I sow that if I remove the FEdit.Free code from destructor TStringEditLink.Destroy the error disappear.
Do you have a solution for this?
Bellow the full code:
unit Unit2;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, VirtualTrees, EditorLink, Vcl.StdCtrls,
Vcl.ExtCtrls, Vcl.Imaging.jpeg;
type
TTreeData = record
Fields: array of String;
end;
PTreeData = ^TTreeData;
const
SizeVirtualTree = SizeOf(TTreeData);
type
TForm2 = class(TForm)
VirtualTree: TVirtualStringTree;
procedure FormCreate(Sender: TObject);
procedure VirtualTreeClick(Sender: TObject);
procedure VirtualTreeCreateEditor(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
procedure VirtualTreeEditing(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; var Allowed: Boolean);
procedure VirtualTreeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
procedure VirtualTreeGetNodeDataSize(Sender: TBaseVirtualTree;
var NodeDataSize: Integer);
procedure VirtualTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
procedure VirtualTreeNewText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; NewText: string);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
procedure TForm2.FormCreate(Sender: TObject);
var
Node: PVirtualNode;
LTreeData: PTreeData;
begin
VirtualTree.Clear;
VirtualTree.BeginUpdate;
//node 1
Node:= VirtualTree.AddChild(nil,nil);
VirtualTree.ValidateNode(Node,False);
LTreeData:= VirtualTree.GetNodeData(Node);
SetLength(LTreeData^.Fields,3);
LTreeData^.Fields[0]:= 'John';
LTreeData^.Fields[1]:= '2500';
LTreeData^.Fields[2]:= 'Production';
//node 2
Node:= VirtualTree.AddChild(nil,nil);
VirtualTree.ValidateNode(Node,False);
LTreeData:= VirtualTree.GetNodeData(Node);
SetLength(LTreeData^.Fields,3);
LTreeData^.Fields[0]:= 'Mary';
LTreeData^.Fields[1]:= '2100';
LTreeData^.Fields[2]:= 'HR';
VirtualTree.EndUpdate;
end;
procedure TForm2.VirtualTreeClick(Sender: TObject);
var
VT: TVirtualStringTree;
Click: THitInfo;
begin
VT:= Sender as TVirtualStringTree;
VT.GetHitTestInfoAt(Mouse.CursorPos.X-VT.ClientOrigin.X, Mouse.CursorPos.Y-VT.ClientOrigin.Y, True, Click);
VT.EditNode(Click.HitNode,Click.HitColumn);
end;
procedure TForm2.VirtualTreeCreateEditor(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
begin
EditLink := TStringEditLink.Create;
end;
procedure TForm2.VirtualTreeEditing(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
begin
Allowed:= True;
end;
procedure TForm2.VirtualTreeFreeNode(Sender: TBaseVirtualTree;
Node: PVirtualNode);
var
LTreeData: PTreeData;
begin
LTreeData:= Sender.GetNodeData(Node);
Finalize(LTreeData^);
end;
procedure TForm2.VirtualTreeGetNodeDataSize(Sender: TBaseVirtualTree;
var NodeDataSize: Integer);
begin
NodeDataSize:= SizeVirtualTree;
end;
procedure TForm2.VirtualTreeGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
LTreeData: PTreeData;
begin
if Assigned(Node) and (Column > NoColumn) then
begin
LTreeData:= Sender.GetNodeData(Node);
CellText:= LTreeData^.Fields[Column];
end;
end;
procedure TForm2.VirtualTreeNewText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; NewText: string);
var
LTreeData: PTreeData;
begin
LTreeData:= Sender.GetNodeData(Node);
LTreeData^.Fields[Column]:= NewText;
end;
end.
and the EditorLink unit
unit EditorLink;
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs,
VirtualTrees, Messages, Windows, StdCtrls, Vcl.ExtCtrls;
type
TStringEditLink = class(TInterfacedObject, IVTEditLink)
private
FEdit: TWinControl;
FTree: TVirtualStringTree;
FNode: PVirtualNode;
FColumn: Integer;
FStopping: Boolean;
protected
procedure EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
public
destructor Destroy; override;
function BeginEdit: Boolean; stdcall;
function CancelEdit: Boolean; stdcall;
function EndEdit: Boolean; stdcall;
function GetBounds: TRect; stdcall;
function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall;
procedure ProcessMessage(var Message: TMessage); stdcall;
procedure SetBounds(R: TRect); stdcall;
end;
implementation
uses unit2;
destructor TStringEditLink.Destroy;
begin
FEdit.Free; //--> seems that due to this I get the access violation
inherited;
end;
procedure TStringEditLink.EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
case Key of
VK_ESCAPE:
begin
FTree.CancelEditNode;
Key := 0;
FTree.setfocus;
end;
VK_RETURN:
begin
PostMessage(FTree.Handle, WM_KEYDOWN, VK_DOWN, 0);
Key := 0;
FTree.EndEditNode;
FTree.setfocus;
end;
end; //case
end;
function TStringEditLink.BeginEdit: Boolean;
begin
Result := not FStopping;
if Result then
begin
FEdit.Show;
FEdit.SetFocus;
end;
end;
function TStringEditLink.CancelEdit: Boolean;
begin
Result := True;
FEdit.Hide;
end;
function TStringEditLink.EndEdit: Boolean;
var
s: String;
begin
Result := True;
s := TComboBox(FEdit).Text;
FTree.Text[FNode, FColumn] := s;
FTree.InvalidateNode(FNode);
FEdit.Hide;
FTree.SetFocus;
end;
function TStringEditLink.GetBounds: TRect;
begin
Result := FEdit.BoundsRect;
end;
function TStringEditLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean;
var
FCellText: String;
FCellTextBounds: TRect;
FCellFont: TFont;
begin
Result := True;
FTree := Tree as TVirtualStringTree;
FNode := Node;
FColumn := Column;
FCellFont:= TFont.Create;
FTree.GetTextInfo(FNode, FColumn, FCellFont, FCellTextBounds, FCellText);
FEdit := TComboBox.Create(nil);
with FEdit as TComboBox do
begin
Visible := False;
Parent := Tree;
Items.Add('Google');
Items.Add('Yahoo');
Items.Add('Altavista');
OnKeyDown := EditKeyDown;
Text:= FCellText;
end;
end;
procedure TStringEditLink.ProcessMessage(var Message: TMessage);
begin
FEdit.WindowProc(Message);
end;
procedure TStringEditLink.SetBounds(R: TRect);
var
Dummy: Integer;
begin
FTree.Header.Columns.GetColumnBounds(FColumn, Dummy, R.Right);
FEdit.BoundsRect := R;
end;
end.
I have no Lazarus but it seems to behave the same on XE4.
In my VST installation, located in ./VirtualTreeviewV5.3.0/Demos/Advanced there is an Editors.pas file where I've found the destructor below. Notice the comment casues issue #357:
destructor TPropertyEditLink.Destroy;
begin
//FEdit.Free; casues issue #357. Fix:
if FEdit.HandleAllocated then
PostMessage(FEdit.Handle, CM_RELEASE, 0, 0);
inherited;
end;
Moreover, FEdit.Free is performed in the PrepareEdit method before its fresh creation:
function TStringEditLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean;
var
FCellText: String;
FCellTextBounds: TRect;
FCellFont: TFont;
begin
Result := True;
FTree := Tree as TVirtualStringTree;
FNode := Node;
FColumn := Column;
FEdit.Free;
FEdit := nil;
FCellFont:= TFont.Create;
FTree.GetTextInfo(FNode, FColumn, FCellFont, FCellTextBounds, FCellText);
FEdit := TComboBox.Create(nil);
with FEdit as TComboBox do
. . .
This solves the VK_ESC and the VK_RETURN issues on my XE4 and XE7 installation.
The issue #357 seems to have not been fixed yet: see - Used fix proposed in issue #361 to fix issue #357 (AV in advanced demo - PropertiesDemo form in XE3+). I've found no evidence of the #361 fix.
Another issue happens to me when clicking on a unassigned node after an edit operation.
Checking if the Click.HitNode is not nil before start editing solves the above.
procedure TForm2.VirtualTreeClick(Sender: TObject);
var
VT: TVirtualStringTree;
Click: THitInfo;
begin
VT:= Sender as TVirtualStringTree;
VT.GetHitTestInfoAt(Mouse.CursorPos.X-VT.ClientOrigin.X, Mouse.CursorPos.Y-VT.ClientOrigin.Y, True, Click);
if Assigned(Click.HitNode) then
VT.EditNode(Click.HitNode,Click.HitColumn);
end;
Notice also you have a circular reference in the EditorLink unit:
uses Unit2;
This pseudo stack trace of your code illustrates the issue:
FEdit.EditKeyDown()
-- calls --
FTree.EndEditNode() { or FTree.CancelEditNode }
-- which calls --
TStringEditLink.Destroy()
-- which calls --
FEdit.Free()
The code in the event handler for FEdit.EditKeyDown() frees FEdit before the key down event handler code finishes running. Thus the access violation error.
We handled this by setting up a signal mechanism so the TStringEditLink could signal the main form when it was done, and the main form could run the code to destroy the TStringEditLink (since it is the one that created the TStringEditLink in the first place). We added a TTimer to the main form, and a property to receive the signal. The TTimer watches the property. The TStringEditLink component has a pointer to the form, so it can set the property.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, VirtualTrees;
type
TEditorAction = (eaCancel, eaAccept, eaNotSet);
TForm1 = class(TForm)
vstTree: TVirtualStringTree;
procedure vstTreeCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
procedure DoWatchTreeEditorTimer(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FEndEditTimer: TTimer;
FEditorAction: TEditorAction;
procedure SetEditorAction(const Value: TEditorAction);
public
property EditorAction: TEditorAction read FEditorAction write SetEditorAction;
end;
TPropertyEdit = class(TInterfacedObject, IVTEditLink)
procedure EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
private
FEdit: TWinControl;
FTree: TVirtualStringTree;
FNode: PVirtualNode;
FColumn: Integer;
public
FForm: TForm1;
destructor Destroy; override;
function BeginEdit: Boolean; stdcall;
function CancelEdit: Boolean; stdcall;
function EndEdit: Boolean; stdcall;
function GetBounds: TRect; stdcall;
function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall;
procedure ProcessMessage(var Message: TMessage); stdcall;
procedure SetBounds(R: TRect); stdcall;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
FEndEditTimer := TTimer.Create(nil);
FEndEditTimer.Enabled := False;
FEndEditTimer.Interval := 100;
FEndEditTimer.OnTimer := DoWatchTreeEditorTimer;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FreeAndNil(FEndEditTimer);
end;
procedure TForm1.vstTreeCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
begin
EditLink := TPropertyEdit.Create;
TPropertyEdit(EditLink).FForm := Self; { lets us signal the form when the editor needs to be destroyed }
FEditorAction := eaNotSet;
end;
procedure TForm1.SetEditorAction(const Value: TEditorAction);
begin
if FEditorAction <> Value then
begin
FEditorAction := Value;
FEndEditTimer.Enabled := True;
end;
end;
procedure TForm1.DoWatchTreeEditorTimer(Sender: TObject);
begin
FEndEditTimer.Enabled := False;
Application.ProcessMessages;
case FEditorAction of
eaCancel:
begin
vstTree.CancelEditNode;
vstTree.SetFocus;
end;
eaAccept:
begin
vstTree.EndEditNode;
vstTree.SetFocus;
end;
end;
end;
{ TPropertyEdit }
function TPropertyEdit.BeginEdit: Boolean;
begin
Result := True;
FEdit.Show;
end;
function TPropertyEdit.CancelEdit: Boolean;
begin
Result := True;
FEdit.Hide;
FForm.FEditorAction := eaCancel;
end;
destructor TPropertyEdit.Destroy;
begin
if FEdit <> nil then
FreeAndNil(FEdit);
inherited;
end;
procedure TPropertyEdit.EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
case Key of
VK_ESCAPE:
begin
Key := Word(0); { some versions of Delphi throw random A/V errors if '0' is not cast as a Word() }
FForm.EditorAction := eaCancel;
end;
VK_RETURN:
begin
Key := Word(0); { some versions of Delphi throw random A/V errors if '0' is not cast as a Word() }
FForm.EditorAction := eaAccept
end;
end;
end;
function TPropertyEdit.EndEdit: Boolean;
begin
Result := True;
{ Do something with the value provided by the user }
FEdit.Hide;
FForm.EditorAction := eaAccept;
end;
function TPropertyEdit.GetBounds: TRect;
begin
Result := FEdit.BoundsRect;
end;
function TPropertyEdit.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean;
begin
Result := True;
FTree := Tree as TVirtualStringTree;
FNode := Node;
FColumn := Column;
{ Setup the editor for user }
FEdit := TSomeWinControl.Create(nil);
FEdit.Properties := Values;
{ Capture keystrokes }
FEdit.OnKeyDown := EditKeyDown;
end;
procedure TPropertyEdit.ProcessMessage(var Message: TMessage);
begin
FEdit.WindowProc(Message);
end;
procedure TPropertyEdit.SetBounds(R: TRect);
var
Dummy: Integer;
begin
FTree.Header.Columns.GetColumnBounds(FColumn, Dummy, R.Right);
FEdit.BoundsRect := R;
end;
end.
Our code does a lot of additional things, so the above code is a copy/paste of the essential parts to demonstrate how to overcome the race condition. It is untested, but should get you pointed in the right direction.
One solution is also to free the previously created controls.
function TStringEditLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean;
var
FCellText: String;
FCellTextBounds: TRect;
FCellFont: TFont;
i: Integer;
Item: TControl;
begin
Result := True;
FTree := Tree as TVirtualStringTree;
FNode := Node;
FColumn := Column;
FCellFont:= TFont.Create;
FTree.GetTextInfo(FNode, FColumn, FCellFont, FCellTextBounds, FCellText);
//----->> free previuous created control <<----------
for i := (FTree.ControlCount - 1) downto 0 do
begin
Item := FTree.controls[i];
if assigned(item) then
begin
if item is TComboBox then FreeAndNil(item);
end;
end;
//---------------------------------------------------
FEdit := TComboBox.Create(nil);
with FEdit as TComboBox do
begin
Visible := False;
Parent := Tree;
Items.Add('Google');
Items.Add('Yahoo');
Items.Add('Altavista');
OnKeyDown := EditKeyDown;
Text:= FCellText;
end;
end;
The solution I used at the end is listed bellow:
TBasePanel = class(TPanel)
private
procedure CMRelease(var Message: TMessage); message CM_RELEASE;
protected
public
procedure Release; virtual;
end;
TStringEditLink = class(TInterfacedObject, IVTEditLink)
private
FBasePanel: TBasePanel;
...
protected
procedure EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
public
destructor Destroy; override;
function BeginEdit: Boolean; stdcall;
function CancelEdit: Boolean; stdcall;
function EndEdit: Boolean; stdcall;
function GetBounds: TRect; stdcall;
function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall;
procedure ProcessMessage(var Message: TMessage); stdcall;
procedure SetBounds(R: TRect); stdcall;
end;
implementation
procedure TBasePanel.CMRelease(var Message: TMessage);
begin
Free;
end;
procedure TBasePanel.Release;
begin
if HandleAllocated then
PostMessage(Handle, CM_RELEASE, 0, 0);
end;
destructor TStringEditLink.Destroy;
begin
if Assigned(FBasePanel) then FBasePanel.Release;
inherited;
end;
FBasePanel should be used as owner and as parent for as many component editors would like to be displayed in the same time.
In HeidiSql source code there is a good example to avoid this error.
The code a little changed is:
procedure TBaseEditorLink.TempWindowProc(var Message: TMessage);
begin
case Message.Msg of
WM_CHAR: //Catch hotkeys
if not (TWMChar(Message).CharCode = VK_TAB) then FOldWindowProc(Message);
WM_GETDLGCODE: //"WantTabs" mode for main control
Message.Result := Message.Result or DLGC_WANTARROWS or DLGC_WANTALLKEYS or DLGC_WANTTAB;
else
begin
try
FOldWindowProc(Message);
except
on E : EAccessViolation do; //EAccessViolation occurring in some cases
on E : Exception do raise;
end;
end;
end;
end;
Before I used THint, and it was working with this code:
procedure TMainForm.FormCreate(Sender: TObject);
begin
Application.OnShowHint := AppShowHint;
end;
procedure TMainForm.AppShowHint(var HintStr: String; var CanShow: Boolean; var HintInfo: Controls.THintInfo);
begin
HintInfo.ReshowTimeout := 1;
end;
Now I use TBalloonHint and want to change hint text when hint is shown. The above procedure is not triggered.
I am changing the hint text each second, so when user enters control, the hint is shown and I want to update the hint text each second, also when user is not moving with the mouse.
How to achieve this with TBalloonHint?
TBalloonHint does not support this functionality. The following code (Delphi XE3) adds it.
Cons:
CPU load - every call TBalloonHint.ShowHint creates a new TCustomHintWindow
flickering when redrawing
type
TMyHintWindow = class(THintWindow)
public
function CalcHintRect(MaxWidth: Integer; const AHint: string;
AData: TCustomData): TRect; override;
function ShouldHideHint: Boolean; override;
end;
var BalloonHint: TBalloonHint;
_HintPos: TPoint;
function TMyHintWindow.CalcHintRect(MaxWidth: Integer; const AHint: string;
AData: TCustomData): TRect;
begin
Result := Rect(0,0,0,0);
end;
function TMyHintWindow.ShouldHideHint: Boolean;
begin
Result := True;
BalloonHint.Free; BalloonHint := nil;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
HintWindowClass := TMyHintWindow;
Application.OnShowHint := AppShowHint;
end;
procedure TMainForm.AppShowHint(var HintStr: String; var CanShow: Boolean; var HintInfo: THintInfo);
begin
HintInfo.ReshowTimeout := 1;
if not Assigned(BalloonHint)
then begin
BalloonHint := TBalloonHint.Create(Self);
_HintPos := Point(MaxInt, MaxInt);
end;
if (_HintPos <> HintInfo.HintPos) or (BalloonHint.Description <> HintStr)
then begin
_HintPos := HintInfo.HintPos;
BalloonHint.Description := HintStr;
BalloonHint.ShowHint(_HintPos);
end;
end;
Another ways:
rewrite TMyHintWindow.CalcHintRect and .Paint taking code from TBalloonHint
rewrite TMyHintWindow using Tooltip Controls
Add: Use tooltip control. Try also set HintInfo.ReshowTimeout := 25.
uses Windows, Vcl.Controls, System.Classes, Winapi.CommCtrl, Winapi.Messages;
type
TTooltipHintWindow = class(THintWindow)
private
TooltipWnd: HWND;
TooltipInfo: TToolInfo;
TooltipText: string;
TooltipPos: TPoint;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure ActivateHint(Rect: TRect; const AHint: string); override;
function CalcHintRect(MaxWidth: Integer; const AHint: string; AData: TCustomData): TRect; override;
function ShouldHideHint: Boolean; override;
end;
implementation
procedure TTooltipHintWindow.ActivateHint(Rect: TRect; const AHint: string);
begin
inherited;
if (TooltipText <> AHint)
then begin // update text
TooltipText := AHint;
TooltipInfo.lpszText := PChar(TooltipText);
SendMessage(TooltipWnd, TTM_UPDATETIPTEXT, 0, LParam(#TooltipInfo));
end;
if (TooltipPos <> Rect.TopLeft)
then begin // update position
TooltipPos := Rect.TopLeft;
SendMessage(TooltipWnd, TTM_TRACKPOSITION, 0, PointToLParam(TooltipPos));
end;
// show
SendMessage(TooltipWnd, TTM_TRACKACTIVATE, WParam(True), LParam(#TooltipInfo));
end;
function TTooltipHintWindow.CalcHintRect(MaxWidth: Integer; const AHint: string;
AData: TCustomData): TRect;
begin
Result := Rect(0,0,0,0);
end;
constructor TTooltipHintWindow.Create(AOwner: TComponent);
var font, boldfont: HFONT;
logfont: TLogFont;
begin
inherited;
// create tooltip
TooltipWnd := CreateWindowEx(WS_EX_TOPMOST or WS_EX_TRANSPARENT,
TOOLTIPS_CLASS, nil,
TTS_NOPREFIX or TTS_ALWAYSTIP or TTS_BALLOON,
0, 0, 0, 0, 0, 0, HInstance, nil);
// set bold font
font := SendMessage(TooltipWnd, WM_GETFONT, 0, 0);
if (font <> 0)
then begin
if GetObject(font, SizeOf(logfont), #logfont) > 0
then begin
logfont.lfWeight := FW_BOLD;
boldfont := CreateFontIndirect(logfont);
SendMessage(TooltipWnd, WM_SETFONT, boldfont, 0);
end;
end;
// set maximum width
SendMessage(TooltipWnd, TTM_SETMAXTIPWIDTH, 0 , 400);
// init
FillChar(TooltipInfo, SizeOf(TooltipInfo), 0);
TooltipInfo.cbSize := SizeOf(TooltipInfo);
TooltipInfo.uFlags := TTF_TRACK or TTF_TRANSPARENT;
TooltipInfo.uId := 1;
SendMessage(TooltipWnd, TTM_ADDTOOL, 0, LParam(#TooltipInfo));
end;
destructor TTooltipHintWindow.Destroy;
begin
DestroyWindow(TooltipWnd);
inherited;
end;
function TTooltipHintWindow.ShouldHideHint: Boolean;
begin
inherited;
// hide
SendMessage(TooltipWnd, TTM_TRACKACTIVATE, WParam(False), LParam(#TooltipInfo));
TooltipPos := Point(MaxInt, MaxInt);
TooltipText := '';
end;
How to implement a TStringGrid using virtual tree view? Does anyone have an example?
I exceeded the character limit on my original answer with this unit, so here it is in a separate answer.
{===============================================================================
Copyright © BJM Software
http://www.bjmsoftware.com
===============================================================================}
unit BaseTree_fr;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, ImgList, VirtualTrees, Contnrs
, DomainObject_intf, Base_fr
;
type
RTreeData = record
CDO: TCustomDomainObject;
end;
PTreeData = ^RTreeData;
TBaseTreeEvent = procedure of object;
TCDONodeList = class(TObject)
private
FCDOs: TObjectList;
FNodes: TList;
protected
public
constructor Create;
destructor Destroy; override;
procedure Add(ACDO: TCustomDomainObject; ANode: PVirtualNode);
procedure Clear;
function IndexOfCDO(ACDO: TCustomDomainObject): Integer;
function NodeOf(ACDO: TCustomDomainObject): PVirtualNode;
procedure Remove(ACDO: TCustomDomainObject);
procedure InvalidateNodeFor(ACDO: TCustomDomainObject; AEvent: TCDOEvent);
function IndexOfNode(ANode: PVirtualNode): Integer;
function CDOOf(ANode: PVirtualNode): TCustomDomainObject;
procedure InvalidateNode(ANode: PVirtualNode);
end;
TBaseTreeFrame = class(TBaseFrame, ICDOObserver)
Frame_Vst: TVirtualStringTree;
procedure Frame_VstGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
var Ghosted: Boolean; var ImageIndex: Integer);
procedure Frame_VstGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: WideString);
procedure Frame_VstFocusChanged(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex);
procedure Frame_VstDblClick(Sender: TObject);
procedure Frame_VstInitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
procedure Frame_VstInitChildren(Sender: TBaseVirtualTree;
Node: PVirtualNode; var ChildCount: Cardinal);
procedure Frame_VstCompareNodes(Sender: TBaseVirtualTree; Node1,
Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
procedure Frame_VstChecked(Sender: TBaseVirtualTree;
Node: PVirtualNode);
private
FNodeCheckType: TCheckType;
FOnCDOChanged: TCDONotifyEvent;
FOnDoubleClicked: TCDONotifyEvent;
FOnSelectionChanged: TCDONotifyEvent;
FOnShowColumnHeaders: TBaseTreeEvent;
protected
FNodeList: TCDONodeList;
procedure ClearFrame; override;
procedure ClearHeaders; override;
function FindParentNode(ACDO: TCustomDomainObject; AParent:
TCustomDomainObject): PVirtualNode;
function GetImageIndexFor(ACDO: TCustomDomainObject; Selected: boolean):
Integer; virtual;
procedure ShowDobs(ACDO: TCustomDomainObject; AParent: TCustomDomainObject); override;
procedure ShowDomainObject(ACDO: TCustomDomainObject; AParent:
TCustomDomainObject); override;
procedure RemoveDomainObject(ACDO: TCustomDomainObject; AParent:
TCustomDomainObject);
procedure SetCDO(const Value: TCustomDomainObject); override;
function ShowChildrenOfList(AFromCDO: TCustomDomainObject): TCustomDomainObject;
virtual;
procedure UpdateCDO(ACDO: TCustomDomainObject; AEvent: TCDOEvent);
procedure HandleDoubleClicked(ACDO: TCustomDomainObject); virtual;
procedure HandleSelectionChanged(ACDO: TCustomDomainObject); virtual;
procedure DoCDOChanged(ACDO: TCustomDomainObject);
procedure DoDoubleClicked(ACDO: TCustomDomainObject);
procedure DoSelectionChanged(ACDO: TCustomDomainObject);
procedure DoShowColumnHeaders;
procedure BeginLoad; override;
procedure EndLoad; override;
procedure ShowColumnHeaders; override;
procedure AddDomainObject(ACDO: TCustomDomainObject; AParent:
TCustomDomainObject);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure CheckAll;
function CheckedCount: integer;
procedure FocusOn(ACDO: TCustomDomainObject);
function GetTree: TCustomDomainObject;
function GetSelection: TCustomDomainObject;
procedure UncheckAll;
property NodeCheckType: TCheckType read FNodeCheckType write FNodeCheckType;
property OnCDOChanged: TCDONotifyEvent read FOnCDOChanged write FOnCDOChanged;
property OnDoubleClicked: TCDONotifyEvent read FOnDoubleClicked write
FOnDoubleClicked;
property OnSelectionChanged: TCDONotifyEvent read FOnSelectionChanged
write FOnSelectionChanged;
property OnShowColumnHeaders: TBaseTreeEvent read FOnShowColumnHeaders
write FOnShowColumnHeaders;
end;
implementation
{$R *.dfm}
uses
BaseGUIApp_fm
, DomainObject_cls
, GUIApplication_cls
;
constructor TCDONodeList.Create;
begin
FCDOs := TObjectList.Create( false );
FNodes := TList.Create;
end;
destructor TCDONodeList.Destroy;
begin
FCDOs.Free;
FNodes.Free;
inherited;
end;
procedure TCDONodeList.Add(ACDO: TCustomDomainObject; ANode: PVirtualNode);
begin
FCDOs.Add( ACDO );
FNodes.Add( ANode );
end;
function TCDONodeList.CDOOf(ANode: PVirtualNode): TCustomDomainObject;
var
Idx: integer;
begin
Idx := FNodes.IndexOf( ANode );
if Idx = -1 then begin
Result := nil;
end else begin
Result := TCustomDomainObject( FCDOs[Idx] );
end;
end;
function TCDONodeList.IndexOfCDO(ACDO: TCustomDomainObject): Integer;
begin
Result := FCDOs.IndexOf( ACDO );
end;
function TCDONodeList.IndexOfNode(ANode: PVirtualNode): Integer;
begin
Result := FNodes.IndexOf( ANode );
end;
procedure TCDONodeList.InvalidateNode(ANode: PVirtualNode);
var
Tree: TBaseVirtualTree;
begin
Tree := TreeFromNode( ANode );
Tree.InvalidateNode( ANode );
end;
procedure TCDONodeList.InvalidateNodeFor(ACDO: TCustomDomainObject; AEvent:
TCDOEvent);
var
Idx: integer;
Node: PVirtualNode;
Tree: TBaseVirtualTree;
begin
Idx := FCDOs.IndexOf( ACDO );
if Idx > -1 then begin // Just in case
Node := PVirtualNode( FNodes[Idx] );
Tree := TreeFromNode( Node );
Tree.InvalidateNode( Node );
end;
end;
function TCDONodeList.NodeOf(ACDO: TCustomDomainObject): PVirtualNode;
var
Idx: integer;
begin
Idx := FCDOs.IndexOf( ACDO );
if Idx = -1 then begin
Result := nil;
end else begin
Result := PVirtualNode( FNodes[Idx] );
end;
end;
procedure TCDONodeList.Remove(ACDO: TCustomDomainObject);
begin
FNodes.Delete( FCDOs.Remove( ACDO ) );
end;
procedure TBaseTreeFrame.ClearFrame;
begin
inherited;
Frame_Vst.Clear;
FNodeList.Clear;
DoSelectionChanged( nil );
end;
constructor TBaseTreeFrame.Create(AOwner: TComponent);
begin
FNodeList := TCDONodeList.Create;
inherited;
Frame_Vst.DefaultText := '';
Frame_Vst.DragOperations := [];
Frame_Vst.NodeDataSize := SizeOf( RTreeData );
// // This is causing heavy recursions and InitNode executions!!!
// Frame_Vst.TreeOptions.AutoOptions := Frame_Vst.TreeOptions.AutoOptions
// + [toAutoSort];
Frame_Vst.TreeOptions.MiscOptions := Frame_Vst.TreeOptions.MiscOptions
- [toEditable]
+ [toCheckSupport{, toReadOnly}]
;
Frame_Vst.TreeOptions.PaintOptions := Frame_Vst.TreeOptions.PaintOptions
- [toHideFocusRect, toHideSelection];
Frame_Vst.TreeOptions.SelectionOptions := Frame_Vst.TreeOptions.SelectionOptions
// - []
+ [toFullRowSelect]
;
Frame_Vst.Images := TBaseGUIAppForm( GUIApp.MainForm ).Images;
Frame_Vst.Header.Images := TBaseGUIAppForm( GUIApp.MainForm ).HeaderImages;
Frame_Vst.NodeDataSize := sizeof( RTreeData );
end;
destructor TBaseTreeFrame.Destroy;
begin
FNodeList.Free;
inherited;
end;
procedure TBaseTreeFrame.RemoveDomainObject(ACDO: TCustomDomainObject; AParent:
TCustomDomainObject);
var
Node: PVirtualNode;
begin
Node := FNodeList.NodeOf( ACDO );
if Node <> nil then begin
FNodeList.Remove( ACDO );
Frame_Vst.DeleteNode( Node );
end;
end;
procedure TBaseTreeFrame.SetCDO(const Value: TCustomDomainObject);
begin
if Value <> FCDO then begin
if FCDO <> nil then begin
FCDO.DetachObserver( self );
end;
inherited;
if FCDO <> nil then begin
FCDO.AttachObserver( self );
end;
end;
end;
procedure TBaseTreeFrame.ShowDomainObject(ACDO: TCustomDomainObject; AParent:
TCustomDomainObject);
begin
// We are dealing with a virtual tree that asks for its data, and so we don't
// need to do anything here.
end;
procedure TBaseTreeFrame.UpdateCDO(ACDO: TCustomDomainObject; AEvent:
TCDOEvent);
//var
// Node: PVirtualNode;
begin
if ACDO = CDO then begin // Root that isn't shown.
end else begin
case AEvent of
ceAddedToList: begin
AddDomainObject( ACDO, ACDO.Container );
FocusOn( ACDO );
end;
ceSaved: begin
FNodeList.InvalidateNodeFor( ACDO, AEvent );
DoCDOChanged( ACDO );
end;
ceRemovedFromList: begin
RemoveDomainObject( ACDO, ACDO.Container );
end;
// ceCheckStateChanged: begin
// FNodeList.InvalidateNodeFor( ACDO, AEvent );
// end;
(*
ceListReloaded: begin
Node := FNodeList.NodeOf( ACDO );
Frame_Vst.ReInitNode( Node, true );
// FNodeList.InvalidateNodeFor( ACDO, AEvent );
end;
*)
end;
end;
end;
procedure TBaseTreeFrame.Frame_VstGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
var Ghosted: Boolean; var ImageIndex: Integer);
var
ACDO: TCustomDomainObject;
NodeData: ^RTreeData;
begin
inherited;
Case Column of
-1, 0 : begin
NodeData := Sender.GetNodeData( Node );
ACDO := NodeData.CDO;
case Kind of
ikState: ImageIndex := -1;
ikNormal: ImageIndex := GetImageIndexFor( ACDO, false );
ikSelected: ImageIndex := GetImageIndexFor( ACDO, true );
ikOverlay: ImageIndex := -1;
else
ImageIndex := -1;
end;
end;
else
end;
end;
procedure TBaseTreeFrame.Frame_VstGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: WideString);
begin
inherited;
// Should be abstract.
end;
procedure TBaseTreeFrame.Frame_VstFocusChanged(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex);
var
ACDO: TCustomDomainObject;
NodeData: ^RTreeData;
begin
inherited;
NodeData := Sender.GetNodeData( Node );
ACDO := NodeData.CDO;
HandleSelectionChanged( ACDO );
end;
procedure TBaseTreeFrame.HandleSelectionChanged(ACDO: TCustomDomainObject);
begin
DoSelectionChanged( ACDO );
end;
function TBaseTreeFrame.GetSelection: TCustomDomainObject;
var
Node: PVirtualNode;
NodeData: ^RTreeData;
begin
Node := Frame_Vst.FocusedNode;
if Node = nil then begin
Result := nil;
end else begin
NodeData := Frame_Vst.GetNodeData( Node );
Result := NodeData.CDO;
end;
end;
procedure TBaseTreeFrame.DoSelectionChanged(ACDO: TCustomDomainObject);
begin
if assigned( FOnSelectionChanged ) then begin
FOnSelectionChanged( ACDO );
end;
end;
procedure TBaseTreeFrame.DoCDOChanged(ACDO: TCustomDomainObject);
begin
if assigned( FOnCDOChanged ) then begin
FOnCDOChanged( ACDO );
end;
end;
procedure TBaseTreeFrame.Frame_VstDblClick(Sender: TObject);
var
ACDO: TCustomDomainObject;
NodeData: ^RTreeData;
Tree: TBaseVirtualTree;
begin
inherited;
if Sender is TBaseVirtualTree then begin
Tree := TBaseVirtualTree( Sender );
if Tree.FocusedNode <> nil then begin
NodeData := Tree.GetNodeData( Tree.FocusedNode );
ACDO := NodeData.CDO;
HandleDoubleClicked( ACDO );
end;
end;
end;
procedure TBaseTreeFrame.HandleDoubleClicked(ACDO: TCustomDomainObject);
begin
DoDoubleClicked( ACDO );
end;
procedure TBaseTreeFrame.DoDoubleClicked(ACDO: TCustomDomainObject);
begin
if assigned( FOnDoubleClicked ) then begin
FOnDoubleClicked( ACDO );
end;
end;
procedure TBaseTreeFrame.BeginLoad;
begin
inherited;
Frame_Vst.BeginUpdate;
end;
procedure TBaseTreeFrame.EndLoad;
begin
Frame_Vst.EndUpdate;
inherited;
end;
procedure TBaseTreeFrame.DoShowColumnHeaders;
begin
if assigned( FOnShowColumnHeaders ) then begin
FOnShowColumnHeaders;
end;
end;
procedure TBaseTreeFrame.ShowColumnHeaders;
begin
inherited;
DoShowColumnHeaders;
end;
procedure TBaseTreeFrame.ClearHeaders;
begin
inherited;
Frame_Vst.Header.Columns.Clear;
end;
procedure TCDONodeList.Clear;
begin
FCDOs.Clear;
FNodes.Clear;
end;
function TBaseTreeFrame.GetImageIndexFor(ACDO: TCustomDomainObject;
Selected: boolean): Integer;
begin
// Should be abstract.
Result := -1;
end;
procedure TBaseTreeFrame.ShowDobs(ACDO, AParent: TCustomDomainObject);
begin
// We are dealing with a virtual tree that asks for its data, so we don't
// need to do anything here.
inherited;
if CDO <> nil then begin
Frame_Vst.RootNodeCount := CDO.CDOCount;
end else begin
Frame_Vst.RootNodeCount := 0;
end;
end;
procedure TBaseTreeFrame.Frame_VstInitNode(Sender: TBaseVirtualTree;
ParentNode, Node: PVirtualNode;
var InitialStates: TVirtualNodeInitStates);
var
ParentNodeData: ^RTreeData;
ParentNodeCDO: TCustomDomainObject;
NodeData: ^RTreeData;
ChildCDO: TCustomDomainObject;
ChildCDOCount: Cardinal;
begin
// Attach CDO to Node, but wich CDO???
// And tell Node whether it can have children. We don't care yet how many.
inherited;
if ParentNode = nil then begin
ParentNodeCDO := CDO;
end else begin
ParentNodeData := Frame_Vst.GetNodeData( ParentNode );
ParentNodeCDO := ParentNodeData.CDO;
end;
NodeData := Frame_Vst.GetNodeData( Node );
if NodeData.CDO = nil then begin
ChildCDO := ShowChildrenOfList( ParentNodeCDO );
if ( ChildCDO <> nil ) then begin
// Prevent warning on comparing signed/unsiged types.
ChildCDOCount := ChildCDO.CDOCount;
if ( ChildCDOCount > Node.Index ) then begin
// if ChildCDO is TDomainObject then begin
// NodeData.CDO := ParentNodeCDO.CDO[Node.Index];
// end else if NodeData.CDO is TDomainObjectList then begin
NodeData.CDO := ChildCDO.CDO[Node.Index];
// end;
FNodeList.Add( NodeData.CDO, Node );
end;
end;
end else begin
// CDO is already set when node was added through AddDomainObject.
end;
Node.CheckType := NodeCheckType;
Sender.CheckState[Node] := csUncheckedNormal;
end;
procedure TBaseTreeFrame.Frame_VstInitChildren(Sender: TBaseVirtualTree;
Node: PVirtualNode; var ChildCount: Cardinal);
begin
inherited;
// This is called when user has clicked on a plus sign.
// We only need to tell the tree for how many children to prepare.
{ TODO -cWishList : This could be defaulted to something like
var
NodeData: ^RTreeData;
begin
inherited;
NodeData := Sender.GetNodeData( Node );
ChildCount := 0;
if NodeData.CDO is TCustomDomainObjectList then begin
ChildCount := NodeData.CDO.CDOCount;
end;
}
end;
procedure TBaseTreeFrame.AddDomainObject(ACDO: TCustomDomainObject; AParent:
TCustomDomainObject);
var
Node: PVirtualNode;
NodeData: ^RTreeData;
ParentNode: PVirtualNode;
begin
inherited;
Node := FNodeList.NodeOf( ACDO );
ParentNode := FindParentNode( ACDO, AParent );
if Node = nil then begin
Frame_Vst.BeginUpdate; // Prevent auto sorting
try
if ParentNode = nil then begin // we need the tree's root
ParentNode := Frame_Vst.RootNode;
Frame_Vst.RootNodeCount := Frame_Vst.RootNodeCount + 1;
end else begin
Frame_Vst.ChildCount[ParentNode] := Frame_Vst.ChildCount[ParentNode] + 1;
end;
Node := Frame_Vst.GetLastChild( ParentNode );
finally
Frame_Vst.EndUpdate;
end;
NodeData := Frame_Vst.GetNodeData( Node );
NodeData.CDO := ACDO;
FNodeList.Add( ACDO, Node );
end else begin
// it exists, so nothing to do.
end;
end;
procedure TBaseTreeFrame.Frame_VstCompareNodes(Sender: TBaseVirtualTree;
Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var
Node1Data: ^RTreeData;
Node2Data: ^RTreeData;
Node1CDO: TCustomDomainObject;
Node2CDO: TCustomDomainObject;
begin
inherited;
Node1Data := Frame_Vst.GetNodeData( Node1 );
Node2Data := Frame_Vst.GetNodeData( Node2 );
Node1CDO := Node1Data.CDO;
Node2CDO := Node2Data.CDO;
//
if ( Node1CDO = nil ) or ( Node2CDO = nil ) then begin
Result := 0;
end else if ( Node1CDO is TDomainObjectList ) <> ( Node2CDO is TDomainObjectList ) then begin
if Node1CDO is TDomainObjectList then begin
Result := -1;
end else begin
Result := 1;
end;
end else begin
Result := AnsiCompareText( Node1CDO.SortString, Node2CDO.SortString );
end;
end;
function TBaseTreeFrame.ShowChildrenOfList(AFromCDO: TCustomDomainObject):
TCustomDomainObject;
begin
// Should be abstract?
Result := AFromCDO;
end;
procedure TBaseTreeFrame.FocusOn(ACDO: TCustomDomainObject);
var
FocusOnNode: PVirtualNode;
begin
FocusOnNode := FNodeList.NodeOf( ACDO );
if FocusOnNode <> nil then begin
Frame_Vst.FocusedNode := FocusOnNode;
Frame_Vst.ClearSelection;
Frame_Vst.Selected[FocusOnNode] := true;
end;
end;
function TBaseTreeFrame.FindParentNode(ACDO,
AParent: TCustomDomainObject): PVirtualNode;
begin
Result := FNodeList.NodeOf( AParent );
if Result = nil then begin
if AParent.Container <> nil then begin
Result := FindParentNode( AParent, AParent.Container );
end;
end;
end;
function TBaseTreeFrame.GetTree: TCustomDomainObject;
begin
Result := CDO;
end;
procedure TBaseTreeFrame.CheckAll;
var
Run: PVirtualNode;
begin
Frame_Vst.BeginUpdate;
try
Run := Frame_Vst.GetFirstVisible;
while Assigned( Run ) do begin
Run.CheckState := csCheckedNormal;
Run := Frame_Vst.GetNextVisible( Run );
end;
GetTree.CheckAll;
finally
Frame_Vst.EndUpdate;
end;
end;
procedure TBaseTreeFrame.UncheckAll;
var
Run: PVirtualNode;
begin
Frame_Vst.BeginUpdate;
try
Run := Frame_Vst.GetFirstVisible;
while Assigned( Run ) do begin
Run.CheckState := csUncheckedNormal;
Run := Frame_Vst.GetNextVisible( Run );
end;
GetTree.UncheckAll;
finally
Frame_Vst.EndUpdate;
end;
end;
function TBaseTreeFrame.CheckedCount: integer;
var
Run: PVirtualNode;
begin
Result := 0;
Run := Frame_Vst.GetFirstVisible;
while Assigned( Run ) do begin
if Run.CheckState in [csCheckedNormal, csCheckedPressed] then begin
inc( Result );
end;
Run := Frame_Vst.GetNextVisible( Run );
end;
end;
procedure TBaseTreeFrame.Frame_VstChecked(Sender: TBaseVirtualTree;
Node: PVirtualNode);
var
ACDO: TCustomDomainObject;
NodeData: ^RTreeData;
begin
inherited;
NodeData := Sender.GetNodeData( Node );
ACDO := NodeData.CDO;
if ACDO <> nil then begin
ACDO.Checked := ( Node.CheckState in [csCheckedNormal, csCheckedPressed] );
end;
end;
end.
An example, though it does cater for a tree structure, can be found here:
http://www.bjmsoftware.com/delphistuff/virtualstringtreeexample.zip
It's something I have been playing with to start some new base frames, so there may be stuff in there you do not need. The BaseTree_fr unit contains the VirtualStringTree stuff from an old project. The Tree_fm.pas unit contains my new effort. Tree_fm.pas doesn't yet include dynamically adding new nodes and deleting existing ones, but you can find that in the BaseTree_fr unit.
To keep in the spirit of StackOverflow standing on its own two feet, I am including both units here.
The new effort
unit Tree_fm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, VirtualTrees, StdCtrls, DomainObject, DogBreed, ImgList;
type
RTreeData = record
CDO: TCustomDomainObject;
end;
PTreeData = ^RTreeData;
TForm1 = class(TForm)
VirtualStringTree1: TVirtualStringTree;
Button1: TButton;
Button2: TButton;
Button3: TButton;
Button4: TButton;
Button5: TButton;
HeaderImages: TImageList;
TreeImages: TImageList;
StateImages: TImageList;
procedure VirtualStringTree1Checked(Sender: TBaseVirtualTree; Node:
PVirtualNode);
procedure VirtualStringTree1CompareNodes(Sender: TBaseVirtualTree; Node1,
Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
procedure VirtualStringTree1DblClick(Sender: TObject);
procedure VirtualStringTree1FocusChanged(Sender: TBaseVirtualTree; Node:
PVirtualNode; Column: TColumnIndex);
procedure VirtualStringTree1GetImageIndex(Sender: TBaseVirtualTree; Node:
PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted:
Boolean; var ImageIndex: Integer);
procedure VirtualStringTree1GetText(Sender: TBaseVirtualTree; Node:
PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText:
string);
procedure VirtualStringTree1InitChildren(Sender: TBaseVirtualTree; Node:
PVirtualNode; var ChildCount: Cardinal);
procedure VirtualStringTree1InitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
private
FIsLoading: Boolean;
FCDO: TCustomDomainObject;
protected
procedure BeginLoad;
procedure EndLoad;
procedure ClearFrame;
procedure ClearHeaders;
procedure ShowColumnHeaders;
procedure ShowDomainObject(aCDO, aParent: TCustomDomainObject);
procedure ShowDomainObjects(aCDO, aParent: TCustomDomainObject);
procedure AddColumnHeaders(aColumns: TVirtualTreeColumns); virtual;
function GetColumnText(aCDO: TCustomDomainObject; aColumn: TColumnIndex;
var aCellText: string): Boolean; virtual;
protected
property CDO: TCustomDomainObject read FCDO write FCDO;
public
constructor Create(AOwner: TComponent); override;
procedure Load(aCDO: TCustomDomainObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.AddColumnHeaders(aColumns: TVirtualTreeColumns);
var
Col: TVirtualTreeColumn;
begin
Col := aColumns.Add;
Col.Text := 'Breed(Group)';
Col.Width := 200;
Col := aColumns.Add;
Col.Text := 'Average Age';
Col.Width := 100;
Col.Alignment := taRightJustify;
Col := aColumns.Add;
Col.Text := 'CDO.Count';
Col.Width := 100;
Col.Alignment := taRightJustify;
end;
procedure TForm1.BeginLoad;
begin
FIsLoading := True;
VirtualStringTree1.BeginUpdate;
end;
procedure TForm1.ClearFrame;
begin
VirtualStringTree1.Clear;
// FNodeList.Clear;
// DoSelectionChanged(nil);
end;
procedure TForm1.ClearHeaders;
begin
VirtualStringTree1.Header.Columns.Clear;
end;
constructor TForm1.Create(AOwner: TComponent);
begin
inherited;
VirtualStringTree1.DefaultText := '';
VirtualStringTree1.NodeDataSize := SizeOf(RTreeData);
VirtualStringTree1.Header.Options := VirtualStringTree1.Header.Options
//- []
+ [hoDblClickResize, hoHotTrack, hoShowImages]
;
VirtualStringTree1.Header.Style := hsXPStyle;
VirtualStringTree1.TreeOptions.AnimationOptions := VirtualStringTree1.TreeOptions.AnimationOptions
//- []
//+ []
;
VirtualStringTree1.TreeOptions.AutoOptions := VirtualStringTree1.TreeOptions.AutoOptions
//- []
// toAutoSort is (was once?) causing heavy recursions and InitNode executions!!!
// It isn't now, but it does cause the entire tree to be loaded!
+ [{toAutoSort,}{ toAutoHideButtons}]
;
//VirtualStringTree1.TreeOptions.ExportMode := emChecked;
VirtualStringTree1.TreeOptions.MiscOptions := VirtualStringTree1.TreeOptions.MiscOptions
- [toInitOnSave]
+ [toCheckSupport]
;
VirtualStringTree1.TreeOptions.PaintOptions := VirtualStringTree1.TreeOptions.PaintOptions
- [toShowTreeLines]
+ [toHotTrack, toGhostedIfUnfocused, toUseExplorerTheme]
;
VirtualStringTree1.TreeOptions.SelectionOptions := VirtualStringTree1.TreeOptions.SelectionOptions
//- []
+ [toExtendedFocus, toFullRowSelect, toMultiSelect]
;
VirtualStringTree1.TreeOptions.StringOptions := VirtualStringTree1.TreeOptions.StringOptions
//- []
//+ []
;
VirtualStringTree1.Header.Images := HeaderImages;
VirtualStringTree1.CheckImageKind := ckXP;
VirtualStringTree1.CustomCheckImages := nil;
VirtualStringTree1.Images := TreeImages;
VirtualStringTree1.StateImages := StateImages;
//VirtualStringTree1.ClipboardFormats := ;
//VirtualStringTree1.DragMode := dmAutomatic;
VirtualStringTree1.DragOperations := [];
end;
procedure TForm1.EndLoad;
begin
VirtualStringTree1.EndUpdate;
FIsLoading := False;
end;
function TForm1.GetColumnText(aCDO: TCustomDomainObject; aColumn: TColumnIndex;
var aCellText: string): Boolean;
begin
if Assigned(aCDO) then begin
case aColumn of
-1, 0: begin
aCellText := aCDO.DisplayString;
end;
1: begin
if aCDO.InheritsFrom(TDogBreed) then begin
aCellText := IntToStr(TDogBreed(aCDO).AverageAge);
end;
end;
2: begin
aCellText := IntToStr(aCDO.Count);
end;
else
// aCellText := '';
end;
Result := True;
end else begin
Result := False;
end;
end;
procedure TForm1.Load(aCDO: TCustomDomainObject);
begin
// This would be in a more generic ancestor.
BeginLoad;
try
if Assigned(CDO) then begin
ClearHeaders;
ClearFrame;
end;
CDO := aCDO;
if Assigned(CDO) then begin
ShowColumnHeaders;
ShowDomainObjects(CDO, nil);
end;
finally
EndLoad;
end;
end;
procedure TForm1.ShowColumnHeaders;
begin
AddColumnHeaders(VirtualStringTree1.Header.Columns);
if VirtualStringTree1.Header.Columns.Count > 0 then begin
VirtualStringTree1.Header.Options := VirtualStringTree1.Header.Options
+ [hoVisible];
end;
end;
procedure TForm1.ShowDomainObject(aCDO, aParent: TCustomDomainObject);
begin
// We are dealing with a virtual tree that asks for its data, and so we don't
// need to do anything here.
end;
procedure TForm1.ShowDomainObjects(aCDO, aParent: TCustomDomainObject);
begin
// We are dealing with a virtual tree that asks for its data, and so we only need
// to set the number of nodes under the root.
if Assigned(aCDO) then begin
VirtualStringTree1.RootNodeCount := aCDO.Count;
end else begin
VirtualStringTree1.RootNodeCount := 0;
end;
end;
procedure TForm1.VirtualStringTree1Checked(Sender: TBaseVirtualTree; Node:
PVirtualNode);
begin
(*
procedure TBaseTreeFrame.Frame_VstChecked(Sender: TBaseVirtualTree;
Node: PVirtualNode);
var
ACDO: TCustomDomainObject;
NodeData: ^RTreeData;
begin
inherited;
NodeData := Sender.GetNodeData(Node);
ACDO := NodeData.CDO;
if ACDO <> nil then begin
ACDO.Checked := (Node.CheckState in [csCheckedNormal, csCheckedPressed]);
end;
end;
*)
end;
procedure TForm1.VirtualStringTree1CompareNodes(Sender: TBaseVirtualTree;
Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
begin
beep;
(*
procedure TBaseTreeFrame.Frame_VstCompareNodes(Sender: TBaseVirtualTree;
Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var
Node1Data: ^RTreeData;
Node2Data: ^RTreeData;
Node1CDO: TCustomDomainObject;
Node2CDO: TCustomDomainObject;
begin
inherited;
Node1Data := Frame_Vst.GetNodeData(Node1);
Node2Data := Frame_Vst.GetNodeData(Node2);
Node1CDO := Node1Data.CDO;
Node2CDO := Node2Data.CDO;
//
if (Node1CDO = nil) or (Node2CDO = nil) then begin
Result := 0;
end else if (Node1CDO is TDomainObjectList) <> (Node2CDO is TDomainObjectList) then begin
if Node1CDO is TDomainObjectList then begin
Result := -1;
end else begin
Result := 1;
end;
end else begin
Result := AnsiCompareText(Node1CDO.SortString, Node2CDO.SortString);
end;
end;
*)
end;
procedure TForm1.VirtualStringTree1DblClick(Sender: TObject);
begin
(*
procedure TBaseTreeFrame.Frame_VstDblClick(Sender: TObject);
var
ACDO: TCustomDomainObject;
NodeData: ^RTreeData;
Tree: TBaseVirtualTree;
begin
inherited;
if Sender is TBaseVirtualTree then begin
Tree := TBaseVirtualTree(Sender);
if Tree.FocusedNode <> nil then begin
NodeData := Tree.GetNodeData(Tree.FocusedNode);
ACDO := NodeData.CDO;
HandleDoubleClicked(ACDO);
end;
end;
end;
procedure TBaseTreeFrame.HandleDoubleClicked(ACDO: TCustomDomainObject);
begin
DoDoubleClicked(ACDO);
end;
procedure TBaseTreeFrame.DoDoubleClicked(ACDO: TCustomDomainObject);
begin
if assigned(FOnDoubleClicked) then begin
FOnDoubleClicked(ACDO);
end;
end;
*)
end;
procedure TForm1.VirtualStringTree1FocusChanged(Sender: TBaseVirtualTree; Node:
PVirtualNode; Column: TColumnIndex);
begin
(*
procedure TBaseTreeFrame.Frame_VstFocusChanged(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex);
var
ACDO: TCustomDomainObject;
NodeData: ^RTreeData;
begin
inherited;
NodeData := Sender.GetNodeData(Node);
ACDO := NodeData.CDO;
HandleSelectionChanged(ACDO);
end;
procedure TBaseTreeFrame.HandleSelectionChanged(ACDO: TCustomDomainObject);
begin
DoSelectionChanged(ACDO);
end;
procedure TBaseTreeFrame.DoSelectionChanged(ACDO: TCustomDomainObject);
begin
if assigned(FOnSelectionChanged) then begin
FOnSelectionChanged(ACDO);
end;
end;
*)
end;
procedure TForm1.VirtualStringTree1GetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted:
Boolean; var ImageIndex: Integer);
begin
(*
procedure TBaseTreeFrame.Frame_VstGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
var Ghosted: Boolean; var ImageIndex: Integer);
var
ACDO: TCustomDomainObject;
NodeData: ^RTreeData;
begin
inherited;
Case Column of
-1, 0 : begin
NodeData := Sender.GetNodeData(Node);
ACDO := NodeData.CDO;
case Kind of
ikState: ImageIndex := -1;
ikNormal: ImageIndex := GetImageIndexFor(ACDO, false);
ikSelected: ImageIndex := GetImageIndexFor(ACDO, true);
ikOverlay: ImageIndex := -1;
else
ImageIndex := -1;
end;
end;
else
end;
*)
end;
procedure TForm1.VirtualStringTree1GetText(Sender: TBaseVirtualTree; Node:
PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText:
string);
var
NodeData: ^RTreeData;
begin
NodeData := Sender.GetNodeData(Node);
if GetColumnText(NodeData.CDO, Column, {var}CellText) then
else begin
if Assigned(NodeData.CDO) then begin
case Column of
-1, 0: CellText := NodeData.CDO.DisplayString;
end;
end;
end;
end;
procedure TForm1.VirtualStringTree1InitChildren(Sender: TBaseVirtualTree; Node:
PVirtualNode; var ChildCount: Cardinal);
var
NodeData: ^RTreeData;
begin
// This is called when user has clicked on a plus sign.
// We only need to tell the tree for how many children to prepare.
ChildCount := 0;
NodeData := Sender.GetNodeData(Node);
if Assigned(NodeData.CDO) then begin
ChildCount := NodeData.CDO.Count;
end;
end;
procedure TForm1.VirtualStringTree1InitNode(Sender: TBaseVirtualTree;
ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
ParentNodeData: ^RTreeData;
ParentNodeCDO: TCustomDomainObject;
NodeData: ^RTreeData;
begin
if Assigned(ParentNode) then begin
ParentNodeData := VirtualStringTree1.GetNodeData(ParentNode);
ParentNodeCDO := ParentNodeData.CDO;
end else begin
ParentNodeCDO := CDO;
end;
NodeData := VirtualStringTree1.GetNodeData(Node);
if Assigned(NodeData.CDO) then begin
// CDO was already set, for example when added through AddDomainObject.
end else begin
if Assigned(ParentNodeCDO) then begin
if ParentNodeCDO.Count > Node.Index then begin
NodeData.CDO := ParentNodeCDO.CDO[Node.Index];
if NodeData.CDO.Count > 0 then begin
InitialStates := InitialStates + [ivsHasChildren];
end;
// FNodeList.Add(NodeData.CDO, Node);
end;
end;
end;
Sender.CheckState[Node] := csUncheckedNormal;
end;
end.
And the old one
Check in second answer, I exceeded the text character limit...
I have a number of complex processing tasks that will produce messages, warnings, and fatal errors. I want to be able to display these messages in a task-independent component. My requirements are:
Different kinds of messages are displayed in different font and/or background colors.
The display can be filtered to include or exclude each kind of message.
The display will properly handle long messages by wrapping them and displaying the entire message.
Each message can have a data reference of some kind attached, and the message can be selected as an entity (eg, writing into an RTF memo won't work).
In essence, I'm looking for some kind of listbox like component that supports colors, filtering, and line wrapping. Can anyone suggest such a component (or another one) to use as the basis for my log display?
Failing that, I'll write my own. My initial thought is that I should base the component on a TDBGrid with a built-in TClientDataset. I would add messages to the client dataset (with a column for message type) and handle filtering through data set methods and coloring through the grid's draw methods.
Your thoughts on this design are welcome.
[Note: At this time I'm not particularly interested in writing the log to a file or integrating with Windows logging (unless doing so solves my display problem)]
I've written a log component that does most of what you need and it is based on VitrualTreeView. I've had to alter the code a bit to remove some dependencies, but it compiles fine (although it hasn't been tested after the alterations). Even if it's not exactly what you need, it might give you a good base to get started.
Here's the code
unit UserInterface.VirtualTrees.LogTree;
// Copyright (c) Paul Thornton
interface
uses
Classes, SysUtils, Graphics, Types, Windows, ImgList,
Menus,
VirtualTrees;
type
TLogLevel = (llNone,llError,llInfo,llWarning,llDebug);
TLogLevels = set of TLogLevel;
TLogNodeData = record
LogLevel: TLogLevel;
Timestamp: TDateTime;
LogText: String;
end;
PLogNodeData = ^TLogNodeData;
TOnLog = procedure(Sender: TObject; var LogText: String; var
CancelEntry: Boolean; LogLevel: TLogLevel) of object;
TOnPopupMenuItemClick = procedure(Sender: TObject; MenuItem:
TMenuItem) of object;
TVirtualLogPopupmenu = class(TPopupMenu)
private
FOwner: TComponent;
FOnPopupMenuItemClick: TOnPopupMenuItemClick;
procedure OnMenuItemClick(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
property OnPopupMenuItemClick: TOnPopupMenuItemClick read
FOnPopupMenuItemClick write FOnPopupMenuItemClick;
end;
TVirtualLogTree = class(TVirtualStringTree)
private
FOnLog: TOnLog;
FOnAfterLog: TNotifyEvent;
FHTMLSupport: Boolean;
FAutoScroll: Boolean;
FRemoveControlCharacters: Boolean;
FLogLevels: TLogLevels;
FAutoLogLevelColours: Boolean;
FShowDateColumn: Boolean;
FShowImages: Boolean;
FMaximumLines: Integer;
function DrawHTML(const ARect: TRect; const ACanvas: TCanvas;
const Text: String; Selected: Boolean): Integer;
function GetCellText(const Node: PVirtualNode; const Column:
TColumnIndex): String;
procedure SetLogLevels(const Value: TLogLevels);
procedure UpdateVisibleItems;
procedure OnPopupMenuItemClick(Sender: TObject; MenuItem: TMenuItem);
procedure SetShowDateColumn(const Value: Boolean);
procedure SetShowImages(const Value: Boolean);
procedure AddDefaultColumns(const ColumnNames: array of String;
const ColumnWidths: array of Integer);
function IfThen(Condition: Boolean; TrueResult,
FalseResult: Variant): Variant;
function StripHTMLTags(const Value: string): string;
function RemoveCtrlChars(const Value: String): String;
protected
procedure DoOnLog(var LogText: String; var CancelEntry: Boolean;
LogLevel: TLogLevel); virtual;
procedure DoOnAfterLog; virtual;
procedure DoAfterCellPaint(Canvas: TCanvas; Node: PVirtualNode;
Column: TColumnIndex; CellRect: TRect); override;
procedure DoGetText(Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var Text: String); override;
procedure DoFreeNode(Node: PVirtualNode); override;
function DoGetImageIndex(Node: PVirtualNode; Kind: TVTImageKind;
Column: TColumnIndex; var Ghosted: Boolean; var Index: Integer):
TCustomImageList; override;
procedure DoPaintText(Node: PVirtualNode; const Canvas: TCanvas;
Column: TColumnIndex; TextType: TVSTTextType); override;
procedure Loaded; override;
public
constructor Create(AOwner: TComponent); override;
procedure Log(Value: String; LogLevel: TLogLevel = llInfo;
TimeStamp: TDateTime = 0);
procedure LogFmt(Value: String; const Args: array of Const;
LogLevel: TLogLevel = llInfo; TimeStamp: TDateTime = 0);
procedure SaveToFileWithDialog;
procedure SaveToFile(const Filename: String);
procedure SaveToStrings(const Strings: TStrings);
procedure CopyToClipboard; reintroduce;
published
property OnLog: TOnLog read FOnLog write FOnLog;
property OnAfterLog: TNotifyEvent read FOnAfterLog write FOnAfterLog;
property HTMLSupport: Boolean read FHTMLSupport write FHTMLSupport;
property AutoScroll: Boolean read FAutoScroll write FAutoScroll;
property RemoveControlCharacters: Boolean read
FRemoveControlCharacters write FRemoveControlCharacters;
property LogLevels: TLogLevels read FLogLevels write SetLogLevels;
property AutoLogLevelColours: Boolean read FAutoLogLevelColours
write FAutoLogLevelColours;
property ShowDateColumn: Boolean read FShowDateColumn write
SetShowDateColumn;
property ShowImages: Boolean read FShowImages write SetShowImages;
property MaximumLines: Integer read FMaximumLines write FMaximumLines;
end;
implementation
uses
Dialogs,
Clipbrd;
resourcestring
StrSaveLog = '&Save';
StrCopyToClipboard = '&Copy';
StrTextFilesTxt = 'Text files (*.txt)|*.txt|All files (*.*)|*.*';
StrSave = 'Save';
StrDate = 'Date';
StrLog = 'Log';
constructor TVirtualLogTree.Create(AOwner: TComponent);
begin
inherited;
FAutoScroll := TRUE;
FHTMLSupport := TRUE;
FRemoveControlCharacters := TRUE;
FShowDateColumn := TRUE;
FShowImages := TRUE;
FLogLevels := [llError, llInfo, llWarning, llDebug];
NodeDataSize := SizeOf(TLogNodeData);
end;
procedure TVirtualLogTree.DoAfterCellPaint(Canvas: TCanvas; Node: PVirtualNode;
Column: TColumnIndex; CellRect: TRect);
var
ColWidth: Integer;
begin
inherited;
if Column = 1 then
begin
if FHTMLSupport then
ColWidth := DrawHTML(CellRect, Canvas, GetCellText(Node,
Column), Selected[Node])
else
ColWidth := Canvas.TextWidth(GetCellText(Node, Column));
if not FShowDateColumn then
ColWidth := ColWidth + 32; // Width of image
if ColWidth > Header.Columns[1].MinWidth then
Header.Columns[1].MinWidth := ColWidth;
end;
end;
procedure TVirtualLogTree.DoFreeNode(Node: PVirtualNode);
var
NodeData: PLogNodeData;
begin
inherited;
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
NodeData.LogText := '';
end;
function TVirtualLogTree.DoGetImageIndex(Node: PVirtualNode; Kind: TVTImageKind;
Column: TColumnIndex; var Ghosted: Boolean;
var Index: Integer): TCustomImageList;
var
NodeData: PLogNodeData;
begin
Images.Count;
if ((FShowImages) and (Kind in [ikNormal, ikSelected])) and
(((FShowDateColumn) and (Column <= 0)) or
((not FShowDateColumn) and (Column = 1))) then
begin
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
case NodeData.LogLevel of
llError: Index := 3;
llInfo: Index := 2;
llWarning: Index := 1;
llDebug: Index := 0;
else
Index := 4;
end;
end;
Result := inherited DoGetImageIndex(Node, Kind, Column, Ghosted, Index);
end;
procedure TVirtualLogTree.DoGetText(Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var Text: String);
begin
inherited;
if (TextType = ttNormal) and ((Column <= 0) or (not FHTMLSupport)) then
Text := GetCellText(Node, Column)
else
Text := '';
end;
procedure TVirtualLogTree.DoOnAfterLog;
begin
if Assigned(FOnAfterLog) then
FOnAfterLog(Self);
end;
procedure TVirtualLogTree.DoOnLog(var LogText: String; var
CancelEntry: Boolean; LogLevel: TLogLevel);
begin
if Assigned(FOnLog) then
FOnLog(Self, LogText, CancelEntry, LogLevel);
end;
procedure TVirtualLogTree.DoPaintText(Node: PVirtualNode; const Canvas: TCanvas;
Column: TColumnIndex; TextType: TVSTTextType);
begin
inherited;
Canvas.Font.Color := clBlack;
end;
function TVirtualLogTree.GetCellText(const Node: PVirtualNode; const
Column: TColumnIndex): String;
var
NodeData: PLogNodeData;
begin
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
case Column of
-1, 0: Result := concat(DateTimeToStr(NodeData.Timestamp), '.',
FormatDateTime('zzz', NodeData.Timestamp));
1: Result := NodeData.LogText;
end;
end;
procedure TVirtualLogTree.AddDefaultColumns(
const ColumnNames: array of String; const ColumnWidths: array of Integer);
var
i: Integer;
Column: TVirtualTreeColumn;
begin
Header.Columns.Clear;
if High(ColumnNames) <> high(ColumnWidths) then
raise Exception.Create('Number of column names must match the
number of column widths.') // Do not localise
else
begin
for i := low(ColumnNames) to high(ColumnNames) do
begin
Column := Header.Columns.Add;
Column.Text := ColumnNames[i];
if ColumnWidths[i] > 0 then
Column.Width := ColumnWidths[i]
else
begin
Header.AutoSizeIndex := Column.Index;
Header.Options := Header.Options + [hoAutoResize];
end;
end;
end;
end;
procedure TVirtualLogTree.Loaded;
begin
inherited;
TreeOptions.PaintOptions := TreeOptions.PaintOptions - [toShowRoot,
toShowTreeLines, toShowButtons] + [toUseBlendedSelection,
toShowHorzGridLines, toHideFocusRect];
TreeOptions.SelectionOptions := TreeOptions.SelectionOptions +
[toFullRowSelect, toRightClickSelect];
AddDefaultColumns([StrDate,
StrLog],
[170,
120]);
Header.AutoSizeIndex := 1;
Header.Columns[1].MinWidth := 300;
Header.Options := Header.Options + [hoAutoResize];
if (PopupMenu = nil) and (not (csDesigning in ComponentState)) then
begin
PopupMenu := TVirtualLogPopupmenu.Create(Self);
TVirtualLogPopupmenu(PopupMenu).OnPopupMenuItemClick :=
OnPopupMenuItemClick;
end;
SetShowDateColumn(FShowDateColumn);
end;
procedure TVirtualLogTree.OnPopupMenuItemClick(Sender: TObject;
MenuItem: TMenuItem);
begin
if MenuItem.Tag = 1 then
SaveToFileWithDialog
else
if MenuItem.Tag = 2 then
CopyToClipboard;
end;
procedure TVirtualLogTree.SaveToFileWithDialog;
var
SaveDialog: TSaveDialog;
begin
SaveDialog := TSaveDialog.Create(Self);
try
SaveDialog.DefaultExt := '.txt';
SaveDialog.Title := StrSave;
SaveDialog.Options := SaveDialog.Options + [ofOverwritePrompt];
SaveDialog.Filter := StrTextFilesTxt;
if SaveDialog.Execute then
SaveToFile(SaveDialog.Filename);
finally
FreeAndNil(SaveDialog);
end;
end;
procedure TVirtualLogTree.SaveToFile(const Filename: String);
var
SaveStrings: TStringList;
begin
SaveStrings := TStringList.Create;
try
SaveToStrings(SaveStrings);
SaveStrings.SaveToFile(Filename);
finally
FreeAndNil(SaveStrings);
end;
end;
procedure TVirtualLogTree.CopyToClipboard;
var
CopyStrings: TStringList;
begin
CopyStrings := TStringList.Create;
try
SaveToStrings(CopyStrings);
Clipboard.AsText := CopyStrings.Text;
finally
FreeAndNil(CopyStrings);
end;
end;
function TVirtualLogTree.IfThen(Condition: Boolean; TrueResult,
FalseResult: Variant): Variant;
begin
if Condition then
Result := TrueResult
else
Result := FalseResult;
end;
function TVirtualLogTree.StripHTMLTags(const Value: string): string;
var
TagBegin, TagEnd, TagLength: integer;
begin
Result := Value;
TagBegin := Pos( '<', Result); // search position of first <
while (TagBegin > 0) do
begin
TagEnd := Pos('>', Result);
TagLength := TagEnd - TagBegin + 1;
Delete(Result, TagBegin, TagLength);
TagBegin:= Pos( '<', Result);
end;
end;
procedure TVirtualLogTree.SaveToStrings(const Strings: TStrings);
var
Node: PVirtualNode;
begin
Node := GetFirst;
while Assigned(Node) do
begin
Strings.Add(concat(IfThen(FShowDateColumn,
concat(GetCellText(Node, 0), #09), ''), IfThen(FHTMLSupport,
StripHTMLTags(GetCellText(Node, 1)), GetCellText(Node, 1))));
Node := Node.NextSibling;
end;
end;
function TVirtualLogTree.RemoveCtrlChars(const Value: String): String;
var
i: Integer;
begin
// Replace CTRL characters with <whitespace>
Result := '';
for i := 1 to length(Value) do
if (AnsiChar(Value[i]) in [#0..#31, #127]) then
Result := Result + ' '
else
Result := Result + Value[i];
end;
procedure TVirtualLogTree.Log(Value: String; LogLevel: TLogLevel;
TimeStamp: TDateTime);
var
CancelEntry: Boolean;
Node: PVirtualNode;
NodeData: PLogNodeData;
DoScroll: Boolean;
begin
CancelEntry := FALSE;
DoOnLog(Value, CancelEntry, LogLevel);
if not CancelEntry then
begin
DoScroll := ((not Focused) or (GetLast = FocusedNode)) and (FAutoScroll);
Node := AddChild(nil);
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
begin
NodeData.LogLevel := LogLevel;
if TimeStamp = 0 then
NodeData.Timestamp := now
else
NodeData.Timestamp := TimeStamp;
if FRemoveControlCharacters then
Value := RemoveCtrlChars(Value);
if FAutoLogLevelColours then
case LogLevel of
llError: Value := concat('<font-color=clRed>', Value,
'</font-color>');
llInfo: Value := concat('<font-color=clBlack>', Value,
'</font-color>');
llWarning: Value := concat('<font-color=clBlue>', Value,
'</font-color>');
llDebug: Value := concat('<font-color=clGreen>', Value,
'</font-color>')
end;
NodeData.LogText := Value;
IsVisible[Node] := NodeData.LogLevel in FLogLevels;
DoOnAfterLog;
end;
if FMaximumLines <> 0 then
while RootNodeCount > FMaximumLines do
DeleteNode(GetFirst);
if DoScroll then
begin
//SelectNodeEx(GetLast);
ScrollIntoView(GetLast, FALSE);
end;
end;
end;
procedure TVirtualLogTree.LogFmt(Value: String; const Args: Array of
Const; LogLevel: TLogLevel; TimeStamp: TDateTime);
begin
Log(format(Value, Args), LogLevel, TimeStamp);
end;
procedure TVirtualLogTree.SetLogLevels(const Value: TLogLevels);
begin
FLogLevels := Value;
UpdateVisibleItems;
end;
procedure TVirtualLogTree.SetShowDateColumn(const Value: Boolean);
begin
FShowDateColumn := Value;
if Header.Columns.Count > 0 then
begin
if FShowDateColumn then
Header.Columns[0].Options := Header.Columns[0].Options + [coVisible]
else
Header.Columns[0].Options := Header.Columns[0].Options - [coVisible]
end;
end;
procedure TVirtualLogTree.SetShowImages(const Value: Boolean);
begin
FShowImages := Value;
Invalidate;
end;
procedure TVirtualLogTree.UpdateVisibleItems;
var
Node: PVirtualNode;
NodeData: PLogNodeData;
begin
BeginUpdate;
try
Node := GetFirst;
while Assigned(Node) do
begin
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
IsVisible[Node] := NodeData.LogLevel in FLogLevels;
Node := Node.NextSibling;
end;
Invalidate;
finally
EndUpdate;
end;
end;
function TVirtualLogTree.DrawHTML(const ARect: TRect; const ACanvas:
TCanvas; const Text: String; Selected: Boolean): Integer;
(*DrawHTML - Draws text on a canvas using tags based on a simple
subset of HTML/CSS
<B> - Bold e.g. <B>This is bold</B>
<I> - Italic e.g. <I>This is italic</I>
<U> - Underline e.g. <U>This is underlined</U>
<font-color=x> Font colour e.g.
<font-color=clRed>Delphi red</font-color>
<font-color=#FFFFFF>Web white</font-color>
<font-color=$000000>Hex black</font-color>
<font-size=x> Font size e.g. <font-size=30>This is some big text</font-size>
<font-family> Font family e.g. <font-family=Arial>This is
arial</font-family>*)
function CloseTag(const ATag: String): String;
begin
Result := concat('/', ATag);
end;
function GetTagValue(const ATag: String): String;
var
p: Integer;
begin
p := pos('=', ATag);
if p = 0 then
Result := ''
else
Result := copy(ATag, p + 1, MaxInt);
end;
function ColorCodeToColor(const Value: String): TColor;
var
HexValue: String;
begin
Result := 0;
if Value <> '' then
begin
if (length(Value) >= 2) and (copy(Uppercase(Value), 1, 2) = 'CL') then
begin
// Delphi colour
Result := StringToColor(Value);
end else
if Value[1] = '#' then
begin
// Web colour
HexValue := copy(Value, 2, 6);
Result := RGB(StrToInt('$'+Copy(HexValue, 1, 2)),
StrToInt('$'+Copy(HexValue, 3, 2)),
StrToInt('$'+Copy(HexValue, 5, 2)));
end
else
// Hex or decimal colour
Result := StrToIntDef(Value, 0);
end;
end;
const
TagBold = 'B';
TagItalic = 'I';
TagUnderline = 'U';
TagBreak = 'BR';
TagFontSize = 'FONT-SIZE';
TagFontFamily = 'FONT-FAMILY';
TagFontColour = 'FONT-COLOR';
TagColour = 'COLOUR';
var
x, y, idx, CharWidth, MaxCharHeight: Integer;
CurrChar: Char;
Tag, TagValue: String;
PreviousFontColour: TColor;
PreviousFontFamily: String;
PreviousFontSize: Integer;
PreviousColour: TColor;
begin
ACanvas.Font.Size := Canvas.Font.Size;
ACanvas.Font.Name := Canvas.Font.Name;
//if Selected and Focused then
// ACanvas.Font.Color := clWhite
//else
ACanvas.Font.Color := Canvas.Font.Color;
ACanvas.Font.Style := Canvas.Font.Style;
PreviousFontColour := ACanvas.Font.Color;
PreviousFontFamily := ACanvas.Font.Name;
PreviousFontSize := ACanvas.Font.Size;
PreviousColour := ACanvas.Brush.Color;
x := ARect.Left;
y := ARect.Top + 1;
idx := 1;
MaxCharHeight := ACanvas.TextHeight('Ag');
While idx <= length(Text) do
begin
CurrChar := Text[idx];
// Is this a tag?
if CurrChar = '<' then
begin
Tag := '';
inc(idx);
// Find the end of then tag
while (Text[idx] <> '>') and (idx <= length(Text)) do
begin
Tag := concat(Tag, UpperCase(Text[idx]));
inc(idx);
end;
///////////////////////////////////////////////////
// Simple tags
///////////////////////////////////////////////////
if Tag = TagBold then
ACanvas.Font.Style := ACanvas.Font.Style + [fsBold] else
if Tag = TagItalic then
ACanvas.Font.Style := ACanvas.Font.Style + [fsItalic] else
if Tag = TagUnderline then
ACanvas.Font.Style := ACanvas.Font.Style + [fsUnderline] else
if Tag = TagBreak then
begin
x := ARect.Left;
inc(y, MaxCharHeight);
end else
///////////////////////////////////////////////////
// Closing tags
///////////////////////////////////////////////////
if Tag = CloseTag(TagBold) then
ACanvas.Font.Style := ACanvas.Font.Style - [fsBold] else
if Tag = CloseTag(TagItalic) then
ACanvas.Font.Style := ACanvas.Font.Style - [fsItalic] else
if Tag = CloseTag(TagUnderline) then
ACanvas.Font.Style := ACanvas.Font.Style - [fsUnderline] else
if Tag = CloseTag(TagFontSize) then
ACanvas.Font.Size := PreviousFontSize else
if Tag = CloseTag(TagFontFamily) then
ACanvas.Font.Name := PreviousFontFamily else
if Tag = CloseTag(TagFontColour) then
ACanvas.Font.Color := PreviousFontColour else
if Tag = CloseTag(TagColour) then
ACanvas.Brush.Color := PreviousColour else
///////////////////////////////////////////////////
// Tags with values
///////////////////////////////////////////////////
begin
// Get the tag value (everything after '=')
TagValue := GetTagValue(Tag);
if TagValue <> '' then
begin
// Remove the value from the tag
Tag := copy(Tag, 1, pos('=', Tag) - 1);
if Tag = TagFontSize then
begin
PreviousFontSize := ACanvas.Font.Size;
ACanvas.Font.Size := StrToIntDef(TagValue, ACanvas.Font.Size);
end else
if Tag = TagFontFamily then
begin
PreviousFontFamily := ACanvas.Font.Name;
ACanvas.Font.Name := TagValue;
end;
if Tag = TagFontColour then
begin
PreviousFontColour := ACanvas.Font.Color;
try
ACanvas.Font.Color := ColorCodeToColor(TagValue);
except
//Just in case the canvas colour is invalid
end;
end else
if Tag = TagColour then
begin
PreviousColour := ACanvas.Brush.Color;
try
ACanvas.Brush.Color := ColorCodeToColor(TagValue);
except
//Just in case the canvas colour is invalid
end;
end;
end;
end;
end
else
// Draw the character if it's not a ctrl char
if CurrChar >= #32 then
begin
CharWidth := ACanvas.TextWidth(CurrChar);
if y + MaxCharHeight < ARect.Bottom then
begin
ACanvas.Brush.Style := bsClear;
ACanvas.TextOut(x, y, CurrChar);
end;
x := x + CharWidth;
end;
inc(idx);
end;
Result := x - ARect.Left;
end;
{ TVirtualLogPopupmenu }
constructor TVirtualLogPopupmenu.Create(AOwner: TComponent);
function AddMenuItem(const ACaption: String; ATag: Integer): TMenuItem;
begin
Result := TMenuItem.Create(Self);
Result.Caption := ACaption;
Result.Tag := ATag;
Result.OnClick := OnMenuItemClick;
Items.Add(Result);
end;
begin
inherited Create(AOwner);
FOwner := AOwner;
AddMenuItem(StrSaveLog, 1);
AddMenuItem('-', -1);
AddMenuItem(StrCopyToClipboard, 2);
end;
procedure TVirtualLogPopupmenu.OnMenuItemClick(Sender: TObject);
begin
if Assigned(FOnPopupMenuItemClick) then
FOnPopupMenuItemClick(Self, TMenuItem(Sender));
end;
end.
If you add any additional features, maybe you could post them here.
I always like to use the VirtualTreeView by Mike Lischke for such a task. Its highly flexible and quite complex, but when you have understood how it works you can nearly acomplish any list or tree visualisation task with it.
I already did something similar with it, but did not encapsulate it in a component at that time.