Проекты Логинова Дмитрия | ||||||||||
|
Разработка интерфейса пользователя
Данная статья ориентирована главным образом на программистов Delphi, не искушенных в разработке пользовательских интерфейсов. Однако, я считаю, что данная статья может быть полезной и для профессионалов, поскольку в ней рассматриваются средства (оформленные в виде компонента) для автоматизации Drag & Dock. Я совершенно не силен в дизайнерском искусстве, поэтому никакого рисования вы здесь не увидите, всему основа - стандартные компоненты Delphi. Описываться будет некоторое MDI-приложение, так как на данным момент, я полагаю, большинство серьезных приложений под Windows пишут с применением MDI. Использование механизма действийПодробно механизм действий описан в литературе по Delphi (например, у Архангельского), так что буду краток. В Delphi механизм действий реализуется в помощью компонентов TActionList и TActionManager. TActionManager - очень мощный компонент, позволяющий создавать панели меню и инструментальные панели с возможностью их перестройки в режиме выполнения программы. Однако у этого компонента есть недостатки:
TActionList - значительно более простое средство, однако в этом случае для каждого пункта главного меню и для каждой кнопки панели TToolBar приходится указывать соответствующее действие (в выпадающем списке Action). Далее будем рассматривать только TActionList. В нем будем формировать коллекцию действий (конечно, для этих целей можно использовать и TActionManager, однако, в этом случае, размер приложения резко увеличится). Итак, создайте две формы. У первой (родительской) укажите FormStyle = fsMDIForm, а у второй (дочерней) FormStyle = fsDMIChild. Если вы запустите приложение сейчас, то увидите обе формы, причем дочерняя форма окажется внутри родительской. Поместите на главную форму компоненты TActionList и TImageList, причем у первого компонента в качестве Images укажите ImageList1. Добавьте в ImageList1 несколько иконок. Щелкните дважды на компоненте TActionList и выберите пункт New Action. Появится новое пустое действие (Action1). Зададим ему следующие свойства:
Желательно сразу задавать действиям осмысленные имена. Ниже будут даны рекомендации по присвоению имен различным элементам проекта 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 у программиста появляется возможность разрабатывать приложения с плавающими панелями инструментов. Минимальные действия программиста следующие:
Можете запустить приложение и проверить, что любую панель инструментов можно отстыковать и пристыковать обратно. Обратите внимание, что отстыкованное окно можно закрыть нажатием на крестик. Обычно пользователю предоставляется возможность включать и выключать те или иные инструментальные панели. В этом случае для каждой панели вы должны создать действие (хотя это не обязательно, т.к. возможность отключения панелей будет доступной только в главном меню), у которого следует задать 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. Данный инструмент позволяет перевести не только строковые ресурсы приложения, но и изменить любое свойство ресурса формы, и предоставить для каждой языковой версии свой набора картинок. Результат работы данного средства - это файл ресурсов, в котором храниться все для полной локализации приложения. Воспользоваться файлом ресурсов можно двумя способами:
Недостаток второго способа заключается в том, что для внесения любых изменений в перевод необходимо заново скомпилировать файл ресурсов. А для этого требуются соответствующие средства. Я предлагаю крайне простое средство, которое во многих случаях успешно заменяет Translation Manager. Это средство вы можете скачать по данной ссылке. Весь функционал заключен в одном-единственном модуле LangReader.pas. Вы можете локализовать как указанный объект, так и все формы приложения. Ресурсы формы для каждого языка должны располагаться в отдельном ini-файле. Там же можно расположить и строковые ресурсы, работа с которыми выполняется с помощью функций SetSMsg() и GetSMsg(). В модуле LangReader.pas вы найдете подробное описание того, как с ним работать. ЗаключениеНу вот и все, что я хотел вам рассказать. Надеюсь, вам было не скучно. А скачать долгожданный архив вы можете здесь. | |||||||||
Логинов Дмитрий © 2005-2015 |