Изображение квадрата Дюрера

ООО АВТОМАТИКА плюс

Rambler's Top100

Рейтинг@Mail.ru

Разработка интерфейса пользователя

Данная статья ориентирована главным образом на программистов Delphi, не искушенных в разработке пользовательских интерфейсов. Однако, я считаю, что данная статья может быть полезной и для профессионалов, поскольку в ней рассматриваются средства (оформленные в виде компонента) для автоматизации Drag & Dock. Я совершенно не силен в дизайнерском искусстве, поэтому никакого рисования вы здесь не увидите, всему основа - стандартные компоненты Delphi. Описываться будет некоторое MDI-приложение, так как на данным момент, я полагаю, большинство серьезных приложений под Windows пишут с применением MDI.

Использование механизма действий

Подробно механизм действий описан в литературе по Delphi (например, у Архангельского), так что буду краток. В Delphi механизм действий реализуется в помощью компонентов TActionList и TActionManager. TActionManager - очень мощный компонент, позволяющий создавать панели меню и инструментальные панели с возможностью их перестройки в режиме выполнения программы. Однако у этого компонента есть недостатки:

  • инструментальные панели недостаточно корректно ведут себя во время причаливания (для нас это довольно чувствительно, поскольку наше приложение будет использовать Drag & Dock);
  • для перестройки панелей и пунктов меню используется компонент TCustomizeDlg, в котором все визуальные элементы названы по-английски;
  • недостаточная гибкость. Отсутствует автоматическое встраивание меню дочерней формы в меню главной формы в MDI-приложении.
  • сложность программирования новых действий (чтобы добавить новый пункт меню, нужно выполнить гораздо больше манипуляций, чем при добавлении пунктов непосредственно в TMainMenu, причем значительная часть этих манипуляций относится к прочтению справки).

TActionList - значительно более простое средство, однако в этом случае для каждого пункта главного меню и для каждой кнопки панели TToolBar приходится указывать соответствующее действие (в выпадающем списке Action). Далее будем рассматривать только TActionList. В нем будем формировать коллекцию действий (конечно, для этих целей можно использовать и TActionManager, однако, в этом случае, размер приложения резко увеличится).

Итак, создайте две формы. У первой (родительской) укажите FormStyle = fsMDIForm, а у второй (дочерней) FormStyle = fsDMIChild. Если вы запустите приложение сейчас, то увидите обе формы, причем дочерняя форма окажется внутри родительской.

Поместите на главную форму компоненты TActionList и TImageList, причем у первого компонента в качестве Images укажите ImageList1. Добавьте в ImageList1 несколько иконок.

Щелкните дважды на компоненте TActionList и выберите пункт New Action. Появится новое пустое действие (Action1). Зададим ему следующие свойства:

  • Caption = "Новое окно"
  • ImageIndex = "0"
  • ShortCut = "Ctrl+N"
  • Name = "actnNewWnd"

Желательно сразу задавать действиям осмысленные имена. Ниже будут даны рекомендации по присвоению имен различным элементам проекта Delphi.

Далее для созданного действия напишите обработчик OnExecute:

procedure TMainForm.actnNewWndExecute(Sender: TObject);
begin
  TChildForm.Create(Self);
end;

Этим мы создадим новую дочернюю форму, причем метод Show для дочерних форм указывать не требуется.

Поместите на родительскую форму компонент TMainMenu. Установите свойство Images. Создайте пункт меню Файл, а в нем подменю. В этом подменю ничего не меняйте, и только в качестве Action укажите actnNewWnd. Пункт меню автоматически окажется связанным с указанным действием.

Запустите приложение. При выборе нового пункта меню или при нажатии комбинации клавиш Ctrl+N будет создаваться новое дочернее окно. При закрытии дочерние окна у нас сворачиваются, поэтому у них следует создать обработчик OnClose со следующим содержимым:

procedure TChildForm.FormClose(Sender: TObject; 
  var Action: TCloseAction);
begin
  Action := caFree;
end;

Разработчиками Delphi предусмотрено множество часто используемых стандартных действий. Изучите их. Будет глупо выглядеть, если вы сами начнете программировать например кнопки "Копировать", "Вырезать", "Вставить" и т.п. В Delphi уже есть соответствующие действия, и реализованы они на таком уровне качества, которого вы вряд ли добьетесь. Также со стандартными действиями автоматически загружаются соответствующие иконки, так что вам не нужно будет их нигде искать.

