Динамический пользовательский
интерфейс
Теренс Гоггин
Если пользователям не нравится тот интерфейс,
который вы им предлагаете, то почему бы не позволить им самостоятельно
переделать его во время работы программы? Имитировать режим конструирования
во время выполнения оказывается проще, чем вы думаете, причем это может
радикально сказаться на привлекательности вашего приложения.
Признаем очевидный факт: люди смотрят на
одни и те же вещи по-разному. Если бы мнения пользователей насчет представления
данных совпадали, существовала бы всего одна персональная информационная
система (Personal Information Manager, PIM). Но этого не происходит — рынок
забит PIM'ами всех размеров и мастей.
Некоторым разработчикам удается отыскать
удачные интерфейсные решения, и их продукты немедленно обретают всеобщее
признание. Другие программы сложны и кажутся интуитивно понятными разве
что своим создателям. Похоже, третьего не дано.
Иногда сложная в использовании программа
оказывается настолько полезной, что пользователи заставляют себя работать
с ней, как бы трудно им ни было. Но не стоит рассчитывать на это при проектировании
новой программы, лучше сразу приготовиться к жалобам.
Идеальный пример — панель инструментов
MS Word 2000. Возможно, вам всегда было понятно, зачем нужны эти кнопочки
с кривыми стрелочками. С другой стороны, вы могли решить, что панель слишком
загромождена и непонятна. Промежуточных вариантов опять же не бывает: интуиция
говорит либо «да», либо «нет».
Поскольку любая компания в конечном счете
стремится продать как можно больше своих продуктов, разработчики графических
интерфейсов не могут просто игнорировать клиентов, живущих под девизом
«все не так» — но они не могут и менять весь дизайн проекта в угоду прихотям
отдельных пользователей.
До сих пор никто толком не занимался этой
проблемой. Никто не пытался разработать для конечного пользователя интерфейс,
построенный по принципу «сделай сам». Но достаточно взять Delphi 5
или Delphi 6, добавить немного изобретательности — и перед вами инспектор
объектов, встроенный прямо в программу!
Сначала мы посмотрим, как может выглядеть
простейшее приложение для работы с базой данных, поддерживающее динамическое
конструирование. Затем мы обсудим некоторые механизмы, которые делают подобный
интерфейс возможным.
Пример приложения
«Настрой меня сам»
На рис. 12.1 представлена «сборная» копия
экрана простейшего приложения, демонстрирующая все возможности, которые
вы можете предложить конечному пользователю.
Раскрытое меню содержит три команды:
|
Adjust
All Fonts (выбрать новый шрифт для всех элементов); |
|
Tab
Order (изменить порядок перебора элементов); |
|
Show
Properties (вызвать инспектор объектов). |
Имеется также контекстное меню, с помощью
которого можно изменить фоновый цвет формы.
Наконец, есть еще одно контекстное меню
с четырьмя командами:
|
Escape/No
changes (отменить возможные изменения); |
|
Adjust
Size & Position (изменить размеры и положение элемента); |
|
Change
Font (изменить шрифт отдельного элемента); |
|
View
Properties (вызвать инспектор объектов). |
На это контекстное меню ссылается свойство
PopupMenu каждого элемента.
В левой части экрана находится инспектор
объектов, доступный во время выполнения. С его помощью пользователи могут
просматривать и изменять некоторые дополнительные свойства элементов.
Как видно из первого примера, мы взяли
многие средства Delphi, доступные только в режиме конструирования, и перенесли
их в режим выполнения.
Рис. 12.1. Средства настройки пользовательского
интерфейса
Строим «мини-Delphi»
для пользователей
При проектировании программы в среде Delphi
вы используете ряд инструментов для настройки внешнего вида программы и
ее поведения. Чтобы пользователи смогли переделать вашу программу
на свой лад, им потребует ся следующее:
|
средства
для перемещения элементов во время выполнения; |
|
средства
для масштабирования элементов во время выполнения; |
|
средства
для изменения порядка перебора элементов после их перемещения; |
|
средства
для изменения некоторых свойств элементов (например, цвета или стиля рамки); |
|
возможность
автоматического сохранения и загрузки внесенных изменений. |
Разумеется, все эти средства должны быть
быстрыми, простыми и удобными. Желательно, чтобы пользователи располагали
почти теми же (если не всеми) возможностями, какие мы, программисты, имеем
в режиме констру ирования. В некотором смысле мы предоставляем им во время
работы приложения специальную, слегка «урезанную» версию Delphi. В этой
главе объясняется, как можно решить каждую задачу из приведенного выше
списка.
Перемещение элементов
Хотя перемещать элементы во время выполнения
программы можно несколькими способами, для наших целей лучше всего подойдет
трюк с почти недокументированным сообщением WM_SYSCOMMAND. Для перемещения
элемента класса TWinControl следует вызвать ReleaseCapture
и послать элементу сообщение WM_SYSCOMMAND, указав в качестве параметра
wParam шестнадцатеричное значение $F012. А теперь то же самое на
языке программы:
ReleaseCapture;
SendMessage(TWinControl(SizingRect1).Handle,
WM_SysCommand,
$F012, 0);
Рис. 12.2. Перемещение кнопки Windows
Результат этого фрагмента с точки зрения
пользователя изображен на рис. 12.2.
Внешне все выглядит, как при перемещении
модального диалогового окна — тонкий пунктирный контур элемента следует
за курсором, пока не будет отпущена кнопка мыши.
Возможно, вы уже заметили, что этот способ
обладает одним ограниче нием — для него необходим логический номер окна.
У потомков TWinControl он имеется, у потомков TGraphicControl
— нет. Следовательно, для компонентов типа TGraphicControl (например,
TLabel) он работать не будет. Чтобы наши динамические формы были
действительно полезными и полноценными, необходимо найти способ перемещения
потомков TGraphicControl.
Только что описанный механизм WM_SYSCOMMAND
придется усовершенствовать. Конечно, его нельзя использовать для потомков
TGraphicControl напрямую, но обходной путь все же существует — мы
создадим прозрачный TWinControl и расположим его над перемещаемым
элементом.
Когда пользователь выбирает из контекстного
меню команду Adjust Size & Position, мы накладываем прозрачный TWinControl
поверх выделенного элемента. Пользователь сможет перетащить прозрачный
элемент (с помощью сообщения WM_SYSCOMMAND с параметром $F012)
так, словно это и есть «выделенный» элемент. Другими словами, когда пользователь
щелкает на выделенном элементе и начинает перетаскивать его, на самом деле
он перетаскивает наш прозрачный TWinControl. Затем, когда пользователь
решит сохранить внесенные изменения (повторно выбрав команду Adjust Size
& Position), мы прячем прозрачный TWinControl и программным
способом перемещаем «выделенный» элемент в новое место.
В сущности, именно это происходит в Delphi
в режиме конструирования. Если присмотреться повнимательнее, вы увидите,
что при перетаскивании элемента на самом деле перемещается прозрачный прямоугольник
в толстой рамке (см. рис. 12.3).
Рис. 12.3. Перетаскивание в режиме конструирования
Delphi
Прозрачный прямоугольник появляется только
над перемещаемым элементом. С того момента, когда вы щелкнули на «выделенном»
элементе, и до отпускания кнопки мыши прозрачный прямоугольник следует
за курсором. При отпускании кнопки мыши прозрачный прямоугольник исчезает,
а перемещаемый элемент оказывается в новом месте.
Наш прозрачный потомок TWinControl
называется SizingRect и принадлежит классу TSizingRect. Объект
класса TSizingRect заменяет элемент на время перетаскивания.
Важнейшие методы класса TSizingRect
—
CreateParams и Paint. Метод Create Params определяет
некоторые аспекты поведения элемента еще до его создания. Мы воспользуемся
этим методом, чтобы сделать наш элемент прозрачным (см. листинг 12.1).
Листинг 12.1. Метод TSizingRect.CreateParams
procedure TSizingRect.CreateParams(var Params:
TCreateParams);
begin
inherited CreateParams(Params);
Params.ExStyle := Params.ExStyle +
WS_EX_TRANSPARENT;
end;
Метод Paint (см. листинг 12.2) рисует
толстую рамку, которую видят наши пользователи при перетаскивании SizingRect.
При рисовании прямоугольника толщиной в 3 пикселя мы задаем свойству Pen.Mode
холста значение pmNot. Тем самым гарантируется, что цвет нарисованного
прямоугольника будет отличаться от цвета формы (как и при масштабировании
элементов в Delphi).
Листинг 12.2. Метод TSizingRect.Paint
procedure TSizingRect.Paint;
begin
inherited Paint;
if fVisible = False then
Exit;
Canvas.Pen.Mode := pmNot;
Canvas.Pen.Width := 3;
Canvas.Brush.Style := bsClear;
Canvas.Rectangle(0, 0, Width, Height);
end;
Масштабирование
элементов
Масштабировать элементы еще проще, чем перемещать
их. За образец мы снова возьмем соответствующий механизм режима конструирования
Delphi. Чтобы изменить размер выделенного элемента, вы щелкаете на одном
из черных квадратиков-маркеров, расположенных по краям элемента, и перетаскиваете
его до тех пор, пока измененные размеры элемента вас не устроят.
Аналогичный способ будет использован и
в нашем случае. Единственное отличие заключается в том, что для простоты
(и для уменьшения объема кода) мы ограничимся лишь одним из восьми возможных
маркеров.
Поскольку класс TSizingRect уже
используется для перемещения элемента, он поможет нам и при масштабировании.
Правый нижний угол TSizingRect назначается «активной областью»,
на которой пользователь будет щелкать для масштабирования элемента.
Кроме того, для упрощения дизайна мы обозначим
«активную область» маленьким белым квадратиком и будем изменять вид курсора
всякий раз, когда он проходит над ним. Вся настоящая работа выполняется
в обработчике MouseMove, полностью приведенном в листинге 12.3.
Код обработчика подробно рассматривается в последующем тексте.
Листинг 12.3. Обработчик события
MouseMove для объекта SizingRect
procedure TFrmMain.SizingRect1MouseMove(Sender:
TObject; Shift: TShiftState; X, Y: Integer); begin { ControlDC и ControlRect
- глобальные переменные, используемые в нескольких процедурах. } ControlDC
:= GetDC(TWinControl(Sender).Handle); GetWindowRect(TWinControl(Sender).Handle,
ControlRect); if ((X > TControl(Sender).Width -SizeVal) and (Y > TControl(Sender).Height
-SizeVal)) then begin TWinControl(Sender).Cursor := crSizeNWSE; Rectangle(ControlDC,
TWinControl(Sender).Width - SizeVal, TControl(Sender).Height -SizeVal,
TControl(Sender).Width, TControl(Sender).Height); end else begin TWinControl(Sender).Cursor
:= crDefault; end; if ((TWinControl(Sender).Cursor = crSizeNWSE) and (ssLeft
in Shift)) then begin TWinControl(Sender).Width := X; TWinControl(Sender).Height
:= Y; end; end;
После подготовки переменных обработчик
проверяет, находится ли курсор в пределах области масштабирования. Константа
SizeVal, определяющая размеры белого маркера, определена в модуле
DynamicForm. Если курсор находится внутри области, обработчик изменяет
его внешний вид и, конечно, рисует прямоугольник:
if ((X > TControl(Sender).Width -SizeVal) and
(Y > TControl(Sender).Height -SizeVal))
then
begin
TWinControl(Sender).Cursor := crSizeNWSE;
Rectangle(ControlDC, TWinControl(Sender).Width
- SizeVal,
TControl(Sender).Height -SizeVal,
TControl(Sender).Width, TControl
(Sender).Height);
end
Если курсор находится за пределами области
масштабирования, мы просто восстанавливаем его вид по умолчанию:
else
begin
TWinControl(Sender).Cursor := crDefault;
end;
Наконец, мы проверяем, продолжает ли пользователь
масштабировать элемент. Если используется курсор crSizeNWSE и нажата
левая кнопка мыши, значит, масштабирование продолжается. В этом случае
обработчик перемещает правый нижний угол элемента за курсором:
if ((TWinControl(Sender).Cursor = crSizeNWSE) and
(ssLeft in Shift)) then
begin
TWinControl(Sender).Width := X;
TWinControl(Sender).Height := Y;
end;
end;
Пока кнопка мыши остается нажатой, а курсор
находится над активной областью, угол элемента перемещается вслед за курсором.
Работа с контекстным
меню
В нашем приложении компонент TSizingRect
активизируется с помощью меню PopupMenu1, которое назначено контекстным
меню для каждого элемента на форме. На рис. 12.4 изображено меню PopupMenu1
во время выполнения программы, после того как пользователь щелкнул правой
кнопкой мыши на компоненте DBImage.
При этом у пользователя есть следующие
варианты:
|
ничего
не делать (Escape/No changes); |
|
масштабировать
или переместить элемент (Adjust Size & Position); |
|
изменить
шрифт элемента (Change Font); |
|
вызвать
мини-инспектора (View Properties). |
Команда Adjust Size & Position вызывает
процедуру TFrmMain.AdjustClick (см. листинг 12.4).
Рис. 12.4. Контекстное меню, вызываемое
правой кнопкой мыши
Листинг 12.4. Обработчик события
OnClick команды Adjust Size & Position
procedure TFrmMain.AdjustClick(Sender: TObject);
begin
if (Adjust.Checked = True) then
begin
if ((PopupMenu1.PopupComponent <>
ComponentBeingAdjusted) and
(PopupMenu1.PopupComponent <>
SizingRect1)) then
begin
MessageDlg( 'You can only adjust one
element at a time.' +
#13#10 +
'Please unselect the current element
before continuing.',
mtWarning, [mbOK], 0);
Exit;
end;
Adjust.Checked := False;
With TWinControl(ComponentBeingAdjusted) do
begin
Top := SizingRect1.Top;
Left := SizingRect1.Left;
Width := SizingRect1.Width;
Height := SizingRect1.Height;
end;
SizingRect1.Cursor := crDefault;
SizingRect1.Visible := False;
SizingRect1.Top := -40;
SizingRect1.Left := -40;
SizingRect1.Width := 40;
SizingRect1.Height := 40;
MiniInspector1.ShowThisComponent
(ComponentBeingAdjusted);
ComponentBeingAdjusted := Self;
{ т. е. выделенный элемент }
{ отсутствует. }
end
else
begin
if ((ComponentBeingAdjusted <> Self)
and
(PopupMenu1.PopupComponent <>
ComponentBeingAdjusted))
then
begin
MessageDlg( 'You can only adjust one
element at a time.'
+ #13#10 +
'Please unselect the current element
before continuing.',
mtWarning, [mbOK], 0);
Exit;
end;
Adjust.Checked := True;
ComponentBeingAdjusted
:= PopupMenu1.PopupComponent;
With TWinControl
(PopupMenu1.PopupComponent) do
begin
SizingRect1.Top := Top;
SizingRect1.Left := Left;
SizingRect1.Width := Width;
SizingRect1.Height := Height;
end;
SizingRect1.Visible := True;
MiniInspector1.ShowThisComponent
(ComponentBeingAdjusted);
end;
end;
После выполнения различных проверок TSizingRect
совмещается с изменяемым элементом (переменная ComponentBeingAdjusted
была создана для тех процедур, которые не могут использовать значение PopupMenu1.PopupComponent).
Делается это так:
ComponentBeingAdjusted
:= PopupMenu1.PopupComponent;
With TWinControl(PopupMenu1.PopupComponent) do
begin
SizingRect1.Top := Top;
SizingRect1.Left := Left;
SizingRect1.Width := Width;
SizingRect1.Height := Height;
end;
SizingRect1.Visible := True;
MiniInspector1.ShowThisComponent
(ComponentBeingAdjusted);
При этом компонент SizingRect остается
активным. Его можно перемещать и масштабировать мышью, как показано на
рис. 12.5.
Завершив настройку элемента, пользователь
снова щелкает правой кнопкой мыши, чтобы сохранить или отменить изменения
(см. рис. 12.6).
Рис. 12.5. Прямоугольник SizingRect
Рис. 12.6. Сохранение или отмена изменений
Если пользователь захочет сохранить результаты
настройки и выберет вторую команду (Adjust Size & Position), то изменяемый
элемент перемещается и масштабируется в соответствии с новыми параметрами,
а прямоугольник SizingRect снова скрывается (этот код также входит
в TFrmMain.AdjustClick):
With TWinControl(ComponentBeingAdjusted) do
begin
Top := SizingRect1.Top;
Left := SizingRect1.Left;
Width := SizingRect1.Width;
Height := SizingRect1.Height;
end;
SizingRect1.Cursor := crDefault;
SizingRect1.Visible := False;
SizingRect1.Top := -40;
SizingRect1.Left := -40;
SizingRect1.Width := 40;
SizingRect1.Height := 40;
{...}
Отмена изменений
Если пользователь не захочет сохранять внесенные
изменения и выберет первую команду меню, прямоугольник TSizingRect
скрывается, а выделенный элемент остается в прежнем состоянии. Это происходит
в процедуре TFrm Main.EscapeClick (см. листинг 12.5).
Листинг 12.5. Обработчик события
OnClick команды Escape/No changes
procedure TFrmMain.Escape1Click(Sender: TObject);
begin
if (Adjust.Checked = True) then
begin
Adjust.Checked := False;
SizingRect1.Cursor := crDefault;
SizingRect1.Visible := False;
SizingRect1.Top := -40;
SizingRect1.Left := -40;
SizingRect1.Width := 40;
SizingRect1.Height := 40;
ComponentBeingAdjusted := Self; { т. е.
выделенный элемент }
{ отсутствует. }
end;
end;
Замечание
В проекте STARTER.DPR компонент
SizingRect спрятан в левой верхней части формы, чтобы он не был
случайно выведен в неподходящий момент. Если вы захотите использовать этот
проект как отправную точку для ваших собственных приложений, не забудьте
найти компонент SizingRect и после добавления на форму всех элементов
перевести его на передний план командой EditдBring To Front из главного
меню Delphi. Кроме того, проследите за тем, чтобы свойства PopupMenu
всех элементов ссылались на контекстное меню PopupMenu1.
Изменение порядка
перебора элементов во время выполнения
Если пользователи смогут перемещать элементы,
скорее всего, они также захотят изменить и порядок их перебора . Более
того, наш дизайн «сделай сам» не пройдет тест на простоту использования,
если пользователи будут навсегда привязаны к исходному порядку перебора.
Перемещение от одного элемента к другому станет крайне запутанным.
В Delphi порядок перебора элементов задается
в диалоговом окне Tab Order, главные элементы которого — список и кнопки
со стрелками б и в. Раз этот способ успешно работает в Delphi, мы воспользуемся
им и в своей системе. На рис. 12.7 изображен наш компонентFrmTabOrder
во время выполнения программы.
Тем не менее сама по себе форма FrmTabOrder
— не более чем удобный интерфейс. Порядком перебора в действительности
управляет фрагмент кода, в котором отображается FrmTabOrder; это
происходит в методе TFrmMain.TabOrder1 Click (см. листинг 12.6).
Сейчас мы подробно рассмотрим его.
Рис. 12.7. Компонент FrmTabOrder во время
выполнения программы
Листинг 12.6. Обработчик события
OnClick команды Tab Order
procedure TFrmMain.TabOrder1Click(Sender:
TObject);
var
i : Integer;
begin
FrmTabOrder.LBControls.Items.Clear;
for i := 0 to ComponentCount -1 do
begin
if ((Components[i] is TWinControl) and
not (Components[i] is TSizingRect)) then
FrmTabOrder.LBControls.Items.Add
(Components[i].Name);
end;
FrmTabOrder.ShowModal;
if FrmTabOrder.ModalResult = mrOK then
begin
for i := 0 to
FrmTabOrder.LbControls.Items.Count -1 do
TWinControl(FindComponent(
FrmTabOrder.LbControls.Items[i])).TabOrder := i;
end;
end;
А теперь углубимся в детали. Процедура
начинает свою работу с очистки списка. Затем она перебирает элементы формы
и заносит в список все элементы класса TWinControl, кроме SizingRect:
FrmTabOrder.LBControls.Items.Clear;
for i := 0 to ComponentCount -1 do
begin
if ((Components[i] is TWinControl) and
not (Components[i] is TSizingRect)) then
FrmTabOrder.LBControls.Items.Add
(Components[i].Name);
end;
Далее процедура отображает форму (упорядочением
элементов занимается список FrmTabOrder.LBControls). Если пользователь
нажимает кнопку OK, программа перебирает FrmTabOrder.LBControls.Items,
определяет порядковый номер каждой строки и назначает его свойству TabOrder
соответствующего элемента:
FrmTabOrder.ShowModal;
if FrmTabOrder.ModalResult = mrOK then
begin
for i := 0 to FrmTabOrder.LbControls.Items.Count
-1 do
TWinControl(FindComponent(
FrmTabOrder.LbControls.Items[i])).TabOrder := i;
end;
Все просто, не правда ли? Для управления
порядком перебора компонентов ничего больше и не требуется.
Изменение других
свойств
Мы вплотную подошли к проблеме изменения других
свойств элементов. Например, что делать, если пользователь захочет изменить
шрифт или цвет некоторых компонентов DBEdit, чтобы выделить их как
обязательные для заполнения? Оказывается, сделать это не так уж сложно.
Как мы только что узнали, порядок перебора элементов можно легко изменить.
То же самое относится и к другим свойствам элементов.
Изменение шрифтов
во время выполнения
В нашем приложении-примере пользователи могут
изменить шрифт всех элементов командой Adjust All Fonts из главного меню.
Как видно из листинга12.7, сделать это не слишком сложно.
Листинг 12.7. Изменение шрифта для
всех элементов формы
procedure TFrmMain.AdjustMenu2Click(Sender:
TObject);
var
i : Integer;
begin
{ Изменяем шрифт для всех элементов }
if FontDialog1.Execute then
begin
for i := 0 to ComponentCount - 1 do
begin
try
if ((Components[i] is TWinControl) or
(Components[i] is TGraphicControl))
and
not ((Components[i] is TMenu) and
(Components[i] is TMenuItem)) then
TMagic(Components[i]).Font := FontDialog1.Font;
except
Continue;
end;
end;
end;
end;
Здесь происходит нечто интересное. Обратите
внимание на преобразо вание типа в TMagic в операторе присваивания.
Вспомогательный класс TMagic определен в модуле TSizingRect,
его программный код не делает абсолютно ничего. Единственная причина существования
этого класса заключается в том, чтобы перевести в категорию public
некоторые protected-свойства (в нашем случае — свойство Font). Поскольку
в большинстве элементов свойство Font относится к категории protected,
его нельзя непосредственно изменить в режиме выполнения. Однако это удается
сделать, предварительно преобразовав тип элемента в TMagic.
В нашем примере можно изменить и шрифт
отдельного элемента, воспользовавшись командой Change Font контекстного
меню. Это тоже сравнительно просто (см. листинг 12.8).
Листинг 12.8. Изменение шрифта отдельного
элемента во время выполнения
procedure TFrmMain.ChangeFont1Click
(Sender: TObject);
begin
if FontDialog1.Execute then
try
TMagic(PopupMenu1.PopupComponent).Font
:= FontDialog1.Font;
except
Exit;
end;
end;
Замечание
Даже применение TMagic не всегда гарантирует
успех. При попытке изменить шрифт элементов некоторых классов (например,
TMenu) возникает исключение. Следовательно, перед попыткой изменения шрифта
желательно проверить тип элемента. Однако в приведенном выше примере нет
смысла отфильтровывать «неподдающиеся» элементы, потому что изменение шрифта
выполняется через контекстное меню. Элементы, обладающие контекстным меню,
допускают изменение шрифта даже в том случае, если в них вообще не отображает
ся текст (например, полоса прокрутки).
Изменение свойств в инспекторе объектов
Теперь мы должны предоставить пользователю
средства для изменения других свойств — таких как Caption, CharCase
или Color. Раз пользователь может менять все остальное, у него может
возникнуть желание изменить и эти свойства.
Как мы делаем это в режиме конструирования
Delphi? С помощью инспектора объектов. В своем проекте мы воспользуемся
собственным инспектором объектов.
Замечание
Поскольку инспектор объектов, представленный
в этой главе, ранее распространялся как коммерческий продукт, на CD-ROM
находится только его демонстрационная версия (без исходного текста). Она
ограничивает типы свойств и элементов, но во всех остальных отношениях
вполне работоспособна и не содержит назойливых призывов купить полную версию.
Более подробная информация приведена в лицензионном соглашении. Сведения
о полной версии класса TMiniInspector, включающей все исходные тексты,
можно найти на прилагаемом CD-ROM или щелкнув на свойстве About_This_Component
в режиме конструи рования. Обратите внимание: на компакт-диске содержатся
две версии мини-инспектора, для Delphi 2 и Delphi 3. Они находятся в каталоге
главы 12, в подкаталогах \Delphi2Lib и \Delphi3Lib соответственно.
Чтобы включить класс TMiniInspector
в палитру элементов, выполните команду Components|Install и выберите MINIOI.DCU.
Кроме того, необходимо проследить, чтобы в одном каталоге с MINIOI.DCU
находились еще три файла:
|
OICOMPDEMO.DCU |
|
OICOMPDEMO.DFM |
|
MINIOI.DCR |
Инспектор в нашем проекте работает точно
так же, как и его прототип из Delphi. Пользователь выбирает элемент из
верхнего выпадающего списка, а затем изменяет его свойства, непосредственно
вводя нужное значение или нажимая кнопку для вызова отдельной формы редактора
(если она есть). На рис. 12.8 изображен класс TMiniInspector во
время выполнения программы.
Рис. 12.8. Мини-инспектор во время выполнения
программы
Когда в нашем примере пользователь выбирает
команду Show Properties из главного меню или View Properties из контекстного
меню, инспектор объектов отображается простым вызовом метода Show:
MiniInspector1.Show;
Затем, если инспектор был вызван из контекстного
меню, мы выводим свойства того элемента, на котором пользователь щелкнул
правой кнопкой мыши:
if PopupMenu1.PopupComponent <> nil then
MiniInspector1.ShowThisComponent
(PopupMenu1.PopupComponent);
Метод ShowThisComponent — функция,
которая получает параметр типа TComponent и возвращает логическое
значение. Если передаваемый компонент присутствует в выпадающем списке,
он становится текущим, а функция возвращает True. Если компонент
не найден или мини-инспектор не отображается на экране, функция возвращает
False.
Сохранение внесенных
изменений
Теперь мы располагаем средствами для изменения
практически любой составляющей пользовательского интерфейса. Желательно
найти способ сохране ния этих изменений, чтобы они становились постоянными.
Пользователь вряд ли обрадуется, если ему придется заново настраивать
интерфейс при каждом запуске приложения! Возникает искушение решить проблему
с помощью INI-файлов (или, для самых смелых — системного реестра Windows
95), но оба способа обладают серьезными недостатками. Проблема заключается
в том, что каждый компонент обладает множеством свойств различных типов,
и вам не удастся написать обобщенный метод Save_This_Component.
Теоретически можно проверять тип каждого
компонента и затем сохранять свойства, относящиеся к данному типу. Но,
согласитесь, такой вариант не слишком эффективен. С другой стороны, можно
сохранять лишь общие свойства всех компонентов. Поскольку тип TComponent
— предок всех остальных компонентов — имеет лишь девять свойств (не считая
Left, Top, Width и Height), это тоже бесполезно.
Но не все потеряно! Существует несколько
очень хороших механизмов сохранения и загрузки свойств компонентов. Нужно
лишь покопаться в документации Borland и немного поэкспериментировать.
Конечная цель этих раскопок — семейство
объектов TFiler/TWriter/TReader. Согласно справочным
файлам Delphi, TFiler — «абстрактный базовый класс для объектов
чтения и записи, которые используются Delphi для сохранения (и загрузки)
форм и компонентов в DFM-файлах».
В этом определении сказано нечто очень
важное для нас, а именно: объекты TWriter и TReader могут
использоваться для сохранения и загрузки свойств компонента. Связывая экземпляр
класса TWriter или TReader с потоком TFile Stream,
мы сможем воспользоваться методами WriteRootComponent и ReadRoot
Component для решения своей проблемы.
Загвоздка: компоненты
со свойствами-компонентами
Единственное ограничение этих методов заключается
в том, что некоторые типы компонентов нельзя сохранить напрямую. Речь идет
о компонентах, которые содержат другие компоненты в качестве свойств.
Проблема возникает при попытке загрузить
такие компоненты-свойства из файла. Поскольку эти компоненты сохраняются
как самостоятельные объекты, попытка загрузить их как свойства другого
компонента приводит к возникновению исключения и выдаче сообщения «A
component named Widget1 already exists» («Компонент с именем Widget1 уже
существует»).
К счастью, эта проблема присуща всего четырем
типам компонентов: TMainMenu, TMenuItem, TPopupMenu
и TForm.
Первые три типа для наших целей несущественны.
Однако мы, скорее всего, должны разрешить пользователям сохранить некоторые
свойства их форм. Вряд ли пользователю понадобится изменять многие свойства
TForm, поэтому будет проще сохранять только те свойства,
которые нас интересуют.
В листинге 12.9 приведен код сохранения
свойств формы, выполняемый при обработке события
FormCloseQuery.
Важнейшие фрагменты этого кода подробно рассматриваются ниже.
Листинг 12.9. Обработчик события
FormCloseQuery
procedure TFrmMain.FormCloseQuery(Sender:
TObject;
var CanClose: Boolean);
var
Writer : TWriter;
FileStream : TFileStream;
i : Integer;
TempRect : TRect;
begin
{ Расширение файла .HPD == High Performance
Delphi }
{ На всякий случай удалим старый файл с
расширением HPD. }
DeleteFile(ExtractFilePath
(Application.ExeName) +
TObject(Self).ClassName + '.HPD');
{ Теперь можно записывать его заново: }
FileStream :=
TFileStream.Create(ExtractFilePath
(Application.ExeName)
+TObject(Self).ClassName + '.HPD',fmOpenWrite
or fmCreate);
for i := 0 to ComponentCount-1 do
begin
{ Некоторые элементы нежелательно (и
даже невозможно)
сохранить таким способом. К счастью, нам
и не придется
их сохранять... }
if ((Components[i] is TSizingRect) or
(Components[i] is TMenu) or
(Components[i] is TMenuItem) or
(Components[i] is TPopupMenu) or
(not(Components[i] is TControl))) then
Continue;
Writer := TWriter.Create(FileStream,
SizeOf(Components[i]));
Writer.WriteRootComponent(Components[i]);
Writer.Free;
end;
{ Сохранение свойств формы }
TempRect.Top := Self.Top;
TempRect.Left := Self.Left;
TempRect.Bottom := TempRect.Top + Self.Height;
TempRect.Right := TempRect.Left + Self.Width;
FileStream.Write(TempRect, SizeOf(TRect));
FileStream.Write(Self.Color, SizeOf(TColor));
FileStream.Free;
{ Не забудьте разрешить закрытие формы! }
CanClose := True;
end;
Давайте подробно рассмотрим этот метод.
Прежде всего мы для надежно сти удаляем старый файл *.HPD, а затем
создаем его заново:
FileStream := TFileStream.Create(ExtractFilePath
(Application.ExeName) +
TObject(Self).ClassName +
'.HPD',fmOpenWrite or fmCreate);
Затем мы отыскиваем те элементы, которые
невозможно сохранить, и не пытаемся ничего с ними делать:
for i := 0 to ComponentCount-1 do
begin
{ Некоторые элементы нежелательно (и даже невозможно)
сохранить таким способом. К счастью, нам и не придется
их сохранять... }
if ((Components[i] is TSizingRect) or
(Components[i] is TMenu) or
(Components[i] is TMenuItem) or
(Components[i] is TPopupMenu) or
(not(Components[i] is TControl))) then
Continue;
Если компонент можно сохранить, мы записываем
его в поток:
Writer := TWriter.Create(FileStream,
SizeOf(Components[i]));
Writer.WriteRootComponent(Components[i]);
Writer.Free;
Перебрав все компоненты формы и сохранив
те, для которых это возможно, мы сохраняем важные для приложения свойства
самой формы:
TempRect.Top := Self.Top;
TempRect.Left := Self.Left;
TempRect.Bottom := TempRect.Top + Self.Height;
TempRect.Right := TempRect.Left + Self.Width;
FileStream.Write(TempRect, SizeOf(TRect));
FileStream.Write(Self.Color, SizeOf(TColor));
FileStream.Free;
Наконец, мы устанавливаем флаг, разрешающий
закрытие формы:
CanClose := True;
Другой подход к
потокам
Возможно, вы заметили, что класс TFileStream
тоже содержит методы для сохранения и загрузки свойств компонента. Хотя
TFileStream содержит целых два набора методов для сохранения и загрузки
компонентов, эти методы выполняют лишнюю работу, что снижает эффективность
такого варианта по сравнению с выбранной нами реализацией TReader/TWriter.
Методы WriteComponentRes и ReadComponentRes
сохраняют и загружают компоненты в формате стандартных ресурсов Windows.
Это связано с лишней вычислительной нагрузкой. К тому же многие данные,
сохраняемые этими методами, просто не представляют для нас интереса и лишь
увеличивают размер файла свойств.
Методы WriteComponents и ReadComponents
приводят к тому же конечному результату, что и в нашем случае, но при этом
вызывается пара лишних функций. Наш способ работает эффективнее и немного
быстрее.
На пути к гибким
пользовательским интерфейсам
Для полноценного рассмотрения настраиваемых
пользовательских интерфейсов одной главы явно недостаточно. Мы узнали,
как предоставить пользователям контроль над большинством стандартных составляющих
интерфейса (к ним относятся положение и размеры элементов, шрифты и порядок
перебора, а также значения других свойств, отображаемые в коммерческом
компоненте, предназначенном для редактирования свойств). Это трудно назвать
даже поверхностным знакомством, особенно если учесть, что действительно
хорошая система настройки пользовательского интерфейса сама должна обладать
хорошим интерфейсом, руководить действиями пользователя и следить за тем,
чтобы он случайно не нарушил работы приложения. Считайте эту главу отправной
точкой, после которой вы сможете исследовать эту тему настолько глубоко,
насколько понадобится вам (и вашим пользователям).
|