Подобно скульптору Родену, программисты регулярно берут глыбу программного кода и отсекают от неё все лишнее. При этом им приходится очень много размышлять, так как каждое “отсечение” хотя и делает код более изящным, но увеличивает сложность всей композиции.
Первые две версии ClassExplorer выглядят немного тяжеловато из-за того, что для некоторых сущностей я захотел иметь два представления: в виде таблицы и в виде дерева. Дерево хорошо тем, что записи можно группировать произвольным образом, что улучшает восприятие в целом, позволяя видеть всю структуру сразу. А традиционная таблица отображает записи в отсортированном виде, что может облегчить визуальный поиск. Возможно, при наличии продвинутой системы поиска, этот аргумент выглядит слабым, но я все же решил оставить возможность отображать дерево как линейный список, при этом удалив дублирующие данную функциональность компоненты.
Теория превращения
Для построения дерева используются как минимум три поля:
- ID – идентификатор записи
- ParentID – идентификатор родительской записи
- Name – отображаемые данные
Названия полей ParentID и Name обычно указывают при визуальной настройке свойств компонента (в ClassExplorer они назначаются программным способом). А вот поле ID неявно используется в компонентах MVDB для отображения таблиц, при этом данное поле даже не отображается в конструкторе БД, но играет важную роль в работе дерева.
При запросе данных дерево выполняет запрос вида:
SELECT ID,ParentID,Name FROM tableName ORDER BY ParentID
Code language: SQL (Structured Query Language) (sql)
Сортировка задается неявно и первым элементом в ней обязательно должен быть ParentID – идентификатор родительской записи. Это связано с внутренним алгоритмом построения дерева, которое происходит следующим образом. Сначала в дерево добавляются все записи, имеющие значение ParentID = NULL, это – корневые элементы (верхние узлы дерева), затем для каждого узла выполняется рекурсивное добавление дочерних элементов. Этот процесс сборки ведется до тех пор, пока все данные из набора выборки не попадут в визуальный компонент.
Обратите внимание, что если первоначальный запрос по каким-то причинам не будет содержать элементов с ParentID = NULL или эти элементы не будут идти в начале выбранных данных, то дерево останется пустым: данные в него загружены не будут.
Для превращения дерева в линейный список достаточно подсунуть в качестве ParentID поле, которое всегда содержит значение NULL. Для этого в структуре таблицы мы добавляем такое поле – nullID – необязательное, которое никогда не будет редактироваться и всегда иметь значение NULL.

