VirtualTreeView Footers

Все мы знаем и любим Virtual TreeView (VT). Бесплатный, быстрый, разнообразный. Mike Lischke подарил миру воистину бесценный инструмент. Настолько бесценный, что Embarcadero весьма активно использует его в своей среде, правда, без включения в стандартный набор компонент. Что само по себе не поддается осмыслению.

Помимо древовидного представления, VT запросто трансформируется в табличный вид. На стыке древа и таблицы у многих возникает желание иметь возможность отображения агрегированных данных. Которое можно транслировать как «хочу футеры, как в Developer Express или EhLib».

В самом VT нет ни намека на возможность реализации футеров. Однако, в очередной раз обратившись к знаменитому демонстрационному примеру Virtual-TreeView\Demos\Advanced, можно убедиться, что могучие возможности VT – это комбинация правильно выставленных свойств и грамотной обработки нужных событий.

Безусловно, попытки сделать футеры в VT были. Были и канули в лету. По причине ошибочности стремления любую проблему облечь в компонент. Прелесть VT в том, что можно решить любую возникающую проблему непосредственно «тут», вот прямо тут в коде проекта сделать маленькое вау-чудо. Просто надо выставить свойство и обработать событие.

Невозможно сделать компонент, решающий все проблемы. Любой компонент ограничивает свободу творчества рамками «заботы» создателя. VT выставил рамки настолько широкие – есть небо, есть земля, дыши и твори – что свобода кажется бесконечной.

Поэтому футеры VT, по крайней мере в контексте этой статьи — это комплекс свойств и обработчиков событий. Такой подход должен закрывать вопросы, типа – а можно будет по центру выровнять текст в футере? Можно. И так тоже можно. Да и так тоже. Как угодно можно.

Постановка задачи

Вкратце, суть задачи представлена на рисунке 1. Нужно из древа на заднем плане получить супер-таблицу с футерами, как на переднем.

Рисунок 1. Постановка задачи и как должно выглядеть.

На входе

Допустим, у нас есть некие данные. Сгруппированные каким-либо, нужным нам, образом. Есть посчитанные агрегированные значения. Для задачи реализации футера это не столь важно, в каком виде представлены эти данные. Задача – отобразить.

На выходе

На выходе хотим видеть таблицу, в которой данные представлены группами, желательно, с цветовым разделением, в стиле, характерном для подобного рода отображения информации. Внизу каждой группы есть полоса с агрегированными значениями. Футер. Если группа свернута, полоса футера этой группы не видна. Внизу таблицы присутствует «глобальный» футер, видимый все время.

Несколько слов в защиту VT

Казалось бы, такая нужная и полезная вещь, как футер, в таком продвинутом и популярном инструменте, как VT, обязана быть. Не обязана.

VT не манипулирует данными, не сортирует, не создает, не уничтожает. Он предоставляет возможности для этого. Это основная концепция — ничего не знать о данных. Как перевозчик, ничего не хочет знать о грузе. Если бы VT был нагружен еще и данными, он потерял бы скорость и сильно ограничил свободу действий.

Футер, как представитель агрегированного сословия, это в первую очередь данные. За данные целиком отвечает программист. В VT достаточно средств и возможностей все организовать самостоятельно.

Формирование групп и данных

Для статьи был написан класс хранилища данных, позволяющий произвольно группировать и агрегировать данные. Его внутренняя реализация в рамках статьи не интересна. На его месте могут быть любые данные, в том числе и несколько наследников TDataSet’ов, один из которых отвечает за данные, другой за агрегированные данные и т.д.

Главное, что для подобной задачи сразу же надо забыть про такую «фишку» VT, как динамическая инициализация данных. Так или иначе, нам нужны сразу все данные, иначе не получится правильно ни сгруппировать, ни агрегировать.

Формирование дерева

[свернуть]

Как видим, все достаточно тривиально. Копируем иерархическую структуру из хранилища в древо с добавлением узла-футера в конец каждой группы. Также, опционально, добавляем главный футер в конец всего набора.

TxIPVTVData – класс хранилища, в котором произведена нужная группировка и расчеты.

TxIPVTVNode – класс узла хранилища, который может быть трех видов – данные, группа или футер.

Таким образом, получили древо, где с каждым узлом связан экземпляр TxIPVTVNode. Обрабатывать OnFreeNode смысла нет, т.к. удаление записи все равно будет (если вообще будет) производиться через интерфейс, освобождение или другие манипуляции с экземпляром лучше делать там.

