I followed the conventional practice of displaying data on TVirtualStringTree. But it displays only the string "node" in each cell. Can someone tell what I am missing here ?
Thanks in advance.
My Code:
type
TRecFileDirectory = record
FileDirectory: WideString;
FileDirectoryLock: wordbool;
end;
TPRecFileDirectory = ^TRecFileDirectory;
implementation
procedure TForm2.btn4Click(Sender: TObject);
var
I: Integer;
NewNode: PVirtualNode;
ptrFileDir: TPRecFileDirectory;
begin
vsTree1.BeginUpdate;
for I := 0 to Length(arrFileDirectory)-1 do
begin
NewNode := vsTree1.AddChild(nil);
ptrFileDir := vsTree1.GetNodeData(NewNode);
ptrFileDir^.FileDirectory := arrFileDirectory[I].FileDirectory;
ptrFileDir^.FileDirectoryLock := arrFileDirectory[I].FileDirectoryLock;
end;
vsTree1.EndUpdate;
btn4.caption := btn4.caption+' DONE';
end;
You need to implement an event handler for the OnGetText event, that extracts the string to be displayed from your data, dependant on the column and node that is supplied. For example:
procedure TForm1.VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
MyData: TPRecFileDirectory;
begin
MyData := Node.GetData();
if Column = 0 then
CellText := MyData.FileDirectory;
end;
Set DefaultText property to empty string, e.g. in code or in Inspector
I am using Delphi XE3 with Virtual Tree View.
My codes are below:
type
TMyData = record
Caption: string;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
RootNode: PVirtualNode;
PData: ^TMyData;
begin
RootNode := tvItems.AddChild(nil);
PData := tvItems.GetNodeData(RootNode);
PData^.Caption := 'This is a test node';
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
tvItems.NodeDataSize := SizeOf(TMyData);
end;
procedure TForm1.tvItemsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
PData: ^TMyData;
begin
if Assigned(Node) then
begin
PData := tvItems.GetNodeData(Node);
if Assigned(PData) then
Celltext := PData^.Caption;
end;
end;
When I click the "Button1", the root node will be created. However, when my mouse clicks the node text, it will not be selected.
Some of my findings:
One must clicks to the beginning of the node text to select the node. If clicking in middle or in the end of the node text, then the node will not be selected.
If I change tvItemsGetText to below, then the problem disappears:
procedure TForm1.tvItemsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
PData: ^TMyData;
begin
CellText := 'This is a test node';
end;
I set a breakpoint in tvItemsGetText and find it will be invoked several times. At the first several times, the PData will be nil, which makes the CellText empty. At the final invokation, the PData will become valid and the CellText will be set to 'This is a test node'.
It seems that the range that allow mouse click and select the node is determined by the initial texts of the node. If the initial text is empty string, then one must click at the very beginning of the node to select it.
Is this a bug of Virtual Tree View?
There are several ways to init new node by user data.
1. Using OnInitNode event:
procedure TForm5.Button1Click(Sender: TObject);
begin
vt1.InsertNode(nil, amAddChildLast); // internal calls vt1InitNode
end;
procedure TForm5.vt1InitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
var InitialStates: TVirtualNodeInitStates);
var
PData: ^TMyData;
begin
PData := Sender.GetNodeData(Node);
PData^.Caption := 'This is a test node';
end;
2 Using UserData param
Variant 1. Dynamic data
Do not forget to remove InitNode event and dont set NodeDataSize property
type
TMyData = record
Caption: string;
end;
PMyData = ^TMyData;
procedure TForm5.Button1Click(Sender: TObject);
var
p: PMyData;
begin
New(p);
p.Caption:='This is a test node';
vt1.InsertNode(nil, amAddChildLast, p); // create node with initialized user data
// by default VirtualTree use NodeDataSize = SizeOf(pointer),
// so there is no reason to use GetNodeDataSize event
end;
procedure TForm5.vt1GetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
PData: PMyData;
begin
if Assigned(Node) then
begin
PData := PMyData(Sender.GetNodeData(Node)^); // little modification
// for correct access to dynamic node data
if Assigned(PData) then
CellText := PData.Caption;
end;
end;
procedure TForm5.vt1FreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
p: PMyData;
begin
p:=PMyData(Sender.GetNodeData(Node)^);
Dispose(p); // as you allocate memory for user data - you should free it to avoid memory leaks
end;
Variant 2. Objects
Add new private function to your form:
private
{ Private declarations }
function GetObjectByNode<T: class>(Node: PVirtualNode): T;
// do not forget to include System.Generics.Collections to `uses`
realization:
function TForm5.GetObjectByNode<T>(Node: PVirtualNode): T;
var
NodeData: Pointer;
tmpObject: TObject;
begin
Result := nil;
if not Assigned(Node) then
exit;
NodeData := vt1.GetNodeData(Node);
if Assigned(NodeData) then
tmpObject := TObject(NodeData^);
if tmpObject is T then
Result := T(tmpObject)
else
Result := nil;
end;
And the main code (almost identical to variant 1):
procedure TForm5.Button1Click(Sender: TObject);
var
d: TMyData;
begin
d := TMyData.Create;
d.Caption := 'This is a test node';
vt1.InsertNode(nil, amAddChildLast, d);
end;
procedure TForm5.vt1FreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
d: TMyData;
begin
d := GetObjectByNode<TMyData>(Node);
d.Free;
end;
procedure TForm5.vt1GetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
d: TMyData;
begin
d := GetObjectByNode<TMyData>(Node);
if Assigned(d) then
CellText := d.Caption;
end;
This is very small project to reproduce the problem (VirtualStringTree version 6.5):
type
TForm1 = class(TForm)
vstTest: TVirtualStringTree;
Images: TImageList;
procedure FormCreate(Sender: TObject);
procedure vstTestGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var CellText: string);
procedure vstTestGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind;
Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: TImageIndex);
private
{ Private declarations }
public
{ Public declarations }
end;
PTestRec = ^TTestRec;
TTestRec = record
Col1: string;
Col2: string;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
var
Node: PVirtualNode;
Data: PTestRec;
begin
vstTest.Header.Columns.Add;
vstTest.Header.Columns.Add;
vstTest.Header.Options := vstTest.Header.Options + [hoVisible];
vstTest.Images := Images;
vstTest.NodeDataSize := SizeOf(TTestRec);
Node := vstTest.AddChild(nil);
Data := vstTest.GetNodeData(Node);
Data.Col1 := 'Col1';
Data.Col2 := 'Col2';
end;
procedure TForm1.vstTestGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode;
Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: TImageIndex);
begin
if Column = 0 then
ImageIndex := 0;
end;
procedure TForm1.vstTestGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var CellText: string);
var
Data: PTestRec;
begin
Data := vstTest.GetNodeData(Node);
case Column of
0: CellText := Data.Col1;
1: CellText := Data.Col2;
end;
end;
Form contain ImageList, which contain only one icon. Done. Now we can run the program and get exception when moving mouse cursor over Icon:
Exception class EAssertionFailed with message 'An image index was supplied for TVTImageKind.ikState but no image list was supplied. (C:\Program Files\VirtualTreeView\Source\VirtualTrees.pas, line 20248)'. Process Project1.exe (3232)
As you can see I'm not use StateImages and OnGetImageIndexEx. Why this? This is from VirtualTrees code on the line 12635
WithStateImages := Assigned(FStateImages) or Assigned(OnGetImageIndexEx);
procedure TForm1.vstTestGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode;
Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean;
var ImageIndex: TImageIndex);
begin
if Column = 0 then
ImageIndex := 0;
end;
This code ignores the Kind argument. That argument can have one of the values from this enumeration:
TVTImageKind = (
ikNormal,
ikSelected,
ikState,
ikOverlay
);
If you return a value for ikState then you must have also provided state images. That is what the error message is telling you. I guess that your event handler should discriminate like so:
procedure TForm1.vstTestGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode;
Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean;
var ImageIndex: TImageIndex);
begin
case Kind of
ikNormal, ikSelected:
if Column = 0 then
ImageIndex := 0;
end;
end;
In short, I'm trying to add Object into VirtualTreeView but am always getting error EAccessViolation at CellText I'm wondering about the reason for the error i tried to do my best but without result...
this is my attempt:
TForm1 = class(TForm)
private
public
end;
TPerson = class;
PPersonNode = ^TPersonNode;
TPersonNode = record
PersonObj: TPerson;
end;
TPerson = class(TObject)
private
FName: string;
public
property Name: string read FName write FName;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
PersonRec: PPersonNode;
begin
PersonRec := VirtualStringTree1.GetNodeData(VirtualStringTree1.AddChild(nil));
PersonRec.PersonObj := TPerson.Create;
PersonRec.PersonObj.Name := 'aaa';
end;
procedure TForm1.VirtualStringTree1GetNodeDataSize(Sender: TBaseVirtualTree;
var NodeDataSize: Integer);
begin
NodeDataSize := SizeOf(TPersonNode);
end;
procedure TForm1.VirtualStringTree1GetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
PersonRec: PPersonNode;
begin
PersonRec := Sender.GetNodeData(Node);
CellText := PersonRec.PersonObj.Name;
end;
It looks like the OnGetText event is being fired before the TPerson object has been created. Try checking that the object isn't nil first:
procedure TForm1.VirtualStringTree1GetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
PersonRec: PPersonNode;
begin
PersonRec := Sender.GetNodeData(Node);
if PersonRec <> nil then // <- check that the object has been created
CellText := PersonRec.PersonObj.Name;
end;
I would change this:
procedure TForm1.Button1Click(Sender: TObject);
var
PersonRec: PPersonNode;
begin
PersonRec := VirtualStringTree1.GetNodeData(VirtualStringTree1.AddChild(VirtualStringTree1.RootNode));
PersonRec.PersonObj := TPerson.Create;
PersonRec.PersonObj.Name := 'aaa';
end;
and this:
procedure TForm1.VirtualStringTree1GetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
PersonRec: PPersonNode;
begin
if Assigned(Node) then
begin
PersonRec := Sender.GetNodeData(Node);
CellText := PersonRec.PersonObj.Name;
end;
end;
I am trying to build my projects with a VirtualStringTree rather than a Listview, because of the vast speed difference. The thing is, even after looking thru the demo's, I just can't figure out exactly how I would use it as a ListView. Like, adding, deleting, and basically just working with ListView items is so easy, but when I look at the VT, it gets almost too complicated.
All I am looking for, is a VT that looks like a ListView, with subitems etc.
Here are some routines using the ListView, that I would like to use with VT (This is just a pseudo example:
procedure Add;
begin
with ListView.Items.Add do
Begin
Caption := EditCaption.Text;
SubItems.Add(EditSubItem.Text):
End;
end;
Procedure ReadItem(I : Integer);
begin
ShowMessage(ListView.Items[I].Caption);
ShowMessage(ListView.Items[I].SubItems[0]);
end;
Of course, also the Delete function, but since thats like 1 line, I didnt bother :P
Could anyone maybe translate the above examples into using a ListView style VT?
Thanks!
Why don't you use a list view in virtual mode? That will look and feel right and perform great.
The Delphi TListView control is a wrapper around the Windows list view component. In its default mode of operation copies of the list data are transferred from your app to the Windows control and this is slow.
The alternative to this is known as a virtual list view in Windows terminology. Your app doesn't pass the data to the Windows control. Instead, when the control needs to display data it asks your app for just the data that is needed.
The Delphi TListView control exposes virtual list views by use of the OwnerData property. You'll have to re-write your list view code somewhat but it's not too hard.
I also offer a link to another question here that covered similar ground. Rather oddly, the accepted answer for that question talked about list boxes even though the question was about list view controls.
with VirtualStringTree it's a bit more complex than the simple TListView, however here's a very simple tutorial that I've created a little while back on how to use VirtualStringTree http://www.youtube.com/watch?v=o6FpUJhEeoY I hope it helps, cheers!
Just use your normal TListView, but use it in virtual mode.
It's really simple:
Set the OwnerData property to true
Implement the OnData event handler.
Sample implementation that shows a simple list of 3 rows:
Type TMyItem=record
Item:String;
SubItem:String;
end;
var Items:Array of TMyItem;
// set up some in-memory dataset.. choose your own layout
SetLength(Items,3);
Items[0].Item := 'foo1';
Items[0].SubItem := 'bar1';
Items[1].Item := 'foo2';
Items[1].SubItem := 'bar2';
Items[2].Item := 'foo3';
Items[2].SubItem := 'bar3';
// tell ListView1 how many items there are
ListView1.Items.Count := Length(Items);
procedure TfrmMain.ListView1Data(Sender: TObject; Item: TListItem);
begin
Item.Caption := IntToStr(Item.Index);
Item.SubItems.Add( MyArray[Item.Index] );
Item.SubItems.Add( UpperCase(MyArray[Item.Index]) );
end;
// Updating a value:
Items[1].Item := 'bzzz';
ListView1.Update;
That's all!
Some things to keep in mind:
You don't call ListView1.Items.Add() anymore.
You need to keep your own list of data somewhere in memory, or come up with the data in real-time, so you cannot 'store' data in the listview any longer.
You need to set the items.count property, or you won't see anything.
Call ListView1.Update() if something changes.
procedure Add;
Var
Data: PLogData;
XNode: PVirtualNode;
begin
with vst do
Begin
XNode := AddChild(nil);
ValidateNode(XNode, False);
Data := GetNodeData(Xnode);
Data^.Name:= EditCaption.Text;
Data^.Msg := EditSubItem.Text;
End;
end;
Procedure ReadItem(I : Integer);
var
Data: PLogData;
begin
if not Assigned(vst.FocusedNode) then Exit;
Data := vst.GetNodeData(vst.FocusedNode);
ShowMessage(Data^.Name);
ShowMessage(Data^.Msg);
end;
Basically that is what you need to do, but the VirtualStringTree has/needs alot of other things working together to fully understand it. And once you "get it" the VST is easy and powerful. The following webpage will help you: http://wiki.freepascal.org/VirtualTreeview_Example_for_Lazarus
and below I will add more code I use for a simple VST Log display. I keep all the code in datamodule, just use the procedure Log to display information and change your FormMain.vstLog to yours...
unit udmVstLog;
interface
uses
SysUtils, Windows, Forms, Classes, Graphics,
VirtualTrees, ActnList, Dialogs, ExtDlgs;
type
PLogData = ^TLogData;
TLogData = record
IsErr : Boolean;
Name: String;
Msg : String;
end;
type
TdmVstLog = class(TDataModule)
actlst1: TActionList;
actClear: TAction;
actSave: TAction;
actCopyLine2Mem: TAction;
sdlgLog: TSaveTextFileDialog;
procedure DataModuleCreate(Sender: TObject);
procedure actClearExecute(Sender: TObject);
procedure actSaveExecute(Sender: TObject);
procedure actCopyLine2MemExecute(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
procedure VSTFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
procedure VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
procedure VSTPaintText(Sender: TBaseVirtualTree;
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType);
end;
procedure Log(aIsErr: Boolean; AName, AMsg: string); overload;
procedure Log(AName, AMsg: string); overload;
procedure Log(AMsg: string); overload;
var
dmVstLog: TdmVstLog;
implementation
uses uFormMain, ClipBrd;
{$R *.dfm}
procedure Log(aIsErr: Boolean; AName, AMsg: string);
Var
Data: PLogData;
XNode: PVirtualNode;
begin
XNode:=FormMain.vstLog.AddChild(nil);
FormMain.vstLog.ValidateNode(XNode, False);
Data := FormMain.vstLog.GetNodeData(Xnode);
Data^.IsErr := aIsErr;
if aIsErr then
Data^.Name:= DateTimeToStr(Now) + ' ERROR ' + AName
else
Data^.Name:= DateTimeToStr(Now) + ' INFO ' + AName;
Data^.Msg:= AMsg;
end;
procedure Log(AName, AMsg: string);
begin
Log(False,AName,AMsg);
end;
procedure Log(AMsg: string);
begin
Log(False,'',AMsg);
end;
// VirtualStringTree Events defined here
procedure TdmVstLog.VSTFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
Data: PLogData;
begin
Data:=Sender.GetNodeData(Node);
Finalize(Data^);
end;
procedure TdmVstLog.VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
var
Data: PLogData;
begin
Data := Sender.GetNodeData(Node);
case Column of
0: CellText := Data^.Name + ' - '+ Data^.Msg;
end;
end;
procedure TdmVstLog.VSTPaintText(Sender: TBaseVirtualTree;
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType);
Var
Data: PLogData;
begin
Data := Sender.GetNodeData(Node);
if Data^.IsErr then
TargetCanvas.Font.Color:=clRed;
end;
//PopUpMenu Actions defined here!
procedure TdmVstLog.actClearExecute(Sender: TObject);
begin
FormMain.vstLog.Clear;
end;
procedure TdmVstLog.actCopyLine2MemExecute(Sender: TObject);
var
Data: PLogData;
begin
if not Assigned(FormMain.vstLog.FocusedNode) then Exit;
Data := FormMain.vstLog.GetNodeData(FormMain.vstLog.FocusedNode);
ClipBoard.AsText := Data^.Name + ' - ' + Data^.Msg;
end;
procedure TdmVstLog.actSaveExecute(Sender: TObject);
Var
XNode: PVirtualNode;
Data: PLogData;
ts: TStringList;
begin
If FormMain.vstLog.GetFirst = nil then Exit;
XNode:=nil;
if sdlgLog.Execute then begin
ts:= TStringList.create;
try
Repeat
if XNode = nil then XNode:=FormMain.vstLog.GetFirst Else XNode:=FormMain.vstLog.GetNext(XNode);
Data:=FormMain.vstLog.GetNodeData(XNode);
ts.Add(Data^.Name + ' - '+ Data^.Msg);
Until XNode = FormMain.vstLog.GetLast();
ts.SaveToFile(sdlgLog.FileName);
finally
ts.Free;
end;
end;
end;
// Datamodule Events defined here
procedure TdmVstLog.DataModuleCreate(Sender: TObject);
begin
with FormMain.vstLog do begin
NodeDataSize := SizeOf(TLogData);
OnFreeNode := VSTFreeNode;
OnGetText := VSTGetText;
OnPaintText := VSTPaintText;
end;
end;
end.
...
procedure RemoveSelectedNodes(vst:TVirtualStringTree);
begin
if vst.SelectedCount = 0 then Exit;
vst.BeginUpdate;
vst.DeleteSelectedNodes;
vst.EndUpdate;
end;
procedure RemoveAllNodes(vst:TVirtualStringTree);
begin
vst.BeginUpdate;
vst.Clear;
vst.EndUpdate;
end;
Get the VT Contributions pack and check out some of the descendants of virtual string tree. That are in there. I haven't used them in projects, but they seem to make Virtual String Tree easier to use.
Here's my getting started primer nonetheless:
I've found after using Virtual String Tree quite a bit that the only way you can make the most of it is by implementing the init node/child functions and setting the root node count, much the same as you would a list view with ownerdraw := true.
It's pretty easy to do stuff with VirtualStringTree, you just need to implement the get text function and the node size functions (set it equal to the size of whatever record you'd like to use as the data behind your tree)
I've found it's almost always easier to do
TVirtualTreeNodeRecordData = record
Data : TVirtualTreeNodeData;
end
and create the data object on the init functions. It creates the pointers for you, but you need to free the objects (again, use another delete node callback).