В ходе экспериментов по созданию динамического интерфейса я столкнулся с необходимостью адаптивной верстки формы редактирования. Обычно адаптивную верстку связывают с разработкой сайтов, но для десктоп приложений данная технология тоже может быть полезной. В данной статье рассматривается решение для среды разработки My Visual Database, которое в дальнейшем будет использовано в проекте DataKeeper.

Требования

  • Пользователь может менять размер формы редактирования.
  • Размеры элементов интерфейса задаются на этапе проектирования в юнитах, целым числом
  • Элементы интерфейса упорядочены, порядок задается целым числом на этапе проектирования.
  • Элементы интерфейса располагаются на форме, заполняя её справа-налево и сверху-вниз, образуя “строки”.
  • Если элемент не помещается по ширине, то он переносится на следующую “строку”.
  • Высота “строки” определяется максимальной высотой находящихся на ней элементов.
  • Минимальная ширина области редактирования равна максимальной ширине элементов.
  • Минимальная высота области редактирования равна 1 юниту (минимальной высоте элементов).
  • Крайний справа элемент в “строке” растягивается по ширине таким образом, чтобы заполнить область редактирования.
  • У элемента можно задать признак, который привяжет его к левой стороне области редактирования.

Реализация

В действующей модели элементы интерфейса представлены панелью TdbPanel (в реальном проекте на панели будут размещены компоненты пользовательского интерфейса – поле ввода, выпадающий список, чекбокс и т.д.). Панели создаются на основании данных, получаемых из запроса. Сами данные хранятся в таблице element:

В проекте имеются две формы:

  • frmMain – главная форма, на которой находится табличное представление для ввода и отображения данных об элементах интерфейса
  • frmEdit – прототип формы редактирования.

frmMain

TableGrid1 настроен на непосредственный ввод данных, а кнопка Button1 настроена на открытие формы frmEdit.

frmEdit

Форма пустая, за исключением кнопки 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)

Результат

Для теста введем немного данных. Семь элементов с различной шириной и высотой, некоторые элементы привязаны к левому краю формы:

Нажмем кнопку “Отобразить”. Панели расположились в области редактирования по правилам, которые были определены в требованиях.

Меняя размеры формы, мы видим, как элементы заполняют область редактирования, переходя на новые строки и меняя свой размер.

При необходимости появляется вертикальная полоса прокрутки:

Примечание

Для использования данного алгоритма с формами редактирования, созданными вручную в конструкторе My Visual Database, необходимо применить альтернативную возможность для хранения данных, например, в свойствах компонент:

  • Panel.Constraints.MinWidth – ширина
  • Panel.Constraints.MinHeight – высота
  • Panel.Anchors = akLeft – привязка к левому краю

Разумеется, процедура Clear() в этом случае не требуется, а Buil() не будет создавать элементы, а только менять положение и размеры существующих компонент. Панели можно сделать незаметными, убрав у них рамки ( bevelWidth = 0 ).

Ссылки

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

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