Подобно скульптору Родену, программисты регулярно берут глыбу программного кода и отсекают от неё все лишнее. При этом им приходится очень много размышлять, так как каждое “отсечение” хотя и делает код более изящным, но увеличивает сложность всей композиции.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *