VirtualTreeView:正确处理选择更改

12

对于那些没有遇到这个问题的人来说,这个问题似乎很明显。

我需要处理 VTV(虚拟树视图)中的选择更改。我有一个节点的平面列表。每当:

  1. 用户点击一个节点;
  2. 用户 Shift/Ctrl-点击一个节点;
  3. 用户使用箭头键导航列表;
  4. 用户通过拖动鼠标创建选择
  5. 用户通过单击空格或 Ctrl-单击唯一选定的节点删除选择

等等。这是最常见和预期的行为,就像 Windows 资源管理器一样:当您使用鼠标和/或键盘选择文件时,信息面板会显示它们的属性。我仅需要这些。

但这就是我的难点所在。

以下是我的一些研究结果。


起初我使用 OnChange。它似乎工作得很好,但我注意到一些奇怪的闪烁,并发现在最常见的情况下(选定一个节点,用户单击另一个节点),OnChange 会触发两次:

  1. 旧节点被取消选择时。此时选择为空。我刷新 GUI 来显示“没有选择”标签代替所有属性。
  2. 新节点被选中时。我再次刷新 GUI 以显示新节点的属性。因此会出现闪烁。

这个问题可以在谷歌上搜索到,所以我发现人们使用 OnFocusChange 和 OnFocusChanging 来代替 OnChange。但是这种方法仅适用于单选。对于多选、拖选和导航键,这种方法不起作用。在某些情况下,焦点事件甚至根本不触发(例如当通过单击空白处移除选择时)。

我进行了一些调试输出研究,以了解这些处理程序在不同场景下的触发方式。我的研究结果没有任何可见的意义或模式。

C   OnChange
FC  OnFocusChange
FCg OnFocusChanging
-   nil parameter
*   non-nil parameter
!   valid selection


Nodes     User action                   Handlers fired (in order)
selected                
0     Click node                    FCg-*   C*!     
1     Click same                    FCg**           
1     Click another                 C-  FCg**   C*! FC*
1     Ctlr + Click  same            FCg**   C*!     
1     Ctrl + Click another          FCg**   C*! FC* 
1     Shift + Click same            FCg**   C*!     
1     Shift + Click another         FCg**   C-! FC* 
N     Click focused selected        C-! FCg**       
N     Click unfocused selected      C-! FCg**   FC* 
N     Click unselected              C-  FCg**   C*! FC*
N     Ctrl + Click unselected       FCg**   C*! FC* 
N     Ctrl + Click focused          FCg**   C*!         
N     Shift + Click unselected      FCg**   C-! FC* 
N     Shift + Click focused         FCg**   C-!         
1     Arrow                         FCg**   FC* C-  C*!
1     Shift + Arrow                 FCg**   FC* C*! 
N     Arrow                         FCg**   FC* C-  C*!
N     Shift + Arrow (less)          C*! FCg**   FC* 
N     Shift + Arrow (more)          FCg**   FC* C*! 
Any   Ctrl/Shift + Drag (more)      C*! C-!     
0     Click empty                   -           
1/N   Click Empty                   C-!         
N     Ctrl/Shift + Drag (less)      C-!         
1     Ctrl/Shift + Drag (less)      C-!         
0     Arrow                         FCg**   FC* C*!

这篇文章有些难以理解。简单来说,根据具体的用户操作,三个处理程序(OnChange、OnFocusChange和OnFocusChanging)将以随机顺序调用,带有随机参数。在我还需要事件处理的时候,有时FC和FCg根本不会被调用,因此显然我必须使用OnChange。

但下一个任务是:在OnChange中,我无法知道是否应该使用此调用或等待下一个调用。有时所选节点集是中间状态且无用,对其进行处理会导致GUI闪烁和/或不必要的重计算。

我只需要表格中标有“!”的调用。但无法从内部区分它们。例如:如果我在“C-”(OnChange,Node = nil,SelectedCount = 0)中,它可能意味着用户已取消选择(然后我需要处理它),也可能意味着他们单击了另一个节点(然后我需要等待下一个OnChange调用以形成新的选择)。


总之,我希望我的研究是不必要的。我希望我错过了某些能使解决方案简单明了的东西,你们可以指出来给我。根据我目前了解的内容解决这个难题会产生一些极其不可靠和复杂的逻辑。

提前感谢!

4个回答

12

