I am converting a Delphi app from using a TTreeView to using a TVirtualStringTree; The node data is held in TItemData records in a TList.
type
TItemData = record
idName: string;
end;
PItemData = ^TItemData
...
TMyForm = class(TForm)
...
private
itemData: TList<TItemData>;
..
end;
I wanted to get the displayed tree up and running in the most straightforward way possible and then gradually convert the app little by little as I got to understand how to use the VirtualStringTree. So, I have a buildTreeFromItemData() method which iterates through the TList elements and creates child nodes in the VirtualStringTree. I [tried to] pass a pointer to each TItemData record in each call to VST.addChild() which would then be dereferenced in vstGetText(), something like this:
procedure buildTreeFromItemData;
var
i: integer;
idA: TItemData;
begin
for i := 0 to itemData.count - 1 do begin
idA := itemData[i];
vst.addChild(NIL, @idA);
end;
end;
Dereference the pointer:
procedure TMyForm.vstGetData(...);
var
idB: TItemData;
begin
idB := node.getData^;
CellText := idB.idName;
end;
It quickly became apparent that no matter how many different ways I tried to code this, e.g. @itemData[i], the only times my code compiled every vst node was actually getting the address of the idA local variable, and every tree node was pointing to the most recent TItemData record pointed to by idA. I was then getting access violations in vstGetText() once buildTreeFromItemData() had completed and the local idA variable went out of scope, i.e. every node's data pointer in vst was now invalid.
Most of my attempts to somehow deference idA and get at the address location of the TItemData stored in idA generated an "invalid typecast" from the Delphi syntax checker, let alone the compiler.
At one point I tried something like this:
ptr1^ := @idA;
I have no idea what that actually means to the Delphi compiler. I know what I wanted it to mean: I wanted it to mean "set ptr1 to the [dereferened] address stored at the address of the idA local variable". To my surprise, it compiled but went bang as soon as the debugger hit that statement. (What does the compiler think "ptr1^ := " means?)
Eventually I hit upon the idea of typecasting idA to a TObject; at least then, my thinking went, the compiler would know we were at least in the realms of dereferencing and might actually let me, eventually, get to the pointer I really needed to pass to vst.addChild().
After much experimentation, and many more "invalid typecast"s, unbelievably [at least to me] the following code works!.....
procedure buildTreeFromItemData;
var
i: integer;
idA: TItemData;
myObj: TObject;
ptr1: pointer;
ptr2: PItemData;
begin
for i := 0 to itemData.count - 1 do begin
idA := itemData[i];
myObj := TObject(@idA);
ptr1 := pointer(myObj)
ptr2 := PItemData(ptr1^);
vst.addChild(NIL, ptr2);
end;
end;
ptr2 is now so far removed, syntactically and semantically, from idA, that the compiler finally allows the dereference in PItemData(ptr1^), although it only allowed it after I added the PItemData(...) typecast.
I don't even have to dereference this pointer in vstGetText!...
procedure TMyForm.vstGetText(...);
var
idB: PItemData;
begin
idB := PItemData(node.getData);
CellText := idB.idName;
end;
The tree displays perfectly and the access violations are gone. (NB. The actual code in buildTreeFromItemData() is a lot more involved and creates child nodes of child nodes to create a complex tree structure several levels deep.)
Although I eventually found a solution at gone 1am this morning after a lot of trial and error, I find it difficult to believe that my jiggerypokery with the local variable is really necessary for something so simple. So my question is this: what is the correct Delphi syntax for getting the address of my TItemData record stored in a plain "idA: TItemData;" local variable?
(I think this is my first ever question to stackoverflow; I hope I have formulated it well enough. I've kept the code to the absolute bare bones necessary to illustrate the issue and I wasn't able to completely reproduce the exact experimentation code I went through. The solution in the final two code blocks, though, is my working code. If I can improve how I've formulated the question and the explanation to meet stackoverflow's stringent standards, please let me know.)
CodePudding user response:
The idea of TVirtualTree is to store your data inside the nodes. TVirtualTree will reserve memory for your data and that's why you need to tell the tree how big is your data.
So, when your form is created (inside OnCreate handler) you should set the NodeDataSize
property of your VirtualTree:
procedure TmyForm.TntFormCreate(Sender: TObject);
begin
inherited;
myVirtualTree.NodeDataSize:=SizeOf(TItemData);
end;
You should also handle the OnFreeNode event from your VirtualTree:
procedure TmyForm.myVirtualTreeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
Data:PItemData;
begin
Data:=Sender.GetNodeData(Node);
if Assigned(Data) then Finalize(Data^);
end;
When you want to create a new node in the tree:
procedure TmyForm.CreateNewNode(data:TItemData);
var
node: PItemData;
P: PVirtualNode;
begin
with myVirtualTree do
begin
P:=AddChild(Nil);
node:=GetNodeData(P);
node^:=data;
// ReinitNode is very important - to update InternalNode precomputed
// text width. Otherwise node can not be properly selected if
// FullRowSelect is FALSE
ReinitNode(P,False);
end;
end;
You will also need to handle other events from TVirtualTree like OnGetText, onPaintText, onCompareNodes, onIncrementalSearch, onDblClick, onGetHint, etc.
CodePudding user response:
what is the correct Delphi syntax for getting the address of my TItemData record stored in a plain "idA: TItemData;" local variable?
Well... That one is simple. You do it like this : @idA
.
The issue here is that it is NOT what you want to do. You want to have the address of the TItemData
in your list (idA
is merely a copy of the record in itemData
).
To get the address of a value inside of a TList<T>
, you can't use property Items[Index: Integer]: T
of TList<T>
as it will only return you a copy of the value. You need to use property List: arrayofT
which will give you direct access the underlying array in which the values are stored, and then you can get the address with : @itemData.List[I]
.
That being said, it is not something I would recommend. There is no guarantees how long that pointer will remain valid. There is a lot of operations on the list that might render that address invalid, or make it point to the wrong TItemData
. If itemData
is immutable by the time you acquire the pointer, it's should be ok. Otherwise, it would be way better to allocate new TItemData
like described by IVO GELOV in his answer.