Добавим в проект стандартные действия, управляющие дочерними окнами. В редакторе компонента TActionList выберите пункт "New Standard Action…" и в появившемся окне выделите с помощью Ctrl и Shift все действия, относящиеся к категории Window, после чего нажмите "ОК". Переведите все названия и подсказки на русский язык. Изменять значение поля Name не следует, так как с этими действиями вам не придется работать непосредственно.

Далее в главном меню создайте пункт Окна, а в нем - подпункты, связанные с добавленными нами действиями. Запустите программу. Раскройте пункт меню окна, и вы увидите, что у половины пунктов уже есть иконки. Нам даже не пришлось о них думать. Закройте все дочерние окна, и вы увидите, что пункты меню стали недоступными.

Теперь поместите на главную форму компонент TToolBar и создайте в нем 3 кнопки. Свяжите их с действиями WindowCascade1, WindowTileHorizontal1, WindowTileVertical1. У самой панели установите свойство Images. Запустите приложение и вы увидите, что быстрые кнопки работают синхронно с соответствующими пунктами главного меню.

Поддержание такой синхронности - это одно из основных достоинств механизма действий.

Надеюсь, мне удалось убедить вас в исключительной полезности использования механизма действий, и в дальнейшем вы не станете программировать то, что уже написано, да еще и так качественно.

Рекомендации по присвоению имен

Эти рекомендации взяты из книги Тейксейра и Пачеко по Delphi 5. Авторы книги - высококлассные специалисты, входящие (если не ошибаюсь) в команду разработчиков Delphi, так что к таким рекомендациям стоит прислушаться.

Название формы следует задавать так: Имя формы + Form. Примеры: MainForm, AboutForm, ChildForm.

Название модуля формы следует задавать так: Имя формы + Frm. Примеры: MainFrm, AboutFrm, ChildFrm.

Если непосредственно в коде программы вы ссылаетесь на какие-либо компоненты, то таким компонентам следует задавать осмысленные имена. При формировании имен желательно использовать префиксы. Обычно они образуются путем удаления из предлагаемого имени всех гласных и двойных согласных. Например, для кнопки TButton префиксом может быть btn, для TSpeedButton - spdbtn и т.д. Благодаря префиксам ускоряется поиск нужного компонента, а благодаря осмысленным именам улучшается "читабельность" программы и появляется возможность писать программы с минимальным количеством комментариев.

Плавающие инструментальные панели

Благодаря компоненту TControlBar у программиста появляется возможность разрабатывать приложения с плавающими панелями инструментов. Минимальные действия программиста следующие:

  • кладете на форму компонент TControlBar. Свойство Align желательно указывать alTop - видимо именно этот режим разработчики компонента продумали лучше остальных.
  • кладете на TControlBar необходимые инструментальные панели. Изменяете свойства Caption (оно будет видимым только у отстыкованной панели), DragKind=dkDock и DragMode=dmAutomatic и Align=alNone. Добейтесь, чтобы инструментальные панельки, лежащие на TControlBar, выглядели прилично, после чего установите у TControlBar AutoSize=True.

Можете запустить приложение и проверить, что любую панель инструментов можно отстыковать и пристыковать обратно. Обратите внимание, что отстыкованное окно можно закрыть нажатием на крестик.

Обычно пользователю предоставляется возможность включать и выключать те или иные инструментальные панели. В этом случае для каждой панели вы должны создать действие (хотя это не обязательно, т.к. возможность отключения панелей будет доступной только в главном меню), у которого следует задать Caption и Checked=True (т.е. панель видима по-умолчанию). Обработчик такого действия может быть следующим:

procedure TMainForm.actnShowStandartPanelExecute(Sender: TObject);
begin
  with TAction(Sender) do
  begin
    Checked := not Checked;
    tlbrStandart.Visible := Checked;
  end;
end;

Все это, согласитесь, элементарно. Остается неясным, как отловить момент, когда пользователь сам спрятал инструментальную панель нажатием на крестик. Здесь своей интуицией вы не обойдетесь. Придется слегка по программировать.

Создайте следующий класс:

type
  TTlbrFloatingForm = class(TCustomDockForm)
  protected
    procedure WM_CLOSE(var Msg: TMessage); message WM_CLOSE;   
    procedure DoShow; override;
  end;

.....................

procedure TTlbrFloatingForm.DoShow;
begin
  inherited;
  // Блокируем все попытки пользователя изменить размеры окошка
  AutoSize := True;
end;

procedure TTlbrFloatingForm.WM_CLOSE(var Msg: TMessage);
begin
  // Информируем главную форму, что соответствующая панелька закрылась.
  with MainForm do 
    if Self.Controls[0] = tlbrStandart then
	  actnShowStandartPanel.Checked := False;  
  
  // Закрываем саму форму
  inherited;
