Визуальное средство разработки позволяет создавать любые формы с помощью мышки, перетягивая нужные элементы по экрану. Ещё несколько минут нужно на то, чтобы отредактировать необходимые параметры в редакторе свойств. А если таких форм нужно много и они практически одинаковые? Тогда, используя технологию 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() есть несколько нюансов в работе, а именно:

  1. Нет полной поддержки стилей
  2. Не загружаются настройки ширины колонок

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

Ссылки

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

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