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