end;

А в событии OnCreate главной формы пишем следующее:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  tlbrStandart.FloatingDockSiteClass := TTlbrFloatingForm;
end;

Дело в том, что при отстыковке панели инструментов (да и любого другого визуального компонента) создается новая форма (в нашем случае это объект типа TTlbrFloatingForm), и именно эту форму вы закрываете щелчком мышки на крестике. В обработчике сообщения WM_CLOSE этот момент отлавливается, и выполняется код actnShowStandartPanel.Checked := False;

В перекрытом методе DoShow мы запрещаем пользователю изменять размеры формы. Кстати, в этом методе вы можете изменить некоторые параметры формы (например разрешить форме разворачиваться на весь экран - вот пользователь обрадуется ;-)

Применение Drag and Dock для других визуальных компонентов

Здесь ситуация несколько сложнее. Вы, конечно, можете попрактиковаться со свойствами DockSite, DragKind, DragMode, однако желаемого результата вы вряд ли достигните. А желаемый результат - это пристыковка элементов формы к разным ее краям с возможностью дальнейшей отстыковки. О сохранении текущей позиции элементов управления я пока вообще молчу.

Рассмотрим достаточно удобный для пользователя (по крайней мере, с виду) и геморный для программиста случай. Есть некоторое дерево TTreeView, хранящее какие-то данные и нам нужно дать возможность пользователю пристыковывать его к любому краю формы, а также менять размеры данного компонента после пристыковки.

Мы не может играть свойством DragMode самого дерева, поэтому его следует поместить на другой элемент формы, например на панельку TPanel. У дерева следует установить свойство Align=alClient, чтобы оно было растянуто по всей панельке.

Далее на каждый край формы следует поместить по панельке, и у каждой из них установить соответствующее выравнивание, а также свойство DockSite = True.

Далее к каждой панельке нужно "приклеить" по Сплиттеру (TSplitter).

Надеюсь, что дальше продолжать не стоит. Поверьте, чтобы все обработать, нужно набить не менее 1000 строк кода и перечитать множество разделов справки и статей. Я это все прошел, и сделал вывод, что лучше разработать новый компонент, который бы все необходимое обрабатывал автоматически. Этот компонент - TLDSPanel. Вы можете найти его в архиве.

Так вот, поставленная задача с помощью компонента TLDSPanel решается без единой строчки кода.

Сохранение положения плавающих компонентов

С помощью двух следующих функций можно сохранить положение указанного элемента управления в ini-файле, и в дальнейшем восстановить его положение.

procedure TMainForm.SaveWinControlPos(AControl: TWinControl);
var
  AControlName: string;
begin
  with FormStorage do
  begin
    AControlName := AControl.Name;
    // Запоминаем имя родителя
    WriteString(AControlName + '_ParentName', AControl.Parent.Name);
    // Запоминаем видимость
    WriteString(AControlName + '_Visible', BoolToStr(AControl.Visible));
    if AControl.Parent.Name = '' then
    begin
      WriteInteger(AControlName + '_DockLeft', AControl.Parent.Left);
      WriteInteger(AControlName + '_DockTop', AControl.Parent.Top);
      WriteInteger(AControlName + '_DockHeight', AControl.Parent.Height);
      WriteInteger(AControlName + '_DockWidth', AControl.Parent.Width);
    end else
    begin
      WriteInteger(AControlName + '_Left', AControl.Left);
      WriteInteger(AControlName + '_Top', AControl.Top);
      WriteInteger(AControlName + '_Height', AControl.Height);
      WriteInteger(AControlName + '_Width', AControl.Width);
    end;
  end;
end;
procedure TMainForm.LoadWinControlPos(AControl: TWinControl);
var
  S, AControlName: string;
begin
  with FormStorage do
  begin
    AControlName := AControl.Name;
    S := ReadString(AControlName + '_ParentName', AControl.Parent.Name);
    AControl.ManualDock(TWinControl(Self.FindComponent(S)), nil, alClient);
    if S = '' then
    begin
      AControl.Parent.Left   := ReadInteger(AControlName + '_DockLeft',
	AControl.Parent.Left);
      AControl.Parent.Top    := ReadInteger(AControlName + '_DockTop',
	AControl.Parent.Top);
      AControl.Parent.Height := ReadInteger(AControlName + '_DockHeight',
	AControl.Parent.Height);
      AControl.Parent.Width  := ReadInteger(AControlName + '_DockWidth',
	AControl.Parent.Width);
    end else
    begin
      AControl.Left   := ReadInteger(AControlName + '_Left',
	AControl.Left);
      AControl.Top    := ReadInteger(AControlName + '_Top',
	AControl.Top);

      // Следующие 2 свойства обычно эффекта не имеют, хотя, кто знает...
      AControl.Height := ReadInteger(AControlName + '_Height',
	AControl.Height);
      AControl.Width  := ReadInteger(AControlName + '_Width',
	AControl.Width);
    end;
    AControl.Visible := StrToBool(ReadString(AControlName + '_Visible',
	'1'));
  end;
