В проекте Data Keeper мне удалось в полной мере реализовать концепцию динамического пользовательского интерфейса для отдельно взятой формы редактирования объекта efmObject – внешний вид формы полностью определяется структурой редактируемого объекта и требует минимальной настройки со стороны разработчика или пользователя:
- На основании описания бизнес-процессов разработчик определяет видимость и последовательность расположения элементов на форме редактирования, а также минимальный размер элементов.
- Пользователь задает размер формы в соотвествии с имеющимся оборудованием (которые могут ограничивать размер формы) или личными предпочтениями.
Структура данных
Доработана структура данных, добавлены новые поля:
- vproperty.fieldHeight – высота элемента на форме редактирования в юнитах
- vproperty.fieldLeft – флаг привязки поля к левой части экрана
- vproperty.is_required – флаг обязательности ввода данных
- class.efmWidth – ширина формы редактирования экземпляра класса для данного класса
- class.efmHeight – высота формы редактирования экземпляра класса для данного класса
![](https://k245.ru/wp-content/uploads/2024/06/image.png)
Модули
Для улучшения архитектуры приложения я вынес весь функционал по созданию и работе с элементами формы редактирования объекта efmObject в отдельные файлы-модули, которые разместил в новой папке-ветке модулей Script\UI. Чтобы не подключать каждый новый модуль по отдельности, я использовал модуль UIClass.pas как коннектор.
// виртуальные компоненты для формы редактирования и фильтров
// 03.06.2024
// регистрация модулей
uses
'UI\UIConstVar.pas', // константы и переменные
'UI\UIEdit.pas', // текст
'UI\UIEditInteger.pas', // целое число
'UI\UIEditReal.pas', // число с плавающей запятой
'UI\UIRef.pas', // ссылка + модальный справочник
'UI\UIComboBox.pas', // ссылки + выпадающий список
'UI\UIChecker.pas', // флажок
'UI\UIMemo.pas', // многострочный текст
'UI\UIDate.pas', // даты
'UI\UIWebLink.pas'; // ссылка на веб-ресурс
begin
end.
Code language: Delphi (delphi)
Каждый компонент содержит в себе основу – панель с заголовком и компонентом для ввода текста. Этот компонент используется для сохранения в базу данных значения свойства объекта.
Дополнительные визуальные компоненты (комбобокс, датапикер и т.д.) служат для удобства пользователя. Визуально они перекрывают основной компонент. Результат выбора переводится в строковое значение и записывается в компонент для ввода текста.
Скрипты
Реализация обработчика события на открытие формы редактирования объекта стала более лаконичной и реализует принцип адаптивной верстки формы редактирования: каждый элемент размещается на своей панели-подложке, которые перемещаются по форме редактирования и растягиваются в зависимости от размера самой формы.
procedure efmObject_OnShow(Sender: TObject; Action: string);
// отображение формы редактирования
var
i: integer;
tmpSQL: string;
tmpIDClass: integer;
tmpCount: integer;
tmpLabel: TdbLabel;
tmpEdit: TdbEdit;
tmpForm: TForm;
tmpParent: TWinControl;
tmpDataSet: TDataSet;
tmpID: string;
tmpControlID: integer;
tmpIDClassList: string;
tmpParentList: string;
tmpChildList: string;
tmpTableForm: TForm;
tmpFirstControl: TWinControl;
tmpControl: TWinControl;
tmpLineTop: integer;
tmpBox: TScrollBox;
begin
try
tmpFirstControl := nil;
tmpForm := TForm(Sender);
CForm(ActiveGrid, tmpTableForm);
if Action = 'NewRecord' then
begin
FindC(tmpTableForm, 'labIDClass', tmpLabel);
efmObject.cmbClass.dbItemID := tmpLabel.Tag;
end;
// название класса - в заголовке окна
tmpForm.Caption := efmObject.cmbClass.Text;
// скроллбокс - для вертикальной прокрутки области редактирования
FindC(tmpForm,'srbMain',tmpBox,False);
if tmpBox = nil then
begin
tmpBox := TScrollBox.Create(tmpForm);
with tmpBox do
begin
parent := efmObject.panEdit;
name := 'srbMain';
align := alClient;
borderStyle := bsNone;
VertSCrollBar.Tracking := True;
end;
// это нужно для корректной работы ограничений размеров ( Constraints.MinWidth )
efmObject.panToolbar.align := alBottom;
efmObject.panEdit.align := alClient;
end;
tmpParent := tmpBox;
// Удалить все компоненты - вариант исключает повторное использование компонент, но упрощает логику их работы.
for i := tmpParent.ControlCount - 1 downto 0 do
begin
tmpParent.Controls[i].Free;
end;
tmpIDClass := efmObject.cmbClass.dbItemID;
// восстанавливаем размер
tmpSQL := 'SELECT coalesce(efmWidth,600) as width, coalesce(efmHeight,400) as height FROM class WHERE id = '+IntToStr(tmpIDClass);
SQLQuery(tmpSQL, tmpDataSet);
tmpForm.ClientWidth := tmpDataSet.FieldByName('width').asInteger;
tmpForm.ClientHeight := tmpDataSet.FieldByName('height').asInteger;
tmpDataSet.Free;
// выборка основных данных
GetChildAndParent(tmpIDClass, tmpParentList, tmpChildList);
tmpCount := 0;
tmpSQL := //
' SELECT ' + CR + //
' coalesce(vproperty.is_detail,0) as is_detail, ' + CR + //
' coalesce(vproperty.fieldWidth,1) as fieldWidth, ' + CR + //
' coalesce(vproperty.fieldHeight,1) as fieldHeight, ' + CR + //
' coalesce(vproperty.fieldLeft,0) as fieldLeft, ' + CR + //
' cproperty.id_ptype, ' + CR + //
' cproperty.id, ' + CR + //
' cproperty.name, ' + CR + //
' cproperty.is_name, ' + CR + //
' class.name as cname, ' + CR + //
' class.id_uicontrol, ' + CR + //
' class.id as ClassID ' + CR + //
' FROM cproperty ' + CR + //
' LEFT JOIN class ON class.id = cproperty.id_class1 ' + CR + //
' LEFT JOIN vproperty ON vproperty.id_cproperty = cproperty.id AND vproperty.id_class = ' +
IntToStr(tmpIDClass) + CR + //
' WHERE cproperty.id_class in (' + tmpParentList + ') AND vproperty.fieldVisible = 1 ' + CR + //
' ORDER BY vproperty.FieldOrderNum';
tmpLineTop := 0;
SQLQuery(tmpSQL, tmpDataSet);
while not tmpDataSet.EOF do
begin
tmpControlID := tmpDataSet.FieldByName('id_uicontrol').asInteger;
// сначала создаем базовый TdbEdit
tmpControl := UIEdit_Create( tmpEdit, tmpCount, efmObject.btnSave.dbGeneralTableId, tmpDataSet.FieldByName('id').asInteger, tmpDataSet.FieldByName('id_ptype').asInteger, tmpParent,
tmpLineTop, App_Scale(8), App_Scale(tmpDataSet.FieldByName('fieldWidth').asInteger * UI_CELL_WIDTH), App_Scale(tmpDataSet.FieldByName('fieldHeight').asInteger * UI_CELL_HEIGHT),
tmpDataSet.FieldByName('fieldLeft').asInteger = 1, tmpDataSet.FieldByName('name').asString, tmpDataSet.FieldByName('is_name').asInteger = 1 );
// затем добавляем нужные настройки или элементы UI
case tmpControlID of
UI_EDIT_INTEGER: tmpControl := UIEditInteger_Create( tmpEdit );
UI_EDIT_REAL: tmpControl := UIEditReal_Create( tmpEdit );
UI_CHECKER: tmpControl := UIChecker_Create( tmpEdit, tmpCount );
UI_REF: tmpControl := UIRef_Create( tmpEdit, tmpCount, tmpDataSet.FieldByName('ClassID').asInteger, efmObject.btnSave.dbGeneralTableId, tmpDataSet.FieldByName('id').asInteger );
UI_COMBOBOX: tmpControl := UIComboBox_Create( tmpEdit, tmpCount, tmpDataSet.FieldByName('ClassID').asInteger, efmObject.btnSave.dbGeneralTableId, tmpDataSet.FieldByName('id').asInteger, tmpDataSet.FieldByName('is_detail').asInteger = 1, Action );
UI_DATE: tmpControl := UIDate_Create( tmpEdit, tmpCount );
UI_WEBLINK: tmpControl := UIWebLink_Create( tmpEdit, tmpCount );
UI_MEMO: tmpControl := UIMemo_Create( tmpEdit, tmpCount );
end; // case
if tmpCount = 0 then // фокус на первый элемент
tmpFirstControl := tmpControl;
inc(tmpCount);
tmpLineTop := tmpLineTop + tmpEdit.Height * 2;
tmpDataSet.Next;
end;
tmpDataSet.Free;
UI_Resize(Sender); // корректировка размещения элементов
if tmpFirstControl <> nil then
tmpFirstControl.SetFocus; // фокус на первый элемент
except
RaiseException('efmObject_OnShow() ' + ExceptionMessage);
end;
end;
Code language: Delphi (delphi)
Улучшения коснулись процедуры создания элементов фильтрации PrepareFilterPanel(), которая представляла собой наглый копипаст efmObject_OnShow(). Методы efmObject_btnSave_OnAfterClick() и GetTextFilterValue() также были переделаны для работы с новой архитектурой элементов редактирования и отображения данных.
Логика работы процедуры UI_Resize подробно описана в статье “Адаптивная верстка“, отмечу лишь, что данные для работы алгоритма (ширина и высота элемента, а также его привязка к левому краю) хранятся в свойстве компонентов, куда они попадают при создании основного элемента в методе UIEdit_Create().
procedure UI_Resize(Sender: TObject);
var
tmpBox: TScrollBox;
tmpForm: TForm;
tmpPan: TdbPanel;
tmpPredPan: TdbPanel;
tmpNumber: integer;
tmpX: integer;
tmpY: integer;
tmpWidth: integer;
tmpHeight: integer;
tmpHeightStep: integer;
tmpMaxWidth: integer;
tmpBreak: boolean;
tmpStBar: TStatusBar;
tmpButton: TdbButton;
procedure StretchW( APan:TdbPanel);
// растянуть панель по ширине до правого края
begin
APan.Width := tmpBox.Width - 20 - APan.Left;
end;
procedure StretchH( APan:TdbPanel);
// растянуть панель по высоте до нижнего края
begin
APan.Height := tmpBox.Height - APan.Top;
end;
begin
tmpForm := TForm(Sender);
FindC(tmpForm,'srbMain',tmpBox,False);
if tmpBox = nil then // первый вызов resize для формы происходит до её отображения, поэтому скроллбокса может не быть
exit; // в этом случае происходит досрочное завершение работы данной процедуры
tmpBox.VertSCrollBar.Position := 0; // чтобы не было глюка при растягивании формы
tmpMaxWidth := 0;
tmpNumber := 0;
tmpPredPan := nil;
tmpX := 0;
tmpY := 0;
tmpHeightStep := 0;
FindC(tmpForm,'panData_'+inttostr(tmpNumber),tmpPan, False);
while tmpPan <> nil do
begin
with tmpPan do
begin
// вытаскиваем нужные для алгоритма данные из свойств компонентов
tmpWidth := constraints.MinWidth; // ширина
if tmpWidth > tmpMaxWidth then
tmpMaxWidth := tmpWidth;
tmpHeight := constraints.MinHeight; // высота
tmpBreak := ShowCaption; // привязка к левому краю
if (tmpX <> 0) and ( tmpBreak or ((tmpX + tmpWidth) > (tmpBox.Width - 20) )) then // условие переноса на следующую линию
begin
tmpX := 0;
tmpY := tmpY + tmpHeightStep;
tmpHeightStep := tmpHeight;
StretchW( tmpPredPan );
end
else
begin
if tmpHeight > tmpHeightStep then
tmpHeightStep := tmpHeight;
end;
top := tmpY;
left := tmpX;
width := tmpWidth;
height := tmpHeight;
tmpX := tmpX + width;
end;
tmpPredPan := tmpPan;
inc(tmpNumber);
FindC(tmpForm,'panData_'+inttostr(tmpNumber),tmpPan, False);
end;
if tmpPredPan <> nil then
StretchW( tmpPredPan );
if (tmpPredPan <> nil) and (tmpPredPan.Height > UI_CELL_HEIGHT) and ( (tmpPredPan.Top + tmpHeight) < tmpBox.Height ) then
StretchH( tmpPredPan );
efmObject.panEdit.Constraints.MinWidth := tmpMaxWidth + 20; // добавить 20 для вертикального скроллбара
tmpBox.Constraints.MinHeight := UI_CELL_HEIGHT;
end;
procedure efmObject_OnResize (Sender: TObject);
begin
UI_Resize(Sender);
end;
Code language: Delphi (delphi)
Компоненты UI
Добавлен компонент для редактирования многострочного текста – UIMemo. Реализован с помощью двух методов:
- UIMemo_Create() – создание компонента
- UIMemo_OnChange() – обработка ввода данных
Создание подразумевает добавление на панель-подложку экземпляра компонента TdbMemo таким образом, чтобы он перекрыл TdbEdit и растянулся на всю доступную часть подложки. UIMemo может растягиваться по вертикали, для этого в настройках свойства нужно указать высоту компонента больше, чем 1 юнит.
Результат
![](https://k245.ru/wp-content/uploads/2024/06/image-4.png)
![](https://k245.ru/wp-content/uploads/2024/06/image-1.png)
![](https://k245.ru/wp-content/uploads/2024/06/image-3.png)
![](https://k245.ru/wp-content/uploads/2024/06/image-2.png)
Что дальше?
С точки зрения отработки технологических приемов при работе с объектной структурой Data Keeper будет полезным создание отображение древовидной структуры. Само по себе это не сложно, но могут быть нюансы, связанные с работой фильтрации данных (когда дерево разрушается и превращается в обычную таблицу), а также с алгоритмами частичной загрузки дерева (аналог пейджинга для таблиц).
Также интересна технология визуализации дерева меню в основном меню приложения. То есть реализация механизма настройки главного меню для стандартной схемы представления доступа к списку объектов через главное меню, когда для каждого такого представления создается вкладка навигатора на главном окне.
Третьей доработкой может стать многооконный режим просмотра/редактирования объектов в немодальных окнах, что позволит использовать современные большие дисплеи в режиме одновременной работы с несколькими объектами без визуального перекрытия и переключения между ними.
Можно поэкспериментировать с языком сборки синтетических объектов. Это когда значения свойств объектов определяется вычислениями и различными манипуляциями с другими объектами. В просторечии это называется отчетами и аналитикой.
Нужно сделать пользовательскую документацию – хотя Data Keepr и претендует на дружественность, как минимум нужны методологические рекомендации по использованию программы. Проектная документация тоже нужна.
Но, по хорошему, давно пора переводить все эти наработки на рельсы Delphi, потому как My Visual Database больше не развивается, а имеющиеся в MVDB ограничения плохо вписываются в концепции, которые мне хочется реализовать в проекте Data Keeper.