【问题标题】:Access Violation when assigning string in InitNode event of TVirtualStringTree在 TVirtualStringTree 的 InitNode 事件中分配字符串时访问冲突
【发布时间】:2011-06-06 07:51:48
【问题描述】:

给定的代码在 Delphi 2007 中没有任何问题。但是在 Delphi 2009 中我遇到了异常。

访问冲突显示读取地址 $00000000。

问题只存在于分配字符串时,它适用于数字。

另外,当我通过调试器选项手动分配 Data.Text 时,我没有得到任何 AV - 它可以工作。

老实说,我迷路了,有人可以帮我解决这个问题吗?

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.

【问题讨论】:

  • 只是伙计们?我知道我是少数,但你必须把它揉进去吗? :-))
  • @Marjan 在美式英语中,人们通常以一种性别中立的方式来称呼他们。也就是说,如果这真的是 Wodzu 的样子,那么我会接受他/她/它想给你打电话的任何东西。看起来不像是那种让人想反其道而行之的生物!
  • @David:是的,我知道,它刚刚引起了我的注意,我无法抗拒发表评论的冲动。一定是因为我有点脾气暴躁,因为我睡过头了,因为我在一个长周末后忘记重新启动我的闹钟......(你确实注意到了笑脸,不是吗?)但你是对的,如果 Wodzu 是像他的头像一样,我会尽量不弄乱他的头发:-)
  • 哈哈,伙计们!;)我是一个非常友好且容易驯服的人,相信我;)
  • 您还能期待什么?您的代码专门设计为崩溃,原因与 Virtual TreeView 无关。

标签: string delphi delphi-2009 virtualtreeview tvirtualstringtree


【解决方案1】:

当您调用AddChild(..., @TestArray[0]) 时,您只是在初始化节点数据的前四个字节。那是Text 字段。 Text 字段包含一个指向 TTest 结构的指针。 应该持有一个string 引用。

GetNodeData 函数返回指向节点数据的指针。树控件分配了一个TVirtualNode 记录,紧接着,在连续内存中,它分配了NodeDataSize 字节供您使用,GetNodeData 返回该空间的地址。您应该将其视为指向 TTest 结构的指针。对于您的某些代码,您确实做到了。看起来您正试图绕过调用AddChild 时仅初始化结构的前四个字节的限制。 (我不能说我建议这样做。还有其他方法可以将数据与不需要太多类型双关的节点相关联。)

您为节点数据的使用方式正确分配了Data。您将NodeData 正确分配给它在初始化时真正 持有的东西——一个指向TTest 结构的指针的指针。您正确地取消引用 NodeData 以读取 Number 字段,并且您还正确读取 Text 字段。但是,Data.Text 字段不能按照您的方式覆盖:

Data.Text := PTest(NodeData^)^.Text;

Data.Text 字段当前不包含有效的 string 值,但 string 变量需要始终保持有效值(或至少在存在它们可能会被读取或写入)。要分配string 变量,程序会增加新值的引用计数并减少旧值的引用计数,但由于在这种情况下“旧值”并不是真正的string,因此没有有效引用计数递减,即使有,该位置的内存也无法释放——它属于TestArray

不过,有办法解决这个问题。分两步复制字符串。首先,将NodeData.Text 中的值读取到备用string 变量中。一旦你这样做了,你就不再需要NodeData,所以你可以覆盖它指向的值。如果将其设置为所有位为零,那么您也将隐式覆盖Data.Text,并使用空字符串的值。此时,将 覆盖为 string 变量是安全的

tmp := PTest(NodeData^)^.Text;
PTest(NodeData^) := nil;
Data.Text := tmp;

解决此问题的另一种方法是重新排列节点数据中字段的顺序。将Integer 字段放在第一位,然后初始化Data.Number 最后而不是Data.TextInteger 值始终可以安全地覆盖,无论其内容如何。

无论你做什么,确保你敲定OnFreeNode事件中的记录:

var
  Data: PTest;
begin
  Data := Sender.GetNodeData;
  Finalize(Data^);
end;

这样可以确保 string 字段在必要时减少其引用计数。

【讨论】:

  • 感谢 Rob 的宝贵时间和出色的回答。我想出了重新排序记录字段的技巧。但出于好奇,我有两个问题: 1. 为什么我可以在调试器中设置 Data.Text 并且它可以工作? 2. 为什么我的例子在 2007 年没有这个问题?而且几年前它就可以工作了......谢谢。
  • 1.我不知道那是什么意思。 2. 字符串和动态数组曾经具有相同的结构,所以当 RTL 查看指向TestArray[0] 的指针并将其视为string 时,它发现引用计数和其他内存信息在同一个地方。当它认为它是在减少字符串的引用计数时,它实际上是在减少动态数组的引用计数。自 Delphi 2009 起,字符串有更多的数据字段,而以前它们只有长度和引用计数,与动态数组相同。您的代码一直是错误的;你以前从未注意到过。
【解决方案2】:

你在这里错过了重点。您已经在按钮的单击事件上初始化了您的节点,因此无需另外使用OnInitNode 来初始化它。您需要的可能是使用OnGetText 来显示您的数据。例如:

procedure TForm1.VirtualStringTree1GetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
  Data: PTest;
begin
  Data := PTest(Sender.GetNodeData(Node)^);

  CellText := Data.Text;
end;

【讨论】:

  • 不,这不是真的。 AddChild 引发 OnInit 事件,AddChild 方法传递的指针用于 OnInit 事件中节点的初始化。
  • @Wodzu 没错,但 OnInitNode 事件基本上用于初始化您的数据结构。在您的情况下,您不需要它,因为您已经在单击按钮时填充了数组。当您将 RootNodeCount 设置为树的特定值时,OnInitNode 很有用。然后它会自动为您调用此方法。我建议您阅读一些 TVirtualStringTree 文档。附言如果您使用 AddChild,最好将新添加的节点的状态设置为 vsInitialized,以确保树会调用所有节点的 OnFreeNode 事件。
猜你喜欢
  • 1970-01-01
  • 2011-07-04
  • 1970-01-01
  • 2023-04-05
  • 1970-01-01
  • 1970-01-01
  • 2020-04-22
  • 2019-04-05
  • 2013-11-26
相关资源
最近更新 更多