Визуальное средство разработки позволяет создавать любые формы с помощью мышки, перетягивая нужные элементы по экрану. Ещё несколько минут нужно на то, чтобы отредактировать необходимые параметры в редакторе свойств. А если таких форм нужно много и они практически одинаковые? Тогда, используя технологию CopyPaste, можно создать десяток форм и не о чем не беспокоиться. До тех пор, пока не понадобится внести какие-либо изменения во внешний вид или механику работы этих форм. И в этот момент разработчики начинают задумываться над альтернативными решениями.
Динамический интерфейс
Часто под динамическим интерфейсом понимают механизм пользовательских настроек расположения элементов UI (кнопки, плавающие панели инструментов и т.д.), или поведение интерфейса, адаптированное под конкретного пользователя с его правами (сокрытие недоступных элементов: кнопок или пунктов меню), но я вкладываю в это определение немного иной смысл.
Динамический интерфейс - это пользовательский интерфейс, который создаётся автоматически, а состав и функциональность его элементов определяется структурой базы данных.
Динамический интерфейс – это мечта любого разработчика: достаточно спроектировать базу данных, и тут же создается пользовательский интерфейс для доступа к ней: табличные формы и формы редактирования. А стоит отредактировать структуру базы, как формы сами трансформируются: добавляются новые или удаляются ненужные уже компоненты ввода и отображения данных, перестраиваются связи, выравниваются размеры.

Но это утопия: если программа не тривиальная, то создать подобный “разумный” механизм крайне сложно. Либо дизайн такого интерфейса настолько плох, что пользователь только тихо стонет, глядя на однообразные таблички с мелкими буковками. Но попробовать всегда хочется.
ClassExplorer – это пример программы с нестандартным интерфейсом и достаточно сложной структурой данных. А это вполне подойдет для эксперимента по созданию динамического интерфейса. Чтобы эксперимент было удобнее проводить, я решил добавить в этот проект модуль ComponentExplorer – скрипт, который позволяет увидеть содержимое созданного в среде разработки My Visual Database проекта, как говорится, изнутри.
Цели эксперимента
- Исследовать возможность программного создания форм табличного представления
- Оценка сложности программного создания форм редактирования
- Сравнение технологий визуального и процедурного программирования
Формы
Как известно, лучший эксперимент – это практический: одними рассуждениями прийти к правильным выводам крайне сложно, поэтому я решил заменить все формы с табличным представлением скриптом. Всего в проекте ClassExplorer я насчитал 15 форм табличного представления, одна из которых заполняется SQL запросом, а остальные были созданы только с помощью средств визуальной настройки. Причем три из них отображают информацию в виде дерева, а 6 зависят от других форм -используют кнопку с настройками “Поиск”.
Структура табличных форм очень простая: на форме находится таблица, растянутая на всю клиентскую область.

Для зависимых форм добавлена кнопка с настроенной функцией “Поиск” и поле ввода, в которое записывается ID родительской записи для фильтрации данных.