end;

Здесь используется компонент библиотеки RXLib TFormStorage. Учтите, что обе функции используют свойство Name передаваемых компонентов, поэтому гарантированно работать они будут только с компонентами, созданными в Design-Time.

Обработка ошибок

Если вы разрабатываете серьезное приложение, то стоит вмешаться в процесс обработки сообщения. Создаете новую форму, кладете на нее TMemo, 2 кнопки и компонент TApplicationEvents. В этом компоненте ставите обработчик на событие OnException, текст которого может быть следующим:

procedure TErrorMsgForm.ApplicationEvents1Exception(Sender: TObject;
  E: Exception);
begin
  with ErrorMsgForm do
  try
    memoErrorText.Lines.Text :=
      'Произошла ошибка времени выполнения с текстом'#13#10 +
      '<' + E.Message + '>'#13#10 +
      'Хотите завершить работу программы немедленно?';
    ActiveControl := btnCancel;
    Position := poScreenCenter;
    MessageBeep(MB_ICONERROR);
    if ShowModal = mrOK then Application.Terminate;
  except
  end;
end; 

Теперь в случае возникновения исключения пользователь видит перед собой модную формочку, из которой, к тому же, можно скопировать текст ошибки. Имейте ввиду, что данная форма должна оставаться в списке автоматически создаваемых форм.

Постоянный контроль ввода данных

Смысл в том, что пользователь просто не сможет закрыть форму, если введенные им данные будут некорректными. В приложении, которое находится в архиве, демонстрируется такая возможность. Используется модуль Hints, разработанный Королевым Сергеем. Я лишь убрал досадную ошибку, связанную с рекурсивным вызовом функции Application.OnMessage (это для тех, кто работал с данным модулем). Контроль выполняется в обработчике события OnExit. Такой обработчик может выглядеть следующим образом:

procedure TTestChildForm.Edit1Exit(Sender: TObject);
begin
  if (Edit1.Text = '') and (not btbtnCancel.Focused) then
    Hints.ShowErrorHintEx(Edit1, 'Вы должны ввести свои ФИО');
end;

Отмечу, что здесь мы оставляем пользователю возможность нажать кнопку "Отмена".

Обработчик нажатия кнопки "Принять" может выглядеть следующим образом:

procedure TTestChildForm.btbtnOkClick(Sender: TObject);
var
  I: Integer;
begin
  for I := 0 to ComponentCount - 1 do
    if Components[I] is TEdit then
      TExWinControl(Components[I]).DoExit;

  btbtnOk.SetFocus;
  Close;
end;

Обычно в визуальных компонентах свойство DoExit закрыто. Для его открытия мы должны ввести новый класс:

  TExWinControl = class(TWinControl);

Визуализация длительного процесса

Если ваше приложение выполняет длительную обработку данных (математические вычисления, архивирование/восстановление базы данных, загрузку данных из файла и т.д.), то, если не принять соответствующих мер, у пользователя может сложиться впечатление, что программа попросту зависла. Если есть возможность выполнять обработку данных в дополнительном потоке, то желательно ее использовать. Однако такая возможность не всегда имеется, и не всегда очевидна.

Рассматривается случай, когда вся обработка ложится на основной поток. Ниже представлен код, который замораживает основной поток на 5 секунд:

procedure TMainForm.Button1Click(Sender: TObject);
begin
  Sleep(5000);
end;

Процедура Sleep сообщает системе Windows, что данному потоку не следует уделять процессорного времени в течение указанного числа миллисекунд. При выполнении данного кода работа с приложением в течение 5 секунд будет невозможна, хотя Windows может позволить перемещать окна мышкой, а также изменять их размеры.

Теперь что будет, если код станет таким:

procedure TMainForm.Button1Click(Sender: TObject);
var
  R: Real;
begin
  R := 0;
  while R < 1000000000 do
    R := R + 1;
