VirtualTreeView: правильная обработка изменений выбора

Этот вопрос будет казаться очевидным для тех, кто сам не сталкивался с проблемой.

Мне нужно обрабатывать изменения выбора в VTV. У меня есть плоский список узлов. Мне нужно делать вещи со всеми выбранными в данный момент узлами всякий раз, когда

  • Пользователь нажимает кнопку node;
  • Пользователь Shift/Ctrl-clicks node;
  • Пользователь использует клавиши со стрелками для навигации по списку;
  • Пользователь создает выбор, перетаскивая мышью
  • Пользователь удаляет выделение, нажимая на пустое пространство или нажав Ctrl, только выбранный node

и т.д.. Это наиболее распространенное и ожидаемое поведение, как и проводник Windows: при выборе файлов с помощью мыши и/или клавиатуры информационная панель показывает свои свойства. Мне не нужно ничего больше. И здесь я застреваю.

Некоторые из моих исследований следует.


Сначала я использовал OnChange. Казалось, что это хорошо работает, но я заметил странное мерцание, и я обнаружил, что в наиболее распространенном сценарии (один node выбран, пользователь нажимает другой) OnChange запускается дважды:

  • Если старый node не выбран. В это время выбор пуст. Я обновляю свой графический интерфейс, чтобы вместо ярлыков вместо ярлыков отображалась метка "ничего не выбрано".
  • Когда выбран новый node. Я снова обновляю свой графический интерфейс, чтобы показать свойства нового node. Отсюда мерцание.

Эта проблема была googleable, поэтому я обнаружил, что люди используют 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 я не могу знать, должен ли я использовать этот вызов или ждать следующего. Иногда набор выбранных узлов является промежуточным и непригодным, и обработка его вызовет мерцание графического интерфейса и/или нежелательные тяжелые вычисления.

Мне нужны только вызовы, отмеченные знаком "!". в таблице выше. Но нет способа отличить их отнутри. Например: если я нахожусь в "C-" (OnChange, node= nil, SelectedCount = 0), это может означать, что пользователь удалил выделение (тогда мне нужно его обработать) или что они нажали на другой node (тогда я необходимо подождать следующего вызова OnChange при создании нового выбора).


В любом случае, я надеюсь, что мои исследования не нужны. Я надеюсь, что я упустил что-то, что сделало бы решение простым и понятным, и что вы, ребята, захотите указать мне на это. Решение этой головоломки с использованием того, что у меня было до сих пор, создало бы ужасно ненадежную и сложную логику.

Спасибо заранее!

Ответ 1

Задайте для свойства ChangeDelay соответствующее, большее, чем нулевое значение в миллисекундах, например. 100. Это реализует таймер с одним выстрелом Роб Кеннеди предлагает в своем ответе.

Ответ 2

Используйте таймер с одним выстрелом. Когда срабатывает таймер, проверьте, отличается ли выбор, обновите дисплей, если он есть, и отключите таймер. Каждый раз, когда вы получаете потенциальное событие смены выбора (которое, я думаю, всегда OnChange), reset таймер.

Это дает вам возможность ждать события, которое вы действительно хотите, и избегать мерцания. Стоимость представляет собой слегка задержанный пользовательский интерфейс.

Ответ 3

Я предполагаю, что вы, возможно, использовали ответы, приведенные здесь, или даже нашли другое решение, но я хотел бы немного поработать здесь...

В среде NON-Multiselect (я не тестировал ее в среде с несколькими выборами) я нашел довольно простое решение без задержки:

Сохранить глобальный указатель PVirtualNode (Позволяет называть его FSelectedTreeNode). Очевидно, что при запуске вы назначите ему nil.

Теперь, когда вы используете клавиши со стрелками для выбора следующего node, OnTreeChange произойдет дважды. Один раз для node, который будет отменен, и один раз для вновь выбранного node. В вашем событии 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;

Это хорошо работает с моим кодом, и он не имеет мерцания и, по крайней мере, без задержки.

Фокус в том, что вновь выбранный node будет присвоен глобальному указателю, и это произойдет последним. Поэтому, когда вы выбираете другой node после этого, он не будет ничего делать в первом OnTreeChange, потому что тогда глобальный указатель будет таким же, как и node, который будет отменен.

Ответ 4

Вы забыли событие OnStateChange. Это событие будет запущено сразу после любого изменения выбора, и вы сможете обработать все выбранные узлы.

procedure TForm1.vstStateChange(Sender: TBaseVirtualTree; Enter,
  Leave: TVirtualTreeStates);
begin
  if tsChangePending in Leave then
    DoSomething;
end;