Для получения экземпляра TxIPVTVNode из узла VT делаем так.

Получение узла Хранилища из узла VT

[свернуть]

В результате всего этого получаем вид древа, который представлен на рис.2.

Рисунок 2. Древо с данным, группами и футерами

Начнем превращать дерево в таблицу.

Группы

Для начала уберем toShowHorzGridLines и toShowTreeLines  из TreeOptions.PaintOptions. Добавим следующие опции — toHideFocusRect и toHideSelection в TreeOptions.PaintOptions.

Далее, нам понадобится разный цвет для групп разного уровня.

Получение цвета уровня

[свернуть]

Далее, обработаем событие OnBeforeCellPaint. В нем нам нужно указать цвет фона для будущей отрисовки содержимого узла.

Красим фон для групп

[свернуть]
Рисунок 3. Покрасили фон групп

Хотелось бы видеть группу полностью, на всю ширину таблицы. Добавим опцию toAutoSpanColumns в TreeOptions.AutoOptions.

Рисунок 4. Фон групп на всю ширину таблицы

Уже похоже на дело. Сделаем группы жирным и выведем данные футера рядом с названием группы.

Для вывода дополнительной информации, статического текста, добавим опцию toShowStaticText в TreeOptions.StringOptions. И обработаем события OnGetCellText и OnPaintText. Первое отвечает за выводимый в ячейку текст, второе за графические параметры отображения текста.

Получение и отображение текста ячейки

[свернуть]
Рисунок 5. Группы и дополнительная информация

Понятно, что на вкус и цвет. Формат отображения можно легко подстроить под свои представления о прекрасном.

Видим, что в принципе уже хорошо, но хочется, чтобы выделение было на всю строку группы. Для этого включим опцию toGridExtensions в TreeOptions.MiscOptions. Также, можем заметить, что фокус не переходит на другие столбцы, перемещается только в пределах главного «древовидного» столбца. Лечим это включением опции toExtendedFocus в TreeOptions.SelectionOptions.

Рисунок 6. Почти удовлетворительные группы

Теперь надо сделать уровни группировки. Вот те самые разноцветные полосы слева. На рисунке 7 видим, как это должно выглядеть. 

Рисунок 7. Что хотелось бы видеть

Разноцветные уровни групп

Создать иллюзию непрерывности можно рисуя в каждой ячейке «древовидного» столбца вертикальные полосы цветом уровня группы. Для определения цвета у нас уже есть функция. Предлагается следующая функция для отрисовки этих полос.

Рисуем полосы групп на уровнях

[свернуть]

Обрабатываем событие OnAfterCellPaint. В нем пишем следующее.

Основная отрисовка

[свернуть]

Сейчас будем постепенно добавлять функционал в этот обработчик.

Рисуем футер

С футером все просто на самом деле. Градировать пока не требуется, это уж на усмотрение программиста или заказчика. Просто закрасим поле футера нужным цветом. Вставим этот фрагмент перед рисованием полос:

Фрагмент для футера в OnAfterCellPaint

[свернуть]

Последующее рисование полос перекрывает и ненужную часть выделения текущего узла и часть футера, «вылезающую» слева за ограничение группы. Также рисуем и текст, т.к. настройка футера может отличаться от настроек столбца в VT. Это вот то самое – «а можно будет по центру?»

Рисунок 8. Футеры в первом приближении

Итак, появились ограничивающие линии. Причина, из-за чего мы исключили из опций рисование горизонтальных линий.

Разделительные линии

Начнем с простого, добавим опцию toShowVertGridLines в TreeOptions.PaintOptions. Т.к. наличие этой опции уже предусмотрели в VTDrawLevels эффект видим сразу.

Однако, возникает вопрос — как посчитать левую координату прочих линий. Как видно на рисунке 9, в зависимости от ситуации, координата должна быть разная. В одних случаях отстоять на определенный интервал от предыдущей линии, в других пересекать ее.

Рисунок 9. Вопрос о координате начала линий

Вначале рисуем лини для ячеек. Там все просто, единственное что, для последней ячейки перед футером меняем цвет пера.

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

Фрагмент для простой ячейки с данными в OnAfterCellPaint

[свернуть]

Для разграничения групп и футеров используем такой код. Вставляем в конец обработчика, после всех отрисовок. Для определения левой координаты линии нас интересует только главный «древовидный» столбец. В конце рисуем вертикальную закрывающую линию.

