dragNdrop all columns between two VirtualTreeView - delphi

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.

Related

TvirtualStringTree custom color for nodes levels ( column and row )

I have TVirtualStringTree and I want to change its nodes background color based on the nodes levels as this picture :
I have tried like this but its giving my desired result for the rows only:
procedure TForm1.VSTBeforeCellPaint(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
CellPaintMode: TVTCellPaintMode; CellRect: TRect; var ContentRect: TRect);
var
R: TRect;
begin
if CellPaintMode = cpmPaint then
begin
R := Sender.GetDisplayRect(Node, Column, True, False, True);
R.Offset(0, -R.Top);
case Sender.GetNodeLevel(Node) of
0: TargetCanvas.Brush.Color := $0000F9FF;
1: TargetCanvas.Brush.Color := $0000BFFF;
2: TargetCanvas.Brush.Color := $000086FF;
end;
TargetCanvas.FillRect(CellRect);
end;
end;
Not sure if its totally correct but its giving good result :
procedure VSTBeforeCellPaint(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
CellPaintMode: TVTCellPaintMode; CellRect: TRect; var ContentRect: TRect);
var
R: TRect;
RectArr:Array of TRect;
NodeLevel,i,k:Integer;
ArrayColors:Array[0..6] of Tcolor ;
begin
NodeLevel:=Sender.GetNodeLevel(Node);
if CellPaintMode = cpmPaint then
begin
if Column=0 then
begin
R := CellRect;
setlength(RectArr,NodeLevel+2);
RectArr[Length(RectArr)-1]:=Rect(r.Left+node.NodeHeight*(NodeLevel+1)-Node.NodeHeight,r.Top,r.Right,r.Bottom);
for i := 0 to Length(RectArr)-2 do
RectArr[i]:=Rect(r.Left+Node.NodeHeight*(i-1),r.Top,r.Left+Node.NodeHeight*i+Node.NodeHeight,r.Bottom);
ArrayColors[0]:= $0000F9FF;
ArrayColors[1] := $0000BFFF;
ArrayColors[2]:= $000086FF;
ArrayColors[3] := ClLime;
ArrayColors[4] := Clred;
ArrayColors[5] := clSkyBlue;
ArrayColors[6]:= clGrayText;
for I := 0 to Length(RectArr)-1 do
begin
K:=i-1;
if k<0 then k:=0;
TargetCanvas.Brush.Color:=ArrayColors[k];
TargetCanvas.FillRect(RectArr[i]);
end;
end else
begin
case Sender.GetNodeLevel(Node) of
0: TargetCanvas.Brush.Color := $0000F9FF;
1: TargetCanvas.Brush.Color := $0000BFFF;
2: TargetCanvas.Brush.Color := $000086FF;
3:TargetCanvas.Brush.Color := ClLime;
4:TargetCanvas.Brush.Color := Clred;
5:TargetCanvas.Brush.Color := clSkyBlue ;
6:TargetCanvas.Brush.Color := clGrayText ;
end;
TargetCanvas.FillRect(CellRect);
end;
end;
end;
The Result :

VirtualTreeView embedding button in the cells