end;
С точки зрения пользователя будет тоже самое, что и в первом случае - программа зависнет на несколько секунд. В данном случае приложению (иначе - основному потоку) будет отводиться необходимое процессорное время, но поток все отведенное ему время будет тратить на выполнение вычислений - ему сейчас не до пользователя. А для того, чтобы заставить основной поток уделять внимание пользователю, следует в тело цикла вставить вызов функции Application.ProcessMessages
procedure TMainForm.Button1Click(Sender: TObject);
var
  R: Real;
begin
  R := 0;
  while R < 10000000 do
  begin
    if Trunc(R) mod 100000 = 0 then
      Application.ProcessMessages;
    R := R + 1;
  end;
end;

Недостатки данного метода следующие:

  • падает скорость обработки данных. В указанном примере и проверка, и вызов самого метода, занимают много времени, в результате чего скорость обработки падает в несколько раз, но зато пользователь может продолжать работать с приложением.
  • пользователь может продолжать работать с приложением. Но нам-то это нежелательно, поскольку, пока в цикле идет обработка одних данных, пользователь в это время может ввести другие данные, что в конце-концов может привести к сбоям в работе приложения.

Получается, что наиболее выгодный для нас случай - когда выполняется обработка данных, а пользователь наблюдает за ходом этой обработки.

В архиве есть модуль ProgressViewer, в котором находится класс-поток TProgressViewer.

Вот примеры работы с ним:

1) Визуализация хода выполнения процесса неопределенной длительности:

procedure TMainForm.Button2Click(Sender: TObject);
var
  R: Real;
begin
  with TProgressViewer.Create('Визуализация без процентов', False) do
  try
    R := 0;
    while R < 1000000000 do
      R := R + 1;  

  finally
    Terminate;
  end;
end;

В данном случае скорость вычислений будет высокой (подобная визуализация на скорость выполнения вычислений влияния практически не оказывает. Если даже и оказывает, то влияние это оценивается долями процента), однако пользователь не знает, сколько ждать, пока вычисления завершатся.

2) Визуализация хода выполнения процесса с индикацией процента выполнения:

procedure TForm1.Button1Click(Sender: TObject);
var
  R: Double;
  Counter: Integer;
begin
  with TProgressViewer.Create('Визуализация с процентами', True) do
  try
    R := 0;
    Counter := 0;
    while R < 1000000000 do
    begin
      Inc(Counter);
      if Counter mod 1000 = 0 then
        CurrentValue := R / 10000000;
      R := R + 1;
    end;

  finally
    Terminate;
  end;
end;

В данном случае скорость вычислений раза в два ниже (каждый раз выполняется операция инкремента, операция получения остатка от деления mod и операция сравнения), однако пользователь видит, через сколько времени вычисления закончатся. В примере приведен крайний случай, в котором визуализация с процентами может оказать влияние на скорость выполнения операции. Однако для большей части задач скорость операции из-за отрисовки падать не будет. К примеру, для визуализации хода обработки записей большого набора данных визуализация с процентами очень удобна.

Локализация приложения

Чаще всего известные программы локализуются на несколько языков, про позволяет расширить круг пользователей таких программ. В Delphi уже имеется весьма мощное средства для этих целей - Translation Manager. Данный инструмент позволяет перевести не только строковые ресурсы приложения, но и изменить любое свойство ресурса формы, и предоставить для каждой языковой версии свой набора картинок. Результат работы данного средства - это файл ресурсов, в котором храниться все для полной локализации приложения. Воспользоваться файлом ресурсов можно двумя способами:

  • скомпилировать приложение с использованием этого файла ресурсов (в этом случае для каждого языка необходимо подготовить свой набор исполняемых файлов).
  • загружать файл ресурсов с тем или иным языком во время работы программы (например, по выбору пользователем). Как это сделать, смотрите в примере: C:\Program Files\Borland\Delphi7\Demos\RichEdit\

Недостаток второго способа заключается в том, что для внесения любых изменений в перевод необходимо заново скомпилировать файл ресурсов. А для этого требуются соответствующие средства.

Я предлагаю крайне простое средство, которое во многих случаях успешно заменяет Translation Manager. Это средство вы можете скачать по данной ссылке. Весь функционал заключен в одном-единственном модуле LangReader.pas. Вы можете локализовать как указанный объект, так и все формы приложения. Ресурсы формы для каждого языка должны располагаться в отдельном ini-файле. Там же можно расположить и строковые ресурсы, работа с которыми выполняется с помощью функций SetSMsg() и GetSMsg(). В модуле LangReader.pas вы найдете подробное описание того, как с ним работать.

Заключение

Ну вот и все, что я хотел вам рассказать. Надеюсь, вам было не скучно. А скачать долгожданный архив вы можете здесь.

Логинов Дмитрий © 2005-2015