Фрагмент для разделительных линий в OnAfterCellPaint

[свернуть]
Рисунок 10. Почти идеальные группы/футеры

Займемся нулевым столбцом.

Нулевой столбец

Белое пятно слева – это во первых непорядок, во-вторых здесь задумывается маркер текущей строки а-ля TDBGrid и иже с ним. Подготовим где-нибудь ранее битмап маркера треугольника.

Создание битмапа маркера-треугольника

[свернуть]

Нарисуем нулевой столбец. Разместим этот фрагмент вначале всех рисовок в обработчике.

Рисуем нулевой столбец с маркером

[свернуть]
Рисунок 11. Нулевой столбец с маркером

Несколько режет глаз стиль заголовков. Стал отличаться от общего вида таблицы. Но вид столбцов – это уже свой собственный субъективный взгляд. К теме статьи отношения не имеет.

Остался небольшой нюанс. Картинка свернуть/развернуть находятся прямо у линии. Такое ощущение, что глиф выровнен по правому краю отведенного ему прямоугольника. Привычней как-то, чтобы он был посередине.

Лечим установкой свойства Margin в 0.

Рисунок 12. Окончательный вид

На рис. 12 видим в том числе и главный футер. Который, к сожалению, исчезает из поля видимости при скролировании. 

Глобальный футер

Можно рисовать главный футер в конце поверх всех отрисовок . Но практика показывает, что во-первых, иногда видны «промаргивания» фона под ним, во-вторых, хочется вернуться к теме – все делаю тут и сейчас, вне рамок заботы обо мне автора возможного компонента и поэтому волен делать все что захочу.

Поэтому глобальным футером назначается TPanel.

Сделаем главный футер больше остальных. Потому что он главный. Для этого включим опцию toVariableNodeHeight в TreeOptions.MiscOptions. И обработаем событие OnMeasureItem.

Определяем высоту главного футера

[свернуть]

FBottomSpace посчитаем где-то раньше. У меня это так:

Далее обработаем событие OnAfterPaint, где будем позиционировать панель в необходимое место.

Определяем местонахождение панели главного футера

[свернуть]

Как замечаем, производится анализ видимости панели. Может кому-то не хочется, чтобы глобальный футер был панелью. В демо примере видимость панели регулируется соответствующей галочкой. На панели разместим TLabel и TPaintBox, внутри обработчика OnPaint которого есть вызов функции рисовки футера.

Рисуем футер

[свернуть]

Да, пришлось писать отдельную функцию для рисования футера. Но это в очередной раз показывает, что при таком подходе можно рисовать все, что душе угодно. Можно разместить логотип компании и накидать меток. И вместо рисовок присваивать значения меткам. Одним словом, масса вариантов.

Конкретно в нашем случае сделал кнопку вызова меню. И вместе с меню это выглядит так.

Рисунок 13. Главный футер с кнопкой меню

Проблема с фокусом

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

Рисунок 14. Фокус прячется за панелью футера

Чтобы ликвидировать это досадное обстоятельство, обработаем свойства OnFocusChanged (фокус сменился) и OnFocusChanging (собираюсь сменить фокус – можно?)

Обработчики событий смены фокуса

[свернуть]

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

Допустим, мы скачем в конец таблицы через Ctrl+End. Если бы у нас был главный футер той же высоты, что и остальные, все было бы идеально. Но у нас он другой. Поэтому, собственно, и другой. В итоге увидим, что-то подобное рисунку 14 – неполное отображение узла. Повторное нажатие Ctrl+End приведет все в норму. Но, как ни крути, это досадный глюк.

В связи с чем комплекс мер. Посмотрим на обработчик OnMeasureItem. Там есть такая запись:

То есть, если главный футер-панель видим, высота узла главного футера становится равной нулю. Конечно, при обработке OnFocusChanging в этом случае, фокус, переходя на предпоследний узел, все равно спрячет этот узел под панель. Вот чтобы этого не было, используем свойство BottomSpace. Это свойство задает видимое пространство под последним узлом.

Как уже говорилось, за показ главного футера панели отвечает галочка в интерфейсе. И в ее обработчике есть такой фрагмент:

Таким образом, помимо обработки «фокусных» событий, необходимо сделать нулевым главный футер в таблице и выставить BottomSpace равным высоте панели.

Для смещения фокуса в нужную позицию делаем скрол VT следующей функцией.

Установка фокуса с учетом BottomSpace

[свернуть]