I'm trying to create node with TButton.
I Create the node and the buttons linked to the nodes.
On the event TVirtualStringTree.AfterCellPaint, I initialise the BoundsRect on the button. But the button is always shown in the first node.
Have you some idea of the problem?
type
TNodeData = record
TextValue: string;
Button: TButton;
end;
PNodeData = ^TNodeData;
procedure TForm1.FormCreate(Sender: TObject);
procedure AddButton(__Node: PVirtualNode);
var
NodeData: PNodeData;
begin
NodeData := VirtualStringTree1.GetNodeData(__Node);
NodeData.Button := TButton.Create(nil);
with NodeData.Button do
begin
Parent := VirtualStringTree1;
Height := VirtualStringTree1.DefaultNodeHeight;
Caption := '+';
Visible := false;
end;
end;
procedure InitializeNodeData(__Node: PVirtualNode; __Text: string);
var
NodeData: PNodeData;
begin
NodeData := VirtualStringTree1.GetNodeData(__Node);
NodeData.TextValue := __Text;
end;
var
Node: PVirtualNode;
begin
VirtualStringTree1.NodeDataSize := SizeOf(TNodeData);
Node := VirtualStringTree1.AddChild(nil);
InitializeNodeData(Node, 'a');
Node := VirtualStringTree1.AddChild(Node);
InitializeNodeData(Node, 'a.1');
Node := VirtualStringTree1.AddChild(nil);
InitializeNodeData(Node, 'b');
Node := VirtualStringTree1.AddChild(Node);
InitializeNodeData(Node, 'Here the button');
AddButton(Node);
end;
procedure TForm1.VirtualStringTree1AfterCellPaint(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; CellRect: TRect);
var
NodeData: PNodeData;
begin
if (Column = 0) then
Exit;
NodeData := VirtualStringTree1.GetNodeData(Node);
if (Assigned(NodeData)) and (Assigned(NodeData.Button)) then
begin
with NodeData.Button Do
begin
Visible := (vsVisible in Node.States)
and ((Node.Parent = VirtualStringTree1.RootNode) or (vsExpanded in Node.Parent.States));
BoundsRect := CellRect;
end;
end;
end;
So the problem with iamjoosy's answer is - even though it works - that as soon as you scroll through this Tree with the drawn buttons/images/whatever, the ones that are supposed to leave the Tree again are still existing, being painted at the lowest/highest location where you left them off. Depending on the amount you just scrolled, it leaves a smaller or larger clutter of buttons in that column. AfterCellPaint doesn't move them anymore, since the cells of that now invisble Node below the bottom/above the top are not painted anymore.
What you can do is traverse all tree nodes (probably very expensive if you have a lot of nodes) and check if they are actually in the visible area of the tree and hide the panels (you might need your buttons inside panels to be painted on top of the tree instead of behind) with your buttons/whatevers accordingly:
procedure TMyTree.MyTreeAfterCellPaint(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
CellRect: TRect);
var
InitialIndex: Integer;
// onInitNode I AddOrSetValue a "DataIndexList" TDictionary<PVirtualNode, LongInt>
// to preserve an original index "InitialIndex" (violating the virtual paradigm),
// because I need it for something else anyways
Data: PMyData;
ANode: PVirtualNode;
begin
if Node <> nil then
begin
if Column = 2 then
begin
ANode := MyTree.GetFirst;
while Assigned(ANode) do
begin
DataIndexList.TryGetValue(ANode, InitialIndex);
if not ( CheckVisibility(Sender.GetDisplayRect(ANode, Column, False)) ) then
begin
MyBtnArray[InitialIndex].Visible := False;
MyPanelArray[InitialIndex].Visible := False;
end
else
begin
MyBtnArray[InitialIndex].Visible := True;
MyPanelArray[InitialIndex].Visible := True;
end;
ANode := MyTree.GetNext(ANode);
end;
DataIndexList.TryGetValue(Node, InitialIndex);
Data := MyTree.GetNodeData(Node);
MyPanelArray[InitialIndex].BoundsRect := Sender.GetDisplayRect(Node, Column, False);
end;
end;
end;
function TMyTree.CheckVisibility(R: TRect): Boolean;
begin
// in my case these checks are the way to go, because
// MyTree is touching the top border of the TForm. You will have
// to adjust accordingly if your placement is different
if (R.Bottom < MyTree.Top) or (R.Bottom > MyTree.Top + MyTree.Height) then
Result := False
else
Result := True;
end;
Needless to say that you can do the traversing with visibilityCheck inside many other OnEvents successfully. It doesn't have to be in AfterCellPaint; maybe another event might be a lot better performance wise.
To create RunTime copies of your one original Panel+Button, to place inside your ButtonArray or whichever structure you're using, you will have to copy their RTTI as well. This procedure is taken from http://www.blong.com/Conferences/BorConUK98/DelphiRTTI/CB140.zip (further RTTI information at http://www.blong.com/Conferences/BorConUK98/DelphiRTTI/CB140.htm) and "uses TypInfo":
procedure CopyObject(ObjFrom, ObjTo: TObject);
var
PropInfos: PPropList;
PropInfo: PPropInfo;
Count, Loop: Integer;
OrdVal: Longint;
StrVal: String;
FloatVal: Extended;
MethodVal: TMethod;
begin
{ Iterate thru all published fields and properties of source }
{ copying them to target }
{ Find out how many properties we'll be considering }
Count := GetPropList(ObjFrom.ClassInfo, tkAny, nil);
{ Allocate memory to hold their RTTI data }
GetMem(PropInfos, Count * SizeOf(PPropInfo));
try
{ Get hold of the property list in our new buffer }
GetPropList(ObjFrom.ClassInfo, tkAny, PropInfos);
{ Loop through all the selected properties }
for Loop := 0 to Count - 1 do
begin
PropInfo := GetPropInfo(ObjTo.ClassInfo, PropInfos^[Loop]^.Name);
{ Check the general type of the property }
{ and read/write it in an appropriate way }
case PropInfos^[Loop]^.PropType^.Kind of
tkInteger, tkChar, tkEnumeration,
tkSet, tkClass{$ifdef Win32}, tkWChar{$endif}:
begin
OrdVal := GetOrdProp(ObjFrom, PropInfos^[Loop]);
if Assigned(PropInfo) then
SetOrdProp(ObjTo, PropInfo, OrdVal);
end;
tkFloat:
begin
FloatVal := GetFloatProp(ObjFrom, PropInfos^[Loop]);
if Assigned(PropInfo) then
SetFloatProp(ObjTo, PropInfo, FloatVal);
end;
{$ifndef DelphiLessThan3}
tkWString,
{$endif}
{$ifdef Win32}
tkLString,
{$endif}
tkString:
begin
{ Avoid copying 'Name' - components must have unique names }
if UpperCase(PropInfos^[Loop]^.Name) = 'NAME' then
Continue;
StrVal := GetStrProp(ObjFrom, PropInfos^[Loop]);
if Assigned(PropInfo) then
SetStrProp(ObjTo, PropInfo, StrVal);
end;
tkMethod:
begin
MethodVal := GetMethodProp(ObjFrom, PropInfos^[Loop]);
if Assigned(PropInfo) then
SetMethodProp(ObjTo, PropInfo, MethodVal);
end
end
end
finally
FreeMem(PropInfos, Count * SizeOf(PPropInfo));
end;
end;
Seeing this old answer of mine later, I now have a different solution running for the VisibilityCheck, which is a lot more reliable and easier:
function TFoo.IsNodeVisibleInClientRect(Node: PVirtualNode; Column: TColumnIndex = NoColumn): Boolean;
begin
Result := VST.IsVisible[Node] and
VST.GetDisplayRect(Node, Column, False).IntersectsWith(VST.ClientRect);
end;
I wrote a small program to create any control for a node. I found out that the best place to set the nodes control visibility it in OnAfterPaint event. The scrolling works as intended and there is almost zero flickering.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, VirtualTrees, StdCtrls, Buttons, ExtCtrls;
type
TForm1 = class(TForm)
VirtualStringTree1: TVirtualStringTree;
procedure FormCreate(Sender: TObject);
procedure VirtualStringTree1GetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: WideString);
procedure VirtualStringTree1AfterPaint(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas);
procedure VirtualStringTree1MeasureItem(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; var NodeHeight: Integer);
private
procedure SetNodesControlVisibleProc(Sender: TBaseVirtualTree; Node: PVirtualNode; Data: Pointer; var Abort: Boolean);
procedure SetNodeControlVisible(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex = NoColumn);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
type
TNodeData = record
Text: WideString;
Control: TControl;
end;
PNodeData = ^TNodeData;
{ Utility }
function IsNodeVisibleInClientRect(Tree: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex = NoColumn): Boolean;
var
OutRect: TRect;
begin
Result := Tree.IsVisible[Node] and
Windows.IntersectRect(OutRect, Tree.GetDisplayRect(Node, Column, False), Tree.ClientRect);
end;
type
TControlClass = class of TControl;
TMyPanel = class(TPanel)
public
CheckBox: TCheckBox;
end;
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
function CreateNodeControl(Tree: TVirtualStringTree; Node: PVirtualNode; ControlClass: TControlClass): TControl;
var
NodeData: PNodeData;
begin
NodeData := Tree.GetNodeData(Node);
NodeData.Control := ControlClass.Create(nil);
with NodeData.Control do
begin
Parent := Tree; // Parent will destroy the control
Height := Tree.DefaultNodeHeight;
Visible := False;
end;
Tree.IsDisabled[Node] := True;
Result := NodeData.Control;
end;
procedure InitializeNodeData(Node: PVirtualNode; const Text: WideString);
var
NodeData: PNodeData;
begin
NodeData := VirtualStringTree1.GetNodeData(Node);
Initialize(NodeData^);
NodeData.Text := Text;
end;
var
Node: PVirtualNode;
MyPanel: TMyPanel;
I: integer;
begin
VirtualStringTree1.NodeDataSize := SizeOf(TNodeData);
// trigger MeasureItem
VirtualStringTree1.TreeOptions.MiscOptions := VirtualStringTree1.TreeOptions.MiscOptions + [toVariableNodeHeight];
// Populate some nodes
for I := 1 to 5 do begin
Node := VirtualStringTree1.AddChild(nil);
InitializeNodeData(Node, Format('%d', [I]));
Node := VirtualStringTree1.AddChild(Node);
InitializeNodeData(Node, Format('%d.1', [I]));
end;
Node := VirtualStringTree1.AddChild(nil);
InitializeNodeData(Node, '[TSpeedButton Parent]');
Node := VirtualStringTree1.AddChild(Node);
InitializeNodeData(Node, 'TSpeedButton');
TSpeedButton(CreateNodeControl(VirtualStringTree1, Node, TSpeedButton)).Caption := '+';
Node := VirtualStringTree1.AddChild(nil);
InitializeNodeData(Node, '[TEdit Parent]');
Node := VirtualStringTree1.AddChild(Node);
InitializeNodeData(Node, 'TEdit');
TEdit(CreateNodeControl(VirtualStringTree1, Node, TEdit)).Text := 'Hello';
Node := VirtualStringTree1.AddChild(nil);
InitializeNodeData(Node, '[TMyPanel Parent]');
Node := VirtualStringTree1.AddChild(Node);
InitializeNodeData(Node, 'TMyPanel');
MyPanel := TMyPanel(CreateNodeControl(VirtualStringTree1, Node, TMyPanel));
with MyPanel do
begin
Caption := 'TMyPanel';
ParentBackground := False;
CheckBox := TCheckBox.Create(nil);
CheckBox.Caption := 'CheckBox';
CheckBox.Left := 10;
CheckBox.Top := 10;
CheckBox.Parent := MyPanel;
end;
for I := 6 to 10 do begin
Node := VirtualStringTree1.AddChild(nil);
InitializeNodeData(Node, Format('%d', [I]));
Node := VirtualStringTree1.AddChild(Node);
InitializeNodeData(Node, Format('%d.1', [I]));
end;
end;
procedure TForm1.VirtualStringTree1GetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: WideString);
var
NodeData: PNodeData;
begin
NodeData := Sender.GetNodeData(Node);
if Assigned(NodeData) then
CellText := NodeData.Text;
end;
procedure TForm1.SetNodeControlVisible(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex = NoColumn);
var
NodeData: PNodeData;
R: TRect;
begin
NodeData := Tree.GetNodeData(Node);
if Assigned(NodeData) and Assigned(NodeData.Control) then
begin
with NodeData.Control do
begin
Visible := IsNodeVisibleInClientRect(Tree, Node, Column)
and ((Node.Parent = Tree.RootNode) or (vsExpanded in Node.Parent.States));
R := Tree.GetDisplayRect(Node, Column, False);
BoundsRect := R;
end;
end;
end;
procedure TForm1.SetNodesControlVisibleProc(Sender: TBaseVirtualTree; Node: PVirtualNode; Data: Pointer; var Abort: Boolean);
begin
SetNodeControlVisible(Sender, Node);
end;
procedure TForm1.VirtualStringTree1AfterPaint(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas);
begin
// Iterate all Tree nodes and set visibility
Sender.IterateSubtree(nil, SetNodesControlVisibleProc, nil);
end;
procedure TForm1.VirtualStringTree1MeasureItem(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; var NodeHeight: Integer);
var
NodeData: PNodeData;
begin
NodeData := Sender.GetNodeData(Node);
if Assigned(NodeData) and Assigned(NodeData.Control) then
// set node special height if control is TMyPanel
if NodeData.Control is TMyPanel then
NodeHeight := 50;
end;
end.
DFM:
object Form1: TForm1
Left = 192
Top = 124
Width = 782
Height = 365
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
DesignSize = (
766
327)
PixelsPerInch = 96
TextHeight = 13
object VirtualStringTree1: TVirtualStringTree
Left = 8
Top = 8
Width = 450
Height = 277
Anchors = [akLeft, akTop, akRight, akBottom]
Header.AutoSizeIndex = 0
Header.Font.Charset = DEFAULT_CHARSET
Header.Font.Color = clWindowText
Header.Font.Height = -11
Header.Font.Name = 'MS Sans Serif'
Header.Font.Style = []
Header.MainColumn = -1
TabOrder = 0
OnAfterPaint = VirtualStringTree1AfterPaint
OnGetText = VirtualStringTree1GetText
OnMeasureItem = VirtualStringTree1MeasureItem
Columns = <>
end
end
Output:
Tested with Delphi 7, VT version 5.3.0, Windows 7
The coordinates of the CellRect parameter in the OnAfterCellPaint event handler are relative to the drawn node. What you need is the absoulte position of the node within the tree window. You can obtain that by calling GetDisplayRect of the tree.
So change your code like this:
procedure TForm1.VirtualStringTree1AfterCellPaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; CellRect: TRect);
var
NodeData: PNodeData;
R: TRect;
begin
if (Column = 0) then
Exit;
NodeData := VirtualStringTree1.GetNodeData(Node);
if (Assigned(NodeData)) and (Assigned(NodeData.Button)) then
begin
with NodeData.Button Do
begin
Visible := (vsVisible in Node.States)
and ((Node.Parent = VirtualStringTree1.RootNode) or (vsExpanded in Node.Parent.States));
R := Sender.GetDisplayRect(Node, Column, False);
BoundsRect := R;
end;
end;
end;

