🟦 🟦 🟦
Компонент отображения деревьев и списков.
Руководство по эксплуатации и приемы использования в проектах.
Впечатляющую широту возможностей и гибкость компонента наглядно демонстрирует DRKB Explorer. Все деревья и списки в приложении реализованы с помощью Virtual TreeView.
Все чаще стали встречаться вопросы по этому, безусловно, отличному компоненту, и все больше программистов переписывают свои проекты для использования с VT (здесь и далее Я буду применять сокращение «VT», вместо «Virtual Treeview»). Соответственно назрела идея написания этой статьи. Да и не просто назрела, а стала необходимостью, так как существующий по компоненту официальный документ не приводит толком никакого кода, ограничиваясь лишь пустым описанием возможностей и иерархий классов. В этой статье Я хотел бы осветить следующее:
-
Назначение компонента;
-
Его преимущества или недостатки перед стандартными
TTreeView
иTListView
(да, да, несмотря на свое название, VT является помесью и того и другого одновременно); -
Приемы и примеры использования компонента в Ваших проектах;
-
А также предостеречь Вас о возможных ошибках при неправильном использовании компонента.
Сразу хочу сказать, что компонент поначалу может показаться Вам слишком громоздким и тяжелым. Но уверяю Вас, как только Вы сделаете первые шаги и начнете работать с этим компонентом, Вы долго потом будете удивляться, почему сразу все не было реализовано именно так, как это сделано в VT.
История компонента начинается в дремучем 1999 году, когда его автор и по существу единственный разработчик вообще, Mike Lischke, после релиза TreeNT (прародителя VT) устроился на новое место работы. Внедряя TreeNT в корпоративную библиотеку компании, он впервые осознал как велико различие между требованиями к индивидуальному, профессиональному программному обеспечению и корпоративным разработкам. TreeNT был враппером вокруг Microsoft'ского CommCtl32.dll
компонента (TTreeView
) и потому нес в себе все самые существенные его недостатки, а именно: низкую скорость работы (добавление 5000-6000 узлов занимало несколько минут), сильную зависимость от версий библиотеки и отсутствие кроссплатформенности.
Было принято решение написать компонент отображения дерева «с нуля» на чистом VCL, который был бы ориентирован на работу с виртуальными данными. Напомню, что это принцип, при котором дерево абсолютно ничего не знало бы о данных своих узлов, даже их текст, например. Единственное, о чем знает VT это структура дерева, представленная, грубо говоря, связанным списком, где каждый элемент — запись (TVirtualNode
), хранящая служебную информацию узла (настройки, состояние, индекс и т.д.). О том, как дерево управляется с данными, будет сказано позже.
Текущая версия компонента (4.3
на момент написания этих строк) реализует прорву возможностей. Перечислять Я их здесь, понятное дело, не буду – этот список займет около двух-трех страниц печатного текста. Скажу только, что все, все, что Вы могли бы захотеть сделать с деревом, Вы сделаете с VT. Думаю, для Вас также приятно будет узнать, что VT используется в средах Borland, начиная с BDS 3 (Delphi 2005), что можно считать гарантом его безупречной стабильности в работе.
Все бы хорошо, но какая польза может быть от этого большого, как уж ни крути, компонента, скажем, для вашей программы-базы данных телефонной книжки или миниатюрного просмотрщика папок, где количество узлов вряд ли когда-нибудь превысит сотню? На самом деле всякая 😊
Помимо своих скоростных преимуществ, VT позволяет контролировать каждый, даже самый маленький механизм своей работы. Около ста (!) событий позволят Вам изменить отрисовку дерева (например, добавить градиентную заливку), увеличить высоту узлов, цвет и стиль текста, добавить всплывающую подсказку для обрезающегося длинного текста или нарисовать подсказку самому, с помощью Ваших компонентов и методов.
Также можно самому отрисовывать колонки, заголовок каждой колонки, отслеживать малейшие изменения дерева, определять порядок сортировки, добавлять собственные редакторы в дерево (кроме обычного текстового поля вроде TEdit
) и т.д. Даже самая неприхотливая программка, работающая со списками, тут же преобразится от использования VT, прибавив в профессиональности, интерактивности и технологичности.
Прежде чем мы начнем, взгляните на эту схему, чтобы ознакомиться с «внутренностями» VT.
Как видите, «Virtual Treeview» это всего лишь общее название для двух классов: TVirtualDrawTree
и TVirtualStringTree
, которые, в свою очередь, являются потомками базового для всех деревьев класса — TBaseVirtualTree
.
Достаточно истории и дифирамб, давайте приступим ближе к сабжу и напишем свое первое приложение с VT. Скачивайте пакет для своей версии Delphi и приготовьтесь к нашим увлекательным экспериментам 😊
2.1. Базовые операции. Создание колонок. Управление контентом дерева. Инициализация данных для дерева.
В этой главе нашей с Вами задачей будет разобраться в том, как VT управляет своими виртуальными данными, а также научиться грамотно заполнять дерево и работать с его структурой. Здесь же будут рассмотрены обработчики самых базовых событий компонента, без которых его работа невозможна.
Итак, Вы кинули компонент на форму. Перво-наперво, неплохо было бы заполнить дерево какими-нибудь узлами, подумаете Вы. Но не тут-то было. В этом заключается первое неудобство компонента — все данные загружаются в дерево исключительно и только в рантайме. Тем не менее, предусмотрено свойство RootNodeCount
, настраиваемое из Object Inspector, которое позволяет добавить в дерево n-ое количество корневых элементов.
Самым корневым элементом, родителем всех веток является RootNode
. Это следует учитывать при рекурсивных проходах дерева. Соответственно у RootNode
в поле Parent
будет значиться уже nil
, и в самом дереве она видна, естественно, не будет.
При изменении этого свойства дерево очищается и RootNode
заполняется дочерними элементами. В любом случае, забивать данными эти узлы нам все равно придется в рантайме 😊
Как уже было сказано выше, физически «заполнить» VT данными невозможно. Для работы с ними компонент использует мощный событийный механизм. Т.е. все данные, которые Вы хотите отобразить в дереве, Вы будете отображать с помощью событий отрисовки, получения текста и т.д. Рассмотрим пример.
У нас есть некая простая база данных телефонной книжки, представленная двумя массивами:
Names: array[0..4] of WideString = (
'Вася',
'Петя',
'Маша',
'Костя',
'Дима'
);
Phones: array[0..4] of WideString = (
'433-56-49',
'545-67-79',
'777-50-50',
'911-03-05',
'02'
);
Как Вы уже, наверное, заметили, нам потребуется 2 колонки для отображения данных в нашей VT-таблице. Добавление колонок осуществляется через класс TVTHeader
и его поле Columns
, обитающий в поле Header
у VT. Самое время сделать паузу и рассмотреть поближе наиболее интересные настройки и свойства этого класса, назначение которого — управлять колонками. Я буду уделять внимание только самым существенным настройкам, думаю, Вы и сами знаете, зачем компоненту могут понадобиться свойства наподобие Color
, ImageIndex
или Text
.
-
AutoSizeIndex — позволяет Вам указать какая из колонок будет иметь приоритет для авто-подбора ширины (грубо говоря, какая колонка будет растягиваться при изменении ширины дерева). Работает только вместе с включенной опцией hoAutoResize.
-
Columns — непосредственно редактор колонок. Каждая колонка представляет собой экземпляр класса
TVirtualTreeColumn
в коллекции. Обратим на него внимание:-
Layout — определяет положение глифа (картинки) на заголовке колонки.
-
Margin — задает отступ для текста заголовка.
-
MaxWidth, MinWidth — максимально и минимально допустимая ширина колонки соответственно.
-
Options — индивидуальные настройки колонки, они имеют больший приоритет, чем настройки
TVTHeader
, т.е. могут активировать или выключать какие-то свойства для данной конкретной колонки, независимо от настроек всего заголовка.-
coAllowClick — аналог свойства HotTrack для
TListView
и его колонок. При наведении курсора на заголовок колонки, он будет изменять свой вид. -
coDraggable — определяет можно ли таскать колонку мышью по заголовку, меняя ее положение.
-
coEnabled, coParentBiDiMode, coParentColor, coResizeable — определяют состояние, направление текста (для среднеазиатских языков, например), цвет колонки, а также можно ли изменять ширину колонки.
-
coShowDropMark — определяет будет ли на заголовке колонки отрисовываться небольшой значок, показывающий направление вставки перетаскиваемой колонки.
-
coAutoSpring — очень интересное свойство. Оно определяет будет ли колонка поддерживать свойство
hoAutoSpring
уTVTHeader
. Назначение этих свойств будет рассмотрено чуть ниже. -
coFixed — если это свойство включено, то данная колонка будет полностью заблокирована (прокрутка, доступ к узлам и т.д.) и не будет отвечать на запросы пользователя. Очень полезно при проектировании многопотоковых приложений, когда Вы хотите на определенный момент времени запретить пользователю вмешиваться в содержимое этой колонки.
-
-
Position — положение колонки в заголовке.
-
Style — позволяет Вам указать будет ли колонка отрисовываться по умолчанию (vsText) или Вы хотите переопределить ее процедуру отрисовки и все делать сами (vsOwnerDraw).
-
-
MainColumn — определяет индекс основной колонки. Именно элементы этой колонки будут выделяться синим цветом и получать фокус при выделении, и именно напротив элементов этой колонки будут располагаться отметки и другие дополнительные элементы управления дерева.
-
Options — настройки заголовка. Вот тут начинается самое интересное 😊
-
hoAutoResize — если включено, то колонка с индексом из свойства AutoResizeIndex будет занимать все доступное пространство дерева при изменении его ширины. Ширина такой колонки будет равна: ширина дерева — ширина всех других колонок. Во всем дереве, понятное дело, может быть только одна такая колонка.
-
hoColumnResize — Глобальное свойство, определяющее можно ли менять ширину колонок.
-
hoDblClickResize — при двойном клике на данной колонке она примет ширину самого длинного ее элемента. Рекомендуется держать в
True
. Опция подавляется свойством hoAutoResize. -
hoDrag — определяет можно ли перетаскивать колонки.
-
hoRestrictDrag — Не дает курсору тянуть заголовок перетаскиваемой колонки к низу или вверх.
-
hoShowSortGlyphs — определяет будет ли на заголовке колонки отрисовываться небольшой значок, показывающий текущее направление сортировки.
-
hoAutoSpring — вот то свойство, о котором нужно сказать отдельно. Оно позволяет колонкам с включенным свойством
coAutoSpring
пропорционально изменять свою ширину при изменении ширины дерева. Учтите, однако, что это свойство подавляется свойствомhoAutoResize
.
-
-
SortColumn — индекс колонки, относительно содержания которой сортируется дерево.
-
SortDirection — направление сортировки колонки с индексом SortColumn.
-
Style — позволяет изменить внешний вид заголовка. Активно только при выключенных темах оформления Windows.
Добавьте две колонки и выставьте настройки по своему вкусу. Я установил свойство AutoSpring
у всех элементов в True
, сделал заголовок видимым и добавил ему немного высоты. Теперь нам во всеоружии можно вернуться к примеру с записной книжкой, чтобы грамотно наполнить дерево контентом.
Для добавления веток в дерево у VT существует два базовых метода. Это AddChild
:
function TBaseVirtualTree.AddChild(Parent: PVirtualNode;
UserData: Pointer = nil): PVirtualNode;
И InsertNode
:
function TBaseVirtualTree.InsertNode(Node: PVirtualNode;
Mode: TVTNodeAttachMode; UserData: Pointer = nil): PVirtualNode;
Оба они возвращают указатель на добавленный узел. Последний отличается от первого лишь тем, что позволяет указать позицию добавляемого узла в свойстве Mode
:
-
amNoWhere — узел не добавляется вообще.
-
amInsertBefore — узел добавляется на позицию выше по отношению к Node.
-
amInsertAfter — узел добавляется на позицию ниже по отношению к Node.
-
amAddChildFirst — узел добавляется первым дочерним элементом в Node.
-
amAddChildLast — узел добавляется последним дочерним элементом в Node.
В нашем первом примере мы коснемся лишь первого метода. Заполним, наконец, наше дерево:
procedure TfrmMain.BtnLoadClick(Sender: TObject);
var
i: Integer;
begin
for i := 0 to Length(Names) - 1 do
VT.AddChild(nil);
end;
Здесь мы просто добавили количество веток, равное количеству имен в записной книжке. Но большой пользы от пустых узлов не добьешься. Синхронизируем наше дерево с данными. В частности, нам необходимо отобразить текст из записной книжки. «Заполнение» дерева текстом осуществляется через событие OnGetText
:
procedure TfrmMain.VTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString);
begin
case Column of
// Используем остаток от деления. Массив может неожиданно кончиться
// после нескольких нажатий BtnLoad подряд без очистки.
// Спасибо OverLord за это исправление (the__teacher@mail.ru).
// Текст для колонки имени
0: CellText := Names[(Node.Index) mod (Length(Names))];
// Текст для колонки телефонного номера
1: CellText := Phones[(Node.Index) mod (Length(Phones))];
end;
end;
Первый параметр события содержит ссылку на экземпляр дерева, вызвавшего это событие, второй — узел, для которого получаем текст, третий — индекс колонки, для которой берется текст, четвертый — тип текста, будет рассмотрен позже, и, наконец, пятый — строка, которую мы и должны наполнить данными. В нашем примере мы просто берем текст из двух массивов для соответствующих колонок.
Теперь, когда мы реализовали обработку этого события, мы также можем получать текст посредством метода Text
, который в свою очередь вызывает событие OnGetText
для получения данных. Сделано это для Вашего удобства, чтобы не писать сотню раз код извлечения данных из узла.
property Text[Node: PVirtualNode; Column: TColumnIndex]: WideString;
Вы наверняка заинтересовались, а что такое CellText
? Ведь дереву не нужны никакие данные, следственно и передавать ничего не надо. Правильно, этот параметр нужен дереву только при перерисовке дерева или при вызове вышеупомянутого Text
. Он нигде не хранится и заполняется заново при каждом событии отрисовки.
Мы рассмотрели довольно простой пример. Но что делать, если Вы оперируете с многоуровневыми динамическими данными? Хранить их все в одном единственном динамическом массиве — затея на грани невозможности по многим причинам. А Вам при этом требуется удалять и добавлять ветки, изменять их содержимое и т.д.?
К счастью, в компоненте предусмотрена возможность хранения некоего указателя на данные для каждого узла, в области памяти которого мы сможем разместить любые нужные нам данные для каждой ветки.
Рассмотрим следующий пример.
Создадим универсальную телефонную книжку, которую динамически можно было бы изменять.
Для начала, решим, как на этот раз мы будем хранить данные каждой ветки. Вариант с массивами исключен, так что же тогда? Для этого воспользуемся записями (record
), которые и будут храниться в указателе на данные для каждого узла:
type
PPhoneNode = ^TPhoneNode;
TPhoneNode = record
Name, // Имя контакта
Phone: WideString; // Телефон
end;
Но прежде чем мы напишем код для добавления новых веток, Я хотел бы обратить Ваше внимание на важнейшую деталь. Поскольку VT возвращает указатель на данные, он должен как-то определять положение этих данных в памяти, чтобы мы вдруг не получили nil
или вообще другие данные, к дереву никак не относящиеся. Для этого в дереве предусмотрено свойство NodeDataSize
, обозначающее размер данных для каждого узла. С помощью этого свойства дерево способно находить именно данные определенного узла посредством метода GetNodeData
:
function TBaseVirtualTree.GetNodeData(Node: PVirtualNode): Pointer;
Разработчик обязан заполнить это свойство, если он собирается использовать такой подход работы с данными, иначе компонент сообщит об ошибке инициализации.
Делается это предельно просто, с помощью стандартной Дельфийской функции SizeOf
, возвращающей размер блока памяти для определенного типа данных. Делать это лучше сразу после создания формы в обработчике OnCreate
:
procedure TfrmMain.FormCreate(Sender: TObject);
begin
VT.NodeDataSize := SizeOf(TPhoneNode);
end;
В нашем случае размер данных будет равен 8 байтам (по четыре на каждый WideString).
Теперь можно смело заполнять наше дерево узлами:
procedure TfrmMain.BtnAddClick(Sender: TObject);
var
NewNode: PVirtualNode;
NewPhone: PPhoneNode;
begin
NewNode := VT.AddChild(VT.FocusedNode);
NewPhone := VT.GetNodeData(NewNode);
if Assigned(NewPhone) then
with NewPhone^ do
begin
Name := EdName.Text;
Phone := EdPhone.Text;
end;
end;
Все просто: мы добавляем ветку, получаем указатель на область ее данных в памяти и заполняем эти самые данные.
Позаботимся и об удалении существующих узлов. Так можно удалить узел, имеющий на данный момент фокус:
procedure TfrmMain.mEntryClick(Sender: TObject);
begin
VT.DeleteNode(VT.FocusedNode);
end;
Есть также способ удалить лишь дочерние элементы узла, оставив его самого:
procedure TfrmMain.mChildrenClick(Sender: TObject);
begin
VT.DeleteChildren(VT.FocusedNode);
end;
Рассмотрим теперь и вставку новых элементов с помощью метода InsertNode
:
//---------------------------------------------------------------------------
// Вставляем новый элемент перед активным узлом
//---------------------------------------------------------------------------
procedure TfrmMain.BtnInsertBeforeClick(Sender: TObject);
var
NewNode: PVirtualNode;
NewPhone: PPhoneNode;
begin
NewNode := VT.InsertNode(VT.FocusedNode, amInsertBefore);
NewPhone := VT.GetNodeData(NewNode);
if Assigned(NewPhone) then
with NewPhone^ do
begin
Name := EdName.Text;
Phone := EdPhone.Text;
end;
end;
//---------------------------------------------------------------------------
// Вставляем новый элемент после активного узла
//---------------------------------------------------------------------------
procedure TfrmMain.BtnInsertAfterClick(Sender: TObject);
var
NewNode: PVirtualNode;
NewPhone: PPhoneNode;
begin
NewNode := VT.InsertNode(VT.FocusedNode, amInsertAfter);
NewPhone := VT.GetNodeData(NewNode);
if Assigned(NewPhone) then
with NewPhone^ do
begin
Name := EdName.Text;
Phone := EdPhone.Text;
end;
end;
//---------------------------------------------------------------------------
// Вставляем новый элемент первым дочерним элементом активного узла
//---------------------------------------------------------------------------
procedure TfrmMain.BtnChildFirstClick(Sender: TObject);
var
NewNode: PVirtualNode;
NewPhone: PPhoneNode;
begin
NewNode := VT.InsertNode(VT.FocusedNode, amAddChildFirst);
NewPhone := VT.GetNodeData(NewNode);
if Assigned(NewPhone) then
with NewPhone^ do
begin
Name := EdName.Text;
Phone := EdPhone.Text;
end;
end;
//---------------------------------------------------------------------------
// Вставляем новый элемент последним дочерним элементом активного узла
//---------------------------------------------------------------------------
procedure TfrmMain.BtnChildLastClick(Sender: TObject);
var
NewNode: PVirtualNode;
NewPhone: PPhoneNode;
begin
NewNode := VT.InsertNode(VT.FocusedNode, amAddChildLast);
NewPhone := VT.GetNodeData(NewNode);
if Assigned(NewPhone) then
with NewPhone^ do
begin
Name := EdName.Text;
Phone := EdPhone.Text;
end;
end;
Я специально не стал разделять процедуры на блоки для большей наглядности, но за Вами остается отдать дань полиморфизму, выделив повторяющиеся блоки кода в отдельные методы 😊
💡 Проект, демонстрирующий работу с данными в VT, заполнение дерева и редактирование, лежит в папке Fig 1.1.
Дерево из простых надписей не очень-то радует глаз пользователя. Давайте добавим интерактивности в наши программы. В частности реализуем картинки для узлов, статический текст, редактирование узлов встроенным редактором VT, сортировку, поиск по инкременту и перебор всех элементов дерева.
Продолжая нашу тему записной книжки, для всего этого нам потребуется запись примерно такого содержания:
type
PPhoneNode = ^TPhoneNode;
TPhoneNode = record
Name, // Первая колонка
Mobile, // Вторая колонка
HomePhone: WideString; // Это будет статический текст второй колонки
Enabled, // Для отображения состояния выключенности
Editable: Boolean; // Можно ли редактировать узел
ImageIndex: Integer; // Индекс картинки в TImageList
Fg, Bg: TColor; // Цвет текста и фона соответсвенно
end;
Начнем с картинок. Представим, что мы хотим позволить пользователю выбрать пол контакта и при этом менять картинку на более темную, как это делает IE при наведении на нее курсором. Нам понадобится три TImageList
, чтобы на их примере показать как в VT можно сделать отображение 3-х состояний картинки (нормальная, активная, выключенная). Если Вам лень или просто негде взять таковые, то можете забрать их из моего примера Fig 1.2.
По идее, картинки к VT подключаются через всем известное свойство Images
. Однако Я в процессе работы с компонентом часто замечал странные глюки картинок, подключенных таким методом. В компоненте помимо этого есть также свойства StateImages
и CheckImages
. Ни в хелпе, ни в исходниках не дается никакой существенной информации по всем этим трем параметрам, поэтому Я не могу точно сказать в каких случаях какое свойство использовать. Просто запомните, если Вы заметите какие-либо странности в отрисовке картинок, просто поменяйте используемое свойство. В данном примере Я использовал свойство StateImages
.
Получить картинку в VT можно с помощью двух почти одинаковых событий:
-
OnGetImageIndex
; -
OnGetImageIndexEx
;
Если Вы посмотрите на их входные параметры, то заметите, что отличаются они лишь на один параметр: второе событие позволяет указывать экземпляр TImageList
, из которого будет использоваться картинка. В нашем случае это идеальный вариант:
procedure TfrmMain.VTGetImageIndexEx(Sender: TBaseVirtualTree;
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
var Ghosted: Boolean; var ImageIndex: Integer;
var ImageList: TCustomImageList);
var
PhoneNode: PPhoneNode;
begin
if Column > 0 then
Exit; // Картинка будет отображаться только в первой колонке
ImageIndex := -1;
PhoneNode := VT.GetNodeData(Node);
if Assigned(PhoneNode) then
begin
ImageList := Self.ImageList; // Обычные картинки
// HotNode — узел, над которым находится курсор,
// т.е. активный.
if Node = Sender.HotNode then
ImageList := ImageListHot; // Активные картинки
if not PhoneNode^.Enabled then
ImageList := ImageListDisabled; // Выключенные картинки
ImageIndex := PhoneNode^.ImageIndex;
end;
end;
Но не забывайте также и про обычное событие. Чаще всего его реализации вполне достаточно.
Чтобы пример работал корректно нужно также включить опцию TreeOptions.PaintOptions.toHotTrack
, чтобы VT имел доступ к HotNode
узлу. Т.е. к узлу, над которым в данный момент «завис» курсор.
Рассмотрим теперь статический текст. Данная возможность предусмотрена в VT для отображения статического, то есть не редактируемого обычными средствами текста. Это позволит Вам запретить редактирование такого текста стандартным редактором VT. Тем не менее, он будет отображаться рядом с обычным текстом. Посмотрим на нашу телефонную книжку. Представьте, что необходимо рядом с номером мобильного указать домашний телефон. При этом мобильники некоторые люди меняют довольно часто, а вот домашний телефон иногда остается на всю жизнь. Идеальное решение для такой ситуации — использование статического текста. Перепишем событие получения текста:
procedure TfrmMain.VTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString);
var
PhoneNode: PPhoneNode;
begin
PhoneNode := VT.GetNodeData(Node);
if Assigned(PhoneNode) then
case Column of
0: // Имя
if TextType = ttNormal then
CellText := PhoneNode^.Name;
1: // Телефон
case TextType of
ttNormal: CellText := PhoneNode^.Mobile; // Мобильный
ttStatic: CellText := '(' + PhoneNode^.HomePhone + ')'; // Домашний
end;
end;
end;
Не забудьте также разрешить отображение статического текста, включив опцию дерева toShowStaticText
из TreeOptions.StringOptions
, т.к. по умолчанию она отключена.
Также это свойство идеально подойдет для отображения всяких префиксов или постфиксов, например, для обозначения физических величин рядом с их числовым значением.
Теперь предположим, что нам необходимо сортировать контакты по алфавиту. Для таких задач в VT имеется событие для сравнения двух узлов и обработчики нажатий для заголовка и колонок. Попробуем все это реализовать на примере.
Для начала обработаем событие сравнения двух узлов:
procedure TfrmMain.VTCompareNodes(Sender: TBaseVirtualTree; Node1,
Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
begin
Result := WideCompareStr(VT.Text[Node1, Column], VT.Text[Node2, Column]);
end;
В нашем случае обработчик очень похож на WinAPI callback функции сортировки. VT требует передачи точно такого же результата сравнения двух элементов:
-
0 — Элементы равны, оба остаются на месте.
-
< 0 — Первый элемент больше второго.
-
> 0 — Второй элемент больше первого.
В нашем случае все за нас делает ОС (выбирает наибольшую по алфавиту строку). Таким образом, если Вам нужно будет отсортировать узлы по величине чисел, то обработчик примет примерно такой вид:
procedure TfrmMain.VTCompareNodes(Sender: TBaseVirtualTree; Node1,
Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var
Data1, Data2: PDataNode;
begin
Data1 := Sender.GetNodeData(Node1);
Data2 := Sender.GetNodeData(Node2);
if Data1^.Value > Data2^.Value then
Result := 1
else if Data1^.Value < Data2^.Value then
Result := -1
else if Data1^.Value = Data1^.Value then
Result := 0;
end;
Помните также, что Вы не должны заботиться о порядке сортировки. Т.е. VT сам отредактирует результат сравнения в соответствии с порядком сортировки, и Вам не придется писать тот же самый код дважды, меняя условия проверки на противоположные.
Теперь нам необходимо отсортировать дерево по клику на определенную колонку заголовка и изменить порядок сортировки на противоположный. Грубо говоря, просто посмотрите как это делает Проводник 😊 Делается это следующим обработчиком:
procedure TfrmMain.VTHeaderClick(Sender: TVTHeader; Column: TColumnIndex;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
// Меняем индекс сортирующей колонки на индекс колонки,
// которая была нажата.
VT.Header.SortColumn := Column;
// Сортируем всё дерево относительно этой колонки
// и изменяем порядок сортировки на противополжный
if VT.Header.SortDirection = sdAscending then
begin
VT.Header.SortDirection := sdDescending;
VT.SortTree(Column, VT.Header.SortDirection);
end
else begin
VT.Header.SortDirection := sdAscending;
VT.SortTree(Column, VT.Header.SortDirection);
end;
end;
end;
Идем дальше. У TTreeView
и TListView
есть такая полезная возможность, как небольшой встроенный редактор текста для узлов. Естественно VT был бы не VT, если бы не поддерживал и эту возможность 😊
Редактор включается в настройках по адресу: TreeOptions.MiscOptions.toEditable
.
После этого он станет доступен также, как и в TTreeView
и TListView
(клик по узлу и пауза до появления).
С редактором связаны такие события:
-
OnEditing — Происходит непосредственно перед появлением редактора. С помощью этого события можно запретить его появление по тем или иным причинам.
-
OnEdited — Происходит сразу же после исчезновения редактора.
-
OnNewText — Происходит, если после исчезновения редактора его (редактора) текст был изменен. С его (события) помощью можно обновить Ваши данные.
-
OnEditCanceled — Происходит, если редактор был закрыт нажатием Esc.
В нашем примере мы рассмотрим два наиболее часто применяемых. Рассмотрим синхронизацию данных с новым текстом:
procedure TfrmMain.VTNewText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; NewText: WideString);
var
PhoneNode: PPhoneNode;
begin
if Length(NewText) = 0 then
Exit;
PhoneNode := VT.GetNodeData(Node);
if Assigned(PhoneNode) then
begin
case Column of
0: PhoneNode^.Name := NewText;
1: PhoneNode^.Mobile := NewText;
end;
end;
end;
Теперь мы должны разрешить или запретить редактирование в соответствии со значением поля Editable
нашей записи. Обработчик onEditing
:
procedure TfrmMain.VTEditing(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; var Allowed: Boolean);
var
PhoneNode: PPhoneNode;
begin
Allowed := False;
PhoneNode := VT.GetNodeData(Node);
if Assigned(PhoneNode) then
Allowed := PhoneNode^.Editable;
end;
Не забудьте обратить внимание на параметр EditDelay
. Он позволяет установить задержку перед началом редактирования узла. Значение по умолчанию — одна секунда.
Вы уже наверняка знакомы с этой возможностью по Visual Studio и Delphi. Выглядит это примерно следующим образом: Вы постепенно вводите какой-то текст и VT по мере ввода находит узел, текст которого наиболее совпадает с вводимым. В конце-концов, когда Вы закончите ввод, выделенным останется искомый узел или узел, более-менее похожий на него. Такой подход очень полезен, когда Вы не знаете точного названия ветки, а только ее часть. В VT вся эта прелесть работает с помощью события OnIncrementalSearch
:
procedure TfrmMain.VTIncrementalSearch(Sender: TBaseVirtualTree;
Node: PVirtualNode; const SearchText: WideString; var Result: Integer);
var
PhoneNode: PPhoneNode;
Len: Integer;
begin
Result := 0;
PhoneNode := VT.GetNodeData(Node);
if Assigned(PhoneNode) then
begin
// Используя StrLIComp, мы можем указать длину сравнения.
// Таким образом, мы сможем найти узлы, совпадающие частично.
Result := StrLIComp(PAnsiChar(AnsiString(SearchText)),
PAnsiChar(AnsiString(PhoneNode^.Name)),
Min(Length(SearchText), Length(PhoneNode^.Name)));
end;
end;
Для работы такого поиска надо также известить VT о том, что мы написали соответствующий обработчик и поиск возможен. Для этого надо присвоить параметру IncrementalSearch
любое значение, кроме isNone
:
-
isAll — поиск во всех узлах.
-
isInitializedOnly — поиск только в инициализированных узлах. О событии инициализации будет сказано в последующих главах.
-
isVisibleOnly — поиск только в видимых узлах. О таких узлах тоже будет сказано чуть позже.
Параметр IncrementalSearchDirection
, судя из названия, отвечает за направление поиска (вверх-вниз).
Очень важен параметр IncrementalSearchStart
. Он управляет диапазоном поиска. Принимает следующие значения:
-
ssAlwaysStartOver — при каждом новом нажатии с клавиатуры поиск начинается заново с самого первого элемента дерева. Медленно, но надежно.
-
ssFocusedNode — поиск начинается с выделенного элемента. Компромиссный вариант между двумя остальными.
-
ssLastHit — самый быстрый, но не такой надежный в плане точности параметр. Поиск начинается всегда с последнего совпадения.
Параметр IncrementalSearchTimeout
— позволяет задать паузу между двумя нажатиями клавиш. По истечении этой паузы поиск завершается.
Пожалуй, это все, что можно сказать о поиске по инкременту. Чтобы его протестировать, кликните по VT и начните нажимать разнообразные сочетания клавиш.
Что Я там еще обещал? Ах, да! Отрисовка элементов. Вот два примера как можно скрасить черно-белые будни VT.
Меняем цвет текста:
procedure TfrmMain.VTPaintText(Sender: TBaseVirtualTree;
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType);
var
PhoneNode: PPhoneNode;
begin
PhoneNode := VT.GetNodeData(Node);
if Assigned(PhoneNode) then
TargetCanvas.Font.Color := PhoneNode^.Fg;
if (vsSelected in Node.States) and (Sender.Focused) then
TargetCanvas.Font.Color := clHighlightText;
end;
И фон узла:
procedure TfrmMain.VTBeforeCellPaint(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
CellRect: TRect);
var
PhoneNode: PPhoneNode;
begin
PhoneNode := VT.GetNodeData(Node);
if Assigned(PhoneNode) then
with TargetCanvas do
begin
Brush.Color := PhoneNode^.Bg;
FillRect(CellRect);
end;
end;
А вот так можно на фоне нарисовать что-то наподобие Windows заголовка:
procedure TfrmMain.VTBeforeCellPaint(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
CellRect: TRect);
var
BigGrad, SmallGrad: TRect;
begin
if Column = 0 then
begin
BigGrad := CellRect;
Dec(BigGrad.Bottom, 2);
SmallGrad := BigGrad;
SmallGrad.Top := SmallGrad.Bottom;
Inc(SmallGrad.Bottom, 2);
GradFill(TargetCanvas.Handle, BigGrad, clInactiveCaptionText, clWindow, gkHorz);
GradFill(TargetCanvas.Handle, SmallGrad, clHighlight, clWindow, gkHorz);
end;
end;
Процедуру градиентной заливки можно взять из все того же проекта Figure 1.2.
Последний пример данной главы демонстрирует перебор всех узлов VT.
procedure TfrmMain.BtnFindClick(Sender: TObject);
function FindNode(ANode: PVirtualNode;
const APattern: WideString): PVirtualNode;
var
NextNode: PVirtualNode;
PhoneNode: PPhoneNode;
begin
Result := nil;
NextNode := ANode.FirstChild;
if Assigned(NextNode) then
repeat
PhoneNode := VT.GetNodeData(NextNode);
if Assigned(PhoneNode) then
if RbName.Checked then
if PhoneNode^.Name = APattern then
begin
Result := NextNode;
Exit;
end
else
else
if PhoneNode^.Mobile = APattern then
begin
Result := NextNode;
Exit;
end;
// Ищем в дочерних ветках
Result := FindNode(NextNode, APattern);
// Нужно добавить проверку для случая, когда
// был найден нужный элемент, иначе, даже если
// элемент был найден, то в результате поиска будет
// возвращен nil, поскольку в начале каждого вызова
// процедуры стоит Result := nil;
// Изменение внес Кривожиха Максим (airmax@fitbmk.cdu.edu.ua),
// за что ему спасибо.
if Result = nil then
// Переходим на соседнюю ветку...
NextNode := NextNode.NextSibling;
until
//...пока не вмажемся лбом в стену 😊
NextNode = nil;
end;
var
FoundNode: PVirtualNode;
begin
if VT.RootNodeCount > 0 then
begin
FoundNode := FindNode(VT.RootNode, EdSearch.Text);
LoadNode(FoundNode);
if Assigned(FoundNode) then
begin
VT.ClearSelection;
VT.FocusedNode := FoundNode;
Include(VT.FocusedNode.States, vsSelected);
end;
end;
end;
Здесь становится заметно еще одно маленькое неудобство. В связи с тем, что VT работает со связанными списками, Вы не сможете использовать для переборки цикл for
. Остаются лишь циклы с проверкой наверху (while
) и внизу (repeat
..until
).
👨💻RA нашел весьма необычное, но рабочее решение для использования цикла for
. Передаю ему слово:
var
I: Integer;
PNode: PVirtualNode;
Data: utilUnit.PVSTRecord;
begin
for I := 0 to VST.TotalCount - 1 do
begin
if I <> 0 then
PNode := VST.GetNext(PNode)
else
PNode := VST.GetFirst;
Data := VST.GetNodeData(PNode);
// Делаю что хочу 😊
end;
end;
В связи с тем, что VT оперирует узлами как связанным списком (каждый узел сам по себе можно представить связанным списком), написание разнообразных циклов для прохода по дереву может занять некоторое время, превратившись в утомительную рутину. К счастью, в VT предусмотрен ряд очень удобных и полезных функций для прохода по дереву, которые превращают эту задачу в пару нажатий клавиш. Их около 30 (!) и уж с ними Вы точно не заблудитесь.
Ниже приведен список всех этих функций с описанием входных параметров (если есть), принципом работы и ее результатом.
-
GetFirst, GetLast
(Node: PVirtualNode = nil)
— возвращают самый первый или самый последний узел дерева соответственно. Самый первый узел — узелRootNode.FirstChild
. Это корневой узел с нулевым индексом по отношению кRootNode
. Самый последний узел — самый последний дочерний узел у самого последнего корневого узла. ФункцияGetLast
позволяет указать узел в качестве входного параметра. Если таковой будет указан, то функция вернет самый последний узел по отношению к указанному. -
GetFirstChecked
(State: TCheckState)
— возвращает первый попавшийся узел, состояние отметки которого эквивалентно указанному во входном параметре. -
GetFirstChild
(Node: PVirtualNode)
,GetLastChild (Node: PVirtualNode)
— возвращают первый и последний дочерний узел родительского узла, указанного во входном параметре соответственно. -
GetFirstCutCopy — возвращает первый попавшийся узел, определенный для копирования или вырезания в буфер обмена.
-
GetFirstInitialized, GetLastInitialized
(Node: PVirtualNode = nil)
— первая функция возвращает первый попавшийся узел, который содержит флагvsInitialized
, т.е. который был инициализирован. Вторая функция возвращает самый последний узел сvsInitialized
флагом по отношению к родительскому узлу, указанному во входном параметре. -
GetFirstNoInit, GetLastNoInit
(Node: PVirtualNode = nil)
— то же самое, что и предыдущие функции, только делают все с точностью до наоборот. -
GetFirstSelected — возвращает первый попавшийся узел, который содержит флаг
vsSelected
, т.е. который является выделенным на данный момент. -
GetFirstVisible, GetLastVisible
(Node: PVirtualNode = nil)
— возвращают первый попавшийся и последний узел, который содержит флагvsVisible
соответственно, т.е. который виден на данный момент. Вторая функция позволяет указать родительский узел во входном параметре относительно которого будет произведен поиск узла. -
GetFirstVisibleNoInit, GetLastVisibleNoInit
(Node: PVirtualNode = nil)
— то же самое, что и предыдущие функции, но поиск производится только в узлах без флагаvsInitialized
. -
GetFirstVisibleChild
(Node: PVirtualNode)
, GetLastVisibleChild(Node: PVirtualNode)
— возвращают первый попавшийся и последний дочерний узел родительского узла, указанного во входном параметре, который выделен на данный момент соответственно. -
GetFirstVisibleChildNoInit
(Node: PVirtualNode)
, GetLastVisibleChildNoInit(Node: PVirtualNode)
— то же самое, что и предыдущие функции, но поиск производится только в узлах без флагаvsInitialized
. -
GetNext
(Node: PVirtualNode)
, GetPrevious(Node: PVirtualNode)
— возвращают следующий и предыдущий узлы, смежные (соседние) с узлом, указанном во входном параметре. Эти функции отличаются от почти аналогичныхGetNextSibling
иGetPreviousSibling
. В случае если по отношению к указанному узлу больше нет соседей, тоGetNext
вернет следующий узел, смежный с родительским узлом, аGetPrevious
вернет сам родительский узел по отношению к указанному. Также если у указанного узла есть дочерние узлы, тоGetNext
вернет первый дочерний узел, аGetPrevious
самый последний дочерний узел предыдущего узла. На самом деле, со слов это довольно трудно понять, просто попробуйте поприменять эти функции на практике. -
GetNextChecked
(Node: PvirtualNode; State: TCheckState = csCheckedNormal)
— возвращает следующий узел по отношению к указанному с отметкой, эквивалентной указанной во входном параметре. -
GetNextCutCopy
(Node: PVirtualNode)
— возвращает следующий узел по отношению к указанному, который определен пользователем для вырезания или копирования. Т.е. узел, содержащий флагvsCutOrCopy
. -
GetNextInitialized
(Node: PVirtualNode)
, GetPreviousInitialized(Node: PVirtualNode)
— возвращают следующий и предыдущий инициализированные узлы по отношению к указанному во входном параметре соответственно. -
GetNextNoInit
(Node: PVirtualNode)
, GetPreviousNoInit(Node: PVirtualNode)
— тоже самое, что иGetNext
. Отличие в том, что приGetNext
узлы инициализируются, а приGetNextNoInit
— нет (спасибо 👨💻forever за подсказку). -
GetNextSelected
(Node: PVirtualNode)
— возвращает следующий выделенный узел по отношению к указанному во входном параметре. -
GetNextSibling
(Node: PVirtualNode)
, GetPreviousSibling(Node: PVirtualNode)
— возвращают следующий соседний узел по отношению к указанному во входном параметре. Эти функции отличаются отGetNext
иGetPrevious
тем, что никаких перескоков через родительские узлы не осуществляется. Диапазон действия функций ограничивается только дочерними узлами. Если соседних узлов по отношению к указанному больше не будет, то функции вернутnil
. -
GetNextVisible
(Node: PVirtualNode)
, GetPreviousVisible(Node: PVirtualNode)
— возвращают следующий видимый узел по отношению к указанному во входном параметре. Т.е. узел, содержащий флагvsVisible
. -
GetNextVisibleNoInit
(Node: PVirtualNode)
, GetPreviousVisibleNoInit(Node: PVirtualNode)
— то же самое, но поиск производится только в узлах безvsInitialized
флага. -
GetNextVisibleSibling
(Node: PVirtualNode)
, GetPreviousVisibleSibling(Node: PVirtualNode)
— то же самое, что иGetNextVisible
иGetPreviousSibling
, но действие функций ограничено только соседними узлами по отношению к указанному. -
GetNextVisibleSiblingNoInit
(Node: PVirtualNode)
, GetPreviousVisibleSiblingNoInit(Node: PVirtualNode)
— то же самое, но поиск производится только в узлах безvsInitialized
флага. -
GetSortedCutCopySet
(Resolve: Boolean)
— возвращает массив узлов, определенных для вырезания или копирования в буфер обмена. Если входной параметр равенTrue
, то в массив не будут включены дочерние узлы вырезаемых или копируемых узлов. Узлы располагаются в массиве в порядке их появления в дереве. -
GetSortedSelection
(Resolve: Boolean)
— возвращает массив выделенных узлов. Очень важная функция при реализации Drag&Drop операций с деревом.
Стоит упомянуть об еще одной очень важной функции для прохода по дереву — IterateSubtree
.
function TBaseVirtualTree.IterateSubtree(Node: PVirtualNode; Callback: TVTGetNodeProc;
Data: Pointer; Filter: TVirtualNodeStates = []; DoInit: Boolean = False;
ChildNodesOnly: Boolean = False): PVirtualNode;
Эта функция без особых забот позволяет пройтись по всем узлам дерева и для каждого выполнить какое-то действие, используя callback-процедуру. Кроме того, с помощью фильтра можно указать для каких именно узлов нужно вызывать callback-процедуру. Node
задает начальный узел, с которого начнется проход по дереву. DoInit
позволяет указать нужно ли проинициализировать узел перед тем, как вызывать callback-процедуру для него. ChildNodesOnly
позволяет указать следует ли вызывать callback-процедуру для всех узлов или только для дочерних. Функция возвращает последний узел, обработанный callback-процедурой.
Следующий пример демонстрирует подсчет выделенных узлов в дереве с помощью IterateSubtree
.
procedure TForm1.IterateNodes(Sender: TBaseVirtualTree; Node: PVirtualNode;
Data: Pointer; var Abort: Boolean);
begin
Inc(PInteger(Data)^);
end;
procedure TForm1.VTChange(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
Sel: Integer;
begin
Sel := 0;
VT.IterateSubtree(nil, IterateNodes, @Sel, [vsSelected], False, False);
Lbl.Caption := 'Выбрано: ' + IntToStr(Sel);
end;
Как видите, callback-процедура также позволяет указать указатель на какую-нибудь переменную. Кроме того, если установить Abort
в True
, то процесс прохода по дереву прервется. Используйте этот параметр для выхода из цикла поиска.
💡 Проект, демонстрирующий работу с наиболее часто используемыми событиями VT, находится в папке Fig 1.2.
В этой сравнительно большой главе мы разобрались с наиболее часто используемыми событиями и методами VT. Но это еще далеко не конец. Далее мы еще больше углубимся в недра VT и реализуем более сложные вещи, такие как Drag&Drop, взаимодействие с шеллом, работу с буфером обмена и сохранение и загрузку дерева из внешних источников.
Бесспорно, на лавры одной из самых удобных возможностей интерфейса, которая когда-либо была изобретена, имеет все основания претендовать Drag&Drop (за исключением, пожалуй, двойного клика 🖱️), позволяющая выполнять многие операции без клавиатуры простыми манипуляциями мышкой. И она же самая нетривиальная в реализации задача.
VT в этом плане не исключение. Несмотря на то, что в компоненте максимально возможно реализована поддержка Drag&Drop различными событиями, свойствами и интерфейсами, нам придется делать основную часть работы самим.
VT позволяет реализовать Drag&Drop двумя базовыми подходами (свойство DragType
):
-
dtOLE — основан на системных методах и функциях, работает через ActiveX. Настоятельно рекомендуется к использованию, т.к. полностью поддерживается Windows и позволяет реализовать больше возможностей с минимумом проблем. Работает через Windows-интерфейсы
IDataObject
,IDragManager
и т.д. -
dtVCL — подход, навязанный Borland специально ради облегчения проектирования кроссплатформенных приложений. Про него можно точно сказать: одно лечит, а другое калечит. Главный его недостаток в том, что получатель должен иметь как можно более полные сведения об отправителе и его данных. Таким образом, в нем отсутствует поддержка Drag&Drop между приложениями. Все будет происходить только в контексте Вашей программы. В ряде случаев это заставляет писать прорву кода только для того, чтобы отделить один класс от другого. Я уже и не говорю о возможностях их кардинального различия. Старайтесь избегать VCL подход. Он теоретически и не является Drag&Drop вовсе.
Думаю, следует побольше сказать о выборе из этих двух методов.
OLE позволяет получать данные из любых приложений, в том числе и из своего. Причем VT при использовании OLE способен также получать данные и из VCL контролов Delphi, обученных только VCL Drag&Drop от Borland.
VCL позволяет VT общаться только с контролами, которые используют VCL подход. Но это, тем не менее, не значит, что VT не будет способен работать с OLE данными, если ему выставить dtVCL
свойство.
Тогда зачем этот параметр DragType
вообще, спросите Вы, если он все равно ничего не меняет? Сейчас все станет ясно.
Данный сеттинг находится в TreeOptions.MiscOptions. Он как раз и позволяет разрешить или запретить вставку OLE данных. Если опция отключена, то дерево с dtOLE
параметром вообще перестанет воспринимать Drag&Drop, тогда как dtVCL
-дерево будет еще способно к VCL Drag&Drop в Вашем приложении. Если же опция включена, то дерево будет способно работать с любым типом Drag&Drop. Все это станет Вам еще более понятно, когда мы напишем пример, который, кстати, будет поддерживать оба этих подхода.
Помимо DragType
, VT позволяет указать различные параметры операции. Рассмотрим их:
-
ClipboardFormats — содержит список форматов, распознаваемых VT для OLE метода (В VCL все придется делать ручками). Как минимум, должен быть включен Plain Text и Virtual Tree Data.
-
DefaultPasteMode — положение вставляемой ветки по умолчанию.
-
DragWidth, DragHeight — размеры прозрачной картинки, отображающей краткое содержимое перетаскиваемых объектов. Чем больше размер, тем больше потребуется мощности процессора для обсчета прозрачности.
-
DragImageKind — позволяет указать будут ли в прозрачной картинке отображаться картинки узлов VT.
-
DragMode — перетаскивание начнется автоматически при
dmAutomatic
или потребует ручного управления приdmManual
. Только не подумайте, что dmAutomatic все сделает за Вас 😊 Нет, этот режим часто применяется вместе с методом OLE, тогда как dmManual предназначен для VCL метода. -
DragOperations — перечень операций для работы с Drag&Drop.
Вот они:
-
OnCreateDataObject — вызывается, когда drag manager’у необходим экземпляр класса
TDataObject
с заполненными полями. -
OnCreateDragManager — позволяет пользователю создавать свои собственные drag manager’ы для дерева. К сожалению, Я не имею опыта работы с этим сообщением.
-
OnGetUserClipboardFormats — позволяет заполнить массив форматами данных, которые Вы способны обрабатывать. Используется совместно с
OnRenderOLEData
. -
OnRenderOLEData — используется для обработки данных, которые не поддерживаются встроенным обработчиком VT.
-
OnDragAllowed — позволяет разрешить или запретить перетаскивание элементов.
-
OnDragDrop — основное событие, происходит непосредственно после того, как была отпущена кнопка мыши и были получены данные.
-
OnDragOver — происходит при наведении указателя на элемент дерева. Позволяет разрешить или запретить перетаскивание в этот элемент.
-
OnEndDrag — Событие, следующее после
OnDragDrop
. Оповещает об окончательном завершении операции. -
OnStartDrag — оповещает о начале перетаскивания.
-
OnNodeCopied — событие, связанное с
CopyToClipboard
/CutToClipboard
методами дерева. Происходит сразу после копирования узла. -
OnNodeCopying — аналогично предыдущему, но происходит до вышеупомянутых методов и позволяет также запретить копирование.
Мы подошли к решающему шагу — написанию своего первого Drag&Drop приложения с VT. Я решил в этом примере одновременно показать реализацию не только Drag&Drop, но и вставки из буфера обмена и работы с шеллом одновременно. Почему Я смешал все в одну кашу? Дело в том, что в VT имеется собственный механизм для копирования и вставки своих узлов. Но что если в буфере обмена не узлы VT, а какие-то другие данные? Тогда вставку можно будет организовать через Drag&Drop интерфейсы, что мы и сделаем.
Нам понадобятся:
-
Дерево VT с
dtOLE
иdmAutomatic
свойствами. -
Дерево VT с
dtVCL
иdmManual
свойствами. -
TRichEdit
в качестве источника OLE Drag&Drop данных. -
TListBox
в качестве источника VCL Drag&Drop данных. Не забудьте также выставить ему dmAutomatic свойство. -
У обоих VT-деревьев установите все ClipboardFormats в
True
. -
Желательно также включить
toMultiSelect
в TreeOptions.SelectionOptions, чтобы можно было выделять несколько узлов разом. -
3 действия (
TAction
) для копирования, вырезания и вставки. Не забудьте им выставить соответствующие Shortcut, чтобы можно было вызывать их прямо с клавиатуры без всяких меню и кнопок.
Данные узла возьмем самые маленькие, чтобы не отвлекаться на лишнее:
type
PItemNode = ^TItemNode;
TItemNode = record
Name: WideString;
end;
Заполняем контролы:
procedure TfrmMain.FormCreate(Sender: TObject);
var
i: Integer;
begin
VT.NodeDataSize := SizeOf(TItemNode);
VT2.NodeDataSize := SizeOf(TItemNode);
VT.RootNodeCount := 30;
VT2.RootNodeCount := 30;
RichEdit.Lines.LoadFromFile('rtf.rtf');
for i := 0 to 9 do
ListBox.Items.Add(Format('String %d', [i]));
end;
Заметили, что мы в вышеприведенном коде не заполнили узлы никакими данными? До этого мы в цикле обрабатывали каждый узел. На этот раз Я решил показать пример работы с еще одним полезным событием VT — OnInitNode
. Оно позволяет заполнить узел данными, если Вы не сделали этого при создании. Помимо этого, Вам будет также предложено задать различные свойства для узла. В нашем случае обработчик этого события для обоих деревьев будет выглядеть так:
procedure TfrmMain.VTInitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
ItemNode: PItemNode;
begin
ItemNode := Sender.GetNodeData(Node);
if Assigned(ItemNode) then
if Length(ItemNode^.Name) = 0 then
ItemNode^.Name := 'Node Index № ' + IntToStr(Node.Index);
end;
Как видите, в нашем случае гораздо удобнее использовать именно это событие, нежели делать все непосредственно при создании узлов. Ведь все нам необходимые данные (индекс узла) мы получаем не отходя от кассы, прямо на месте. Данное событие не является обязательным. Впредь Вы будете вольны решать, какой подход Вам удобнее.
Следующие два обработчика Вам уже знакомы, назначьте их обоим деревьям:
procedure TfrmMain.VTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString);
var
ItemNode: PItemNode;
begin
ItemNode := Sender.GetNodeData(Node);
if Assigned(ItemNode) then
CellText := ItemNode^.Name;
end;
procedure TfrmMain.VTNewText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; NewText: WideString);
var
ItemNode: PItemNode;
begin
ItemNode := Sender.GetNodeData(Node);
if Assigned(ItemNode) then
ItemNode^.Name := NewText;
end;
Вот так выглядят обработчики копирования и вырезания для действий из TActionList
:
procedure TfrmMain.BtnCopyClick(Sender: TObject);
begin
if ActiveControl = VT then
VT.CopyToClipBoard
else if ActiveControl = VT2 then
VT2.CopyToClipBoard
else if ActiveControl = RichEdit then
RichEdit.CopyToClipboard;
end;
procedure TfrmMain.BtnCutClick(Sender: TObject);
begin
if ActiveControl = VT then
VT.CutToClipBoard
else if ActiveControl = VT2 then
VT2.CutToClipBoard
else if ActiveControl = RichEdit then
RichEdit.CutToClipBoard;
end;
А вот процедура вставки уже будет различаться. Помните, Я говорил, что вставку OLE данных мы сделаем через те же интерфейсы, что и Drag&Drop? Вот что у нас получится:
procedure TfrmMain.BtnPasteClick(Sender: TObject);
var
DataObject: IDataObject;
EnumFormat: IEnumFormatEtc;
Format: TFormatEtc;
Formats: TFormatArray;
Fetched: Integer;
Tree: TVirtualStringTree;
begin
if ActiveControl is TVirtualStringTree then
begin
Tree := ActiveControl as TVirtualStringTree;
// Попробуем сначала вставить данные простым методом VT.
if not Tree.PasteFromClipboard then
begin
// Если VT сам не смог справиться со вставкой, значит пришли данные
// несколько другого типа, чем просто TVirtualNode узлы.
// Это может быть, к примеру, текст или картинка.
// Сейчас мы это узнаем, покопавшись в форматах буфера обмена.
OLEGetClipboard(DataObject);
// Получить список доступных в буфере обмена форматов в массив Formats.
if Succeeded(DataObject.EnumFormatEtc(DATADIR_GET, EnumFormat)) then
begin
EnumFormat.Reset;
while EnumFormat.Next(1, Format, @Fetched) = S_OK do
begin
SetLength(Formats, Length(Formats) + 1);
Formats[High(Formats)] := Format.cfFormat;
end;
InsertData(Tree, DataObject, Formats, DROPEFFECT_COPY, Tree.DefaultPasteMode);
end;
end;
end
else if ActiveControl is TRichEdit then
RichEdit.PasteFromClipboard;
end;
Теперь следуют собственно обработчики Drag&Drop. Все комментарии Я поместил в код. Для обоих деревьев два одинаковых обработчика. onDragDrop
и onDragOver
:
//---------------------------------------------------------------------------
// На нас что-то кинули...
//---------------------------------------------------------------------------
procedure TfrmMain.VTDragDrop(Sender: TBaseVirtualTree; Source: TObject;
DataObject: IDataObject; Formats: TFormatArray; Shift: TShiftState;
Pt: TPoint; var Effect: Integer; Mode: TDropMode);
// Определяем как поступать с данными. Перемещать, копировать или ссылаться
procedure DetermineEffect;
begin
// Нажаты ли какие-нибудь управляющие клавиши?
if Shift = [] then
begin
// Неа, не нажаты
// Тогда, если отправитель и получатель - одинаковые объекты (например,
// если узлы перемещаются из одного и того же дерева), то
// надо переместить узлы, в противном случае - копировать.
if Source = Sender then
Effect := DROPEFFECT_MOVE
else
Effect := DROPEFFECT_COPY;
end
else begin
// Нажаты. В зависмости от комбинации решаем что делать
if (Shift = [ssAlt]) or (Shift = [ssCtrl, ssAlt]) then
Effect := DROPEFFECT_LINK
else
if Shift = [ssCtrl] then
Effect := DROPEFFECT_COPY
else
Effect := DROPEFFECT_MOVE;
end;
end;
var
Attachmode: TVTNodeAttachMode;
Nodes: TNodeArray;
i: Integer;
begin
Nodes := nil;
// Определяем, куда добавлять узел в зависимости от того, куда была
// брошена ветка.
case Mode of
dmAbove:
AttachMode := amInsertBefore;
dmOnNode:
AttachMode := amAddChildLast;
dmBelow:
AttachMode := amInsertAfter;
else
AttachMode := amNowhere;
end;
if DataObject = nil then
begin
// Если не пришло интерфейса, то вставка проходит через VCL метод
if Source is TVirtualStringTree then
begin
// Вставка из VT. Можем спокойно пользоваться его методами
// копирования и перемещения.
DetermineEffect;
// Получаем список узлов, которые будут участвовать в Drag&Drop
Nodes := VT2.GetSortedSelection(True);
// И работаем с каждым
if Effect = DROPEFFECT_COPY then
begin
for i := 0 to High(Nodes) do
VT2.CopyTo(Nodes[i], Sender.DropTargetNode, AttachMode, False);
end
else
for i := 0 to High(Nodes) do
VT2.MoveTo(Nodes[i], Sender.DropTargetNode, AttachMode, False);
end
else if Source is TListBox then
begin
// Вставка из объекта какого-то другого класса
AddVCLText(Sender as TVirtualStringTree,
(Source as TListBox).Items.Strings[(Source as TListBox).ItemIndex],
AttachMode);
end;
end
else begin
// OLE drag&drop.
// Effect нужен для передачи его источнику drag&drop, чтобы тот решил
// что он будет делать со своими перетаскиваемыми данными.
// Например, при DROPEFFECT_MOVE (перемещение) их нужно будет удалить,
// при копировании - сохранить.
if Source is TBaseVirtualTree then
DetermineEffect
else begin
if Boolean(Effect and DROPEFFECT_COPY) then
Effect := DROPEFFECT_COPY
else
Effect := DROPEFFECT_MOVE;
end;
InsertData(Sender as TVirtualStringTree, DataObject, Formats, Effect, AttachMode);
end;
end;
//---------------------------------------------------------------------------
// В этом событии мы должны проверить есть ли среди перетаскиваемых веток
// родитель ветки, в которую происходит перетаскивание. Ведь нельзя
// же ветку-родитель перетащить в её дочерние элементы 😊
//---------------------------------------------------------------------------
procedure TfrmMain.VTDragOver(Sender: TBaseVirtualTree; Source: TObject;
Shift: TShiftState; State: TDragState; Pt: TPoint; Mode: TDropMode;
var Effect: Integer; var Accept: Boolean);
// Возвращает True, если AParent - дочерний узел ANode.
function IsNodeParent(AParent, ANode: PVirtualNode): Boolean;
var
NextParent: PVirtualNode;
begin
NextParent := AParent;
repeat
NextParent := NextParent.Parent;
until
(NextParent = Sender.RootNode) or (NextParent = nil) or
(NextParent = ANode);
Result := ANode = NextParent;
end;
var
i: Integer;
Nodes: TNodeArray;
begin
Accept := True;
if (Assigned(Sender.DropTargetNode)) and
(Sender.DropTargetNode <> Sender.RootNode) then
Nodes := (Sender as TVirtualStringTree).GetSortedSelection(True);
if Length(Nodes) > 0 then
begin
for i := 0 to Length(Nodes) - 1 do
begin
Accept :=
// Узел не должен быть родителем ветки, в которую производится
// вставка
(not IsNodeParent(Sender.DropTargetNode, Nodes[i]))
// Также, узел не должен равняться ветке-местоназначению вставки.
// Т.е. мы должны запретить вставку узла в самого себя.
and (not(Sender.DropTargetNode = Nodes[i]));
// Отключаем вставку, если хотя бы одно из условий вернуло False
if not Accept then
Exit;
end;
end;
end;
Этот обработчик onDragAllowed
назначьте только дереву с VCL Drag&Drop:
procedure TfrmMain.VTDragAllowed(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; var Allowed: Boolean);
begin
Allowed := True;
end;
Метод InsertData
предназначен непосредственно для вставки новых OLE данных в дерево после того, как мы их получили. Он будет искать среди полученных форматов тот, который способен обработать. В нашем случае это Unicode-текст и файлы, брошенные на нас из Explorer’a. Вот как он выглядит:
procedure TfrmMain.InsertData(Sender: TVirtualStringTree;
DataObject: IDataObject; Formats: TFormatArray;
Effect: Integer; Mode: TVTNodeAttachMode);
var
FormatAccepted: Boolean; // True, если принятые данные уже обработались
i: Integer;
begin
// Ищем в переданных форматах тот, который можем обработать
FormatAccepted := False;
for i := 0 to High(Formats) do
begin
case Formats[i] of
CF_HDROP: // Прибыл список файлов из Explorer'a.
begin
if not FormatAccepted then
begin
AddFile(DataObject, Sender as TVirtualStringTree, Mode);
FormatAccepted := True;
end;
end;
CF_UNICODETEXT: // ЮНИКОД-текст
begin
if not FormatAccepted then
begin
AddUnicodeText(DataObject, Sender as TVirtualStringTree, Mode);
FormatAccepted := True;
end;
end;
else if Formats[i] = CF_VIRTUALTREE then
// Родной формат VT. Обрабатывает вставку своих же
// TVirtualNode-узлов.
begin
if not FormatAccepted then
begin
Sender.ProcessDrop(DataObject, Sender.DropTargetNode, Effect, Mode);
FormatAccepted := True;
end;
end;
end;
end;
end;
Теперь посмотрим, как выглядят процедуры AddVCLText
, AddFile
и AddUnicodeText
. Все они предназначены для обработки какого-то одного типа пришедших данных. Вот обработчики каждого из них с подробным описанием каждого действия:
//---------------------------------------------------------------------------
// Метод предназначен для обработки ЮНИКОД-текста, хранящегося в буфере
// обмена.
// Хранимый текст получается и разделяется на линии, которые затем
// вставляются в дерево в качестве новых узлов.
//---------------------------------------------------------------------------
procedure TfrmMain.AddUnicodeText(DataObject: IDataObject;
Target: TVirtualStringTree; Mode: TVTNodeAttachMode);
var
// Структура, которая будет запрашивать указатеь на глобальные данные
FormatEtc: TFormatEtc;
// Структура с данными
Medium: TStgMedium;
// Указатель на строку
OLEData,
// Для прохода цикла с целью разделения текста на строки
Head, Tail: PWideChar;
// Узел, получающий новую ветку
TargetNode,
// Новая ветка
Node: PVirtualNode;
// Её данные
Data: PItemNode;
begin
if Mode <> amNowhere then
begin
// Заполняем струкутру для получения ЮНИКОД-текста
with FormatEtc do
begin
cfFormat := CF_UNICODETEXT;
// Нет у нас никакого девайса...
ptd := nil;
// Нам нужно содержание (текст в данном случае)
dwAspect := DVASPECT_CONTENT;
// Нет страницы для мультистраничных данных
lindex := -1;
// Мы будем получать указатель на данные через глобальную память
tymed := TYMED_HGLOBAL;
end;
// Есть ли ЮНИКОД-текст для нашего запроса?
if DataObject.QueryGetData(FormatEtc) = S_OK then
begin
// Опа, есть, можем получить данные
if DataObject.GetData(FormatEtc, Medium) = S_OK then
begin
// Вот они:
OLEData := GlobalLock(Medium.hGlobal);
if Assigned(OLEData) then
begin
Target.BeginUpdate;
// Выбираем место для вставки, если переданное = nil
TargetNode := Target.DropTargetNode;
if TargetNode = nil then
TargetNode := Target.FocusedNode;
// Разбиваем текст на строки
Head := OLEData;
try
while Head^ <> #0 do
begin
Tail := Head;
while not (Tail^ in [WideChar(#0), WideChar(#13), WideChar(#10), WideChar(#9)]) do
Inc(Tail);
if Head <> Tail then
begin
// Добавляем новую ноду, если есть хотя бы один символ
// для строки.
Node := Target.InsertNode(TargetNode, Mode);
Data := Target.GetNodeData(Node);
Data^.Name := Head;
SetLength(Data^.Name, (Tail - Head));
end;
// Пропускаем табы
if Tail^ = #9 then
Inc(Tail);
// Символы переноса каретки и конца строки
if Tail^ = #13 then
Inc(Tail);
if Tail^ = #10 then
Inc(Tail);
// Шагаем дальше
Head := Tail;
end;
finally
GlobalUnlock(Medium.hGlobal);
Target.EndUpdate;
end;
end;
// Вот это лучше не забывать делать
ReleaseStgMedium(Medium);
end;
end;
end;
end;
//---------------------------------------------------------------------------
// Получение имён файлов, брошеных на нас из Explorer’а.
//---------------------------------------------------------------------------
procedure TfrmMain.AddFile(DataObject: IDataObject;
Target: TVirtualStringTree; Mode: TVTNodeAttachMode);
var
FormatEtc: TFormatEtc;
Medium: TStgMedium;
OLEData: PDropFiles;
Files: PChar;
Str: String;
TargetNode,
Node: PVirtualNode;
Data: PItemNode;
begin
if Mode <> amNowhere then
begin
// На этот раз нас интересует есть ли в буфере CF_HDROP формат
with FormatEtc do
begin
cfFormat := CF_HDROP;
ptd := nil;
dwAspect := DVASPECT_CONTENT;
lindex := -1;
tymed := TYMED_HGLOBAL;
end;
if DataObject.QueryGetData(FormatEtc) = S_OK then
begin
if DataObject.GetData(FormatEtc, Medium) = S_OK then
begin
OLEData := GlobalLock(Medium.hGlobal);
if Assigned(OLEData) then
begin
Target.BeginUpdate;
TargetNode := Target.DropTargetNode;
if TargetNode = nil then
TargetNode := Target.FocusedNode;
try
// А вот с этим Я долго мучался. В Microsoft зажрались и не дали
// примера работы с DROPFILES структурой.
// Оказывается, список брошенных файлов хранится в адресе
// структуры
// + offset, который и есть OLEData^.pFiles.
Files := PChar(OLEData) + OLEData^.pFiles;
// Список оканчивается двойным null символом
while Files^ <> #0 do
begin
if OLEData^.fWide then
begin
Str := PWideChar(Files);
// +1 нужен для того, чтобы перешагнуть null символ одного
// из имён файлов в списке
Inc(Files, (Length(PWideChar(Files)) + 1)*SizeOf(WideChar));
end
else begin
Str := Files;
// Аналогично
Inc(Files, (Length(PChar(Files)) + 1)*SizeOf(Char));
end;
Node := Target.InsertNode(TargetNode, Mode);
Data := Target.GetNodeData(Node);
Data^.Name := Str;
end;
finally
GlobalUnlock(Medium.hGlobal);
Target.EndUpdate;
end;
end;
ReleaseStgMedium(Medium);
end;
end;
end;
end;
//---------------------------------------------------------------------------
// Метод для получения строки из VCL контролов.
// Режем на линии.
//---------------------------------------------------------------------------
procedure TfrmMain.AddVCLText(Target: TVirtualStringTree;
const Text: WideString; Mode: TVTNodeAttachMode);
var
Head, Tail: PWideChar;
TargetNode,
Node: PVirtualNode;
Data: PItemNode;
begin
if Mode <> amNoWhere then
begin
Target.BeginUpdate;
try
TargetNode := Target.DropTargetNode;
if TargetNode = nil then
TargetNode := Target.FocusedNode;
Head := PWideChar(Text);
while Head^ <> #0 do
begin
Tail := Head;
while not (Tail^ in [WideChar(#0), WideChar(#13), WideChar(#10), WideChar(#9)]) do
Inc(Tail);
if Head <> Tail then
begin
Node := Target.InsertNode(TargetNode, Mode);
Data := Target.GetNodeData(Node);
SetString(Data^.Name, Head, Tail - Head);
end;
if Tail^ = #9 then
Inc(Tail);
if Tail^ = #13 then
Inc(Tail);
if Tail^ = #10 then
Inc(Tail);
Head := Tail;
end;
finally
Target.EndUpdate;
end;
end;
end;
Мы сделали это. Можете теперь скомпилировать пример и проверить его работоспособность. В качестве теста, попробуйте копировать и вырезать узлы, перемещать их между деревьями, перетаскивать текст из TRichEdit
, TListBox
, Microsoft Word, Visual Studio, Total Commander, выделите несколько папок и файлов в Windows Explorer и киньте их на деревья. Можете попробовать взаимодействовать и с другими приложениями, а также перетаскивать узлы из деревьев в TRichEdit
или другие приложения. Все в итоге должно работать на ура. Почти всегда реализованных нами методов вполне хватает, разве что обработку вставки графики мы не реализовали, но это можете оставить уже себе в качестве тренировки 😊
Поздравляю, Ваше приложение полностью совместимо Drag&Drop!
VT предоставляет замечательную возможность создавать свои собственные редакторы данных, кроме обычного текстового поля для редактирования текста. Эту возможность можно использовать, чтобы облегчить пользователю работу с какими-то специализированными данными. Например, при изменении цвета можно использовать ComboBox со списком цветов, а при изменении даты — вызывать календарь. Конечно, это непременно прибавит профессиональности вашей программе.
Чтобы как-то обобщить редактирование различных типов данных и позволить взаимодействовать редакторам дерева с приложением, компонент использует интерфейс IVTEditLink
. Давайте создадим небольшой пример, где постараемся реализовать собственные редакторы для различных типов данных.
Для начала определимся, какие форматы данных, а следственно редакторы, мы будем использовать в примере. Думаю, этого будет достаточно для начала:
type
TVTEditorKind = (
ekString, // TEdit
ekMemo, // TMemo
ekComboBox, // TComboBox
ekColor, // TColorBox
ekDate, // TDateTimePicker
ekMask, // TMaskedEdit
ekProgress // TProgressBar
);
Мы напишем один класс для реализации интерфейса для каждого типа редактора. Это наиболее рациональный подход вместо того, чтобы для каждого редактора создавать свой класс, реализующий один и тот же интерфейс. Вот как он будет выглядеть:
TVTCustomEditor = class(TInterfacedObject, IVTEditLink)
private
FEdit: TWinControl; // Базовый класс для каждого типа редактора
FTree: TVirtualStringTree; // Ссылка на дерево, вызвавшее редактирование
FNode: PVirtualNode; // Редактируемый узел
FColumn: Integer; // Его колонка, в которой оно происходит
protected
procedure EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
public
destructor Destroy; override;
function BeginEdit: Boolean; stdcall;
function CancelEdit: Boolean; stdcall;
function EndEdit: Boolean; stdcall;
function GetBounds: TRect; stdcall;
function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex): Boolean; stdcall;
procedure ProcessMessage(var Message: TMessage); stdcall;
procedure SetBounds(R: TRect); stdcall;
end;
Как видите, все специализированные редакторы будут потомками TWinControl
. С помощью типа TVTEditorKind
мы будем определять, какой конкретно класс редактора будет создаваться. В комментариях, напротив полей перечисления, указан класс редактора для каждого поля.
Теперь нам необходимо решить, как будет выглядеть структура данных для каждого узла. Нам необходимо где-то хранить строковую переменную, которая в соответствии с типом редактора будет трансформироваться в нужный тип данных, а также переменную типа TVTEditorKind
, чтобы мы могли знать какой тип редактора будет создан для данного конкретного узла.
Получаем:
type
PVTEditNode = ^TVTEditNode;
TVTEditNode = record
Kind: TVTEditorKind;
Value: String;
Changed: Boolean;
end;
Сюда Я также добавил поле Changed, чтобы определить, что узел был отредактирован и не содержит больше своего значения по умолчанию. Это необязательный параметр.
Наш пример будет содержать 2 колонки (параметр-значение) и 7 узлов для каждого типа редактора. Чтобы не отвлекаться и не писать лишнего кода, определим для данных 3 массива: типы редакторов для каждого из семи узлов, значения по умолчанию для поля Changed и текст для первой колонки.
ValueTypes: array[0..6] of TVTEditorKind = (
ekString, // Имя
ekMemo, // Описание
ekComboBox, // Тип
ekColor, // Цвет
ekDate, // Дата
ekMask, // Маска
ekProgress // Процесс
);
// Значения по умолчанию
DefaultValues: array[0..6] of String = (
'Свитер',
'Мягкий и тёплый.',
'Шерсть',
'clRed',
'24.06.2006',
'798-77-66',
'70 %'
);
// Имена параметров
ValueNames: array[0..6] of String = (
'Имя изделия',
'Комментарий',
'Материал',
'Цвет изделия',
'Дата изготовления',
'Телефон склада',
'Процесс доставки'
);
Все, теперь можно приступать к написанию кода, реализующего методы нашего класса. По ходу работы Я старался давать максимально полные и понятные комментарии к каждому действию.
//---------------------------------------------------------------------------
{* * * * * * * * * * * * * * * * TVTCustomEditor * * * * * * * * * * * * * *}
//---------------------------------------------------------------------------
destructor TVTCustomEditor.Destroy;
begin
FreeAndNil(FEdit);
inherited;
end;
//---------------------------------------------------------------------------
// Для обработки нажатий с клавиатуры.
// Отмена редактирования по Escape, завершение редактирования по Enter,
// и переход между узлами по Up/Down, если список элементов у комбо бокса или
// редактора даты не выпущен.
//---------------------------------------------------------------------------
procedure TVTCustomEditor.EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
var
CanContinue: Boolean;
begin
CanContinue := True;
case Key of
VK_ESCAPE: // Нажали Escape
if CanContinue then
begin
FTree.CancelEditNode;
Key := 0;
end;
VK_RETURN: // Нажали Enter
if CanContinue then
begin
// Если Ctrl для TMemo не зажат, то завершаем редактирование
// Сделаем так, чтобы можно было по Ctrl+Enter вставлять в Memo
// новую линию.
if (FEdit is TMemo) and (Shift = []) then
FTree.EndEditNode
else if not(FEdit is TMemo) then
FTree.EndEditNode;
Key := 0;
end;
VK_UP, VK_DOWN:
begin
// Проверить, не идёт ли работа с редактором. Если идёт, то запретить
// активность дерева, если нет, то передать нажатие дереву.
CanContinue := Shift = [];
if FEdit is TComboBox then
CanContinue := CanContinue and not TComboBox(FEdit).DroppedDown;
if FEdit is TDateTimePicker then
CanContinue := CanContinue and not TDateTimePicker(FEdit).DroppedDown;
if CanContinue then
begin
// Передача клавиши дереву
PostMessage(FTree.Handle, WM_KEYDOWN, Key, 0);
Key := 0;
end;
end;
end;
end;
//---------------------------------------------------------------------------
// Началось редактирование, нужно показать редактор и установить ему фокус.
//---------------------------------------------------------------------------
function TVTCustomEditor.BeginEdit: Boolean;
begin
Result := True;
with FEdit do
begin
Show;
SetFocus;
end;
end;
//---------------------------------------------------------------------------
// Отменилось, прячем редактор.
//---------------------------------------------------------------------------
function TVTCustomEditor.CancelEdit: Boolean;
begin
Result := True;
FEdit.Hide;
end;
//---------------------------------------------------------------------------
// Успешно завершилось, прячем редактор, обновляем данные узла и возвращаем
// фокус дереву.
//---------------------------------------------------------------------------
function TVTCustomEditor.EndEdit: Boolean;
var
Txt: String;
begin
Result := True;
if FEdit is TEdit then
Txt := TEdit(FEdit).Text
else if FEdit is TMemo then
Txt := TEdit(FEdit).Text
else if FEdit is TComboBox then
Txt := TComboBox(FEdit).Text
else if FEdit is TColorBox then
Txt := ColorToString(TColorBox(FEdit).Selected)
else if FEdit is TDateTimePicker then
begin
Txt := DateToStr(TDateTimePicker(FEdit).DateTime);
end
else if FEdit is TMaskEdit then
Txt := TMaskEdit(FEdit).Text
else if FEdit is TProgressBar then
Txt := IntToStr(TProgressBar(FEdit).Position) + ' %';
// Изменяем текст узла (поле Value у TVTEditNode) через событие OnNewText
// у дерева
FTree.Text[FNode, FColumn] := Txt;
FEdit.Hide;
FTree.SetFocus;
end;
//---------------------------------------------------------------------------
// Возвращаем границы редактора.
//---------------------------------------------------------------------------
function TVTCustomEditor.GetBounds: TRect;
begin
Result := FEdit.BoundsRect;
end;
//---------------------------------------------------------------------------
// В подготовке к редактированию мы должны создать экземпляр TWinControl
// нужного класса потомка в соответствии с полем Kind у TVTEditNode.
//---------------------------------------------------------------------------
function TVTCustomEditor.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex): Boolean;
var
VTEditNode: PVTEditNode;
begin
Result := True;
FTree := Tree as TVirtualStringTree;
FNode := Node;
FColumn := Column;
FreeAndNil(FEdit);
VTEditNode := FTree.GetNodeData(Node);
case VTEditNode.Kind of
ekString:
begin
FEdit := TEdit.Create(nil);
with FEdit as TEdit do
begin
AutoSize := False;
Visible := False;
Parent := Tree;
Text := VTEditNode.Value;
OnKeyDown := EditKeyDown;
end;
end;
ekMemo:
begin
FEdit := TMemo.Create(nil);
with FEdit as TMemo do
begin
Visible := False;
Parent := Tree;
ScrollBars := ssVertical;
Text := VTEditNode.Value;
OnKeyDown := EditKeyDown;
end;
end;
ekComboBox:
begin
FEdit := TComboBox.Create(nil);
with FEdit as TComboBox do
begin
Visible := False;
Parent := Tree;
Text := VTEditNode.Value;
Items.Add('Шерсть');
Items.Add('Хлопок');
Items.Add('Шёлк');
Items.Add('Кожа');
Items.Add('Велюр');
OnKeyDown := EditKeyDown;
end;
end;
ekColor:
begin
FEdit := TColorBox.Create(nil);
with FEdit as TColorBox do
begin
Visible := False;
Parent := Tree;
Selected := StringToColor(VTEditNode.Value);
Style := Style + [cbPrettyNames];
OnKeyDown := EditKeyDown;
end;
end;
ekMask:
begin
FEdit := TMaskEdit.Create(nil);
with FEdit as TMaskEdit do
begin
AutoSize := False;
Visible := False;
Parent := Tree;
EditMask := '999-99-99';
Text := VTEditNode.Value;
OnKeyDown := EditKeyDown;
end;
end;
ekDate:
begin
FEdit := TDateTimePicker.Create(nil);
with FEdit as TDateTimePicker do
begin
Visible := False;
Parent := Tree;
Date := StrToDate(VTEditNode.Value);
OnKeyDown := EditKeyDown;
end;
end;
ekProgress:
begin
FEdit := TProgressBar.Create(nil);
with FEdit as TProgressBar do
begin
Visible := False;
Parent := Tree;
Position := StrToIntDef(VTEditNode.Value, 70);
end;
end
else
Result := False;
end;
end;
//---------------------------------------------------------------------------
// Обработка сообщений Windows для редактора.
//---------------------------------------------------------------------------
procedure TVTCustomEditor.ProcessMessage(var Message: TMessage);
begin
FEdit.WindowProc(Message);
end;
//---------------------------------------------------------------------------
// Устанавливает границы редактора в соответствии с шириной и высотой
// колонки.
//---------------------------------------------------------------------------
procedure TVTCustomEditor.SetBounds(R: TRect);
var
Dummy: Integer;
begin
FTree.Header.Columns.GetColumnBounds(FColumn, Dummy, R.Right);
FEdit.BoundsRect := R;
if FEdit is TMemo then
FEdit.Height := 80;
end;
Основную работу мы проделали. Настало время соединить наш класс с VT. Делается это передачей интерфейса в событии OnCreateEditor
:
procedure TfrmMain.VTCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; out EditLink: IVTEditLink);
begin
EditLink := TVTCustomEditor.Create;
end;
Суть его в простой передаче экземпляра нашего редактора. Теперь при начале редактирования VT будет вызывать методы нашего класса вместо своего стандартного редактора. Не забудьте обработать изменившиеся поля дерева:
procedure TfrmMain.VTNewText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; NewText: WideString);
var
VTEditNode: PVTEditNode;
begin
VTEditNode := Sender.GetNodeData(Node);
with VTEditNode^ do
begin
if not Changed then
Changed := Value <> NewText;
Value := StringReplace(NewText, #13#10, ' ', [rfReplaceAll]);
end;
end;
Редактирование само по себе не включится. В событии OnEditing
необходимо разрешить его:
procedure TfrmMain.VTEditing(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; var Allowed: Boolean);
begin
Allowed := Column > 0;
end;
Об остальных событиях Вы можете сами позаботиться, благо знаний у Вас уже достаточно. Включите свойство TreeOptions.MiscOptions.toEditable
у VT и запустите скомпилированное приложение. Каждый из семи узлов будет обладать собственным редактором.
Приведенный пример предоставляет достаточный запас информации для начала написания собственных элементов управления и внедрения их в VT. Вы имеете мощный инструмент для редактирования узлов, границы которого ограничиваются лишь вашей фантазией. В качестве домашнего задания можете попробовать связать VT с числовым редактором.
💡 Проект, демонстрирующий работу со специализированными редакторами данных, находится в папке Fig 1.4.
В этой главе мы с Вами рассмотрим работу с классом TVirtualDrawTree
, специально предназначенного для отрисовки нестандартных в отображении узлов. Он не ориентирован на отображение каких-то специфических данных, вроде текста. TVirtualDrawTree
специально предназначен исключительно для отрисовки различных элементов дизайна вашей программы.
В нашем примере мы рассмотрим создание полностью видоизмененного дерева, напоминающего собой менеджер закачек Mozilla Firefox с отображением различных элементов управления. Но для начала разберемся в отличиях TVirtualDrawTree
от TVirtualStringTree
, с которым мы ранее работали.
Различия минимальны. Сразу бросается в глаза отсутствие привычных событий OnGetText
и OnNewText
, что говорит о непредназначенности дерева для работы с текстовыми данными. Тем не менее, Вы по-прежнему можете использовать собственные редакторы, все для этого на месте. Появилось целых четыре новых события:
-
OnDrawNode — если у
TVirtualStringTree
основа — событиеOnGetText
, то уTVirtualDrawTree
этоOnDrawNode
. В этом событии необходимо будет полностью реализовать всю отрисовку узла. -
OnGetNodeWidth — предназначено для указания ширины узла. Напомню, что высота узла указывается в параметре DefaultNodeHeight для деревьев с узлами одинаковой высоты или в событии
OnMeasureItem
для деревьев с узлами различной высоты. -
OnGetHintSize — предназначено для указания размеров всплывающей подсказки.
-
OnDrawHint — событие, позволяющее отрисовку всплывающей подсказки. До сих пор мы могли лишь указать текст подсказки, но с этого момента мы должны будем заниматься полным отображением подсказки самостоятельно.
Все остальное не претерпело никаких изменений.
В тексте данной главы Я ограничился лишь указанием примерной реализации событий OnDrawNode
и OnDrawHint
, а также реализацией потока. Все остальное в рамках данной главы довольно объемно и не представляет для нас большого интереса, поэтому, чтобы увидеть результат работы наших трудов, необходимо скомпилировать пример из папки Fig 1.5.
Предположим, что нам необходимо создать небольшую базу данных заказов продуктового магазина. Мы должны предусмотреть отображение картинок изделий, их названий, цен и т.д. Также, нам необходимо визуализировать процесс доставки какого-то заказа. Задание нетривиальное, но вполне реализуемое с помощью TVirtualDrawTree
.
Как обычно, начинаем с продумывания структуры данных каждого узла:
type
PItemNode = ^TItemNode;
TItemNode = record
Image, Name: String;
Mass, PriceKg: Word;
Process: Byte;
PersonalThread: Cardinal;
end;
Нам необходимо хранить путь к картинке, имя изделия, вес и цену за килограмм. Поле Process необходимо для имитации процесса доставки. Его значение мы будем изменять с помощью отдельного потока, а отображать с помощью TProgressBar
в выделенном узле. Хендл потока будет храниться в поле PersonalThread.
Переходим к реализации событий:
//---------------------------------------------------------------------------
// Отрисовка всплывающей подсказки.
//---------------------------------------------------------------------------
procedure TfrmMain.VTDrawHint(Sender: TBaseVirtualTree; HintCanvas: TCanvas;
Node: PVirtualNode; R: TRect; Column: TColumnIndex);
var
NodeData: PItemNode;
Th: Integer;
ImgRect, RealRect, NameRect, PriceRect: TRect;
Img: TPicture;
begin
// Области отрисовки различных элементов
Th := HintCanvas.TextHeight('Wj');
RealRect := R;
// Границы картинки
ImgRect := RealRect;
with ImgRect do
begin
Inc(Left, 16);
Inc(Top, 8);
Right := Left + 64;
Bottom := Top + 64;
end;
// Границы текста наименования
NameRect := RealRect;
with NameRect do
begin
Inc(Left, 96);
Inc(Top, 8);
Bottom := Top + Th;
end;
// Границы текста общей цены
PriceRect := RealRect;
with PriceRect do
begin
Inc(Left, 96);
Dec(Bottom, 16);
Top := Bottom - Th;
end;
// Рисуем фон
with HintCanvas do
begin
with Brush do
begin
Color := clInfoBk;
Style := bsSolid;
end;
Pen.Color := clBlack;
Pen.Width := 1;
// Жёлтый фон и чёрная рамка по бокам
Rectangle(RealRect);
end;
// Текст
NodeData := Sender.GetNodeData(Node);
with HintCanvas do
begin
Brush.Style := bsClear;
Font.Color := clWindowText;
// Наименование
Font.Style := [fsBold];
if NodeData^.Process = 0 then
TextOut(NameRect.Left, NameRect.Top, NodeData^.Name + ' (не доcтавлено)')
else if (NodeData^.Process > 0) and (NodeData^.Process < 100) then
TextOut(NameRect.Left, NameRect.Top, NodeData^.Name + ' (доставляется)')
else if NodeData^.Process = 100 then
TextOut(NameRect.Left, NameRect.Top, NodeData^.Name + ' (доставлено)');
// Общая цена
Font.Style := [];
TextOut(PriceRect.Left, PriceRect.Top, 'Общая цена: ' + IntToStr(NodeData^.PriceKg * NodeData^.Mass)
+ ' р.');
end;
// Картинка
Img := TPicture.Create;
try
Img.LoadFromFile(NodeData^.Image);
HintCanvas.Draw(ImgRect.Left, ImgRect.Top, Img.Graphic);
with HintCanvas, ImgRect do
begin
Pen.Width := 2;
Pen.Color := $00B99D7F;
Rectangle(Rect(ImgRect.Left, Top, Left + Img.Width, Top + Img.Height));
end;
finally
FreeAndNil(Img);
end;
// Отображаем процесс выполнения
with FProgress do
begin
Position := NodeData^.Process;
Width := RealRect.Right - 96 - 16;
PaintTo(HintCanvas, NameRect.Left, NameRect.Top + Th + Th div 2);
end;
end;
//---------------------------------------------------------------------------
// Отрисовка узла.
//---------------------------------------------------------------------------
procedure TfrmMain.VTDrawNode(Sender: TBaseVirtualTree;
const PaintInfo: TVTPaintInfo);
var
NodeData: PItemNode;
Th: Integer;
ImgRect, RealRect, NameRect, GetRect, CancelRect, MassRect, PriceRect: TRect;
Img: TPicture;
begin
// Области отрисовки различных элементов
Th := PaintInfo.Canvas.TextHeight('Wj');
RealRect := PaintInfo.CellRect;
ImgRect := RealRect;
with ImgRect do
begin
Inc(Left, 16);
Inc(Top, 16);
Right := Left + 64;
Bottom := Top + 64;
end;
NameRect := RealRect;
with NameRect do
begin
Inc(Left, 96);
Inc(Top, 16);
Bottom := Top + Th;
end;
GetRect := RealRect;
with GetRect do
begin
Dec(Bottom, 16 + Th);
Top := Bottom - Th;
Dec(Right, 16);
Left := Right - PaintInfo.Canvas.TextWidth('Доставить');
end;
CancelRect := GetRect;
with CancelRect do
begin
Inc(Top, Th);
Inc(Bottom, Th);
end;
MassRect := RealRect;
with MassRect do
begin
Inc(Left, 96);
Dec(Bottom, 16 + Th);
Top := Bottom - Th;
end;
PriceRect := MassRect;
with PriceRect do
begin
Inc(Top, Th);
Inc(Bottom, Th);
end;
// Рисуем фон
with PaintInfo.Canvas do
begin
with Brush do
begin
Color := clWindow;
Style := bsSolid;
end;
Pen.Color := $00B99D7F;
end;
if PaintInfo.Node = Sender.FocusedNode then
GradFill(PaintInfo.Canvas.Handle, RealRect, $00EAE2D9, $00D7C8B7, gkVert)
else
PaintInfo.Canvas.FillRect(RealRect);
// Текст
NodeData := Sender.GetNodeData(PaintInfo.Node);
with PaintInfo.Canvas do
begin
Brush.Style := bsClear;
Font.Color := clWindowText;
// Наименование
Font.Style := [fsBold];
if NodeData^.Process = 0 then
TextOut(NameRect.Left, NameRect.Top, NodeData^.Name + ' (не доcтавлено)')
else if (NodeData^.Process > 0) and (NodeData^.Process < 100) then
TextOut(NameRect.Left, NameRect.Top, NodeData^.Name + ' (доставляется)')
else if NodeData^.Process = 100 then
TextOut(NameRect.Left, NameRect.Top, NodeData^.Name + ' (доставлено)');
// Масса
Font.Style := [];
TextOut(MassRect.Left, MassRect.Top, 'Масса: ' + IntToStr(NodeData^.Mass) + ' кг.');
// Цена
TextOut(PriceRect.Left, PriceRect.Top, 'Цена за килограмм: ' + IntToStr(NodeData^.PriceKg) + ' р.');
// Доставить
with Font do
begin
Style := [fsUnderline];
Color := clBlue;
end;
TextOut(GetRect.Left, GetRect.Top, 'Доставить');
// Отмена
TextOut(CancelRect.Left, CancelRect.Top, 'Отмена');
end;
// Картинка
Img := TPicture.Create;
try
Img.LoadFromFile(NodeData^.Image);
PaintInfo.Canvas.Draw(ImgRect.Left, ImgRect.Top, Img.Graphic);
if PaintInfo.Node = Sender.FocusedNode then
with PaintInfo.Canvas, ImgRect do
begin
Pen.Width := 2;
Rectangle(Rect(ImgRect.Left, Top, Left + Img.Width, Top + Img.Height));
end;
finally
FreeAndNil(Img);
end;
// Процесс
if PaintInfo.Node = Sender.FocusedNode then
with FProgress do
begin
Position := NodeData^.Process;
Width := RealRect.Right - 96 - 16;
PaintTo(PaintInfo.Canvas, NameRect.Left, NameRect.Top + Th + Th div 2);
end;
end;
//---------------------------------------------------------------------------
// Метод предназначен для отображения курсора руки над надписями "Доставить"
// и "Отмена", которые нарисованы в стиле ссылок.
//---------------------------------------------------------------------------
procedure TfrmMain.VTGetCursor(Sender: TBaseVirtualTree; var Cursor: TCursor);
var
Pt: TPoint;
Node: PVirtualNode;
NRect: TRect;
D: Integer;
Inf: tagScrollInfo;
begin
GetCursorPos(Pt);
Pt := Sender.ScreenToClient(Pt);
Node := Sender.GetNodeAt(Pt.X, Pt.Y);
with Inf do
begin
cbSize := SizeOf(tagScrollInfo);
fMask := SIF_RANGE;
end;
GetScrollInfo(Sender.Handle, SB_VERT, Inf);
if (Inf.nMax > Sender.Height) then
D := GetSystemMetrics(SM_CXHTHUMB)
else
D := 0;
if Node <> nil then
begin
NRect := Sender.GetDisplayRect(Node, -1, False);
if ((Pt.X > Sender.Width - 71 - D) and (Pt.X < Sender.Width - 16 - D) and
(Pt.Y > NRect.Bottom - 42) and
(Pt.Y < NRect.Bottom - 29))
or
((Pt.X > Sender.Width - 71 - D) and (Pt.X < Sender.Width - 35 - D) and
(Pt.Y > NRect.Bottom - 29) and
(Pt.Y < NRect.Bottom - 16))
then
Cursor := crHandPoint
else
Cursor := crDefault;
end;
end;
//---------------------------------------------------------------------------
procedure TfrmMain.VTGetHintSize(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; var R: TRect);
begin
with R do
begin
Left := 0;
Top := 0;
Right := 250;
Bottom := 80;
end;
end;
//---------------------------------------------------------------------------
// Получение ширины узла. Если Вы включили опцию toFullRowSelect, то
// в контексте данного примера это событие необязательно.
//---------------------------------------------------------------------------
procedure TfrmMain.VTGetNodeWidth(Sender: TBaseVirtualTree; HintCanvas: TCanvas;
Node: PVirtualNode; Column: TColumnIndex; var NodeWidth: Integer);
var
D: Integer;
Inf: tagScrollInfo;
begin
// Смотрим, виден ли вертикальный скрол.
// Если есть, то его ширину нужно будет вычесть из ширины узла
with Inf do
begin
cbSize := SizeOf(tagScrollInfo);
fMask := SIF_RANGE;
end;
GetScrollInfo(Sender.Handle, SB_VERT, Inf);
if (Inf.nMax > Sender.Height) then
D := GetSystemMetrics(SM_CXHTHUMB)
else
D := 0;
// Вычитаем из ширины дерева ширину вертикального скрола, отступа и края
// дерева
NodeWidth := Sender.Width - D - VT.Indent - VT.Margin;
end;
Реализация потока, имитирующего процесс доставки, будет выглядеть так (комментарии даны по ходу листинга):
// Поток, имитирующий выполнение какой-то операции
TProcessThread = class(TThread)
private
FWnd: HWND; // Хендл формы. Необходим для отсылки сообщений ему
FNode: PVirtualNode; // Узел, за который поток отвечает
FValue: Byte; // Процесс доставки
procedure RepaintTree;
procedure CheckDeleted;
public
constructor Create(Suspended: Boolean; var InitialValue: Byte;
CallingForm: HWND; VTNode: PVirtualNode);
protected
procedure Execute; override;
end;
constructor TProcessThread.Create(Suspended: Boolean;
var InitialValue: Byte; CallingForm: HWND; VTNode: PVirtualNode);
begin
inherited Create(Suspended);
FWnd := CallingForm;
FNode := VTNode;
FValue := InitialValue;
FreeOnTerminate := True;
end;
//---------------------------------------------------------------------------
// Отсылаем сообщение форме о том, что узлу FNode необходима перерисовка
// Параметры:
// WParam - адрес узла;
// LParam - текущее значение процесса доставки.
//---------------------------------------------------------------------------
procedure TProcessThread.RepaintTree;
begin
SendMessage(FWnd, VT_REPAINTTREE, Integer(FNode), FValue);
end;
//---------------------------------------------------------------------------
// Проверить, не был ли удалён узел, за который мы отвечает. Если был, то
// делать нам больше нечего.
//---------------------------------------------------------------------------
procedure TProcessThread.CheckDeleted;
begin
if not Assigned(FNode) then
Terminate;
end;
//---------------------------------------------------------------------------
procedure TProcessThread.Execute;
begin
while FValue < 100 do
begin
Sleep(100);
Synchronize(CheckDeleted);
Inc(FValue);
Synchronize(RepaintTree);
end;
end;
Обработка сообщения VT_REPAINTTREE
у формы:
const
// Сообщение для уведомления дерева потоком о том, что необходимо
// перерисовать указанный узел
VT_REPAINTTREE = WM_USER + 500;
procedure TfrmMain.VTREPAINTTREE(var Message: TMessage);
var
Node: PVirtualNode;
ItemNode: PItemNode;
begin
Node := Pointer(Message.WParam);
if Assigned(Node) then
begin
ItemNode := VT.GetNodeData(Node);
ItemNode^.Process := Message.LParam;
VT.RepaintNode(Node);
end;
end;
Для правильной работы примера необходимо настроить дерево. Чтобы включить подсказку, необходимо изменить свойство HintMode
на hmHint
(это свойство будет подробнее рассмотрено позже).
Теоретически, наше задание можно было бы выполнить и с помощью обычного TVirtualStringTree
, но он ориентирован только на отображение текстовых данных и неизвестно какие проблемы и опасности могут поджидать Вас при работе с ним. Просто запомните, если Вам требуется существенное изменение отображения данных, то используйте TVirtualDrawTree
, в противном случае достаточно TVirtualStringTree
.
💡 Полная версия проекта, демонстрирующего измененную отрисовку дерева с использованием TVirtualDrawTree
, находится в папке Fig 1.5. В нем также присутствуют дополнительные комментарии к коду.
Какой толк от данных, если результат вычислений нигде не хранить? Давайте посмотрим что есть в VT для обеспечения сохранения и загрузки его содержимого и какими способами это можно сделать. Мы возьмем максимально простой пример: текстовое дерево из двух колонок с возможной вложенностью узлов. Такая картина наиболее часто встречается в повседневной жизни.
VT реализует такие методы, как LoadFromFile
и SaveToFile
, предназначенные для загрузки и сохранения дерева в файл. Основа их действия — методы LoadFromStream
и SaveToStream
, предназначенные для работы с потоком данных. Давайте попробуем вызвать их и проанализировать полученный файл.
Данные для узла:
type
PNodeData = ^TNodeData;
TNodeData = record
Caption: WideString;
Value: Integer;
end;
Заполняем дерево случайными данными:
const
Strs: array[0..4] of WideString = (
'Εκλογές στο Ισραήλ',
'Απεβίωσε ο Γιάννης Ξενάκης',
'Μεταμόσχευση χεριού',
'Σάμπα, κλόουν, μασκαράδες και πενιές',
'Δοκιμάζει τις αντοχές του'
);
Ints: array[0..4] of Integer = (
938,
5346,
23,
789,
2888
);
procedure TfrmMain.BtnFillClick(Sender: TObject);
var
i, j: Integer;
NewNode1, NewNode2: PVirtualNode;
ItemData: PNodeData;
begin
Randomize;
for i := 0 to 1 do
begin
NewNode1 := VT.AddChild(nil);
ItemData := VT.GetNodeData(NewNode1);
with ItemData^ do
begin
Caption := Strs[Random(4)];
Value := Ints[Random(4)];
end;
for j := 0 to 9 do
begin
NewNode2 := VT.AddChild(NewNode1);
ItemData := VT.GetNodeData(NewNode2);
with ItemData^ do
begin
Caption := Strs[Random(4)];
Value := Ints[Random(4)];
end;
end;
end;
end;
Попробуем без лишних телодвижений просто вызвать оба метода и посмотреть на результат:
procedure TfrmMain.BtnLoadClick(Sender: TObject);
begin
VT.LoadFromFile('vt.dat');
end;
procedure TfrmMain.BtnSaveClick(Sender: TObject);
begin
VT.SaveToFile('vt.dat');
end;
Добавьте обработчик OnGetText
дереву для отображения текста и скомпилируйте пример. Нажмите на кнопку, которой назначен обработчик BtnFillClick
, чтобы заполнить дерево. Затем жмите на кнопку сохранения. В папке с примером появится файл vt.dat, откроем его блокнотом или любым hex-редактором. Что мы видим? Бинарный файл, в котором проскакивает текстовая информация, очевидно взятая из поля Caption нашей структуры. Хорошо, очистите дерево и попробуйте загрузить его, нажав на кнопку загрузки. Структура дерева восстановится, но все его узлы будут пусты... Зачем же тогда в полученном файле присутствует содержимое узлов? Дерево сохраняет содержимое узлов, но при этом никак не загружает его. Получается, что это мусор в файле? И да, и нет.
Такое поведение дерева объясняется тем, что текст узлов (но не данные узла) помещается в поток TStream
при операциях с буфером обмена и Drag&Drop. Затем он читается из потока для восстановления узла. Но нужно это только для передачи текста узлов сторонним приложениям и контролам, например TRichEdit
или Microsoft Word (они же ничего про данные узла знать не могут), а в нашем случае мы работаем с файлом, поэтому эта информация будет проигнорирована деревом, как информация, не предназначенная для загрузки из файла.
Но что тогда отвечает за процесс загрузки и сохранения? Как избавиться от этого «мусора» в файле? Для начала, запретим дереву помещать эту информацию в поток. За это отвечает опция toSaveCaptons
из StringOptions раздела настроек дерева, которую необходимо в нашем случае поставить в False
. Теперь необходимо самостоятельно реализовать запись содержимого узла в поток. Для этого нам в помощь присутствуют два простых события:
-
OnLoadNode — вызывается при чтении данных для каждого узла.
-
OnSaveNode — вызывается при сохранении данных для каждого узла.
Таким образом, наша задача заключается только в заполнении содержимого потока по мере записи в него структуры дерева. О записи структуры дерева VT заботится самостоятельно.
Добавим обработчики обоим событиям:
procedure TfrmMain.VTLoadNode(Sender: TBaseVirtualTree; Node: PVirtualNode;
Stream: TStream);
var
Reader: TReader;
NodeData: PNodeData;
begin
Reader := TReader.Create(Stream, 8096);
try
NodeData := Sender.GetNodeData(Node);
with NodeData^, Reader do
begin
Caption := ReadWideString;
Value := ReadInteger;
end;
finally
FreeAndNil(Reader);
end;
end;
procedure TfrmMain.VTSaveNode(Sender: TBaseVirtualTree; Node: PVirtualNode;
Stream: TStream);
var
Writer: TWriter;
NodeData: PNodeData;
begin
Writer := TWriter.Create(Stream, 8096);
try
NodeData := Sender.GetNodeData(Node);
with Writer, NodeData^ do
begin
WriteWideString(Caption);
WriteInteger(Value);
end;
finally
FreeAndNil(Writer);
end;
end;
Тем, кто работал с массивами и реализовывал их методы загрузки и сохранения, этот код покажется довольно знакомым. Мы используем классы TReader
и TWriter
для работы с потоком. В нашем случае мы помещаем в поток строку и число от каждого узла. При загрузке они также легко извлекаются из потока. Снова скомпилируйте пример и повторите предыдущие действия по сохранению дерева в файл. На этот раз мы увидим иную картину. В файле уже не будет прежнего мусора, а строки будут сохранены в Юникоде и завершены null-terminating (\#0
) символом. По соседству с ними будут располагаться числа. Вернемся в приложение. Очистите дерево и нажмите на кнопку загрузки.
Вуаля! Дерево восстановлено в своем первозданном виде. Таким образом, Вы можете хранить любые типы данных (кроме referenced естественно) в бинарном файле и легко их загружать.
Но что если Вас не устраивает перспектива хранения дерева в бинарном файле? Что если у Вас есть собственный формат данных, который Вы хотите использовать? В этом случае все действия по загрузке и сохранения можно полностью выполнить самому. Вот простой пример как можно то же самое дерево из предыдущего примера хранить в XML файле. Для работы с XML Я использовал стандартный парсер от Microsoft (TXMLDocument
). Тем не менее, Вам ничто не мешает переписать пример под Ваш любимый парсер.
Загрузка:
procedure TfrmMain.BtnLoadXMLClick(Sender: TObject);
var
XMLDocument: TXMLDocument;
procedure LoadXML(const ANodeList: IXMLNodeList; AParent: PVirtualNode);
var
i: Integer;
NewNode: PVirtualNode;
NodeData: PNodeData;
begin
for i := 0 to ANodeList.Count - 1 do
begin
NewNode := VT.AddChild(AParent);
NodeData := VT.GetNodeData(NewNode);
with NodeData^ do
begin
Caption := VarToWideStr(ANodeList[i].Attributes['Caption']);
Value := StrToIntDef(
VarToStr(
ANodeList[i].Attributes['Value']
), 0);
end;
LoadXML(ANodeList[i].ChildNodes, NewNode);
end;
end;
begin
if not FileExists('vt.xml') then
begin
MessageBox(Handle, 'Файл vt.xml не найден.', PChar(Application.Title),
MB_ICONINFORMATION + MB_OK);
Exit;
end;
XMLDocument := TXMLDocument.Create(Self);
try
XMLDocument.LoadFromFile('vt.xml');
// Для каждой ветки XML дерева создать узел в дереве и зарузить поля
// для структуры данных из аттрибутов
// Не забываем использовать блоки Begin/End Update.
VT.BeginUpdate;
try
LoadXML(XMLDocument.DocumentElement.ChildNodes, nil);
finally
VT.EndUpdate;
end;
finally
FreeAndNil(XMLDocument);
end;
end;
Сохранение:
procedure TfrmMain.BtnSaveXMLClick(Sender: TObject);
var
XMLDocument: TXMLDocument;
procedure SaveXML(ANode: PVirtualNode; const AParent: IXMLNode);
var
i: Integer;
NewNode: IXMLNode;
NextNode: PVirtualNode;
NodeData: PNodeData;
begin
NextNode := ANode.FirstChild;
if Assigned(NextNode) then
repeat
NodeData := VT.GetNodeData(NextNode);
NewNode := AParent.AddChild('Node');
with NewNode, NodeData^ do
begin
Attributes['Caption'] := Caption;
Attributes['Value'] := Value;
end;
SaveXML(NextNode, NewNode);
NextNode := NextNode.NextSibling;
until
NextNode = nil;
end;
begin
XMLDocument := TXMLDocument.Create(Self);
try
with XMLDocument do
begin
Active := True;
Encoding := 'UTF-16';
AddChild('VirtualTreeview');
Options := Options + [doNodeAutoIndent];
end;
SaveXML(VT.RootNode, XMLDocument.DocumentElement);
XMLDocument.SaveToFile('vt.xml');
finally
FreeAndNil(XMLDocument);
end;
end;
VT предлагает замечательную возможность экспортировать свое содержимое в различные форматы данных. На данный момент поддерживается текст, Юникод, RTF и HTML. Что примечательно, VT экспортирует содержимое в текст, сохраняя при этом табличную структуру. Вот короткий пример экспорта в HTML с сохранением в файл. Экспорт для всех остальных форматов проходит примерно также.
procedure TfrmMain.BtnExportHTMLClick(Sender: TObject);
const
HTMLHead = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">'#13#10 +
'<html>'#13#10 +
'<head>'#13#10 +
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8">'#13#10 +
' <title>Virtual Treeview HTML</title>'#13#10 +
'</head>'#13#10 +
'<body>'#13#10;
HTMLFoot = '</body>'#13#10 +
'</html>' + #13#10;
var
Fs: TFileStream;
Str: String;
Data: Pointer;
begin
Fs := TFileStream.Create('html.html', fmCreate);
try
Str := HTMLHead + VT.ContentToHTML(tstAll) + HTMLFoot;
Data := PChar(Str);
Fs.WriteBuffer(Data^, Length(Str));
finally
FreeAndNil(fs);
end;
ShellExecute(Handle, 'open', 'html.html', nil, nil, SW_RESTORE);
end;
До сих пор наша работа с VT неизменно была связана с узлами, представленными типом TVirtualNode
, но мы не уделяли ему особого внимания. А ведь он содержит много полезных и интересных свойств, например, позволяет задать тип отметки для конкретных узлов и получить доступ ко многим его свойствам. Восполним этот пробел.
Каждый узел, являясь экземпляром типа TVirtualNode
, содержит множество вспомогательных и информативных свойств, которые с успехом можно использовать ради любых целей.
-
Index — индекс узла в дереве по отношении к своему родителю. Т.е. абсолютным индексом он не является.
-
ChildCount — количество дочерних элементов узла. Вручную не изменяется. Заполняется в обработчике события
OnInitChildren
. -
NodeHeight — высота узла. VT позволяет узлам иметь различные размеры, применение этой возможности будет подробнее рассмотрено ниже. Фактически, это read-only поле, т.к. его изменение, кроме как к глюкам отрисовки, ни к чему не приводит. Корректное изменение этого свойства возможно только в событии
OnMeasureItem
. Если планируете использование этого поля, то желательно включитьTreeOptions.MiscOptions.toVariableNodeHeight
опцию дерева. -
States — набор настроек узла.
-
vsInitialized —
True
, если данный узел прошел событиеOnInitNode
. -
vsChecking —
True
, если пользователь зажал отметку и еще не отпустил кнопку мыши. -
vsCutOrCopy — узел был определен для копирования или вставки.
-
vsDisabled — узел выключен и не отвечает на действия пользователя.
-
vsDeleting — устанавливается деревом в
True
сразу перед тем, как узел должен быть удален. -
vsExpanded —
True
, если узел раскрыт. -
vsHasChildren — позволяет указать наличие дочерних узлов. При этом узел не обязательно должен их иметь.
-
vsVisible —
True
, если узел видно в дереве. -
vsSelected —
True
, если узел выделен. -
vsAllChildrenHidden —
True
, если у узла включено свойствоvsHasChildren
, но все его дочерние элементы спрятаны (имеют свойствоvsVisible
вTrue
). -
vsClearing —
True
, если дочерние узлы начали удаляться. -
vsMultiline —
True
, если текст узла должен быть перенесен, чтобы совпасть с шириной колонки. -
vsHeightMeasured —
True
, если высота узла была определена в обработчике событияOnMeasureItem
. -
vsToggling — используется деревом для предотвращения зацикливания при сворачивании/разворачивания узла.
-
-
Align — смещение узла в диапазоне от 0 до 255. Чем больше значение, тем ниже будут располагаться кнопка сворачивания/разворачивания и отметка. Данное поле тем не менее не влияет на положение текста в узле. Часто используется вместе со свойством дерева
NodeAlignment
, которое будет рассмотрено ниже. Возможно установить только в событии инициализации. -
CheckState — содержит текущее состояние отметки.
-
csUncheckedNormal — узел не отмечен и не зажат.
-
csUncheckedPressed — узел не отмечен, но пользователь зажал кнопку мыши над отметкой.
-
csCheckedNormal — узел отмечен.
-
csCheckedPressed — узел зажат, но пользователь зажал кнопку мыши над отметкой.
-
csMixedNormal — только для отметок типа
TCheckBox
в узлах со свойствомctTriStateCheckBox
. Обозначает, что отметка имеет промежуточное состояние (аналогGrayed
вTCheckBox
). -
csMixedPressed — то же самое, но отметка зажата левой кнопкой мыши.
-
-
CheckType — тип отметки. Возможно установить только в событии инициализации.
-
ctNone — узел без отметки.
-
ctTriStateCheckBox — узел с отметкой типа
TCheckBox
, которая может иметь 3 состояния (отмечена, grayed, не отмечена). -
ctCheckBox — узел с отметкой типа
TCheckBox
, которая может иметь 2 состояния. -
ctRadioButton — узел с отметкой типа
TRadioButton
. -
ctButton — узел с кнопкой слева от надписи.
-
-
Dummy — зарезервировано. Необходимо для выравнивания размера структуры узла до четырех байтов (
DWORD
). -
TotalCount — сумма узла и всех его дочерних узлов и их дочерних узлов и т.д.
-
TotalHeight — общая высота, занимаемая на экране данным узлом и всеми его дочерними узлами.
-
Parent — указатель на узел родителя для данного узла.
-
PrevSibling — указатель на предыдущий узел связанного списка. Если данный узел является первым в списке, то поле равно
nil
. -
NextSibling — то же самое, но указывает на следующий узел в списке.
-
FirstChild — указатель на самый первый дочерний узел.
-
LastChild — указатель на самый последний дочерний узел.
Как Вы могли уже видеть выше, в отличие от TListView
и TTreeView
у VT есть аж четыре (!) вида отметки. Рассмотрим каждый подробнее.
-
ctCheckBox — отметка типа
TCheckBox
, знакомая Вам поTListView
иTTreeView
. Один раз нажал — повесил галочку, другой раз нажал — снял галочку. -
ctRadioButton — отметка типа
TRadioButton
. В списке узлов одновременно может быть отмечен только один узел. -
ctTriStateCheckBox — это отметка наподобие
TCheckBox
, но включающая в себя также третье состояние (grayed), изображенное в виде квадрата, а не галки. Такой вид отметок часто можно встретить в приложениях, предлагающих выбрать какой-то набор компонентов для установки. В них отметка принимает grayed-вид, если выбрана только часть компонент из предложенных для установки. -
ctButton — немного необычный вид отметки. Очень удобно использовать для показа popup-меню для индивидуального узла. Смотрится довольно оригинально и профессионально.
Учтите, что отметки будут видны только в главной колонке дерева, т.е. в той, что имеет индекс MainColumn.
Идеальное место для включения отметок — событие OnInitNode
:
procedure TfrmMain.VTInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode;
var ChildCount: Cardinal);
begin
if Sender.GetNodeLevel(Node) < 5 then
ChildCount := 6
else
ChildCount := 0;
end;
procedure TfrmMain.VTInitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
Lvl: Integer;
begin
Lvl := Sender.GetNodeLevel(Node);
if Lvl < 5 then
begin
InitialStates := InitialStates + [ivsHasChildren];
case Lvl of
0: Node.CheckType := ctButton;
1: Node.CheckType := ctRadioButton;
2: Node.CheckType := ctTriStateCheckBox;
3: Node.CheckType := ctTriStateCheckBox;
4: Node.CheckType := ctCheckBox;
5: Node.CheckType := ctNone;
end;
end;
end;
В этом примере дерево будет заполняться шестью узлами для каждого родительского узла пока их уровень вложенности не достигнет шести. Тип отметки выбирается в соответствии с уровнем вложенности узла.
С отметками связаны события:
-
OnChecking — происходит после нажатия на отметку и перед тем, как отпускается кнопка мыши.
-
OnChecked — происходит сразу после того, как была отпущена кнопка мыши.
VT также позволяет указать изображения, которые будут использоваться для отметок. Кроме того, Вы можете нарисовать собственную картинку для каждого типа отметки и ее состояний. VT уже имеет несколько готовых вариантов картинок для отметок, Вы можете просмотреть их в свойстве CheckImageKind
. Чтобы использовать собственные изображения, поместите их в TImageList
и присоедините к дереву через свойство CustomCheckImages
. Переключите свойство CheckImageKind в ckCustom и отметки примут вид Ваших картинок. В папке Fig 1.7/res
находится файл-пример, содержащий измененные изображения отметок. Вы можете использовать его как шаблон для создания собственных изображений.
Если Вы уже скомпилировали пример и проверили его работу, то наверняка заметили, что у узлов с отметкой типа ctTriStateCheckBox состояние устанавливается автоматически в соответствии с состоянием отметки дочерних узлов. Если ни один из дочерних узлов не отмечен, то и данный узел не отмечен. Если все дочерние узлы отмечены, то и данный узел отмечен. Если только часть дочерних узлов отмечена, то данный узел будет иметь состояние отметки grayed (csMixedNormal
). Надо отдать должное разработчику компонента за такую удобную возможность. Впрочем, если Вы не хотите избежать такого поведения дерева, то просто отключите опцию toAutoTriStateTracking
, обитающую в TreeOptions.AutoOptions. Именно она отвечает за все это.
Чтобы лучше закрепить знания о TVirtualNode
типе, ознакомьтесь с примером Figure 1.7. В нем очень хорошо видна работа каждого свойства типа.
8.0. Небольшие примеры кода. Описание различных свойств VT, не попадающих под категории, рассмотренные выше.
Я специально решил выделить такие моменты в отдельную главу, чтобы не устраивать кашу среди предыдущих примеров и в то же время акцентировать внимание и на них. В конце статьи Я также полностью рассмотрю назначение всех опций VT.
VT выделяет все настраиваемые цвета (кроме Color
) в отдельный небольшой класс — TVTColors
, наследованный от TPersistent
, что говорит о том, что Вы сможете легко обменивать цветовые настройки между деревьями простым методом Assign. Все цвета VT доступны для изменения в поле Colors
.
-
BorderColor — цвет дополнительной границы, устанавливаемой в свойстве BorderWidth.
-
DisabledColor — цвет текста отключенного (vsDisabled) узла.
-
DropMarkColor — цвет небольшой отметки, появляющейся при Drag&Drop переносе узла по верху или по низу другого.
-
DropTargetBorder — цвет границы прямоугольника, закрашивающего узел, в который производится Drag&Drop вставка (
DropTargetNode
). -
DragTargetColor — цвет самого прямоугольника, закрашивающего узел, в который производится Drag&Drop вставка (
DropTargetNode
). -
FocusedSelectionBorderColor — цвет границы выделенного узла.
-
FocusedSelectionColor — цвет выделенного узла.
-
GridLineColor — цвет линий сетки у дерева.
-
HeaderHotColor — цвет текста заголовка колонки, над которой сейчас находится курсор.
-
HotColor — цвет узла, над которым находится курсор.
-
SelectionRectangleBlendColor — цвет фона, заполняющего прямоугольник выделения.
-
SelectionRectangleBorderColor — цвет границы прямоугольника выделения.
-
TreeLineColor — цвет соединительных линий узлов.
-
UnfocusedSelectionBorderColor — цвет границы выделенного, но не имеющего фокус узла.
-
UnfocusedSelectionColor — цвет выделенного, но не имеющего фокус узла.
Особенно интересны пары параметров, где один отвечает за границу, а другой за фон. Комбинируя различные цвета между ними, можно получать довольно интересные сочетания.
VT позволяет выводить подсказку тремя различными вариантами. Режим подсказки задается в параметре HintMode
:
-
hmDefault — передает вывод подсказки стандартным обработчикам VCL. В этом случае в подсказке будет выведен текст из поля
Hint
. -
hmHint — использует для получения текста подсказки событие
OnGetHint
. Предусмотрен для вывода индивидуальной подсказки для каждого узла. -
hmHintAndDefault — комбинация двух предыдущих параметров. Если курсор будет находится над узлом, то действие будет эквивалентно действию параметру
hmHint
, если же курсор будет находится непосредственно над пустой областью дерева, то действие будет эквивалентно параметруhmDefault
. -
hmTooltip — особый параметр. Применяется, если Вы хотите отобразить текстовую подсказку над узлами, текст которых не умещается в колонке и обрезается. В этом случае обработка события
OnGetHint
не нужна, VT сам все сделает за Вас. Учтите, что если Вы все же обработаете это событие, то своим действием оно перекроет этот параметр.
Наиболее часто используемый параметр — hmTooltip
. Он не обязует Вас лишними обработчиками и в то же время облегчает навигацию по дереву с узлами с длинным текстом. Если же Вы хотите выводить собственный текст в подсказке, то можете воспользоваться параметром hmHint
. В этом случае обработчик получения подсказки будет примерно такой:
procedure TfrmMain.VTGetHint(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; var LineBreakStyle: TVTTooltipLineBreakStyle;
var HintText: WideString);
begin
if VT.HintMode <> hmTooltip then
HintText := 'Хинт из OnGetHint события: текст подсказки...'
else
HintText := VT.Text[Node, Column];
end;
Параметр hmDefault
не требует никаких обработчиков. Это индивидуальная подсказка дерева из его поля Hint
, как и у любых других VCL классов.
VT позволяет использовать анимацию для всплывающей подсказки. Ее можно настроить параметром HintAnimation
дерева.
-
hatNone — выключить анимацию подсказки.
-
hatFade — эффект затухания, плавное появление подсказки.
-
hatSlide — скользящий эффект появления подсказки.
-
hatSystemDefault — используется эффект анимации, установленный в системе. Рекомендуемый параметр.
Цвет выделения задается в полях параметра Colors
, который уже был рассмотрен выше. Кроме этих параметров, VT позволяет указать прозрачность для выделенных узлов. Это означает, что через выделенные узлы будет просвечиваться фон дерева. Степень прозрачности задается в поле SelectionBlendFactor
. Сама эта возможность по умолчанию отключена в настройках дерева. Включить ее можно, изменив опцию TreeOptions.PaintOptions.toUseBlendedSelection
в True
.
Помимо этого, VT позволяет изменить форму выделения. Стандартный прямоугольник выделения можно закруглить с помощью поля SelectionCurveRadius
.
VT на данный момент не поддерживает закругление выделения при включенных настройках прозрачности выделения. Т.е. действие поля SelectionCurveRadius
опускается при включенной опции toUseBlendedSelection
.
Прямоугольник выделения (тот, что растягивается мышью) тоже может менять свое отображение. С помощью параметра DrawSelectionMode
Вы можете изменить его стиль. smDottedRectangle
для прямоугольника с границами в виде точек и отсутствием заливки (используется стандартными TTreeView
и TListView
), smBlendedRectangle
для прямоугольника выделения в стиле Windows (прозрачный синий прямоугольник по умолчанию). Свойство активно только при выключенных темах оформления. Это означает, что если дерево поддерживает визуальные стили, то значение параметра всегда будет smBlendedRectangle
.
Несколько параметров анимации и прокрутки.
-
AnimationDuration — длительность анимации при раскрытии/скрытии узла. Анимация включается опцией
TreeOptions.AnimationOptions.toAnimatedToggle
дерева. -
AutoExpandDelay — длительность паузы перед автоматическом раскрытии узла. Включается опцией
TreeOptions.AutoOptions.toAutoExpand
дерева. -
AutoScrollDelay — длительность паузы перед автоматической прокруткой дерева. Включается опцией
TreeOptions.AutoOptions.toAutoScroll
дерева. -
AutoScrollInterval — количество узлов, прокручиваемое при перемещении колесика мыши.
Даже для такой мелочи как соединительные линии и линии сетки предусмотрены свои настройки. Цвет, как Вы уже знаете, изменяется параметрами GridLineColor
и TreeLineColor
. Параметры, рассмотренные здесь, позволяют управлять стилем отрисовки этих самих линий.
Для линий, соединяющих узлы, имеется параметр LineMode
. Он интересен тем, что помимо обычного стиля отрисовки lmNormal
(а-ля TTreeView
/ TListView
) позволяет указать стиль lmBands
. Тогда соединительные линии узлов будут образовывать ряды и колонки, похожие на таблицу или диаграмму. Смотрится довольно необычно по сравнению с обычным стилем.
Для линий сетки существует параметр LineStyle
:
-
lsCustomStyle — определяет, что Вы сами будете задавать стиль линий сетки через событие
OnGetLineStyle
. -
lsDotted — линии сетки в виде пунктира.
-
lsSolid — стиль, используемый
TTreeView
/TListView
(обычные сплошные линии).
Если вам не хватает стилей lsDotted и lsSolid, то Вы вольны указать свой собственный стиль в обработчике события OnGetLineStyle
. Его параметру Bits
необходимо передать битмап 6*6 пикселей, представленный указателем на массив байт (TByteArray
). Вот простой пример, увеличивающий расстояние между пунктирами на максимальное:
procedure TfrmMain.VTGetLineStyle(Sender: TBaseVirtualTree; var Bits: Pointer);
var
i: Integer;
begin
Bits := @Arr;
for i := 0 to Length(Arr) - 1 do
Arr[i] := 254;
end;
Нами остались нерассмотренными еще несколько свойств, влияющих на положение узла в ячейке.
NodeAlignment
позволяет указать относительно чего будет смещаться узел. Смещение, как Вы помните, задается в поле Align
TVirtualNode
записи.
-
naFromBottom — смещение относительно нижнего края ячейки.
-
naFromTop — смещение относительного верхнего края.
-
naProportional — пропорциональное смещение, не направленное ни в ту, ни в другую сторону. В этом случае на положение узла будет влиять только значение поля
Align
.
Параметр ButtonStyle
позволяет изменить стиль кнопки развертывания/свертывания узла. У него может быть всего два значения:
-
bsRectangle — кнопка в виде прямоугольника с плюсом/минусом.
-
bsTriangle — кнопка в виде небольшого черного треугольника. Похожий имеют списки и деревья браузера Opera.
Параметр активен только при отключенных стилях оформления.
И, наконец, последний параметр — Indent
. Он позволяет указать длину отступа для каждого уровня вложенности узлов. Чем больше значение этого параметра, тем дальше друг от друга будут расположены дочерние и родительские узлы.
Во всех наших примерах отрисовки мы не изменили отображение одного очень важного элемента дерева — его заголовка. Думаю, об этом стоит сказать пару слов.
За отрисовку заголовков отвечают три события: OnAdvancedHeaderDraw
, OnHeaderDrawQueryElements
и OnHeaderDraw
. В рамках данной статьи Я рассмотрел первые два, так как они более сложны в исполнении, нежели последнее. С ним уж как-нибудь сами справитесь 😊
VT реализует очень удобный механизм отрисовки заголовков, позволяя какие-то отдельные элементы рисовать пользователю, а какие-то оставлять на попечение VT. Например, Вы можете изменить отрисовку фона заголовка колонки, но при этом оставить неизменным ее текст. Или же вообще изменить отрисовку только какого-то одного заголовка, оставив остальные в стандартном виде. Что примечательно, измененная отрисовка доступна как при включенных стилях оформления, так и при выключенных.
Событие OnHeaderDrawQueryElements
как раз предназначено для того, чтобы передать дереву список изменяемых вами элементов заголовка.
-
hpeBackground — фон заголовка.
-
hpeDropMark — иконка Drag&Drop перетаскивания.
-
hpeHeaderGlyph — иконка слева от текста заголовка.
-
hpeSortGlyph — иконка сортировки.
-
hpeText — текст заголовка.
В нашем примере мы заменим фон и текст заголовка, оставив все остальное без изменений:
//---------------------------------------------------------------------------
// Передаём в набор те элементы, которые мы будем заменять.
// Непереданные элементы (hpeDropMark, hpeHeaderGlyph, hpeSortGlyph)
// останутся на совести дерева.
//---------------------------------------------------------------------------
procedure TfrmMain.VTHeaderDrawQueryElements(Sender: TVTHeader;
var PaintInfo: THeaderPaintInfo; var Elements: THeaderPaintElements);
begin
// Мы будем заменять текст колонок и фон всего заголовка
Elements := [hpeBackground, hpeText];
end;
А вот так будет выглядеть сама процедура отрисовки:
procedure TfrmMain.VTAdvancedHeaderDraw(Sender: TVTHeader;
var PaintInfo: THeaderPaintInfo; const Elements: THeaderPaintElements);
begin
if hpeBackground in Elements then
with PaintInfo.TargetCanvas do
begin
// Рисуем фон всего заголовка дерева
Brush.Color := $00C9C9C9;
FillRect(PaintInfo.PaintRectangle);
// Растягиваем градиент
StretchDraw(PaintInfo.PaintRectangle, ImgGrad.Graphic);
if PaintInfo.Column <> nil then
begin
// Необходимо нарисовать заголовок какой-то конкретной колонки
case PaintInfo.Column.Index of
// Пользователь
0: Draw(PaintInfo.PaintRectangle.Left + ((PaintInfo.PaintRectangle.Right -
PaintInfo.PaintRectangle.Left) div 2) - (ImgUser.Width div 2),
PaintInfo.PaintRectangle.Top, ImgUser.Graphic);
// Кол-во сообщений
1: Draw(PaintInfo.PaintRectangle.Left + ((PaintInfo.PaintRectangle.Right -
PaintInfo.PaintRectangle.Left) div 2) - (ImgMsg.Width div 2),
PaintInfo.PaintRectangle.Top, ImgMsg.Graphic);
end;
end;
end;
end;
Не забудьте включить измененную отрисовку опцией заголовка hoOwnerDraw
.
VT позволяет своим узлам иметь многострочные надписи, разрешая это опцией vsMultiline
для узла. В этом случае редактор дерева будет заменен на многострочный с удобной вертикальной полосой прокрутки.
Кроме своего явного применения (хранение нескольких строк в одном узле), это свойство можно использовать как альтернативу обрезающемуся тексту, который не умещается в границах колонки. Вы можете просто переносить неумещающийся текст на новые и новые линии. Для включения многострочности принято использовать событие инициализации узла:
procedure TfrmMain.VTInitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
begin
// Включаем/выключаем поддержку многострочности
if CbMultiline.Checked then
Include(InitialStates, ivsMultiline)
else begin
Exclude(InitialStates, ivsMultiline);
// НЕ ЗАБЫВАЙТЕ ВЫПОЛНЯТЬ ЭТО:
Node.States := Node.States - [vsMultiline];
// Это выключит многострочность для узлов, где она раньше была.
end;
end;
Но это еще не все. Этот код поможет разбить текст на линии, но высоту узлов он не изменит. Т.е. может случиться так, что перенесенные линии текста уже не будут видны. Для корректного подбора высоты с учетом многострочности VT имеет очень полезный метод — ComputeNodeHeight
. Он подсчитывает высоту, необходимую для полного отображения узла в ячейке. Применять мы его будет соответственно в обработчике события OnMeasureItem
, там, где и указывается высота узлов:
//---------------------------------------------------------------------------
// Подсчёт высоты узлов в соответствии с высотой переносимого текста.
//---------------------------------------------------------------------------
procedure TfrmMain.VTMeasureItem(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; var NodeHeight: Integer);
begin
if CbMultiline.Checked then
begin
NodeHeight := VT.ComputeNodeHeight(TargetCanvas, Node, 0) + 4;
NodeHeight := Max(18, NodeHeight);
end
else
NodeHeight := 18;
end;
Теперь все сделано верно.
Последнее наше упущение — параметры Background
, BackgroundOffsetX
и BackgroundOffsetY
.
Используются они, когда необходимо нарисовать на фоне дерева что-то наподобие тематической картинки, «водяного» (прозрачного) знака. Яркий пример — папки картинок или музыки в Windows XP, где в нижнем правом углу изображена тематическая картинка для каждой из папок, отражающая ее суть.
-
Background — сама картинка, которая будет отображаться.
-
BackgroundOffsetX и BackgroundOffsetY — ее смещения по X и Y координатам соответственно.
Для включения отображения картинки необходимо установить опцию дерева TreeOptions.PaintOptions.toShowBackground
в True
. Опция toStaticBackround
из того же раздела позволяет разрешить или запретить заполнение дерева изображением на всю видимую область.
В событии OnResize
очень удобно изменять положение картинки, например, центрировать ее:
//---------------------------------------------------------------------------
// Располагаем фоновую картинку по центру.
//---------------------------------------------------------------------------
procedure TfrmMain.VTResize(Sender: TObject);
begin
VT.BackgroundOffsetX := (VT.Width div 2) - (VT.Background.Width div 2);
VT.BackgroundOffsetY := (VT.Height div 2) - (VT.Background.Height div 2);
end;
Как и обещал, привожу полный список настроек VT (TreeOptions) с описанием назначения каждого параметра.
-
AnimationOptions:
- toAnimatedToggle — отвечает за анимацию свертывания/развертывания.
-
AutoOptions:
-
toAutoDropExpand — развертывает узел, если он будет оставаться целью Drag&Drop (
DropTargetNode
) некоторое время, заданное параметром AutoExpandDelay. Рекомендуется установить вTrue
. -
toAutoExpand — автоматически раскрывает/сворачивает узел при получении им фокуса.
-
toAutoScroll — прокручивает дерево, если происходит операция Drag&Drop и курсор мыши находится рядом с краями дерева.
-
toAutoScrollOnExpand — прокручивает дерево на максимально возможное число позиций при развертывании узла.
-
toAutoSort — автоматически сортирует дерево при изменении параметров
Header.SortDirection
, илиHeader.SortColumn
, или при добавлении нового узла. -
toAutoSpanColumns — довольно необычная опция. Она позволяет перенести текст, не помещающийся в данной колонке на соседнюю, если она пуста (не содержит текста).
-
toAutoTristateTracking — автоматическое отслеживание grayed-состояния для узлов с типом отметки
ctTriStateCheckBox
. -
toAutoHideButtons — кнопки развертывания/свертывания будут автоматически скрываться для данного узла, если все его дочерние будут спрятаны (
vsVisible
). -
toAutoDeleteMovedNodes — источники перемещенных узлов будут автоматически удаляться после операций Drag&Drop.
-
toDisableAutoscrollOnFocus — выключает автоматическую прокрутку колонки к видимой области, если она получает фокус.
-
toAutoChangeScale — изменяет высоту узлов в соответствии с настройками размера шрифтов Windows.
-
toAutoFreeOnCollapse — при сворачивании узла все его дочерние узлы будут удалены, но опция
vsHasChildren
для данного узла сохранится. -
toDisableAutoscrollOnEdit — не прокручивает дерево в горизонтальном направлении при редактировании узла, чтобы расположить видимую область по центру.
-
-
MiscOptions:
-
toAcceptOLEDrop — зарегистрировать дерево, как возможную цель для OLE Drag&Drop.
-
toCheckSupport — включить поддержку отметок для узлов.
-
toEditable — включить режим редактирования для узлов. В этом случае станет доступен стандартный редактор VT и Ваши собственные.
-
toFullRepaintOnResize — полностью перерисовывать дерево при любом изменении его размеров.
-
toGridExtensions — включить поддержку некоторых расширений для симуляции элемента управления сетки (наподобие
TDBGrid
). В частности, изменяется реакция на некоторые горячие клавиши. Удобно использовать при создании приложений баз данных с VT. -
toInitOnSave — производить инициализацию узлов при сохранении в поток или файл.
-
toReportMode — дерево ведет себя как
TListView
с поддержкой report mode. -
toToggleOnDblClick — узлы будут сворачиваться/разворачиваться при двойном клике на них.
-
toWheelPanning — разрешает навигацию по дереву с помощью движений мыши (panning). Это особый режим прокрутки дерева, в который оно входит по нажатию средней кнопки мыши. После этого курсор начнет принимать вид стрелок и дерево можно будет прокручивать по определенным направлениям, передвигая курсор к соответствующим краям дерева. Чем ближе пододвигать курсор, тем быстрее будет происходить прокрутка.
-
toReadOnly — запрещается любое изменение дерева, в том числе взаимодействие с узлами и их редактирование.
-
toVariableNodeHeight — включается, когда требуется отображение узлов различной высоты. В этом случае высоту узлов дерево будет получать из обработчика события
OnMeasureItem
. -
toFullRowDrag — разрешает начало перетаскивания узла при нажатии мышью в любую область узла, а не обязательно по тексту или картинке. Должно использоваться вместе с опцией
toDisableDrawSelection
.
-
-
PaintOptions:
-
toHideFocusRect — не отображает пунктирный прямоугольник фокуса по границам узла.
-
toHideSelection — не отображает бежевый (по умолчанию) прямоугольник выделения для выделенных узлов, когда само дерево не имеет фокуса. В этом случае, когда дерево не будет иметь фокуса, Вы не сможете разобрать выделенные узлы от не выделенных.
-
toHotTrack — включает отслеживание узла, находящегося под курсором. По умолчанию, такой узел будет отображаться подчеркнутым, если Вы включите эту опцию.
-
toPopupMode — отображает дерево, как будто бы оно всегда имеет фокус.
-
toShowBackground — отрисовывать фоновую картинку, если есть (параметр
Background
). -
toShowButtons — Отображать кнопки развертывания/свертывания напротив узлов.
-
toShowDropmark — показывать значок, отображающий положение будущего узла при операциях Drag&Drop.
-
toShowHorzGridLines — включить отображение горизонтальных линий сетки.
-
toShowRoot — учитывать отступ и рисовать соединительные линии для самых верхних узлов первого уровня вложенности (дочерних узлов RootNode).
-
toShowTreeLines — отображать соединительные линии для узлов.
-
toShowVertGridLines — включить отображение вертикальных линий сетки.
-
toThemeAware — включает отображении всех элементов управления дерева (кнопок, отметок и т.д.) в соответствии с текущей темой оформления Windows XP. Приложение должно поддерживать визуальные темы оформления.
-
toUseBlendedImages — включить прозрачность для ghosted узлов и для узлов, участвующих на данный момент в копировании/вырезании.
-
toGhostedIfUnfocused — картинки будут отображаться прозрачными до тех пор, пока узел не будет выделен.
-
toFullVertGridLines — продлевает вертикальные линии сетки до конца дерева. Если данная опция будет отключена, то линии закончатся на последнем видимом узле.
-
toAlwaysHideSelection — отображает узлы как невыделенные независимо от их состояния.
-
toUseBlendedSelection — отображать выделенные узлы прозрачными.
-
toStaticBackground — запрещает заполнять фоновой картинкой видимую область дерева.
-
-
SelectionOptions (только для
TVirtualStringTree
дерева):-
toDisableDrawSelection — запрещает пользователю добавлять в текущее выделение узлы с помощью прямоугольника выделения.
-
toExtendedFocus — позволяет выделять ячейки и редактировать текст во всех колонках, а не только в
MainColumn
. -
toFullRowSelect — дерево будет рисовать прямоугольник выделения на всю свою ширину для данного узла. Таким образом, длина прямоугольника выделения не будет зависеть от ширины текста. Помимо этого, узлы можно будет выбирать нажатием мыши в любую область дерева, а не только по тексту узла.
-
toLevelSelectConstraint — запрещает выделение узлов различных уровней. Это означает, что в текущее выделение смогут попасть только узлы с таким же уровнем вложенности, что и у первого выделенного.
-
toMiddleClickSelect — разрешает выделение узлов средней кнопкой мыши.
-
toMultiSelect — разрешает выделение более чем одного узла.
-
toRightClickSelect — разрешает выделение узлов правой кнопкой мыши.
-
toSiblingSelectConstraint — ограничивает текущее выделение только узлами одного и того же родителя.
-
toCenterScrollIntoView — располагает видимую (клиентскую) область дерева центрально по вертикали по отношению к узлу, получающему фокус. Очень удобно для длинных списков.
-
toSimpleDrawSelection — упрощает операцию выделения узлов, в частности, не требует, чтобы прямоугольник выделения пересекся с текстом узла для его выделения. Если опция будет установлена в
False
, то для выделения узла придется тащить прямоугольник выделения до текстаMainColumn
колонки.
-
-
StringOptions:
-
toSaveCaptions — сохраняет текст из ячеек узла при операциях с буфером обмена, Drag&Drop и при сохранении в поток или файл. Рекомендуется включать при Drag&Drop и выключать при работе с файлами, т.к. сохранение данных узла в потоке строго рекомендуется делать самостоятельно.
-
toShowStaticText — включает отображение статического текста, который отображается рядом с обычным, но в отличие от него не может быть отредактирован редактором VT.
-
toAutoAcceptEditChange — автоматически принимать отредактированный текст, если редактирование не было отклонено по Esc или завершено по Enter. В этом случае новый текст передается в обработчик события
OnNewText
.
-
VT содержит в себе множество предопределенных горячих клавиш почти для всех своих действий. Нижеприведенный список горячих клавиш взят из официального хелпа, и по сему здесь Вы видите его перевод. Не поленитесь ознакомиться с этим списком, он довольно интересный 😊
Клавиша | Модификаторы | Результат |
---|---|---|
Home | Нет | Выделяет первый видимый узел в активной колонке. Этот узел также получает фокус ввода. Добавления в grid mode: Активный узел не изменяется, но фокус передается первой видимой колонке. |
Shift | Выделяет первый видимый узел в активной колонке и включает в выделение все видимые узлы от предыдущего активного до нового активного. Эффект очень схож с Проводником Windows. Добавления в grid mode: Активный узел не изменяется, но фокус передается первой видимой колонке. Выделение не изменяется. |
|
Ctrl | Прокручивает дерево до левого верхнего края, не изменяя ни фокус, ни выделение. | |
End | Нет | Выделяет последний видимый узел в активной колонке. Этот узел также получает фокус ввода. Добавления в grid mode: Активный узел не изменяется, но фокус передается последней видимой колонке. |
Shift | Выделяет последний видимый узел в активной колонке и включает в выделение все видимые узлы от предыдущего активного до нового активного узла. Эффект очень схож с Проводником Windows. Добавления в grid mode: Активный узел не изменяется, но фокус передается первой видимой колонке. Выделение не изменяется. |
|
Ctrl | Прокручивает дерево до правого нижнего края, не изменяя ни фокус, ни выделение. | |
Prior (страница вверх) |
Нет | Прокручивает дерево на страницу вверх, выделяя при этом узел сверху. Этот узел также получает фокус. |
Shift | То же самое, но включает в выделение страницу прокрученных узлов. | |
Ctrl | То же самое, но при прокрутке активный узел не теряет своего фокуса и выделение не изменяется. | |
Next (страница вниз) |
Нет | Тот же эффект, что и Prior, но прокручивается страница вниз. |
Shift | Тот же эффект, что и Prior, но прокручивается страница вниз. | |
Ctrl | Тот же эффект, что и Prior, но прокручивается страница вниз. | |
Up | Нет | Перемещает фокус с текущего узла на предыдущий. |
Shift | Перемещает фокус с текущего узла на предыдущий добавляет его в выделение. | |
Ctrl | Прокручивает дереву на одну линию вверх. Высота одной линии определена значением параметра DefaultNodeHeight. | |
Down | Нет | Тот же эффект, что и Up, но по направлению вниз. |
Shift | Тот же эффект, что и Up, но по направлению вниз. | |
Ctrl | Тот же эффект, что и Up, но по направлению вниз. | |
Left | Нет | Перемещает фокус в родительский узел текущего узла и выделяет его, если у текущего узла нет дочерних узлов, и он уже свернут. В противном случае фокус не изменится, но узел будет свернут. В обоих случаях узел, имеющий фокус, останется единственным выделенным узлом дерева. Добавления в grid mode: если расширенный фокус включен (опция toExtendedFocus в настройках), то результатом будет простой переход на предыдущую видимую колонку. |
Shift | В противоположность случаю с отсутствием клавиши-модификатора состояние раскрытия узла не изменяется. Фокус передается предыдущему узлу в любом случае, но при этом предыдущий узел добавляется в текущее выделение. | |
Ctrl | Дерево прокручивается влево на количество пикселей, которые берутся из параметра Indent . |
|
Right | Нет | Перемещает фокус на первый дочерний узел текущего узла, если у данного узла есть дочерние узлы, и он уже развернут. В противном случае, фокус останется неизменным, но узел будет развернут. В обоих случаях узел, имеющий фокус, останется единственным выделенным узлом дерева. Добавления в grid mode: если расширенный фокус включен (опция toExtendedFocus в настройках), то результатом будет простой переход на следующую видимую колонку. |
Shift | Аналогично случаю с отсутствием клавиши-модификатора, но выделение расширяется дочерним узлом. | |
Ctrl | Тот же эффект, что и у Left, но дерево прокручивается вправо. | |
Back | Нет | Перемещает выделение в родительский узел текущего узла и делает его единственным выделенным узлом дерева. |
Shift | Эффект отсутствует. | |
Ctrl | Эффект отсутствует. | |
Tab | Нет | Клавиша табуляции имеет несколько особое назначение, потому что используется только в дереве с grid extensions для перехода от ячейки к ячейке. Без модификатора фокус изменяется слева-направо и сверху-вниз. Чтобы клавиша была активна, необходимо включить поддержку табуляции, выставив параметр WantTabs в True . |
Shift | То же, что и без клавиши-модификатора, но фокус перемещается в обратную сторону, справа-налево и снизу-вверх. | |
Ctrl | Эффект отсутствует. | |
F1 | Нет | Эта функциональная клавиша активирует индивидуальную справку для узла. Через событие OnGetHelpContext приложение запрашивает необходимое значение контекста помощи для отображения. |
Shift | Эффект отсутствует. | |
Ctrl | Эффект отсутствует. | |
F2 | Нет | Эта функциональная клавиша включает режим редактирования для узла, имеющего на данный момент фокус, если такой узел есть, дерево редактируемо, и приложение разрешает редактирование через событие OnEditing . |
Shift | Эффект отсутствует. | |
Ctrl | Эффект отсутствует. | |
+ (add) |
Нет | Разворачивает текущий узел. |
Shift | Этот модификатор в одиночку не имеет действия. См. комментарий ниже. | |
Ctrl | Нажатие клавиши Ctrl вместе с клавишей + приводит к автоматическому изменению размеров всех колонок дерева. Если также будет нажата клавиша Shift, то будут развернуты абсолютно все узлы дерева. | |
- (subtract) |
Нет | Сворачивает текущий узел. |
Shift | Этот модификатор в одиночку не имеет действия. См. комментарий ниже. | |
Ctrl | Нажатие клавиши Ctrl вместе с клавишей - приводит к восстановлению размеров колонок дерева. Если также будет нажата клавиша Shift, то будут свернуты абсолютно все узлы дерева. | |
* (умножить) |
Нет | Рекурсивно разворачивает текущий узел со всеми его дочерними узлами. |
Shift | Эффект отсутствует. | |
Ctrl | Эффект отсутствует. | |
/ (разделить) |
Нет | Рекурсивно сворачивает текущий узел со всеми его дочерними узлами. |
Shift | Эффект отсутствует. | |
Ctrl | Эффект отсутствует. | |
Escape (отмена) |
Нет | Останавливает действия дерева, требующие специального состояния, например, редактирование, выделение мышью, Drag&Drop и т.д. |
Shift | Эффект отсутствует. | |
Ctrl | Эффект отсутствует. | |
Space (пробел) |
Нет | Используется, если поддержка отметок включена (опция toCheckSupport в настройках) и текущий узел имеет какой-нибудь тип отметки, отличный от ctNone. В этом случае клавиша переключает состояние отметки узла. |
Shift | Эффект отсутствует. | |
Ctrl | Эффект отсутствует. | |
Apps (клавиша меню) |
Нет | По аналогии с F1 активирует popup меню клавишей приложения для каждого узла. Popup меню присваивается узлу через событие OnGetPopupMenu . |
Shift | Эффект отсутствует. | |
Ctrl | Эффект отсутствует. | |
A | Нет | Это единственная буквенная клавиша, используемая деревом как горячая. Она имеет эффект только при использовании с модификатором Ctrl. |
Shift | Эффект отсутствует. | |
Ctrl | Нажатие A вместе с клавишей Ctrl приводит к выделению всех видимых узлов дерева. |
Несмотря на то, что VT не ориентирован на использование в приложениях баз данных, его заманчивые возможности могут найти применение и там. VT легко может быть использован в небольших приложениях баз данных с помощью нехитрых телодвижений. Причиной его использования вполне может быть скорость работы и богатые возможности в реализации интерфейса (редакторы, отметки, функции отрисовки и т.д.).
Здесь Я приведу простой пример использования VT в приложении с БД, который будет демонстрировать самые основные функции: чтение, запись и сортировка записей таблицы. Суть примера заключается в извлечении значений некоторых полей всех записей базы данных и выводе этих значений в VT. Частичный код примера дан ниже со всеми необходимыми комментариями. Обратите внимание, что пример использует стандартную базу данных Delphi из «DBDEMOS» набора (employee.db).
type
PDBRec = ^TDBRec;
TDBRec = record
ANum: Integer;
AFirstName,
ALastName: WideString;
Initialized: Boolean; // True, если узел был заполнен начальными данными
end;
//--------------------------------------------------------------------------
// Подготавливает дерево и начинает заполнение.
//--------------------------------------------------------------------------
procedure TfrmMain.LoadDB;
begin
if not Table.Active then
Table.Open;
with Table do
begin
Filtered := False;
Filter := '';
First;
end;
with VT do
begin
Clear;
NodeDataSize := SizeOf(TDBRec);
RootNodeCount := Table.RecordCount;
Header.SortColumn := 0;
// Для сортировки дерева рекомендуется передать последний параметр как
// True, чтобы каждый отсортированный узел обновил свои данные.
SortTree(0, sdAscending, True);
end;
end;
//--------------------------------------------------------------------------
// Чистим память от строк.
//--------------------------------------------------------------------------
procedure TfrmMain.VTFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
DBRec: PDBRec;
begin
DBRec := Sender.GetNodeData(Node);
if Assigned(DBRec) then
Finalize(DBRec^);
end;
//--------------------------------------------------------------------------
procedure TfrmMain.VTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString);
var
DBRec: PDBRec;
begin
DBRec := Sender.GetNodeData(Node);
case Column of
0: CellText := IntToStr(DBRec.ANum);
1: CellText := DBRec.AFirstName;
2: CellText := DBRec.ALastName;
end;
end;
//--------------------------------------------------------------------------
procedure TfrmMain.VTHeaderClick(Sender: TVTHeader; Column: TColumnIndex;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
if Sender.SortColumn <> Column then
Sender.SortColumn := Column;
if Sender.SortDirection = sdAscending then
Sender.SortDirection := sdDescending
else
Sender.SortDirection := sdAscending;
VT.SortTree(Sender.SortColumn, Sender.SortDirection, True);
end;
end;
//--------------------------------------------------------------------------
// Основная процедура в этом примере. Её задача - заполнять каждый новый
// узел данными из таблицы. После считывания очередной запиcи из таблицы,
// мы перемещаемся на следующую методом Next, пока не закончится база
// данных.
// В сортировке этот метод необходим для обновления данных. Хотя, на самом
// деле, можно обойтись и без инициализации во время сортировки.
//--------------------------------------------------------------------------
procedure TfrmMain.VTInitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
DBRec: PDBRec;
begin
DBRec := Sender.GetNodeData(Node);
if not DBRec.Initialized then
begin
Initialize(DBRec^);
with DBRec^ do
begin
ANum := StrToInt(Table.FieldByName('EmpNo').AsString);
AFirstName := Table.FieldByName('FirstName').AsString;
ALastName := Table.FieldByName('LastName').AsString;
Initialized := True;
end;
end
else begin
// Запись уже была заполнена начальными данными при загрузке значений
// таблицы. Значит, нужно просто обновить данные узла. После
// редактирования,
// к примеру.
with Table do
begin
Filtered := True;
Filter := 'EmpNo = ' + #39 + IntToStr(FUpdatingNum) + #39;
if RecordCount > 0 then
with DBRec^ do
begin
ANum := StrToInt(Table.FieldByName('EmpNo').AsString);
AFirstName := Table.FieldByName('FirstName').AsString;
ALastName := Table.FieldByName('LastName').AsString;
end;
end;
end;
if not Table.Eof then
Table.Next;
end;
//--------------------------------------------------------------------------
// Вносит изменения в базу данных.
//--------------------------------------------------------------------------
procedure TfrmMain.VTNewText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; NewText: WideString);
var
DBRec: PDBRec;
begin
if NewText = '' then
Exit;
DBRec := Sender.GetNodeData(Node);
with Table do
begin
Filtered := True;
// Включает фильтрацию и по индексу сотрудника находит необходимую
// запись в таблице.
Filter := 'EmpNo = ' + #39 + IntToStr(DBRec.ANum) + #39;
if RecordCount > 0 then
begin
Table.Edit;
try
// Если запись найдена, то можно внести изменения
case Column of
0: Table.FieldByName('EmpNo').AsInteger := StrToIntDef(NewText,
0);
1: Table.FieldByName('FirstName').AsString := NewText;
2: Table.FieldByName('LastName').AsString := NewText;
end;
Table.Post;
// Эта переменная нужна будет в событии инициализации узла.
// С помошью неё будет выбрана изменившаяся запись путём фильтрации
// и обновлены данные узла.
// Необходимость в этой переменной состоит в том, что
// в событии инициализации нельзя использовать фильтрацию по
// DBRec.ANum, т.к. пользователь мог отредактировать первую колонку
// и DBRec.ANum указывал бы на старый индекс записи, тогда как новый
// содержался бы в NewText.
if Column = 0 then
FUpdatingNum := StrToIntDef(NewText, 0)
else
FUpdatingNum := StrToIntDef(VT.Text[Node, 0], 0);
Sender.ReinitNode(Node, False);
except
// Ошибка...
end;
end
else
MessageBox(Self.Handle, 'Ошибка: Запись не найдена.',
PChar(Application.Title), MB_OK or MB_ICONERROR or MB_APPLMODAL or
MB_DEFBUTTON1);
end;
end;
Не забудьте выставить опцию дерева toGridExtensions
в True
, чтобы оно больше походило на редактор ячеек базы данных (TDBGrid
).
💡 Полная версия проекта, демонстрирующего взаимодействие приложения баз данных с VT, находится в папке Fig 1.9.
Вот, наконец, мы и подошли к концу. Теперь Вы можете быть твердо уверены, что знаете о VT более чем достаточно. Я старался сделать статью как можно более полной и информативной, не упуская ни единого параметра и события, в отличие от официального хелпа по компоненту, но, к сожалению, везде есть свои рамки, и поэтому что-то осталось за кадром, а о чем-то Я просто мог забыть упомянуть. Если бы Я уделял пристальное внимание абсолютно каждому элементу VT, то и без того получившаяся большая статья растянулась бы страниц на сто и походила бы скорее на полноценное печатное издание 😊 Нами остались нерассмотренными всего несколько событий компонента, не имеющие принципиального значения. Это, к примеру, OnResetNode
, OnShortenString
, OnGetPopupMenu
. Уверен, Вам самим будет интересно разобраться в них самостоятельно.
Я надеюсь, что Вам понравился компонент, и Вы убедились в его исключительной мощности и удобстве. Думаю, теперь VT станет вашим повседневным компонентом для создания деревьев и списков, а про TTreeView
и TListView
Вы забудете, как о страшном сне.
Если Вам понравилась моя статья, можно закинуть 🍩