问题 VirtualTreeView:正确处理选择更改


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

我需要处理VTV中的选择变化。我有一个扁平的节点列表。我需要随时处理所有当前选定的节点

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

这是最常见和预期的行为,就像Windows资源管理器一样:当您使用鼠标和/或键盘选择文件时,信息面板会显示其属性。我只需要那个。 这就是我陷入困境的地方。

我的一些研究如下。


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

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

这个问题是可谷歌的,所以我发现人们使用OnFocusChange和OnFocusChanging而不是OnChange。但这种方式仅适用于单一选择。通过多个选择,拖动选择和导航键,这不起作用。在某些情况下,Focus事件甚至根本不会触发(例如,通过单击空白区域删除选择时)。

我做了一些调试输出研究,以了解这些处理程序如何在不同的场景中被解雇。我发现的是一团糟,没有任何明显的感觉或模式。

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调用)。


无论如何,我希望我的研究是不必要的。我希望我错过了一些可以使解决方案变得简单明了的东西,并且你们,伙计们,我会指出它。使用我到目前为止解决这个难题会产生一些非常不可靠和复杂的逻辑。

提前致谢!


11126
2017-11-03 13:18


起源



答案:


设置 ChangeDelay 属性为适当的,大于零的值,以毫秒为单位,例如, 100。这实现了Rob Kennedy在他的回答中建议的一次性计时器。


12
2017-11-03 13:58



谢谢,@ TOndrej!我之前从未注意过这个属性。说实话,我不希望这样的事情存在。但这似乎是解决我问题的“官方”方式。我尝试了它并且它有效,但感觉有点尴尬...用计时器解决这些问题对我来说似乎是一个非常糟糕的主意。但如果随着时间的推移没有出现更好的解决方案,我将不得不坚持这一点。 - 13x666
@ 13x666如果你考虑一下,在这种情况下避免闪烁就意味着如果他们一个接一个地跟着“太快”而压制屏幕更新......相反,推迟直到事情(用户输入)“冷静下来”。 - Ondrej Kelle
+1。 @ 13x666,一个计时器实际上是一个 非常 轻量级解决方案等待用户输入“冷静下来”,正如TOndrej所说的那样。它本质上只是对SetTimer API的调用。为了这个目的,我已多次明确地使用计时器,并且成功。用户将不会注意到200毫秒以下的延迟,但是用户会注意到由于不必要地绘制GUI而导致处理后续命令时的闪烁和延迟。 - Cosmin Prund
@TOndrej,是的,资源管理器明显使用延迟。顺便说一下感觉很糟糕,不是吗? :)它必须这样做,以尽量减少硬盘访问速度。在我的情况下,所有内容都存储在内存中,因此唯一可以将我与完美世界无滞后用户体验区分开来的是VTV扭曲的选择处理方法。嗯,VTV在很多其他方面仍然很棒,我仍然喜欢它。 :d - 13x666
@ 13x666,我对许多类似的问题使用了相同的技术,并且用户还没有抱怨延迟,尽管它很明显。当用户快速更改其选择时(或许纠正错误),延迟还可以防止闪烁。最终,它提供了更令人满意的体验,并迅速变得对用户直观。 - Marcus Adams


答案:


设置 ChangeDelay 属性为适当的,大于零的值,以毫秒为单位,例如, 100。这实现了Rob Kennedy在他的回答中建议的一次性计时器。


12
2017-11-03 13:58



谢谢,@ TOndrej!我之前从未注意过这个属性。说实话,我不希望这样的事情存在。但这似乎是解决我问题的“官方”方式。我尝试了它并且它有效,但感觉有点尴尬...用计时器解决这些问题对我来说似乎是一个非常糟糕的主意。但如果随着时间的推移没有出现更好的解决方案,我将不得不坚持这一点。 - 13x666
@ 13x666如果你考虑一下,在这种情况下避免闪烁就意味着如果他们一个接一个地跟着“太快”而压制屏幕更新......相反,推迟直到事情(用户输入)“冷静下来”。 - Ondrej Kelle
+1。 @ 13x666,一个计时器实际上是一个 非常 轻量级解决方案等待用户输入“冷静下来”,正如TOndrej所说的那样。它本质上只是对SetTimer API的调用。为了这个目的,我已多次明确地使用计时器,并且成功。用户将不会注意到200毫秒以下的延迟,但是用户会注意到由于不必要地绘制GUI而导致处理后续命令时的闪烁和延迟。 - Cosmin Prund
@TOndrej,是的,资源管理器明显使用延迟。顺便说一下感觉很糟糕,不是吗? :)它必须这样做,以尽量减少硬盘访问速度。在我的情况下,所有内容都存储在内存中,因此唯一可以将我与完美世界无滞后用户体验区分开来的是VTV扭曲的选择处理方法。嗯,VTV在很多其他方面仍然很棒,我仍然喜欢它。 :d - 13x666
@ 13x666,我对许多类似的问题使用了相同的技术,并且用户还没有抱怨延迟,尽管它很明显。当用户快速更改其选择时(或许纠正错误),延迟还可以防止闪烁。最终,它提供了更令人满意的体验,并迅速变得对用户直观。 - Marcus Adams


使用一次性计时器。当计时器触发时,检查选择是否不同,如果是,则更新显示,并禁用计时器。每次收到潜在的选择更改事件(我认为总是OnChange)时,请重置计时器。

这为您提供了一种等待您真正想要的事件并避免闪烁的方法。成本是一个稍微延迟的UI。


3
2017-11-03 13:50



谢谢你的回答,Rob。我确实在某个时候考虑过这个解决方案,但价格太高了。实际上,如果没有干净的解决方案会显示出来,只需使用每个OnChange都会花费更少:闪烁比延迟更容易忍受。尽管如此,这两种权衡都是丑陋的。 - 13x666
对于没有ChangeDelay属性的控件,这是要走的路。 - Marcus Adams


我假设你可能已经使用了这里给出的答案,甚至找到了另一种解决方案,但我想在这里贡献一点......

在NON-Multiselect环境中(我还没有在多选环境中测试它)我找到了一个非常简单的解决方案,没有延迟:

保留全局PVirtualNode指针(让我们称之为FSelectedTreeNode)。在启动时,显然你会给它分配nil。

现在eveytime使用箭头键盘键选择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执行任何操作,因为全局指针将与要取消选择的节点相同。


0
2017-12-27 12:05