如何在使用TreeNode.MoveTo时修复TTreeView错误?

aza*_*zad 4 delphi delphi-xe5

使用TreeNode.MoveTo(...)方法有时无法正常工作并引发"访问冲突"异常.

大多数时候有效,有些时候没有.

大多数时候'模块COMCTL32.DLL中的访问冲突.读取地址FEEEFEFA'并程序崩溃/冻结.

这是我的代码.

procedure TForm1.FormShow(Sender: TObject);
var
  I, sectioncount: Integer;
  parent, child: TTreeNode;
begin
  sectioncount := 0;
  for I := 0 to 19 do
  begin

    if I mod 5 = 0 then
    begin
      parent := TreeView1.Items.AddChild(nil, 'Section: ' + IntToStr(sectioncount));
      Inc(sectioncount);
    end;

    child := TreeView1.Items.AddChild(parent, 'Child: ' + IntToStr(I));

  end;

  TreeView1.Items[0].Expand(True);
end;

procedure TForm1.TreeView1DragDrop(Sender, Source: TObject; X, Y: Integer);
var src, dst : TTreeNode;
I : Integer;
begin
   dst := TreeView1.DropTarget;

   for I := 0 to TreeView1.SelectionCount - 1 do
   begin
     src := TreeView1.Selections[I];

     src.MoveTo(dst,naInsert);
   end;

end;

procedure TForm1.TreeView1DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
Accept := true;
end;
Run Code Online (Sandbox Code Playgroud)

添加一个from到一个项目,添加一个树视图集树视图dragmode dmAutomatic和multiselect true.

然后

使用ctrl选择具有以下顺序的3个连续节点.选择中间节点,选择底部节点,选择顶部节点.并将第一个节点的节点拖到另一个可以看到AV错误的地方.

或从上到下选择三个节点并从底部节点AV拖动出现.

或者用控制键按以下顺序选择三个节点: - 首先'子1'然后'子2'然后'子0'最后通过选择'子0'拖放节点

Dav*_*nan 5

一个明显的问题是,当您调用时MoveTo,您将使for循环无效.

 for I := 0 to TreeView1.SelectionCount - 1 do
 begin
   src := TreeView1.Selections[I];
   src.MoveTo(dst,naInsert);
 end;
Run Code Online (Sandbox Code Playgroud)

在调用之后MoveTo,您会发现SelectionCount当您进入循环时不再是这样.例如,我在这里看一个案例,SelectionCount3循环开始时,但是1在第一次调用之后MoveTo.这意味着后续使用Selections[I]是超出界限的.

您需要首先记下所选节点,然后再移动它们来解决问题.

procedure TForm1.TreeView1DragDrop(Sender, Source: TObject; X, Y: Integer);
var
  i: Integer;
  src, dst: TTreeNode;
  nodesToMove: TArray<TTreeNode>;
begin
  dst := TreeView1.DropTarget;
  SetLength(nodesToMove, TreeView1.SelectionCount);
  for i := 0 to high(nodesToMove) do
    nodesToMove[i] := TreeView1.Selections[i];
  for src in nodesToMove do
    src.MoveTo(dst, naInsert);
end;
Run Code Online (Sandbox Code Playgroud)

除了这个问题,我可以重现访问冲突.似乎需要以非常特定的顺序移动项目.您似乎需要先移动底部节点,然后移动下一个前一个节点,最后移动顶部节点.此代码似乎解决了这个问题:

procedure TForm1.TreeView1DragDrop(Sender, Source: TObject; X, Y: Integer);
var
  i: Integer;
  src, dst: TTreeNode;
  nodesToMove: TList<TTreeNode>;
begin
  dst := TreeView1.DropTarget;
  nodesToMove := TList<TTreeNode>.Create;
  try
    for i := TreeView1.Items.Count-1 downto 0 do
      if TreeView1.Items[i].Selected then
        nodesToMove.Add(TreeView1.Items[i]);
    for src in nodesToMove do
      src.MoveTo(dst, naInsert);
  finally
    nodesToMove.Free;
  end;
end;
Run Code Online (Sandbox Code Playgroud)

但这并不是很令人满意,而且我很清楚我还没有理解这里发生了什么.

我现在无法进一步研究这个问题,但我会在这里留下答案,因为我认为这将有助于其他回答者深入挖掘.希望有人能够解释AV的情况.


好的,我挖得更深了一点.该问题似乎与MoveTo试图维持被移动节点的选择状态的内部代码有关.我还没有深入研究问题所在,但在我看来,除了接管选择保留的实施之外,你无法从外部做很多事情来避免这个问题.

因此,我提出以下解决方案作为我提出的最佳方法:

procedure TForm1.TreeView1DragDrop(Sender, Source: TObject; X, Y: Integer);
var
  i: Integer;
  src, dst: TTreeNode;
  nodesToMove: TArray<TTreeNode>;
begin
  dst := TreeView1.DropTarget;
  SetLength(nodesToMove, TreeView1.SelectionCount);
  for i := 0 to high(nodesToMove) do
    nodesToMove[i] := TreeView1.Selections[i];
  TreeView1.ClearSelection;
  for src in nodesToMove do
  begin
    src.MoveTo(dst, naInsert);
    TreeView1.Select(src, [ssCtrl]);
  end;
end;
Run Code Online (Sandbox Code Playgroud)

我们在这里做以下事情:

  1. 记下所选节点,需要移动的节点.
  2. 清除选择.
  3. 将每个节点移动到其新目标,移动后,将该移动的节点添加到选择中.