В ходе экспериментов по созданию динамического интерфейса я столкнулся с необходимостью адаптивной верстки формы редактирования. Обычно адаптивную верстку связывают с разработкой сайтов, но для десктоп приложений данная технология тоже может быть полезной. В данной статье рассматривается решение для среды разработки My Visual Database, которое в дальнейшем будет использовано в проекте DataKeeper.
Требования
- Пользователь может менять размер формы редактирования.
- Размеры элементов интерфейса задаются на этапе проектирования в юнитах, целым числом
- Элементы интерфейса упорядочены, порядок задается целым числом на этапе проектирования.
- Элементы интерфейса располагаются на форме, заполняя её справа-налево и сверху-вниз, образуя “строки”.
- Если элемент не помещается по ширине, то он переносится на следующую “строку”.
- Высота “строки” определяется максимальной высотой находящихся на ней элементов.
- Минимальная ширина области редактирования равна максимальной ширине элементов.
- Минимальная высота области редактирования равна 1 юниту (минимальной высоте элементов).
- Крайний справа элемент в “строке” растягивается по ширине таким образом, чтобы заполнить область редактирования.
- У элемента можно задать признак, который привяжет его к левой стороне области редактирования.
Реализация
В действующей модели элементы интерфейса представлены панелью TdbPanel (в реальном проекте на панели будут размещены компоненты пользовательского интерфейса – поле ввода, выпадающий список, чекбокс и т.д.). Панели создаются на основании данных, получаемых из запроса. Сами данные хранятся в таблице element:
![](https://k245.ru/wp-content/uploads/2024/05/image-21.png)
В проекте имеются две формы:
- frmMain – главная форма, на которой находится табличное представление для ввода и отображения данных об элементах интерфейса
- frmEdit – прототип формы редактирования.
frmMain
![](https://k245.ru/wp-content/uploads/2024/05/image-22.png)
TableGrid1 настроен на непосредственный ввод данных, а кнопка Button1 настроена на открытие формы frmEdit.
frmEdit
![](https://k245.ru/wp-content/uploads/2024/05/image-23.png)
Форма пустая, за исключением кнопки btnSave, которая настроена на закрытие формы.
У формы есть два обработчика событий:
- frmEdit_OnShow – отображение формы
- frmEdit_OnResize – изменение размера формы
Внутри них вызываются процедура Clear() для очистки формы и процедура Build() для создания и размещения элементов на форме в зависимости от её размера.
const
CELL_WIDTH = 100; // ширина юнита
CELL_HEIGHT = 32; // высота юнита
procedure frmEdit_OnShow (Sender: TObject; Action: string);
begin
Clear(Sender);
Build(Sender);
end;
procedure frmEdit_OnResize (Sender: TObject);
begin
Build(Sender);
end;
Code language: Delphi (delphi)
Clear()
В процедуре реализован бесконечный цикл repeat..until, внутри которого на форме ищутся и удаляются элементы с именем panElement_*. Если очередной элемент не найден, то цикл завершается.
procedure Clear(Sender: TObject;);
// удалить элементы
var
tmpForm: TForm;
tmpPan: TdbPanel;
tmpNumber: integer;
begin
tmpNumber := 0;
tmpForm := TForm(Sender);
repeat
FindC(tmpForm,'panElement_'+inttostr(tmpNumber),tmpPan, False);
if tmpPan = nil then
break;
tmpPan.Free;
inc(tmpNumber);
until 1=0;
end;
Code language: Delphi (delphi)
Build()
Построение начинается с добавления на форму компонента класса TScrollBox для прокрутки области редактирования, а также панели статуса ( экземпляр класса TStatusBar ), у которой имеется удобный уголок для растягивания формы. Кнопка “Сохранить” переносится с формы на панель статуса.
Затем производится чтение данных об элементах интерфейса и цикл обработки, внутри которого каждый элемент будет представлен панелью panElement_* (TdbPanel). Панели последовательно размещаются в области редактирования, при этом проверяется, не выходит ли очередной элемент за пределы ширины. Если выходит, то размещение начинается с новой “строки”, а предыдущий элемент растягивается до правой стороны области редактирования. Причиной “перевода строки” может быть специальное указание в данных.
Последний элемент в списке также растягивается по горизонтали, а если он имеет высоту больше одного юнита, то и по вертикали. Такие размеры характерны для компонентов многострочного редактирования (TdbMemo) или редактирования с форматированием (TdbRichEdit).
procedure Build (Sender: TObject);
var
tmpBox: TScrollBox;
tmpForm: TForm;
tmpPan: TdbPanel;
tmpPredPan: TdbPanel;
tmpDataSet:TDataSet;
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
begin
tmpBox := TScrollBox.Create(tmpForm);
with tmpBox do
begin
parent := tmpForm;
name := 'srbMain';
align := alClient;
borderStyle := bsNone;
VertSCrollBar.Tracking := True;
// добавляем статус-бар с удобным уголком для растягивания формы
tmpStBar := TStatusBar.Create(tmpForm);
with tmpStBar do
begin
top := tmpForm.Height;
parent := tmpForm;
align := alBottom;
height := 40;
SimplePanel := True;
end;
// кнопку "Сохранить" переносим на панель статуса
FindC(tmpForm,'btnSave',tmpButton);
tmpButton.Parent := tmpStBar;
tmpButton.Top := 8;
end;
end;
FindC(tmpForm,'btnSave',tmpButton);
tmpButton.Left := tmpBox.Width - tmpButton.width - 20; // придется корректировать положение, так как anchors почему-то не работают
tmpBox.VertSCrollBar.Position := 0; // чтобы не было глюка при растягивании формы
tmpMaxWidth := 0;
tmpNumber := 0;
tmpPredPan := nil;
tmpX := 0;
tmpY := 0;
tmpHeightStep := 0;
SQLQuery('SELECT * FROM element ORDER BY orderNum', tmpDataSet);
while not tmpDataSet.EOF do
begin
tmpWidth := tmpDataSet.FieldByName('width').asInteger * CELL_WIDTH;
if tmpWidth > tmpMaxWidth then
tmpMaxWidth := tmpWidth;
tmpHeight := tmpDataSet.FieldByName('height').asInteger * CELL_HEIGHT;
tmpBreak := tmpDataSet.FieldByName('AnchLeft').asInteger = 1;
FindC(tmpForm,'panElement_'+inttostr(tmpNumber),tmpPan, False);
if tmpPan = nil then
begin
tmpPan := TdbPanel.Create(tmpForm);
with tmpPan do
begin
name := 'panElement_'+inttostr(tmpNumber);
caption := tmpDataSet.FieldByName('name').asString;
parent := tmpBox;
end;
end;
with tmpPan do
begin
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;
tmpDataSet.Next;
inc(tmpNumber);
end;
if tmpPredPan <> nil then
StretchW( tmpPredPan );
if (tmpPan.Height > CELL_HEIGHT) and ( (tmpPan.Top + tmpHeight) < tmpBox.Height ) then
StretchH( tmpPan );
tmpBox.Constraints.MinWidth := tmpMaxWidth + 20; // добавить 20 для вертикального скроллбара
tmpBox.Constraints.MinHeight := CELL_HEIGHT;
end;
Code language: Delphi (delphi)
FindC()
Сервисная процедура FindC() служит для поиска элементов на форме с проверкой. Она часто используется в других моих проектах.
procedure FindC(AForm: TForm; AName: string; var AComponent: TComponent; ACheck: boolean = True);
// поиск компонента на форме с контролем
begin
if AForm = nil then
RaiseException('FindC() - AForm = nil');
AComponent := AForm.FindComponent(AName);
if ACheck and (AComponent = nil) then
RaiseException('FindC() - Не найден компонент ' + AForm.Name + '.' + AName);
end;
Code language: PHP (php)
Результат
Для теста введем немного данных. Семь элементов с различной шириной и высотой, некоторые элементы привязаны к левому краю формы:
![](https://k245.ru/wp-content/uploads/2024/05/image-24.png)
Нажмем кнопку “Отобразить”. Панели расположились в области редактирования по правилам, которые были определены в требованиях.
![](https://k245.ru/wp-content/uploads/2024/05/image-25.png)
Меняя размеры формы, мы видим, как элементы заполняют область редактирования, переходя на новые строки и меняя свой размер.
![](https://k245.ru/wp-content/uploads/2024/05/image-26.png)
![](https://k245.ru/wp-content/uploads/2024/05/image-27.png)
При необходимости появляется вертикальная полоса прокрутки:
![](https://k245.ru/wp-content/uploads/2024/05/image-28.png)
Примечание
Для использования данного алгоритма с формами редактирования, созданными вручную в конструкторе My Visual Database, необходимо применить альтернативную возможность для хранения данных, например, в свойствах компонент:
- Panel.Constraints.MinWidth – ширина
- Panel.Constraints.MinHeight – высота
- Panel.Anchors = akLeft – привязка к левому краю
Разумеется, процедура Clear() в этом случае не требуется, а Buil() не будет создавать элементы, а только менять положение и размеры существующих компонент. Панели можно сделать незаметными, убрав у них рамки ( bevelWidth = 0 ).