首页 > 解决方案 > 我应该如何更新 Virtual TreeView 中的节点?

问题描述

我正在使用 Delphi XE3 和 Virtual TreeView。

我想用Virtual TreeView来实现一棵树,当点击“开始”按钮时,程序会递归搜索一个驱动器下的所有文件和文件夹,然后将它们一一添加到树中,就像Windows资源管理器一样。此外,应该有一个数字表示文件夹下的文件和子文件夹的数量,使用如下静态文本:

VirtualTreeView - 同一节点中不同颜色的文本

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

因此,每当文件/子文件夹的数量发生变化时,我认为刷新节点的方式如下:

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

  2. 调用 tvItems.InvalidateNode(PNode)。

  3. 调用 tvItems.RepaintNode(PNode)。

  4. 调用 tvItems.UpdateAction。

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

标签: delphivirtualtreeview

解决方案


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

如果你的树在屏幕上:

在此处输入图像描述

您可以简单地调用:

tvItems.Invalidate;

这告诉 Windows整个树现在“无效”,需要重新绘制。我可以表示这个“无效”区域,该区域将在下次树绘制自身时更新:

在此处输入图像描述

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

性能改进

很多时候,强制棵树重新绘制自己是完全合理的。

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

执行此操作的 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 没有意识到这一点,但您的控件代表一棵树,以及树和节点。有时您可能想要使“节点”的矩形无效:

在此处输入图像描述

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

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

所以:

tvItems.InvalidateNode(someNode);

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

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).

当您的树有孩子时,这很有用:

在此处输入图像描述

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

tvItems.InvalidateChildren(someNode, True);

和其他辅助方法

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

  • 让某个有趣的矩形失效
  • 调用 Windows.InvalidateRect

那是:

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

无效与重绘

您的另一个问题是关于以下区别的混淆:

  • 无效
  • 重新粉刷

当您使一个矩形(例如整个窗体、整个控件或一些较小的矩形,如节点、节点及其子级或列)无效时,您是在告诉 Windows 它需要请求控件来绘制自身。那是:

屏幕上的像素现在无效,必须重新绘制

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

这意味着任何和所有的绘画都发生,你必须处理消息——即你必须让你的程序“空闲”

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

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

循环永远不会结束,树也永远不会有机会实际绘制自己。

德尔福可怕的重绘黑客

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

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

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

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

这是一个丑陋的黑客,因为:

  • 在第一种情况下,我们的代码没有处理 Windows 消息
  • 在修改后的情况下,我们仍然没有做正确的事情,并试图用锤子拧螺丝

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


推荐阅读