VirtualTreeView: properly handling selection changes
Asked Answered
B

4

12

This question will seem obvious to those who haven't encountered the problem themselves.

I need to handle selection changes in VTV. I have a flat list of nodes. I need to do stuff with all currently selected nodes whenever

  1. User clicks a node;
  2. User Shift/Ctrl-clicks a node;
  3. User uses arrow keys to navigate the list;
  4. User creates selection by dragging the mouse
  5. User removes selection by clicking on empty space or Ctrl-clicking the only selected node

etc. It's the most common and expected behavior, just like Windows Explorer: when you select files with mouse and/or keyboard, the information panel shows their properties. I need nothing more than that. And this is where I get stuck.

Some of my research follows.


At first I used OnChange. It seemed to work well, but I noticed some strange flickering and I found that in the most common scenario (one node is selected, the user clicks another one) OnChange is fired twice:

  1. When the old node is deselected. At this time the selection is empty. I refresh my GUI to show "nothing is selected" label in place of all the properties.
  2. When the new node is selected. I refresh my GUI again to show the properties of new node. Hence the flickering.

This problem was googleable, so I found that people use OnFocusChange and OnFocusChanging instead of OnChange. But this way only works for single selection. With multiple selection, drag-selection and navigation keys this doesn't work. In some cases Focus events don't even fire at all (e.g. when selection is removed by clicking empty space).

I did some debug output study to learn how these handlers are fired in different scenarios. What I found out is a total mess without any visible sense or pattern.

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*!

This is quite hard to read. In the nutshell it says that depending on the specific user action, the three handlers (OnChange, OnFocusChange and OnFocusChanging) are called in random order with random parameters. FC and FCg are sometimes never called when I still need the event handled, so it is obvious I have to use OnChange.

But the next task is: inside OnChange I can't know if I should use this call or wait for the next one. Sometimes the set of selected nodes is intermediate and non-useful, and processing it will cause GUI flickering and/or unwanted heavy calculations.

I only need the calls that are marked with "!" in the table above. But there is no way to distinguish them from inside. E.g.: if I'm in "C-" (OnChange, Node = nil, SelectedCount = 0) it could mean that user removed selection (then I need to handle it) or that they clicked another node (then I need to wait for the next OnChange call when new selection is formed).


Anyway, I hope my research was unnecessary. I hope that I'm missing out something that would make the solution simple and clear, and that you, guys, are going to point it out for me. Solving this puzzle using what I have so far would generate some terribly unreliable and complex logic.

Thanks in advance!

Bullpen answered 3/11, 2011 at 13:18 Comment(0)
D
12

Set the ChangeDelay property to an appropriate, greater than zero value in milliseconds, e.g. 100. This implements the one-shot timer Rob Kennedy suggests in his answer.

Decease answered 3/11, 2011 at 13:58 Comment(8)
Thanks, @TOndrej! I never noticed this property before. And I wouldn't expect such thing to exist, to be honest. But this seems to be the 'official' way to solve my problem. I tried it and it works, but feels a little awkward... solving such problems with timers seems like a really bad idea to me. But if no better solution comes up over time, I'll have to stick to this one.Bullpen
@Bullpen if you think about it, avoiding flicker in this case means supressing screen updates if they follow one after another "too fast"... instead, deferring until things (user input) "calm down".Decease
+1. @13x666, a timer is actually a very light weight solution to wait for user input to "calm down", as TOndrej puts it. It's essentially just a call to the SetTimer API. I've explicitly used timers for this purpose many times, with grate success. The user will not notice the sub-200-ms delay, but the user will notice flicker and delays in processing subsequent commands caused by unnecessarily painting the GUI.Seedtime
@TOndrej, I don't mind my GUI flickering with properties for different list items if, say, the user holds down an arrow key. If the user manages to perform actions this fast, GUI update with the same speed feels OK for them. PC is supposed to be at least as fast as a human, isn't it? :) On the other hand, any delay in this case will always feel bad. What I'm trying to avoid is flickering caused by some internal magic, not direct user actions. E.g., user input is as calm as it can be: single click. Yet, two updates within split-second.Bullpen
After all, I think I'm going to use this solution. Thanks again!Bullpen
@Bullpen Try, for example, Windows Explorer: it doesn't update the right pane immediately when you navigate up and down the treeview using the keyboard.Decease
@TOndrej, yes, Explorer clearly uses a delay. And it feels really bad by the way, doesn't it? :) It has to do so to minimize HDD access which is slow by itself. In my case everything is stored in memory, so the only thing that separates me from the perfect-world lag-less user experience is VTV's twisted approach to selection handling. Well, VTV is still great in so many other things, I still love it. :DBullpen
@13x666, I've used the same technique for many similar problems, and the users have yet to complain about the delay, though it is noticeable. The delay also prevents flicker when the user changes their selection quickly (perhaps correcting a mistake). Ultimately, it presents a more satisfying experience and quickly becomes intuitive to the user.Considering
G
3

Use a one-shot timer. When the timer fires, check whether the selection is different, update your display if it is, and disable the timer. Each time you receive a potential selection-changing event (which I think is always OnChange), reset the timer.

This gives you a way of waiting for the event you really want and avoid flickering. The cost is a slightly delayed UI.

Geordie answered 3/11, 2011 at 13:50 Comment(2)
Thanks for the answer, Rob. I did consider this solution at some point, but the price is too high. Actually, if no clean solution will show itself, simply using every OnChange will cost less: flicker is easier to tolerate than delay. Still, both trade-offs are ugly.Bullpen
For controls without a ChangeDelay property, this is the way to go.Considering
D
2

You forgot OnStateChange event. This event will be fired right after any selection change and you can then handle all selected nodes.

procedure TForm1.vstStateChange(Sender: TBaseVirtualTree; Enter,
  Leave: TVirtualTreeStates);
begin
  if tsChangePending in Leave then
    DoSomething;
end;
Dulosis answered 18/5, 2019 at 10:18 Comment(0)
L
0

I assume that you might have used the answers given here or even found another solution but I would like to contribute a bit here...

In a NON-Multiselect environment (I have not tested it in a multi-select environment) I have found a quite simple solution without the delay:

Keep a global PVirtualNode pointer (Lets call it FSelectedTreeNode). On startup obviously you will assign nil to it.

Now eveytime you use your arrow keyboard keys to select the next node the OnTreeChange will happen twice. Once for the node that will be deselected and once for the newly selected node. In your OnTreeChange event you do the following:

  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;

This works quite well with my code and it has no flicker and at least no delay.

The trick is that the newly selected node will be assigned to the global pointer and it will happen last. So when you select another node afterwards it will not do anything on the first OnTreeChange because then the global pointer will be the same as the node being deselected.

Larrikin answered 27/12, 2015 at 12:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.