Динамический пользовательский интерфейс 

Теренс Гоггин

Если пользователям не нравится тот интерфейс, который вы им предлагаете, то почему бы не позволить им самостоятельно переделать его во время работы программы? Имитировать режим конструирования во время выполнения оказывается проще, чем вы думаете, причем это может радикально сказаться на привлекательности вашего приложения.

Признаем очевидный факт: люди смотрят на одни и те же вещи по-разному. Если бы мнения пользователей насчет представления данных совпадали, существовала бы всего одна персональная информационная система (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 заменяет элемент на время перетаскивания. 

Важнейшие методы класса TSizingRectCreateParams и 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 приводят к тому же конечному результату, что и в нашем случае, но при этом вызывается пара лишних функций. Наш способ работает эффективнее и немного быстрее. 

На пути к гибким пользовательским интерфейсам

Для полноценного рассмотрения настраиваемых пользовательских интерфейсов одной главы явно недостаточно. Мы узнали, как предоставить пользователям контроль над большинством стандартных составляющих интерфейса (к ним относятся положение и размеры элементов, шрифты и порядок перебора, а также значения других свойств, отображаемые в коммерческом компоненте, предназначенном для редактирования свойств). Это трудно назвать даже поверхностным знакомством, особенно если учесть, что действительно хорошая система настройки пользовательского интерфейса сама должна обладать хорошим интерфейсом, руководить действиями пользователя и следить за тем, чтобы он случайно не нарушил работы приложения. Считайте эту главу отправной точкой, после которой вы сможете исследовать эту тему настолько глубоко, насколько понадобится вам (и вашим пользователям).

 

Предыдущая Содержание Следующая

Используются технологии uCoz

Rambler's Top100 Rambler's Top100
Используются технологии uCoz