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

Первые две версии ClassExplorer выглядят немного тяжеловато из-за того, что для некоторых сущностей я захотел иметь два представления: в виде таблицы и в виде дерева. Дерево хорошо тем, что записи можно группировать произвольным образом, что улучшает восприятие в целом, позволяя видеть всю структуру сразу. А традиционная таблица отображает записи в отсортированном виде, что может облегчить визуальный поиск. Возможно, при наличии продвинутой системы поиска, этот аргумент выглядит слабым, но я все же решил оставить возможность отображать дерево как линейный список, при этом удалив дублирующие данную функциональность компоненты.

Теория превращения

Для построения дерева используются как минимум три поля:

  • ID – идентификатор записи
  • ParentID – идентификатор родительской записи
  • Name – отображаемые данные

Названия полей ParentID и Name обычно указывают при визуальной настройке свойств компонента (в ClassExplorer они назначаются программным способом). А вот поле ID неявно используется в компонентах MVDB для отображения таблиц, при этом данное поле даже не отображается в конструкторе БД, но играет важную роль в работе дерева.

При запросе данных дерево выполняет запрос вида:

SELECT ID,ParentID,Name FROM tableName ORDER BY ParentIDCode 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 не будет опубликован. Обязательные поля помечены *