ChangeDelay属性设置为大于零的适当毫秒值,例如100。这将实现Rob Kennedy在他的答案中建议的一次性定时器。


谢谢,@TOndrej!我以前从未注意到这个属性。说实话,我也不会想到这种东西的存在。但这似乎是解决我的问题的“官方”方法。我尝试了一下,它确实起作用,但感觉有点笨拙...用计时器来解决这样的问题对我来说似乎是一个非常糟糕的主意。但如果在一段时间内没有更好的解决方案出现,我就必须坚持这个方案。 - 13x666
2
+1. @13x666,计时器实际上是一种非常轻量级的解决方案,用于等待用户输入“平静下来”,正如TOndrej所说。它本质上只是对SetTimer API的调用。我已经明确地多次使用计时器来达到这个目的,并取得了巨大的成功。用户不会注意到小于200毫秒的延迟,但用户会注意到闪烁和处理后续命令的延迟,这是由于不必要地绘制GUI造成的。 - Cosmin Prund
@TOndrej,如果用户按住箭头键快速操作,我不介意我的GUI在不同列表项的属性下闪烁。如果用户能够以这样的速度执行操作,GUI更新速度对他们来说感觉还可以接受。电脑应该至少和人一样快,不是吗? :) 另一方面,在这种情况下任何延迟都会让人感到不好。我试图避免的是由某些内部魔法引起的闪烁,而不是直接的用户操作。例如,用户输入尽可能平静:单击。然而,在瞬间内进行了两次更新。 - 13x666
1
@TOndrej,是的,Explorer明显使用了延迟。顺便说一句,感觉真的很糟糕,不是吗? :) 它必须这样做以最小化HDD访问,因为它本身就很慢。在我的情况下,所有东西都存储在内存中,所以唯一将我与完美世界无延迟用户体验分开的是VTV对选择处理的扭曲方法。好吧,VTV在很多其他方面仍然很棒,我仍然喜欢它。 :D - 13x666
1
@13x666,我已经在许多类似的问题上使用了相同的技术,用户尚未抱怨延迟,尽管它是可以察觉到的。延迟还可以防止用户快速更改选择时出现闪烁(也许是为了纠正错误)。最终,它呈现了更令人满意的体验,并且很快变得对用户直观。 - Marcus Adams
显示剩余3条评论

3
使用一次性定时器。当定时器触发时,检查选择是否不同,如果是,则更新您的显示,并禁用定时器。每次收到可能更改选择的事件(我认为始终是OnChange),都要重置定时器。
这样可以等待您真正想要的事件,避免闪烁。代价是稍微延迟了用户界面。

谢谢你的回答,Rob。我曾经考虑过这个解决方案,但价格太高了。实际上,如果没有更好的解决方案出现,仅仅使用每个OnChange会更为便宜:闪烁比延迟更容易忍受。不过,两种折衷方案都不够完美。 - 13x666
对于没有ChangeDelay属性的控件,这是可行的方法。 - Marcus Adams

2
你忘记了OnStateChange事件。此事件将在任何选择更改后立即触发,然后您可以处理所有选定的节点。"最初的回答"。
procedure TForm1.vstStateChange(Sender: TBaseVirtualTree; Enter,
  Leave: TVirtualTreeStates);
begin
  if tsChangePending in Leave then
    DoSomething;
end;

0

我猜你可能已经使用了这里给出的答案或者找到了其他解决方案,但是我想在这里做出一点贡献...

在一个非多选环境下(我没有在多选环境中测试过),我发现了一个相当简单的解决方案,而且没有延迟:

保持全局 PVirtualNode 指针(我们称之为 FSelectedTreeNode)。在启动时,你显然会将其赋值为 nil。

现在,每次你使用箭头键选择下一个节点时,OnTreeChange 事件会发生两次。一次是为了取消选择的节点,另一次是为了新选择的节点。在你的 OnTreeChange 事件中,你需要执行以下操作:

  If Node <> FSelectedTreeNode then
    begin
      FSelectedTreeNode := Node;
      If Node = nil then
        {Do some "Node Deselected" code}
      else
        {Do whatever you want to do when a new node is selected}
    end;

这个方法在我的代码中非常有效,没有闪烁,至少没有延迟。

诀窍在于新选择的节点将被分配给全局指针,并且它将是最后发生的。因此,当您之后选择另一个节点时,它不会对第一个OnTreeChange做任何事情,因为全局指针将与被取消选择的节点相同。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接