我应该如何更新 Virtual TreeView 中的节点?

How should I update the node in Virtual TreeView?

我正在使用 Delphi XE3 和 Virtual TreeView。

我想用Virtual TreeView实现一棵树,当点击"Start"按钮时,程序会递归搜索一个驱动器下的所有文件和文件夹,然后一个一个地添加到树中,就像Windows 探险家。此外,一个文件夹下应该有一个数字表示文件和子文件夹的数量,使用这样的静态文本:

VirtualTreeView - different color of text in the same node

在我的实现中,我发现有时数字没有正确更新。

因此,我想到了以下方式来刷新节点,每当files/subfolders的数量发生变化时:

  1. 调用tvItems.Change(PNode)更新节点

  2. 调用tvItems.InvalidateNode(PNode).

  3. 调用tvItems.RepaintNode(PNode).

  4. 呼叫tvItems.UpdateAction.

但是,1 是无法调用的受保护方法。 2 和 3 都可以,但不知道哪个更适合更新。 4 没有记录,不知道如何调用它。

基本思想是,如果幕后发生变化,则需要重新绘制树。这意味着下次树绘制自己时,它将使用新的基础值。

如果你的树就在屏幕上:

您只需拨打:

tvItems.Invalidate;

这告诉 Windows 整个 树现在是 "invalid" 并且需要重新绘制。我可以代表这个 "invalid" 区域,下次树绘制自己时会更新:

这很好,正确,并且可以很好地工作。

性能改进

很多时候强制整个树重绘所有本身是完全合理的。

但也有可能开始优化事情。如果您知道只有 Windows 控件的某个区域是 "invalid",那么您可以使该部分无效。

Windows 函数是 InvalidateRect:

InvalidateRect(tvItems.Handle, Rect(13, 18, 30, 38), True); 

这将使 13,18 处的 30x38 正方形无效:

事实上 TWinControl.Invalidate 所做的只是转身调用 Windows InvalidateRect 函数:

//true means to also trigger an erase of the background
InvalidateRect(Self.Handle, Self.BoundsRect, True); 

这么奇葩的矩形要作废可能用处不大。但是您可能会想到其他想要无效的矩形。

使节点失效

Windows没有意识到,但是你的控件代表了一棵树,树和节点。有时您可能想要使 "node":

的矩形无效

您不必计算节点的坐标和大小, TVirtualTree 已经为您提供了一个方便的方法来 使 节点无效:

function InvalidateNode(Node: PVirtualNode): TRect; virtual;
// Initiates repaint of the given node and returns the just invalidated rectangle.

所以:

tvItems.InvalidateNode(someNode);

它还提供了一种使节点及其所有children无效的方法:

procedure TBaseVirtualTree.InvalidateChildren(Node: PVirtualNode; Recursive: Boolean);
// Invalidates Node and its immediate children.
// If Recursive is True then all grandchildren are invalidated as well.
// The node itself is initialized if necessary and its child nodes are created (and initialized too if
// Recursive is True).

当你的树 children:

时这很有用

您可以使 parent 节点和现在需要用新数字更新的所有 children 节点无效:

tvItems.InvalidateChildren(someNode, True);

和其他辅助方法

虚拟树还有其他有用的方法:

  • 获取某个有趣的矩形以使其无效
  • 致电Windows.InvalidateRect

即:

  • InvalidateToBottom(Node: PVirtualNode); 从给定节点开始重新绘制客户区。如果此节点不可见或尚未初始化,则什么也不会发生。
  • TBaseVirtualTree.InvalidateColumn(Column: TColumnIndex); 使列的客户区部分无效。

无效与重绘

你的另一个问题是关于混淆之间的区别:

  • 正在失效
  • 正在重绘

当您使一个矩形无效时(例如整个表单、整个控件或一些较小的矩形,例如节点、节点及其 children,或列)你告诉 Windows 它需要让控件来绘制自己。即:

the pixels on screen are now invalid and must be repainted

这将在下次要求树自己绘画时发生。 Windows 是 message-based。当您的应用程序运行时,它会处理消息,包括 WM_PAINT 消息。当 VirtualTree 收到 WM_PAINT 消息时,它会绘制要求重新绘制的部分。

这意味着要进行任何绘制,您必须处理消息 - 即您必须让程序运行 "idle".

如果你坐在那里是一个繁忙的循环,永远不要让你的代码退出:

procedure TForm1.Button1Click(Sender: TObject);
begin
   while (true) do 
   begin
      tvItems.Invalidate;
      Sleep(100);
   end;
end;

循环永远不会结束,并且树永远不会有机会实际上画自己。

Delphi 可怕的重绘技巧

Delphi 有一个可怕的 hack,强制 绘制控件。

  • 它假装它是Windows要求控件绘制自己
  • 然后直接跳转到控件的绘制例程

这意味着控件将自行绘制,即使它没有收到来自 Windows 的 WM_PAINT 消息 - 它只是进行涂鸦。

procedure TForm1.Button1Click(Sender: TObject);
begin
   while (true) do 
   begin
      tvItems.Repaint; //force the control to repaint itself
      Sleep(100);
   end;
end;

这是一个丑陋的 hack,因为:

  • 在第一种情况下,我们的代码没有处理 Windows 消息
  • 修改后的情况下还是没做好,想用锤子拧螺丝

在这些情况下正确的解决方案是拥有一个后台线程。并让后台线程向主应用程序发出信号,告知它需要使树无效。然后主程序将收到 WM_PAINT 消息并正常绘制自己。