0

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:

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

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

alancc
  • 487
  • 2
  • 24
  • 68
  • `the range that allow mouse click and select the node is determined by the initial texts` - yes. Use `tvItems.BeginUpdate` -> change tree structure -> `tvItems.EndUpdate`. Alternatelly - use `AddChild` second parameter (UserData: Pointer) – kami Sep 23 '19 at 06:02
  • @kami, thank you very much. I have two questions: 1. Should I use BeginUpdate & EndUpdate for each node I add? THe nodes are not added in batch. 2. How to use UserData parameter? I check the document and it does not mention it. And the sample codes do not use that either. – alancc Sep 23 '19 at 07:15

1 Answers1

1

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;
kami
  • 1,438
  • 1
  • 16
  • 23
  • Thank you very much. If I understand correctly, the mine error is that I try to set the data for the node AFTER it is created, so when it is created, its text is set to empty string, which makes it is selectable only at the beginning of the node. Is that correct? – alancc Sep 24 '19 at 05:54
  • @alancc ... yes. To be more accurate - cell text is set to `VirtualTreeView.DefaultText` property, because you not set `CellText` to empty string in OnGetText event when PData not assigned – kami Sep 24 '19 at 06:11
  • Thank you. I read FAQ in VirtualTreeView help document and it said one should call InvalidateNode after the node data are updated. So this is the third method to solve the problem? – alancc Sep 25 '19 at 07:46
  • @alancc. Yes, but... When you use InvalidateNode to recalculate just created node - you do "wrong actions" and then "fix the problem". Imho, InvalidateNode / InvalidateToBottom / etc should be used only when you change data of already existing node. For create node more preferable way - using UserData. – kami Sep 25 '19 at 09:44