Зачем обнулять высоту узла главного футера, если его можно вообще не добавлять? У нас же есть функция UpdateDataTree, в которой указывается – добавлять или нет главный футер?

Дело в том, что если вдруг (т.е. скорее всего) нам лень делать экспорт своими руками и хотим все сделать средствами VT, узел главного футера нам все таки нужен.

Экспорт в HTML и RTF

[свернуть]

Даже при нулевой высоте, главный футер будет присутствовать в результате экспорта.

Так как опцию — показывать или нет главный футер — вряд ли понадобится переключать в реальности, особо обрабатывать ситуацию не будем. Как настроили один раз, так и отработает. Манипуляции с BottomSpace и высотой узла VT не сильно любит, т.к. вся рисовка и вызовы событий оптимизированы, да и мы не хотим в лишний раз вызывать UpdateData.

Сортировка

Немаловажная тема. Футеры должны всегда располагаться внизу своей группы. Но тут все очень просто. Нам нужно событие OnCompareNodes.

Обработчик OnCompareNodes

[свернуть]

Пара функций из хранилища.

Функции сравнения вариантных типов

[свернуть]

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

Предполагается, что варианты сравнения в реальной жизни могут быть значительно разнообразней. В этом случае надо менять и дописывать OnCompareNodes.

Автоматической сортировки как таковой в VT не предусмотрено, поэтому пишем это дело руками. Обрабатываем событие OnHeaderClick.

Сортировка в столбце по клику

[свернуть]

Со свойствами, которые присутствуют в VT по умолчанию, должно работать. Иначе устанавливаем все, что связано с сортировкой.

Кратко о вышесказанном

Как уже было сказано, предлагаемое решение – это комбинация свойств и событий.

Отличный справочник по свойствам и событиям VT смотрим тут.

Обязательно должны быть

TreeOptions.AutoOptions  toAutoSpanColumnsПозволит рисовать текст группы во всю ширину таблицы. Переносит текст, не помещающийся в данной колонке на соседнюю, если она не содержит текста  
TreeOptions.MiscOptionstoGridExtensionsСимуляция элемента управления а-ля TDBGrid  
TreeOptions.PaintOptionstoShowButtonsОтображать кнопки +/- напротив узлов  
 toShowVertGridLinesОтображать вертикальны линии сетки (не обязательно)  Если снять опцию, просто ячейки не будут разграничены. На отображение футеров и групп не влияет  
 toShowRoot Учитывать отступ для самых верхних узлов первого уровня вложенности, дочерних узлов RootNode  
 toHideFocusRectНе рисовать прямоугольник фокуса по границам узла.  
 toHideSelectionНе рисует бежевый прямоугольник выделения для выделенных узлов, когда само дерево не имеет фокуса.  
TreeOptions.SelectionOptionstoDisableDrawSelectionЗапрещает пользователю добавлять в текущее выделение узлы с помощью прямоугольника выделения  
 toExtendedFocusПозволяет выделять ячейки и редактировать текст во всех колонках, а не только в MainColumn  
TreeOptions.StringOptionstoShowStaticTextВключает статический текст, который отображается рядом с обычным. Используется для вывода данных футера рядом с названием группы.  

Убрать из опций обязательно

TreeOptions.PaintOptionstoShowHorzGridLines Горизонтальные линии будем рисовать сами  
 toShowTreeLines Соединительные лини древа нам не нужны  

Необходимо обработать события:

OnBeforeCellPaint   Подготовить фон для узлов.  
OnAfterCellPaintРисовка групп, футеров, данных, разделительных линий. Основной отрисовщик таблицы.  
OnMeasureItemВысота главного футера. Вообще, высота любого узла. Допустим, захотим группы 0-го уровня сделать прям очень высокими.  
OnAfterPaintПозиционирование панели – главного футера  
OnGetCellTextВернуть VT выводимый в ячейку текст  
OnPaintTextГрафические параметры выводимого текста.  
OnFocusChangedСмена фокуса. Определить и при необходимости отскролировать VT так, чтобы сфокусированный узел не заходил за панель.  
OnFocusChangingОпределить, что надо перекинуть фокус на предпоследний видимый элемент, если фокус хочет получить главный футер.  
OnCompareNodesДля сортировки. Сравнение значений в столбцах  
OnHeaderClickКлик по столбцу. Обеспечить правильную смену направления сортировки и осуществить сортировку.  

Нюансы

Помним про нюансы с BottomSpace и нулевой высотой последнего узла. Также, Margin желательно сделать нулевым. Можно сделать и отрицательным, если того требует предметная область.

