TVirtualStringTree 的 RootNode 的父级

Parent of RootNode of TVirtualStringTree

根据文档,属性 TVirtualStringTree 的 RootNode 是一个隐藏节点,是所有用户创建的节点的父节点。但是设置了 RootNode 的 Parent。我面临以下问题:

有时,当关闭其中包含 TVirtualStringTree 的表单时,我会收到访问冲突错误。调试,尝试访问组件(virtualstringtree)的Name 属性时出现错误。如果我尝试评估 Name 属性,我会得到“无法访问的值”。深入研究问题,我发现组件的名称 属性 cannot be changed in runtime:

Warning: Changing Name at runtime causes any references to the old name to become undefined. Any subsequent code that uses the old name will cause an exception.

碰巧我没有在我的代码中更改它。在 Name 属性 中使用数据断点,我看到它在 virtualstringtree 循环中发生变化,类似于这样(我在示例应用程序中使用的示例代码来测试问题,但生产代码类似):

var
  N: PNode;
  P: PVirtualNode;
begin
  P := tree.FocusedNode;
  while Assigned(P) do
  begin
    N := tree.GetNodeData(P);
    P := P.Parent;
  end;

GetNodeData 如下:

 if (FNodeDataSize <= 0) or (Node = nil) or (Node = FRoot) then
    Result := nil
  else begin
    Result := PByte(@Node.Data) + FTotalInternalDataSize;
    Include(Node.States, vsOnFreeNodeCallRequired); // We now need to call OnFreeNode, see bug #323
  end;

鉴于我有以下树:

Node 1
  Node 2
    Node 3

选中节点3时,循环执行5次。 3 在我创建的节点中,然后是 RootNode(隐藏),然后是根的 Parent。只有父根节点的Parent为nil。在GetNodeData方法中验证是否为根节点,returns nil。但是对于根的这个父节点,因为它不是根,所以进入 else 代码。现在问题来了:

virtualstrintree 的名称 属性 的地址是 $141B9768

根节点父节点的州属性地址是$141B976A

将数据断点放在名称属性时,此时会发生变化:

Include(Node.States, vsOnFreeNodeCallRequired);

并生成 AV。

我知道我可以更改循环以检查根节点 ,但我想了解这一点,所以我可能会修复组件(如果它是组件错误),而不是我的代码.

代码:

type
  TNode = record
    Text: string;
  end;
  PNode = ^TNode;
...
procedure TForm1.FormCreate(Sender: TObject);
var
  N1, N2, N3: TNode;
  TreeNode: PVirtualNode;
begin
  N1.Text := 'Node 1';
  TreeNode := tree.AddChild(nil, PNode(N1));

  N2.Text := 'Node 2';
  TreeNode := tree.AddChild(TreeNode, PNode(N2));

  N3.Text := 'Node 3';
  TreeNode := tree.AddChild(TreeNode, PNode(N3));
end;

只需在表单中放一棵树和一个按钮,我将循环放在单击按钮中。

首先,文档:(例如) https://documentation.help/VirtualTreeview/TBaseVirtualTree_RootNode.html

说(强调我的)

property RootNode: PVirtualNode;

Description

For anchoring the tree hierarchy an internal tree node is maintained which is mostly just like any other tree node but has sometimes differently handled. The root node is always expanded and initialized. Its parent member points to the treeview to which the node belongs to and its PreviousSibling and NextSibling members point to the root node itself to make it possible to actually recognize this node.

Notes

You should not use the root node to iterate through the tree. It is only publicly accessible because it is the parent of all top level nodes and can be used to test a node whether it is a top level node or not.

其次

在你的代码中

var
  N: PNode;
  P: PVirtualNode;
begin
  P := tree.FocusedNode;
  while Assigned(P) do
  begin
    N := tree.GetNodeData(P);
    P := P.Parent;
  end;

你无条件地向上遍历内部根节点,因为你没有通过与 tree.RootNode 比较(或与 P.NextSibling 比较)来检查 P 是否是内部根节点。

也许会改变

while Assigned(P) do

条件为

while P <> tree.RootNode do

会适合你

所以,感谢 Tom 的回答和组件的文档,RootNode 的父级实际上是树本身。因此,当以 Parent = nil 作为停止点进行循环时,当到达父级的根(树)时,将执行 GetNodeData 中的代码 Include,并且在我的例子中,当访问 State 属性,它的地址非常接近树的名称地址,因为“节点”的地址是树。

因此,我修复了循环以检查 Node <> Tree.RootNode 并修复了 GetNodeData 以检查 Node 是否为 Self:or (Node = Pointer(Self) 以防万一我应用程序中的其他代码(200 万+ 代码的遗留)使用相同的错误循环:

之前:

function TBaseVirtualTree.GetNodeData(Node: PVirtualNode): Pointer;
begin
  Assert(FNodeDataSize > 0, 'NodeDataSize not initialized.');
  if (FNodeDataSize <= 0) or (Node = nil) or (Node = FRoot) then
    Result := nil
  else begin
    Result := PByte(@Node.Data) + FTotalInternalDataSize;
    Include(Node.States, vsOnFreeNodeCallRequired); // We now need to call OnFreeNode, see bug #323
  end;
end;

之后:

function TBaseVirtualTree.GetNodeData(Node: PVirtualNode): Pointer;
begin
  Assert(FNodeDataSize > 0, 'NodeDataSize not initialized.');
  if (FNodeDataSize <= 0) or (Node = nil) or (Node = FRoot) or (Node = Pointer(Self)) then
    Result := nil
  else begin
    Result := PByte(@Node.Data) + FTotalInternalDataSize;
    Include(Node.States, vsOnFreeNodeCallRequired); // We now need to call OnFreeNode, see bug #323
  end;
end;