Pai de RootNode de TVirtualStringTree

Sep 18 2020

De acordo com a documentação, a propriedade RootNode de TVirtualStringTree é um nó oculto, pai de todos os nós criados pelo usuário. Mas o pai do RootNode está definido. Estou enfrentando o seguinte problema:

Às vezes, ao fechar um formulário que contém uma TVirtualStringTree, recebo um erro de violação de acesso. Depurando, o erro ocorre ao tentar acessar a propriedade Name do componente (virtualstringtree). Se tento avaliar a propriedade Name, obtenho "Valor inacessível". Explorando o problema, descobri que a propriedade Name de um componente não pode ser alterada no tempo de execução :

Aviso: alterar o nome em tempo de execução faz com que todas as referências ao nome antigo se tornem indefinidas. Qualquer código subsequente que use o nome antigo causará uma exceção.

Acontece que não estou mudando isso no meu código. Usando um ponto de interrupção de dados na propriedade Name, vi que ele estava mudando em um loop virtualstringtree, algo como este (código de exemplo que estou usando em um aplicativo de amostra para testar o problema, mas o código de produção é semelhante):

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

O GetNodeData é o seguinte:

 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;

Dando que tenho a seguinte árvore:

Node 1
  Node 2
    Node 3

Quando o Nó 3 é selecionado, o loop é executado 5 vezes. 3 em meus nós criados, em seguida, o RootNode (oculto) e, em seguida, o pai da raiz. Apenas o pai do nó raiz pai é nulo. No método GetNodeData é verificado se é o nó raiz, retorna nulo. Mas para este pai da raiz, uma vez que não é a raiz, insira o código else. Agora, aqui está o problema:

O endereço da propriedade Name da virtualstrintree é $ 141B9768

O endereço da propriedade dos Estados do pai do nó raiz é $ 141B976A

Ao colocar o ponto de interrupção de dados na propriedade Name, ele é alterado neste ponto:

Include(Node.States, vsOnFreeNodeCallRequired);

e gerando o AV.

Sei que posso mudar o loop, para verificar o nó raiz corretamente , mas quero entender isso, então talvez conserte o componente (se for um bug de componente), em vez do meu código.

Código:

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;

Basta colocar uma árvore e um botão no formulário, coloco o loop no clique do botão.

Respostas

2 TomBrunberg Sep 18 2020 at 14:19

Primeiro, a documentação: (por exemplo) https://documentation.help/VirtualTreeview/TBaseVirtualTree_RootNode.html

diz (ênfase minha)

propriedade RootNode: PVirtualNode;

Descrição

Para ancorar a hierarquia da árvore, um nó interno da árvore é mantido, o qual é praticamente igual a qualquer outro nó da árvore, mas às vezes tem um tratamento diferente. O nó raiz é sempre expandido e inicializado. Seu membro pai aponta para a visualização em árvore à qual o nó pertence e seus membros PreviousSibling e NextSibling apontam para o próprio nó raiz para possibilitar o reconhecimento real desse nó .

Notas

Você não deve usar o nó raiz para iterar na árvore. Ele só é acessível publicamente porque é o pai de todos os nós de nível superior e pode ser usado para testar um nó, seja ele um nó de nível superior ou não.

Em segundo lugar

Em seu código

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

você está cruzando incondicionalmente para cima, além do nó raiz interno porque não está verificando se Pé a raiz interna, comparando com tree.RootNode(ou comparando com P.NextSibling).

Talvez mudando o

while Assigned(P) do

condição para

while P <> tree.RootNode do

serviria para você

1 RodrigoCaetano Sep 18 2020 at 19:57

Portanto, graças à resposta de Tom e à documentação do componente, o pai de RootNode é na verdade a própria árvore. Assim, ao fazer o loop com o Parent = nilponto de parada as, ao atingir a raiz do pai (árvore), o código Includeem GetNodeDataé executado, e, no meu caso, ao acessar a Statepropriedade, seu endereço estava muito próximo do endereço do nome da árvore , porque o endereço do "nó" era a árvore.

Então, eu corrigi o loop para verificar Node <> Tree.RootNodee também corrigir o GetNodeDatatambém verificar se o nó é manobrista: or (Node = Pointer(Self), apenas no caso de outro código na minha aplicação (legado de 2 milhões + código) usa o mesmo circuito errado :

Antes:

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;

Após:

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;