Итоговый обработчик события OnAfterCellPaint

Ничто не совершенно

В этом вся и прелесть. Допустим, некоторые вещи делать не стал, чтобы не перегружать и так объемную статью и демо-пример.

  1. Клавиши вправо/влево – раскрывают/сворачивают группу. Сейчас этого нет, а прям просится.
  2. Если на группе нажать вправо, или фокус не на главном столбце, выделение группы пропадет, можно доработать. Можно вообще другой цвет какой-то использовать. Одним словом, творчество.
  3. При фокусе на футере выделение не показано никак, кроме маркера слева. Также, просится доработка.
  4. Можно сделать опционально запрет перевода фокуса на футеры, т.е. проскакивать до данных. Это событие OnFocusChanging. Только надо анализировать последний футер, иначе до последних футеров можно будет добраться только мышкой.
  5. Футеры также можно градировать, но может и мазня пестрая получится. Требуются эксперименты и чувство прекрасного )

Друзья, спасибо за внимание!

Информация о новых статьях смотрим в телеграм-канале.

Не забываем комментировать и подписываться )))


Скачать

Версия 0

Исходники (Delphi XE 7-10) 113 Кб

Исполняемый файл 1.07 Мб

Версия 1

С учетом самой свежей версии Virtual TreeView (7.6) на 29.09.2021 чуть изменился исходник. Также, добавлены подсказки на столбцах, в ответ на вопрос Андрея.

Исходники (Delphi XE 7-10) 113 Кб

Исполняемый файл 1.08 Мб


5 11 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

22 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Vad

Вот блин, ну всё прекрасно, кроме того, что картинки мелкие и их нельзя увеличить кликом!

Vad

Вот теперь отлично, спасибо!
Жду новых статей, очень понравился стиль и подход к их написанию 🙂

PS Что-то, несмотря на чекнутую галку «Сохранить моё имя, email и адрес сайта в этом браузере для последующих моих комментариев», они не сохранились, приходится вводить еще раз 🙁

Андрей

Классная статья!!!
А можете подсказать, как отображать подсказку столбца при наведении на заголовок столбца?

fraks

Такая функция встроена в сам VTV.

fraks

У меня Delphi7 и VirtualTreeview_4.8.7
Хинты показываются стабильно, независимо от фокуса.
Что бы они не гасли, еще делаю в главной форме отключение автопогашения хинтов.

Андрей

Увы, я это уже пробовал — ни чего 🙁 Я пробую на Lazarus v2.0.12

Андрей

Спасибо! Вот он, коварный глюк! А я думал что всё дело в хитрых настройках инспектора, он всё таки огромен. Спасибо!

fraks

Классная статья, причем очень вовремя — как раз опять ковыряюсь с VTree в своем проекте. Использую вместо грида, но вот сейчас захотелось еще вид дерева присобачить.
Из годных идей хотел подкинуть такую — менюшку какие колонки показывать, логично присобачивать прямо на заголовки колонок, по смыслу туда ближе чем в левый нижний угол.
Меню ПКМ на хедере у меня вот такое.
Поля — видимые/невидимые — можно натыкать галок на нужных полях.
Поля — сортировка — бывает нужно посмотреть по каким полям, в каком порядке задана сортировка. У меня можно сортировать по нескольким сразу.
Поля — типы данных и форматирование — это не для пользователя, а в основном для меня как разработчика.

10.jpg
Oleg

Вопрос — а как в VT реализовать следующее: в конкретной колонке (это определяю сам) в определенных ячейках нужно просто сделать при входе мыши подчеркнутым текст и покрашенным в нужный цвет. И при выходе — возврат обратно. Короче говоря, реализация простой реакции на гиперссылку. Странно, что при богатейшем наборе событий в VT похожий случай не рассматривается. Там есть только типа подчеркивание всего текста в строке (когда опция toHotTrack включена, а опция toThemeAware выключена в TreeOptions->PaintOptions). Но это не совсем то ((

Oleg

Проверил — все отлично работает! Большое спасибо! Однако… если усложнить задачу и сделать реакцию только на наведение мыши на текст, а не на всю ячейку в целом?

Oleg

Спасибо! Работает, но… есть небольшой недочет. Если войти в широкую ячейку скажем с «коротким» текстом, но не наводя на текст сразу, а рядом. А потом уже навести на текст — эффекта подчеркивания не будет.

22
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x