Теперь понадобится немного скриптовой магии, чтобы в одно мгновение превращать дерево в список и обратно.
Скрипты
Для начала создадим небольшое соглашение по наименованию полей, которое позволит нам выполнять дальнейшие манипуляции с деревом.
const
// соглашения по наименованию полей для переключения режима таблица/список
TREE_LIST_MODE_FIELD_NAME = 'nullID'; // режим списка
TREE_TREE_MODE_FIELD_NAME = 'parentID'; // режим дерева
// заголовки для меню
TREE_LIST_MODE_MENU_CAPTION = 'Список'; // режим списка
TREE_TREE_MODE_MENU_CAPTION = 'Дерево'; // режим дерева
Code language: Delphi (delphi)
Теперь можно добавить три процедуры, которые будут отвечать за переключение режима и его контроль.
procedure Tree_SwitchToList( ATree:TdbTreeView; AFilter:string; ACustomOrderBy:string );
// переключить в режим списка
begin
// задаем поле родительского элемента, фильтр и сортировку
ATree.dbFieldParentID := TREE_LIST_MODE_FIELD_NAME;
ATree.dbFilter := AFilter;
ATree.dbCustomOrderBy := ACustomOrderBy;
ATree.dbUpdate; // обновить данные
// скрыть графические элементы дерева
TNXTreeColumn(ATree.Columns[0]).ShowLines := False;
TNXTreeColumn(ATree.Columns[0]).ShowButtons := False;
end;
procedure Tree_SwitchToTree( ATree:TdbTreeView; AFilter:string; ACustomOrderBy:string );
// переключить в режим дерева
begin
// задаем поле родительского элемента, фильтр и сортировку
ATree.dbFieldParentID := TREE_TREE_MODE_FIELD_NAME;
ATree.dbFilter := AFilter;
ATree.dbCustomOrderBy := ACustomOrderBy;
ATree.dbUpdate; // обновить данные
// показать графические элементы дерева
TNXTreeColumn(ATree.Columns[0]).ShowLines := True;
TNXTreeColumn(ATree.Columns[0]).ShowButtons := True;
end;
function Tree_isListMode( ATree:TObject;):boolean;
// определить режим работы дерева
begin
Result := TdbTreeView(ATree).dbFieldParentID = TREE_LIST_MODE_FIELD_NAME;
end;
Code language: Delphi (delphi)
Ещё одна процедура будет добавлять во всплывающее меню элементы для переключения режима отображения дерева: Tree_AddSwitchToPopup() позволяет добавить два пункта меню и подключить к ним обработчики нажатий. Такая схема сделана для максимальной гибкости использования: кроме изменения свойства dbFieldParentID, процедуры управления режимами нуждаются в параметрах сортировки и фильтрации (параметры dbFilter и dbCustomOrderBy), а они могут зависеть от отображаемых данных.
procedure Tree_AddSwitchToPopup( ATree:TdbTreeView; ASwitchToListProc:string; ASwitchToTreeProc:string; );
// добавить во всплывающее меню переключатель режима отображения
// ASwitchToListProc - название процедуры обработчика для переключения в режим списка
// ASwitchToTreeProc - название процедуры обработчика для переключения в режим дерева
var
tmpMenu: TPopupMenu;
tmpItem: TMenuItem;
tmpForm: TForm;
begin
CForm(ATree,tmpForm);
tmpMenu := ATree.PopupMenu;
// добавить визуальный разделитель
tmpItem := TMenuItem.Create(tmpForm);
tmpItem.Caption := '-';
tmpMenu.Items.Add(tmpItem);
// добавить переключение в режим дерево, по умолчанию
tmpItem := TMenuItem.Create(tmpForm);
tmpItem.Caption := TREE_TREE_MODE_MENU_CAPTION;
tmpItem.GroupIndex := 1; // пункты связываем в группу
tmpItem.RadioItem := True; // радиокнопка
tmpItem.Checked := True; // чекнутая
tmpItem.AutoCheck := True; // автоматическое переключение при клике
tmpItem.onClick := ASwitchToTreeProc;
tmpMenu.Items.Add(tmpItem);
// добавить переключение в режим списка
tmpItem := TMenuItem.Create(tmpForm);
tmpItem.Caption := TREE_LIST_MODE_MENU_CAPTION;
tmpItem.GroupIndex := 1;
tmpItem.RadioItem := True;
tmpItem.AutoCheck := True;
tmpItem.onClick := ASwitchToListProc;
tmpMenu.Items.Add(tmpItem);
end;
Code language: Delphi (delphi)
Две следующих процедуры содержат условности проекта, что лишает их статуса универсальности. Возможно, в дальнейшем я решу эту проблему, а пока, чтобы на застыть надолго в позе Мыслителя, я добавил настройки фильтров и сортировок прямо в процедуры DTF_SwitchToList_OnClick() и DTF_SwitchToTree_OnClick().
procedure DTF_SwitchToList_OnClick(Sender:TObject);
// переключение в режим списка
var
tmpMenu:TPopupMenu;
tmpTree:TdbTreeView;
tmpFilter: string;
tmpForm: TForm;
begin
tmpMenu := TPopupMenu( TMenuItem(Sender).GetParentMenu );
tmpTree := TdbTreeView( tmpMenu.PopupComponent );
tmpFilter := '';
CForm(tmpTree,tmpForm);
case tmpForm.Name of
'dtfTask_Tree': tmpFilter := 'isGroup = 0';
'dtfClassType_Tree': tmpFilter := 'isType = 0';
'dtfFuncProc_Tree': tmpFilter := '( id_classType is NULL ) and (isGroup = 0)';
end;
Tree_SwitchToList( tmpTree, tmpFilter, 'name' );
end;
procedure DTF_SwitchToTree_OnClick(Sender:TObject);
// переключение в режим дерева
var
tmpMenu:TPopupMenu;
tmpTree:TdbTreeView;
tmpFilter: string;
tmpForm: TForm;
begin
tmpMenu := TPopupMenu( TMenuItem(Sender).GetParentMenu );
tmpTree := TdbTreeView( tmpMenu.PopupComponent );
tmpFilter := '';
CForm(tmpTree,tmpForm);
case tmpForm.Name of
'dtfTask_Tree': tmpFilter := '';
'dtfClassType_Tree': tmpFilter := 'isType = 0';
'dtfFuncProc_Tree': tmpFilter := 'id_classType is NULL';
end;
Tree_SwitchToTree( tmpTree, tmpFilter, 'parentID,name');
end;
Code language: Delphi (delphi)
Осталось запустить процедуру Tree_AddSwitchToPopup() при создании динамической формы, и все заработает. Ниже приведу фрагмент процедуры DTF_Create(), где в строке 31 добавлен нужный вызов.
function DTF_Create(AName:string; ATable:string; AListFields:string; AListFieldsNames:string; ASort:string; AParentField:string; AFilter:string; AIsDetail:boolean = False):TForm;
// создание динамической формы с табличным представлением
// AName - название формы; если содержит суффикс _Tree, то форма с деревом
// ATable - dbGeneralTable для таблиц и кнопок фильтрации; или dbForeignKey для дерева
// AListFields - список отображаемых полей, можно включать вычисляемые и поля из связанных таблиц; разделитель - запятая; формат: <таблица>.<поле>
// AListFieldsNames - список надписей к полям
// ASort - dbSortField для таблиц; dbCustomOredrBy для деревьев
// AParentField - dbFieldParentID для деревьев и dbField для зависимых таблиц
// AFilter - строка для фильтрации данных
// AIsDetail - признак того, что форма является зависимой
var
tmpGrid: TdbStringGridEx;
tmpButton: TdbButton;
tmpEdit: TdbEdit;
begin
// создаём форму
Result := TForm.Create(Application);
with Result do
begin
Name := T_DYNAMIC_TABLE_FORM + AName;
Width := DTF_WIDTH_DEF;
Height := DTF_HEIGHT_DEF;
Position := poScreenCenter;
Scaled := False;
end;
// создаём таблицу или дерево
if GetSuffix(AName) = SX_TREE then
begin
tmpGrid := TdbTreeView.Create(Result);
tmpGrid.Name := T_TREE_VIEW+GRID_DEFAULT_NAME;
Tree_AddSwitchToPopup( TdbTreeView(tmpGrid), 'DTF_SwitchToList_OnClick','DTF_SwitchToTree_OnClick' );
end
else
begin
tmpGrid := TdbStringGridEx.Create(Result);
tmpGrid.Name := T_TABLE_GRID+GRID_DEFAULT_NAME;
end;
//
Code language: Delphi (delphi)
Теперь осталось “отсечь все лишние” визуальные компоненты на главной форме приложения. Я решил избавиться от нагромождения многостраничников, оставив только необходимые. Там, где к ним примыкали разделители, их пришлось заменить на панели.

Ещё какое-то время ушло на чистку кода от ненужной теперь логики вычисления, какой из вариантов (дерево или список) отображается. Затем подправил UserApp_InitForm() – сократил список создаваемых форм. В результате количество форм и компонент уменьшилось, а функциональность программы сохранилась.


Если вы внимательно посмотрите на скриншоты, то увидите, что были добавлены вкладки “Команды” и “Ключевые слова”. В них предполагается отображение сведений о языке ObjectPascal, который используется в скриптах My Visual Database. Но полной уверенности в их необходимости у меня нет, поэтому буду признателен за комментарии и пожелания по развитию функционала данной программы.