© 2005 Василий Кругаль
Недавно мне необходимо было быстро создать двух мастеров для двух отдельных, но близких по функционалу приложений. В процессе анализа требований выяснилось, что часть страниц одного мастера может быть использована как в качестве страниц второго мастера, так и самостоятельно вне мастера в разные моменты выполнения приложений. После поиска в Интернете я не нашел подходящего компонента, который позволял бы повторно использовать отдельные страницы мастера. Поэтому пришлось изобретать что-то свое. О том, что у меня получилось и изложено в этой заметке.
Прежде всего, необходимо было решить основную задачу мастера - это реализовать механизм определения списка страниц, механизм навигации по этому набору и метод сбора и форму хранения данных, введенных пользователем в процессе выполнения мастера.
В качестве реализации страницы я использовал класс TFrame (кадр), т.к. TFrame может быть использован повторно. Таким образом, набор страниц мастера превращается в набор кадров, и тогда навигация превращается в воспроизведение этого набора кадров.
Каждый кадр в наборе должен обеспечить три основные функции:
Первая функция реализуется в каждом конкретном случае индивидуально, т.к. довольно сложно придумать некую простую единую реализацию для всех видов кадров.
Вторая функция вполне может быть реализована в виде единого интерфейса, который должен реализовать каждый кадр. В качестве интерфейса я использовал следующий интерфейс:
unit wizardFrameIntf;
interface
uses Classes, XMLIntf;
type IWizardFrameIntf = interface
// Подключение кадра. Вызывается мастером при открытии.
// Метод может быть использован кадром для восстановления
// настроек, установления значений
// по умолчанию и т.п.
procedure connect();
// Отключение кадра. Вызывается мастером при закрытии.
// Метод может быть использован кадром для сохранения настроек.
procedure disconnect();
// Определение наименования кадра. Вызывается мастером при открытии
// кадра для определения заголовка окна мастера для открываемого кадра.
function getCaption():String;
// Обработка события Idle приложения. Вызывается всякий раз, когда
// приложение переходит в состояние idle. Метод может быть использован
// для определения текущего состояния кадра.
procedure idle();
// Обработка события установки фокуса в кадре. Вызывается мастером при
// открытии кадра. Метод может быть использован для установления
// начального фокуса ввода в кадре.
procedure setFrameFocus();
// Проверка возможности перехода к следующему кадру. Вызывается мастером
// при попытке пользователя перейти на следующий кадр. Метод может быть
// использован для проверки правильности ввода данных.
function canGoNext():Boolean;
// Сбор (сериализация) данных кадра. Вызывается мастером при завершении
// ввода данных и формировании XML документа, содержащего результат
// работы мастера. Метод должен быть использован для записи результата
// ввода данных кадра в виде дочерних элементов элемента inode.
procedure serialize(inode:IXMLNode);
end;
Последняя процедура (serialize) реализует третью функцию - механизм сбора и сохранения данных, введенных пользователем. В качестве формата хранения данных я использовал XML документ как наиболее подходящий с моей точки зрения. Таким образом, каждый кадр оформляется в виде наследника класса TFrame, реализующего интерфейс IwizardFrameIntf. Вот пример реализации кадра для ввода личных данных (фамилия, имя, отчество и адрес электронной почты):
unit nameWizardFrameUnit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
XMLIntf, Dialogs, ComCtrls, ExtCtrls, StdCtrls, wizardFrameIntf;
const
_NODE_NAME_ = 'name'; // имя тэга в итоговом XML документе
// для хранения личных данных
_CAPTION_ = 'Личные данные'; // заголовок кадра
_NOT_ALL_DATA_TYPED_IN_ = 'Поля "Фамилия" и/или "Имя" должны быть заданы';
type
TNameWizardFrame = class(TFrame,IWizardFrameIntf)
pan: TPanel;
lFirst: TLabel;
lLast: TLabel;
lMiddle: TLabel;
lEmail: TLabel;
email: TEdit;
middle: TEdit;
last: TEdit;
first: TEdit;
private
// методы интерфейса IWizardFrameIntf
procedure idle();
function getCaption():String;
procedure connect();
procedure disconnect();
procedure serialize(inode:IXMLNode);
procedure setFrameFocus();
function canGoNext():Boolean;
public
end;
implementation
{$R *.dfm}
// подключение кадра
procedure TNameWizardFrame.connect();
begin
// здесь можно вставить код для установки значений по умолчанию,
// восстановления значений, введенных пользователем при предыдущем
// вызове мастера и т.п.
end;
// отключение кадра
procedure TNameWizardFrame.disconnect();
begin
// здесь можно вставить код для сохранения
// значений, введенных пользователем
end;
// определение наименования кадра
function TNameWizardFrame.getCaption():String;
begin
result:= _CAPTION_;
end;
// обработка события Idle приложения
procedure TNameWizardFrame.idle();
begin
// здесь можно вставить код для определения
// состояния элементов кадра и его отображения
end;
// обработка события установки фокуса в кадре
procedure TNameWizardFrame.setFrameFocus();
begin
first.SetFocus();
end;
// проверка возможности перехода к следующему кадру
function TNameWizardFrame.canGoNext():Boolean;
begin
result:= true;
// пользователь должен ввести фамилию и имя => проверим, так ли это
if ((first.text = '') or (last.text = '')) then
begin
showMessage(_NOT_ALL_DATA_TYPED_IN_);
if (first.text = '') then first.SetFocus()
else
if (last.text = '') then last.SetFocus();
result:= false;
end;
end;
// сериализация данных кадра
procedure TNameWizardFrame.serialize(inode:IXMLNode);
var
node:IXMLNode;
begin
// сохраним значения, введенные пользователем в виде XML фрагмента
if (inode <> nil) then
begin
node:= inode.AddChild(_NODE_NAME_,'');
with node.AddChild(first.name,'') do NodeValue:= first.Text;
with node.AddChild(last.name,'') do NodeValue:= last.Text;
with node.AddChild(middle.name,'') do NodeValue:= middle.Text;
with node.AddChild(email.name,'') do NodeValue:= email.Text;
end;
end;
end.
Теперь осталось реализовать механизм проигрывания (навигации) кадров, который я выполнил в виде отдельной формы:
unit wizardPlayerFormUnit; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, StdCtrls, wizardFrameIntf, AppEvnts, Contnrs, XMLIntf, XMLDoc; Const // текст подтверждения закрытия мастера _CONFIRM_CLOSE_ = 'Вы действительно хотите завершить?'; // текст подтверждения создания объекта мастера _WIZARD_FINISHED_ = 'Сбор данных завершен. Создать '; // _NEXT_ = 'Вперед'; _READY_ = 'Готово'; // заготовка XML документа с данными пользователя _RESULT_XML_ = '<?xml version="1.0" encoding="windows-1251"?>'; type TFrameWizardClass = class of TFrame; TWizardPlayerForm = class(TForm) pBtn: TPanel; btnCancel: TButton; btnNext: TButton; btnBack: TButton; ApplicationEvents: TApplicationEvents; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormShow(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); procedure ApplicationEventsIdle(Sender: TObject; var Done: Boolean); procedure btnBackClick(Sender: TObject); procedure btnNextClick(Sender: TObject); procedure btnCancelClick(Sender: TObject); private FCurrentFrame:TFrame; // текущий кадр FCurrentFrameIndex:Integer; // индекс текущего кадра в списке FFrameList:TObjectList; // список кадров FResultXml: IXMLDocument; // XML документ. Содержит результат // выполнения мастера FObjectTitle:String; // наименование объекта, данные о котором // собираются с помощью мастера // формирование XML документа procedure serialize(); // переход на кадр с заданным индексом function gotoFrame(index:Integer):TFrame; // чтение документа в виде строки function getResultXml():WideString; public // регистрация (добавление) кадра в список кадров мастера function addFrame(frameClass: TFrameWizardClass):TFrame; // наименование объекта, данные о котором собираются с помощью мастера property objectTitle:String read FObjectTitle write FObjectTitle; // результирующий XML документ в виде строки property resultXml:WideString read getResultXml; end; implementation {$R *.dfm} procedure TWizardPlayerForm.FormCreate(Sender: TObject); begin FCurrentFrame:= nil; FCurrentFrameIndex:= -1; // создание списка кадров мастера FFrameList:= TObjectList.Create(); FFrameList.OwnsObjects:= true; // создание заготовки документа FResultXml:= LoadXMLData(_RESULT_XML_); FResultXml.Options:= [doNodeAutoIndent]; FResultXml.ParseOptions:= []; FResultXml.Active:= true; end; procedure TWizardPlayerForm.FormDestroy(Sender: TObject); begin FResultXml:= nil; freeAndNil(FFrameList); end; procedure TWizardPlayerForm.FormShow(Sender: TObject); var i:Integer; wizardFrameIntf:IWizardFrameIntf; frame:TFrame; begin // подключение кадров из списка for i:= 0 to FFrameList.Count - 1 do begin frame:= TFrame(FFrameList.Items[i]); wizardFrameIntf:= frame as IWizardFrameIntf; wizardFrameIntf.connect(); end; FCurrentFrame:= gotoFrame(FCurrentFrameIndex); modalResult:= mrCancel; end; procedure TWizardPlayerForm.FormClose(Sender: TObject; var Action: TCloseAction); var i:Integer; wizardFrameIntf:IWizardFrameIntf; frame:TFrame; begin // отключение кадров из списка for i:= 0 to FFrameList.Count - 1 do begin frame:= TFrame(FFrameList.Items[i]); wizardFrameIntf:= frame as IWizardFrameIntf; wizardFrameIntf.disconnect(); end; end; procedure TWizardPlayerForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); var msg:String; begin CanClose:= false; msg:= _CONFIRM_CLOSE_; if (MessageDlg(msg, mtConfirmation,[mbYes,mbNo],0)= mrYes) then CanClose:= true; end; function TWizardPlayerForm.getResultXml():WideString; begin result:= FResultXml.xml.text; end; procedure TWizardPlayerForm.ApplicationEventsIdle(Sender: TObject; var Done: Boolean); var i:Integer; wizardFrameIntf:IWizardFrameIntf; frame:TFrame; begin // определение состояния кнопки "Назад" btnBack.Enabled:= (FCurrentFrameIndex > 0); // обработка события Idle в кадрах for i:= 0 to FFrameList.Count - 1 do begin frame:= TFrame(FFrameList.Items[i]); wizardFrameIntf:= frame as IWizardFrameIntf; wizardFrameIntf.idle(); end; end; procedure TWizardPlayerForm.btnBackClick(Sender: TObject); begin // переход на предыдущий кадр FCurrentFrame.Visible:= false; FCurrentFrameIndex:= FCurrentFrameIndex - 1; FCurrentFrame:= gotoFrame(FCurrentFrameIndex); btnNext.Caption:= _NEXT_; end; procedure TWizardPlayerForm.btnNextClick(Sender: TObject); var msg:String; begin // переход на следущий кадр // можно ли перейти на следующий кадр? if ((FCurrentFrame as IWizardFrameIntf).canGoNext() = true) then // да begin // текущий кадр - последний? if (FCurrentFrameIndex < FFrameList.Count - 1) then // нет => перейдем на следующий begin FCurrentFrame.Visible:= false; FCurrentFrameIndex:= FCurrentFrameIndex + 1; FCurrentFrame:= gotoFrame(FCurrentFrameIndex); // если кадр, на который мы перешли последний, то // показать кнопку "Готово" if (FCurrentFrameIndex = FFrameList.Count - 1) then btnNext.Caption:= _READY_; end else // да => выполнить сбор введенных данных и завершить работу begin msg:= _WIZARD_FINISHED_+' '+FObjectTitle+'?'; if (MessageDlg(msg, mtConfirmation,[mbYes,mbNo],0)= mrYes) then begin serialize(); self.OnCloseQuery:= nil; close(); modalResult:= mrOk; end; end; end; end; procedure TWizardPlayerForm.btnCancelClick(Sender: TObject); begin close(); end; function TWizardPlayerForm.gotoFrame(index:Integer):TFrame; var wizardFrameIntf:IWizardFrameIntf; begin // переход на кадр с указанным индексом result:= TFrame(FFrameList.Items[index]); wizardFrameIntf:= result as IWizardFrameIntf; result.Visible:= true; result.SetFocus(); self.Caption:= wizardFrameIntf.getCaption(); wizardFrameIntf.setFrameFocus(); end; function TWizardPlayerForm.addFrame(FrameClass: TFrameWizardClass):TFrame; var index:Integer; begin // регистрация (добавление) кадра в мастере result:= FrameClass.Create(nil); result.Visible:= false; result.Parent:= self; result.Align:= alClient; index:= FFrameList.Add(result); if (FCurrentFrameIndex = -1) then FCurrentFrameIndex:= index; end; procedure TWizardPlayerForm.serialize(); var i:Integer; wizardFrameIntf:IWizardFrameIntf; frame:TFrame; begin // сбор введенных данных по каждому кадру for i:= 0 to FFrameList.Count - 1 do begin frame:= TFrame(FFrameList.Items[i]); wizardFrameIntf:= frame as IWizardFrameIntf; wizardFrameIntf.serialize(FResultXml.DocumentElement); end; end; end.
Данная форма может служить для проигрывания любого набора кадров. Далее пример фрагмента кода для запуска мастера:
with TWizardPlayerForm.Create(nil) do
begin
// определим наименование объекта –
// результата работы мастера (здесь – Контакт)
objectTitle:= 'Контакт';
try
// Добавим кадр "личные данные"
addFrame(TNameWizardFrame);
// Добавим кадр "адрес"
addFrame(THomeWizardFrame);
// Добавим кадр "Данные о работе"
addFrame(TWorkWizardFrame);
. . .
// Создадим новый контакт с помощью нашего мастера
if (showModal() = mrOk) then
begin
// отобразим результат выполнения мастера
showMessage(resultXml);
end;
finally
free();
end;
end;
Полный пример, демонстрирующий описанный метод реализации мастера, можно скачать здесь (11.8K).
Copyright© 2005 Василий Кругаль Специально для Delphi Plus