The given code which works without any problems in Delphi 2007. However in Delphi 2009 I am getting an exception.
Access violation shows read of address $00000000.
The problem exists only when assigning string, it works for numbers.
Also, when I am assigning Data.Text
manually via the debugger options I am getting no AV - it works.
Honestly I am lost, anyone could help me with this please?
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, VirtualTrees, StdCtrls;
type
TTest = record
Text: String;
Number: Integer;
end;
PTest = ^TTest;
type
TTestArray = array of TTest;
type
TForm1 = class(TForm)
VirtualStringTree1: TVirtualStringTree;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure VirtualStringTree1InitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
TestArray: array of TTest;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
SetLength(TestArray, 1);
TestArray[0].Text := 'test';
TestArray[0].Number := 12345;
VirtualStringTree1.AddChild(VirtualStringTree1.RootNode, @TestArray[0]);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
VirtualStringTree1.NodeDataSize := SizeOf(TTest);
end;
procedure TForm1.VirtualStringTree1InitNode(Sender: TBaseVirtualTree;
ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
Data: PTest;
NodeData: PPointer;
begin
Data := Sender.GetNodeData(Node);
NodeData := Sender.GetNodeData(Node);
Data.Number := PTest(NodeData^)^.Number;
Data.Text := PTest(NodeData^)^.Text; //crash here!
end;
end.
When you call AddChild(..., @TestArray[0])
, you're only initializing the first four bytes of the node's data. That's the Text
field. The Text
field holds a pointer to a TTest
structure. It's supposed to hold a string
reference.
The GetNodeData
function returns a pointer to the node's data. The tree control has allocated a TVirtualNode
record, and immediately after that, in consecutive memory, it has allocated NodeDataSize
bytes for you to use, and GetNodeData
returns the address of that space. You're supposed to treat that as a pointer to a TTest
structure. And you do, for some of your code. It looks like you're trying to skirt the limitation that only the first four bytes of the structure get initialized when you call AddChild
. (I can't say I recommend that. There are other ways to associate data with a node that don't require so much type punning.)
You assign Data
correctly for the way the node data is supposed to be used. You assign NodeData
correctly for what it really holds at the time of initialization — a pointer to a pointer to a TTest
structure. You correctly dereference NodeData
to read the Number
field, and you also read the Text
field correctly. However, the Data.Text
field can't be overwritten the way you have it:
Data.Text := PTest(NodeData^)^.Text;
The Data.Text
field doesn't current hold a valid string
value, but string
variables are required to hold valid values at all times (or at least all times where there's a possibility they'll be read or written). To assign a string
variable, the program increments the reference count of the new value and decrements the reference count of the old one, but since the "old value" in this case isn't really a string
, there's no valid reference count to decrement, and even if there were, the memory at that location couldn't be freed anyway — it belongs to TestArray
.
There's a way around this, though. Copy the string in two steps. First, read the value from NodeData.Text
into a spare string
variable. Once you do that, you have no need for NodeData
anymore, so you can overwrite the value it points to. If you set it to all-bits-zero, then you'll implicitly overwrite Data.Text
as well, and with the value of an empty string. At that point, it's safe to overwrite as a string
variable:
tmp := PTest(NodeData^)^.Text;
PTest(NodeData^) := nil;
Data.Text := tmp;
Another way around this is to re-arrange the order of the fields in the node data. Put the Integer
field first, and the initialize Data.Number
last instead of Data.Text
. Integer
values are always safe to overwrite, no matter their contents.
Whatever you do, make sure you finalize the record in the OnFreeNode
event:
var
Data: PTest;
begin
Data := Sender.GetNodeData;
Finalize(Data^);
end;
That makes sure the string
field gets its reference count reduced, if necessary.