Скрипты
Для перехода на динамические табличные формы (DTF – Dynaimic Table Form) мне потребовалось написать всего два скрипта: DTF_Create() для создания формы и DTF_GetGrid() для доступа к компоненту отображения данных по имени формы (для оптимизации кода). Также потребовалась оптимизация кода и создание универсального обработчика UserApp_btnUpdate_OnClick() для события onClick для кнопки btnUpdate.
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; end else begin tmpGrid := TdbStringGridEx.Create(Result); tmpGrid.Name := T_TABLE_GRID+GRID_DEFAULT_NAME; end; // with tmpGrid do begin Parent := Result; AppearanceOptions := aoAlphaBlendedSelection+aoBoldTextSelection; // размеры шрифтов и высота строк Font.Name := 'Segoe UI'; Font.Size := 11; RowSize := 24; HeaderSize := 24; // К сожалению, таблицы не совсем поддерживают стили, если их создавать программно // поэтому пока настройка для одного стиля, потом нужно что-то придумать EnableVisualStyles := True; // не помогло... HeaderStyle := hsFlatBorders; // Color := $00CEDDD1; // clLime; Font.Color := $00294431; // GridLinesColor := $00CEDDD1; // SelectionColor := clBlack; InactiveSelectionColor := $00CEDDD1; HighlightedTextColor := $00294431; // используем anchors, чтобы можно было сделать рамку Top := 0; Left := 0; Width := Result.ClientWidth; Height := Result.ClientHeight; Anchors := akTop+akBottom+akLeft+akRight; // для подключения альтернативных обработчиков AssignEvents(tmpGrid); // начинаем настройку для извлечения данных dbFilter := AFilter; // фильтрация dbListFields := AListFields; // список отображаемых полей dbListFieldsNames := AListFieldsNames; // список надписей к полям if tmpGrid is TdbTreeView then begin TdbTreeView(tmpGrid).dbForeignKey := ATable; // главная таблица TdbTreeView(tmpGrid).dbFieldParentID := AParentField; dbCustomOrderBy := ASort; // сортировка end else begin dbGeneralTable := ATable; // главная таблица dbSortField := ASort; // сортировка end; // извлечь данные, чтобы появились колонки dbUpdate; // теперь обработчики событий if tmpGrid is TdbTreeView then begin // для дерева dbOnCellClick := 'UserApp_UpdateDD'; dbOnChange := 'Tree_RestoreCollapseList'; dbOnClick := 'UserApp_SetActiveGrid'; dbOnExit := 'Tree_SetCollapseList'; dbOnKeyUp := 'UserApp_Grid_OnKeyUp'; dbOnResize := 'Grid_OnResize'; end else begin // для таблицы OnCellClick := 'UserApp_UpdateDD'; OnClick := 'UserApp_SetActiveGrid'; dbOnKeyUp := 'UserApp_Grid_OnKeyUp'; OnResize := 'Grid_OnResize'; end; end; // добавляем рамку Grid_AddFrame(tmpGrid); // теперь вариант, если форма зависимая и есть главная форма if AIsDetail then begin // создаем кнопку tmpButton := TdbButton.Create(Result); AssignEvents(tmpButton); with tmpButton do begin Parent := Result; Name := 'btnUpdate'; Visible := False; dbActionType := adbSearch; dbListControls := 'edtIDMaster'; dbResultControl := 'tgrMain'; dbOnClick := 'UserApp_btnUpdate_OnClick'; // dbGeneralTable := ATable; // главная таблица dbListFields := AListFields; // список отображаемых полей dbListFieldsNames := AListFieldsNames; // список надписей dbSortField := ASort; // сортировка end; // создаем поле для ID tmpEdit := TdbEdit.Create(Result); with tmpEdit do begin Parent := Result; Name := 'edtIDMaster'; Visible := False; dbFilter := '='; dbTable := ATable; dbField := AParentField; end; tmpButton.Click; // загружаем данные end; // загрузить настройки ширины колонок Grid_LoadColumnWidths(tmpGrid); end; function DTF_GetGrid(AFormName:string):TdbStringGridEx; // доступ к компоненту отображения данных по имени формы var tmpForm: TForm; begin tmpForm := App_GetFormByName(AFormName); // находим форму if tmpForm = nil then RaiseException('DTF_GetGrid() - не найдена форма '+AFormName) else begin // ищем таблицу Result := TdbStringGridEx( tmpForm.FindComponent(T_TABLE_GRID+GRID_DEFAULT_NAME) ); if Result = nil then // если не нашли, то begin // ищем дерево Result := TdbStringGridEx( tmpForm.FindComponent(T_TREE_VIEW+GRID_DEFAULT_NAME) ); if Result = nil then RaiseException('DTF_GetGrid() - на форме '+AFormName+' не найдена таблица или дерево'); end; end; end;
В процессе работы выяснилось, что у класса TdbStringGridEx при создании его с помощью конструктора Create() есть несколько нюансов в работе, а именно:
- Нет полной поддержки стилей
- Не загружаются настройки ширины колонок
Эти нюансы можно обойти. Для загрузки настроек ширины добавлена процедура Grid_LoadColumnWidths()
procedure Grid_LoadColumnWidths(AGrid: TdbStringGridEx;); // восстанавливаем настройки ширины колонок из файла настроек var tmpForm: TAForm; tmpName: string; tmpCol: integer; tmpIniFile: TIniFile; begin tmpIniFile := TIniFile.Create(Application.SettingsFile); CForm(AGrid, tmpForm); for tmpCol := 0 to AGrid.Columns.Count - 1 do begin tmpName := tmpForm.name+'.'+AGrid.Name+'.'+IntToStr(tmpCol); AGrid.Columns[tmpCol].Width := tmpIniFile.ReadInteger('Grids',tmpName, 30); end; tmpIniFile.Free; end;
А вот со стилями простого решения нет: можно настроить цвета таблицы, но нет возможности определить эти цвета из настроек стиля. То есть придется для каждого стиля прописать несколько констант, определяющих цвет таблицы, текста и выделения ячеек в различных режимах.
Довольно странным выглядит процедура инициализации форм. Тут явно напрашивается решение с табличным хранением данных о формах. Вопрос только в том, где разместить такую таблицу: в базе данных, в массивах или отдельном файле .csv
procedure UserApp_InitForm; // инициализация форм var tmpSplitter: TSplitter; begin // убрать главное меню // frmMain.Menu := nil; // App_SetDoubleBuffer; App_AddFrame; // добавляем сплиттеры и выравнивание Spl_1 := Splitter_Create( frmMain.pgcClassView, frmMain.pgcClass, alLeft); // классы Spl_2 := Splitter_Create( frmMain.pgcTypeView, frmMain.pgcType, alLeft); // типы Spl_3 := Splitter_Create( frmMain.pgcFunctionView, frmMain.pgcFunction, alLeft); // функции frmMain.pgcVariableView.align := alClient; // переменные frmMain.pgcTaskView.align := alClient; // задачи Spl_4 := Splitter_Create( frmMain.pgcMethodParamView, frmMain.panMethod, alBottom ); // методы // динамические формы Нзвание формы Таблица Список отображаемых полей Список подписей полей Сортировка Родитель Фильтр Form_ShowOnWinControl( DTF_Create('ClassType_List', 'classType', 'classType.name', 'Название', 'classType.name', '', 'isType = 0'), frmMain.tshClassList ); Form_ShowOnWinControl( DTF_Create('Task_List', 'task', 'task.name', 'Название', 'task.name', '', 'isGroup = 0'), frmMain.tshTaskList ); Form_ShowOnWinControl( DTF_Create('ClassType_TypeList', 'classType', 'classType.name,classType.Description', 'Название,Описание', 'classType.name', '', 'isType = 1'), frmMain.tshTypeList ); Form_ShowOnWinControl( DTF_Create('FuncProc_FunctionList', 'funcProc', 'funcProc.name,funcProc.ResultType,funcProc.Description', 'Название,Тип,Описание', 'funcProc.name', '', '( id_classType is NULL ) and (isGroup = 0)'), frmMain.tshFunctionList ); Form_ShowOnWinControl( DTF_Create('Property_VariableList', 'property', 'property.name,property.TypeName,property.Description', 'Название,Тип,Описание', 'property.name', '', '( id_classType is NULL )'), frmMain.tshVariableList ); Form_ShowOnWinControl( DTF_Create('ClassType_Tree', 'classType', 'classType.name', 'Название', 'ParentID,name', 'parentID', 'isType = 0'), frmMain.tshClassTree ); Form_ShowOnWinControl( DTF_Create('Task_Tree', 'task', 'task.name', 'Название', 'ParentID,name', 'parentID', ''), frmMain.tshTaskTree ); Form_ShowOnWinControl( DTF_Create('FuncProc_Tree', 'funcProc', 'funcProc.name,funcProc.ResultType,funcProc.description', 'Название,Тип,Описание', 'ParentID,name', 'parentID', 'id_classType is NULL'), frmMain.tshFunctionTree ); Form_ShowOnWinControl( DTF_Create('Property', 'property', 'property.name,property.TypeName,property.description', 'Название,Тип,Описание', 'property.name', 'id_classType','',True), frmMain.tshProperty ); Form_ShowOnWinControl( DTF_Create('FuncProc_Method', 'funcProc', 'FuncProc.name,FuncProc.ResultType,FuncProc.description', 'Название,Тип,Описание', 'FuncProc.name', 'id_classType','',True), frmMain.panMethod ); Form_ShowOnWinControl( DTF_Create('ClassEvent', 'classEvent', 'classEvent.name,classEvent.description', 'Название,Описание', 'classEvent.name', 'id_classType','',True), frmMain.tshEvent ); Form_ShowOnWinControl( DTF_Create('FuncProcParam_Method', 'funcProcParam','funcProcParam.orderNum,funcProcParam.name,funcProcParam.description', '№,Название,Описание', 'funcProcParam.orderNum','id_funcProc', '',True), frmMain.tshMethodParamList ); Form_ShowOnWinControl( DTF_Create('FuncProcParam_Function','funcProcParam','funcProcParam.orderNum,funcProcParam.name,classType.name,funcProcParam.SubType,funcProcParam.description','№,Название,Тип,*,Описание','funcProcParam.orderNum','id_funcProc', '',True), frmMain.tshFunctionParam ); Form_ShowOnWinControl( DTF_Create('TypeConst', 'typeConst', 'TypeConst.name,TypeConst.description', 'Название,Описание', 'TypeConst.name', 'id_classType','',True), frmMain.tshTypeConst ); // статические формы Form_ShowOnWinControl( frmSearchResult, frmMain.tshSearchResult ); Form_ShowOnWinControl( frmExampleView, frmMain.tshExample ); // загрузить сотсояние узлов деревьев Tree_LoadCollapseList( TdbTreeView( DTF_GetGrid('dtfFuncProc_Tree') ) ); Tree_LoadCollapseList( TdbTreeView( DTF_GetGrid('dtfClassType_Tree') ) ); Tree_LoadCollapseList( TdbTreeView( DTF_GetGrid('dtfTask_Tree') ) ); // UserApp_InitAboutForm; end;
Выводы
Я решил свести итоги своих исследований в сравнительную таблицу. Плюсы и минусы распределились, выбор той или иной технологии создания форм зависит от задач, которые стоят у разработчика.
Аспект | Статическая форма | Динамическая форма |
---|---|---|
Затраты на разработку механизма | Нулевые. В MVDB уже имеется визуальный редактор форм. | Высокие. Требуется проектирование и разработка системы создания и управления динамическими формами. |
Затраты на создание единичной формы | Низкие | Низкие |
Затраты на редактирование (доработку) форм | Средние. Зависят от числа форм. Требуется одновременная правка нескольких экземпляров форм: настройка их свойств и/или переписывание обработчиков. | Низкие. Не зависят от числа форм. Вносятся точечные изменения в скрипт. |
Сложность написания скриптов | Средняя. Зависит от стиля написания. Допустимы прямые ссылки на формы и компоненты. | Высокая. Доступ к формам или компонентам только через процедуры. |
Надёжность скриптов | Средняя. Зависит от стиля написания: если использовать прямые ссылки на формы и компоненты, то работает контроль имен во время компиляции. | Низкая. Нет контроля имен форм и компонентов во время компиляции. |
Поддержка стилей | Полная | Частичная. Компонент TdbStringGridEx не полностью поддерживает стиль приложения |
Скорость компиляции приложения | Средняя. Зависит от числа форм | Высокая. Не зависит от числа форм |
Скорость запуска приложения | Средняя. Зависит от числа форм | Высокая. Не зависит от числа форм |
Если форм мало, их доработки относительно редкие или количество условностей в работе интерфейса велико, то предпочтительней использовать статические формы.
Если форм много, проект постоянно дорабатывается и алгоритмы работы строго формализованы, то я рекомендую динамические формы.
Что дальше?
Два года назад я сделал неплохую CRM/PM (Client Relation Management – система управления клиентами; Project Management – система управления проектами), которой пользуюсь до сих пор. Но её внешний вид и стиль написания скриптов, который я тогда использовал, оставляет желать лучшего.

Поэтому в моих планах на ближайшее будущее – создать новый проект с использованием всех прогрессивных технологий, доступных в My Visual Database (статическая система прав, статические формы, стили оформления, генератор отчетов, визуализация аналитических графиков и т.д.), а также тех наработок, которые у меня накопились за это время (элементы динамического интерфейса, динамическая система прав, локализация, управление графическим контентом и т.д.).
Проект ClassExplorer также будет развиваться, но в большей степени за счет наполнения базы данных информацией о My Visual Database, используемых компонентах и функциях, а также добавлением новых примеров решений практических задач.

Ссылки
- “Component Explorer”
- “Эффект бабочки” – статья о ClassExplorer v.1.1
- ClassExplorer v.1.2 – архив программы
- ClassExplorer v.1.2 – файлы проекта (доступны только подписчикам библиотеки “Визуальное программирование”)