How to implement a TStringGrid using virtual tree view?

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...

Delphi - ListView or similar with owner draw button

How do I do create a listview (or similar) with a button on each line? The button needs to be able to have different text/color on each line as required.
I'm sure Virtual Treeview would be perfect for this, but I'm a little lost with it.
Thanks
-Brad
With a virtualtreeview...... add vstButton to your uses and select your Virtualtreeview in the object inspector and set the following events for your tree:
procedure TForm1.VSTCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; out EditLink: IVTEditLink);
begin
EditLink:=TStringEditLink.Create;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
VST.NodeDataSize := SizeOf(TTreeData);
AddRandomNodesToTree(Vst);
end;
procedure TForm1.VSTFreeNode(Sender: TBaseVirtualTree;
Node: PVirtualNode);
var
Data: PTreeData;
begin
Data:=VST.GetNodeData(Node);
if Assigned(Data) then begin
Data^.Column0 := '';
Data^.Column1 := '';
Data^.Column2 := '';
end;
end;
procedure TForm1.VSTGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
Data: PTreeData;
begin
Data := VST.GetNodeData(Node);
case Column of
0: CellText := Data^.Column0;
1: CellText := Data^.Column1;
2: CellText := Data^.Column2;
end;
end;
procedure TForm1.VSTNewText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; NewText: string);
Var
Data: PTreeData;
begin
Data := VST.GetNodeData(Node);
Case Column of
0: Data^.Column0:= NewText;
1: Data^.Column1:= NewText;
2: Data^.Column2:= NewText;
End;
end;
procedure TForm1.VSTPaintText(Sender: TBaseVirtualTree;
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType);
Var
Data: PTreeData;
begin
if Odd(Node.Index) then
TargetCanvas.Font.Color:= clRed;
end;
... this assumes the record is:
type
PTreeData = ^TTreeData;
TTreeData = record
Column0: String;
Column1: String;
Column2: String;
end;
...add the following unit:
unit vstButton;
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, VirtualTrees,
messages, windows, StdCtrls, ShlObj;
type
TStringEditLink = class(TInterfacedObject, IVTEditLink)
private
FEdit: TWinControl;
FTree: TVirtualStringTree;
FNode: PVirtualNode;
FColumn: Integer;
FSelectedFolder: string;
protected
procedure ButtonClick(Sender: TObject);
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;
function GetFolderDialog(Handle: Integer; Caption: string; var strFolder: string): Boolean;
implementation
destructor TStringEditLink.Destroy;
begin
FEdit.Free;
inherited;
end;
procedure TStringEditLink.ButtonClick(Sender: TObject);
var
s: string;
begin
s := 'c:\';
if GetFolderDialog(Application.Handle, 'Select a folder', s) then
FSelectedFolder := s;
FTree.EndEditNode;
FTree.setfocus;
end;
function TStringEditLink.BeginEdit: Boolean;
begin
Result := True;
FSelectedFolder := FTree.Text[FNode, FColumn];
TButton(FEdit).CAption := FTree.Text[FNode, FColumn];
FEdit.Show;
FEdit.SetFocus;
end;
function TStringEditLink.CancelEdit: Boolean;
begin
Result := True;
FEdit.Hide;
FTree.EndEditNode;
FTree.setfocus;
end;
function TStringEditLink.EndEdit: Boolean;
var
S: WideString;
begin
Result := True;
FTree.Text[FNode, FColumn] := FSelectedFolder;
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;
begin
Result := True;
FTree := Tree as TVirtualStringTree;
FNode := Node;
FColumn := Column;
FEdit.Free;
FEdit := nil;
FEdit := TButton.Create(nil);
with FEdit as TButton do
begin
Visible := False;
Parent := Tree;
Font.Color := FTree.Colors.HeaderHotColor;
OnClick := ButtonClick;
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;
//------------------------------------------------------------------------------\\
function BrowseCallbackProc(hwnd: HWND; uMsg: UINT; lParam: LPARAM; lpData: LPARAM): Integer; stdcall;
begin
if (uMsg = BFFM_INITIALIZED) then
SendMessage(hwnd, BFFM_SETSELECTION, 1, lpData);
BrowseCallbackProc := 0;
end;
function GetFolderDialog(Handle: Integer; Caption: string; var strFolder: string): Boolean;
const
BIF_STATUSTEXT = $0004;
BIF_NEWDIALOGSTYLE = $0040;
BIF_RETURNONLYFSDIRS = $0080;
BIF_SHAREABLE = $0100;
BIF_USENEWUI = BIF_EDITBOX or BIF_NEWDIALOGSTYLE;
var
BrowseInfo: TBrowseInfo;
ItemIDList: PItemIDList;
JtemIDList: PItemIDList;
Path: PChar;
begin
Result := False;
Path := StrAlloc(MAX_PATH);
SHGetSpecialFolderLocation(Handle, CSIDL_DRIVES, JtemIDList);
with BrowseInfo do
begin
hwndOwner := GetActiveWindow;
pidlRoot := JtemIDList;
SHGetSpecialFolderLocation(hwndOwner, CSIDL_DRIVES, JtemIDList);
{ return display name of item selected }
pszDisplayName := StrAlloc(MAX_PATH);
{ set the title of dialog }
lpszTitle := PChar(Caption);//'Select the folder';
{ flags that control the return stuff }
lpfn := #BrowseCallbackProc;
{ extra info that's passed back in callbacks }
lParam := LongInt(PChar(strFolder));
end;
ItemIDList := SHBrowseForFolder(BrowseInfo);
if (ItemIDList <> nil) then
if SHGetPathFromIDList(ItemIDList, Path) then
begin
strFolder := Path;
Result := True
end;
end;
End.
the above code is based upon code found at this website:
http://wiki.freepascal.org/VirtualTreeview_Example_for_Lazarus
If you look at the unit vstButton, to get a TEdit, or TCombo...etc... just replace any TButton reference with TEdit or TCombo etc... adjust events for it etc... The above link code actually uses a TCombo
that website helped me alot to learn how to use the virtualtreeview
the above code will insert a Tbutton into every cell, and when you click on the button it will open a BrowseForFolder dialog and insert the result back into the virtualtreeview cell
Hope this helps
did you want a button visible in a cell column all the time? Could simulate the button with an image... like a dropdown mark on one side of the cell

Component to display log info in Delphi

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.

Resources