Pipeline компоненты должны
поддерживать интерфейс IPipelineComponent, а также
несколько других. Обо всех будет рассказано
поподробнее ниже.
Представим себе, что мы хотим
создать компонент, который сбрасывает
содержимое IDictionary в xml-файл на диск. Причем мы
хотим иметь возможность задавать имя этого файла
в Properties Page внутри Pipeline Editor. Для ознакомления с
Pipeline Editor советую обратиться на сайт Microsoft.
В первую очередь, для создания
компонента в Delphi необходимо создать ActiveX Library. Для
этого выполним команду File|New -> Activex tabsheet -> ActiveX
Library. Затем там добавим Automation Object. Назовем объект
DumpOrderToXml. Добавим методы SetXmlFilename и GetXmlFilename.
Результатом должны быть следующие объявления:
function SetXmlFilename(XmlFileName:
WideString): HResult [dispid $00000001]; stdcall;
function GetXmlFileName(retval XmlFileName: WideString):
HResult [dispid $00000002]; stdcall;
Для дальнейшей успешной работы
Вы должны иметь на диске следующие файлы:
COMMERCELib_TLB.pas, MSCSAspHelpLib_TLB.pas, MSCSCoreLib_TLB.pas, PIPELINELib_TLB.pas.
Их можно сгенерировать с помощью tipe library editor,
предоставляемого Delphi, либо скачать у меня. Также
необходимо иметь на диске ComPUtil.pas и PipeConsts.pas файлы,
которые есть у меня.
Delphi поможет Вам создать макет
модуля с классом TDumpOrderToXml. В объявление этого
класса добавьте дополнительные интерфейсы и
соответсвующие методы для их реализации:
type
TDumpOrderToXml = class(TAutoObject, IDumpOrderToXml, IPipelineComponent,
ISpecifyPropertyPages, IPersistStreamInit)
private
FXmlFileName: WideString;
protected
{ IDumpOrderToXml methods }
function GetXmlFileName(out XmlFileName:
WideString): HResult; stdcall;
function SetXmlFilename(const XmlFileName: WideString):
HResult; stdcall;
{ IPipelineComponent methods }
function EnableDesign(fEnable: Integer): HResult; stdcall;
function Execute(const pdispOrder, pdispContext:
IDispatch;
lFlags: Integer; out plErrorLevel: Integer): HResult; stdcall;
{ ISpecifyPropertyPages methods }
function GetPages(out pages: TCAGUID):
HResult; stdcall;
{ IPersistStreamInit methods }
function GetClassID(out classID: TCLSID):
HResult; stdcall;
function IsDirty: HResult; stdcall;
function Load(const stm: IStream): HResult; stdcall;
function Save(const stm: IStream; fClearDirty: BOOL):
HResult; stdcall;
function GetSizeMax(out cbSize: Largeint): HResult; stdcall;
function InitNew: HResult; stdcall;
end;
Интерфейс IDumpOrderToXml
предоставляет нам возможность задавать и
получать имя xml-файла для хранения на диске.
Интерфейс IPipelineComponent - стержневой для класса, он
позволяет запустить компонент на выполнение с
помощью метода Execute. Интерфейс ISpecifyPropertyPage
позволяет задать classid для Property Page нашего нового
класса. Интерфейс IPersistStreamInit позволяет хранить
введеные параметры с помощью Pipeline Editor в файле .pcf.
Приступим к реализации этих
методов. Методы GetXmlFilename и SetXmlFilename достаточно
просты - они просто читают (пишут) значение из (в)
поле FXmlFileName. Метод EnableDesing вызывается для
уведомления класса, что редактор переводит его в
режим дизайна. В принципе крутые компоненты
могут что-либо делать в этот момент. Нам это не
нужно, поэтому просто вернем S_OK. Точно также
поступим с методами InitNew и IsDirty. Это
несущественные методы, которые в принципе можно
реализовать более детально, но не для нас.
Методы Save и Load позволяют
записать в поток наш параметр - имя xml-файла. В
принципе ничего сложного в них нет, поэтому
привожу код без комментариев
function TDumpOrderToXml.Save(const
stm: IStream;
fClearDirty: BOOL): HResult;
var OleStream: TOleStream;
FileNameLen: Byte;
begin
OleStream := TOleStream.Create(stm);
try
FileNameLen := Length(FXmlFileName);
OleStream.Write(FileNameLen, 1);
OleStream.Write(FXmlFileName[1], FileNameLen * Sizeof(WideChar));
finally
OleStream.Free;
end;
Result := S_OK;
end;
function TDumpOrderToXml.Load(const
stm: IStream): HResult;
var OleStream: TOleStream;
FileNameLen: Byte;
begin
OleStream := TOleStream.Create(stm);
try
OleStream.Read(FileNameLen, 1);
SetLength(FXmlFileName, FileNameLen);
OleStream.Read(FXmlFileName[1], FileNameLen * Sizeof(WideChar));
finally
OleStream.Free;
end;
Result := S_OK;
end;
Метод GetClassID позволяет вернуть
наш classid для внешнего потребителя. Ниже
приведенное решение в принципе универсальное
для любого класса.
function TDumpOrderToXml.GetClassID(out
classID: TCLSID): HResult;
begin
classID := Factory.ClassID;
Result := S_OK;
end;
Метод GetSizeMax возвращает размер,
который наш класс хочет занять в потоке. Пусть
это будет 255 widechar-ов.
function TDumpOrderToXml.GetSizeMax(out
cbSize: Largeint): HResult;
begin
cbSize := 255 * sizeof(WideChar) + 1;
Result := S_OK;
end;
Теперь приступим к реализации
метода Execute. В первую очередь нам необходимо
получить ссылку на IDictionary из параметров метода.
Для этого воcпользуемся функцией GetDictFromDispatch из
модуля ComPUtil.pas. Затем вызовем функцию ExportDictionaryToXml,
сохраним результат во временной строке,
представляющей собой xml-текст и запишем эту
строку в файл на диске.
function TDumpOrderToXml.Execute(const
pdispOrder, pdispContext: IDispatch;
lFlags: Integer; out plErrorLevel: Integer): HResult;
var
hFile: Integer;
tmpXML: WideString;
Order: IDictionary;
tmpOutXml: string;
begin
try
tmpXML := '';
if GetDictFromDispatch(pdispOrder, Order) = S_OK then begin
ExportDictionaryToXML(Order, tmpXML);
tmpXML := '<SO>' + tmpXML + '</SO>';
end;
tmpOutXml := tmpXML;
hFile := FileCreate(string(FXmlFileName));
FileWrite(hFile, tmpOutXml[1], Length(tmpOutXML));
FileClose(hFile);
finally
Result := S_OK;
Order := nil;
end;
end;
Как видим, метод довольно
несложный - вся нагрузка ложится на метод
ExportDictionaryToXml. Рассмотрим его поподробнее. Как
известно, dictionary представляет собой список
именованных вариантов. Вариант сам по себе может
быть IDictionary, ISimpleList или другой интерфейс. Для
перечисления своих элементов dictionary поддерживает
интерфейс IEnumVARIANT. Соотвественно, наша задача -
взять IEnumVARIANT, пробежаться по его элементам и
сохранить их имена и значение в строке.
Result := E_FAIL;
hr := InitKeyEnumInDict(Dict, Enum);
if hr = S_OK then begin
repeat
hr := GetNextKeyInDict(Enum, Key);
if hr <> S_OK then Break;
hr := GetDictValueVariant(Dict, LPCWSTR(Key), ItemValue);
if hr <> S_OK then Break;
case VarType(ItemValue) of
...
else
Break;
end;
until hr <> S_OK;
end;
XmlStr := Res;
Result := S_OK;
Основное место в теле
метода занимает оператор case. В нем определяются
обычные значения варианта и сложные, такие как
интерфейсы. Для обычных типов обработка будет
такая:
Res := Res + Format('<%s>%s</%s>',
[string(Key), string(ItemValue), string(Key)]);
Для типа varUnknown обработка будет
еще проще. Понятно, что для более продвинутой
информации эту обработку можно расширить:
Res := Res + Format('<%s>IUnknown</%s>',[string(Key),
string(Key)]);
Наиболее сложная обработка для
типа varDispatch. Здесь нам необходимо убедится, что
элемент является либо IDictionary, либо ISimpleList. Для
других случаев используем тоже самое, как для
varUnknown:
if GetDictFromDispatch(ItemValue,
NewDict) = S_OK then begin
if ExportDictionaryToXML(NewDict, NewXml) = S_OK then begin
Res := Res + Format('<%s
type="Dictionary">%s</%s>',
[string(Key), string(NewXml), string(Key)]);
end else begin
Exit;
end;
end else if GetSimpleListFromDispatch(ItemValue, NewList) = S_OK then
begin
if ExportSimpleListToXML(NewList, NewXml) = S_OK then begin
Res := Res + Format('<%s
type="SimpleList">%s</%s>',
[string(Key), string(NewXml), string(Key)]);
end else begin
Exit;
end;
end else begin
Res := Res + Format('<%s>IDispatch</%s>',
[string(Key), string(Key)]);
end;
Поскольку вариант может быть
другим IDictionary, то в результате получим
рекурсивный алгоритм. Замечу, что в случае ISimpleList
вызывается еще один метод - ExportSimpleListToXml. Его
реализация достаточно проста. Необходимо
пробежаться по элементам списка, каждый из
которых IDictionary, и вызывать ExportDictioanryToXml:
Result := E_FAIL;
hr := GetNumItems(List, Count);
if hr <> S_OK then Exit;
for I := 0 to Count - 1 do begin
if GetNthItem(List, I, NewDict) = S_OK then begin
if ExportDictionaryToXML(NewDict, NewXml) = S_OK then begin
Res := Res + Format('<LISTITEM%d>'#13#10'%s</LISTITEM%d>'#13#10,
[I, string(NewXml), I]);
end else begin
Exit;
end;
end;
end;
XmlStr := Res;
Result := S_OK;
Вот собственно и вся реализация
метода Execute. Для полной красоты картины, нам
необходимо научиться редактировать поле FXmlFilename
в Pipeline редакторе. Для этого добавим в проект Property
Page. На форму добавим из палитры Textbox, Label, Button и
SaveDialog.
В обработчик нажатия кнопки
добавим код по вызову SaveDialog:
if SaveDialog1.Execute then
begin
Edit1.Text := SaveDialog1.FileName;
end;
Для реализации поведения Property
Page, мы должны реализовать два метода UpdatePropertyPage и
UpdateObject. Первый метод восстанавливает значение из
объекта в textbox. Второй, наоборот, записывает
значение из textbox в объект.
procedure TDumpToXMLPropertyPage.UpdatePropertyPage;
var StrXmlFilename: WideString;
begin
{ Update your controls from OleObject }
(OleObjects.First as IDumpOrderToXml).GetXmlFileName(StrXmlFilename);
Edit1.Text := StrXmlFilename;
end;
procedure TDumpToXMLPropertyPage.UpdateObject;
var StrXmlFilename: WideString;
begin
{ Update OleObject from your controls }
StrXmlFilename := Edit1.Text;
(OleObjects.First as IDumpOrderToXml).SetXmlFileName(StrXmlFilename);
end;
Для того, чтобы Pipeline Editor знал,
что у компонента есть дополнительные
property-странички, необходимо реализовать метод
GetPages у нашего класса.
function TDumpOrderToXml.GetPages(out
pages: TCAGUID): HResult;
begin
pages.cElems := 1;
pages.pElems := CoTaskMemAlloc(sizeof(TGUID));
if pages.pElems = nil then begin
Result := E_OUTOFMEMORY;
end else begin
pages.pElems^[0] := Class_DumpToXMLPropertyPage;
Result := S_OK;
end;
end;
Этот метод занимается тем, что
наполняет структуру, в которой хранятся все guid-ы
наших property-страничек. В нашем случае это одна
страничка - Class_DumpToXmlPropertyPage. Этот guid
генерируется автоматически средой, когда мы
создаем новую property page.
Теперь подошел черед
модифицировать .dpr файл. В нем указывается
экспортная функция DllRegisterServer, которую надо
переделать:
function DllRegisterServer: HResult;
begin
Result := ComServ.DllRegisterServer;
if Result = S_OK then begin
{ Register DumpOrderToXml class }
Result := RegisterCATID(CLASS_DumpOrderToXml, CATID_MSCSPIPELINE_COMPONENT);
if Result >= 0 then begin
Result := RegisterCATID(CLASS_DumpOrderToXml, CATID_MSCSPIPELINE_ANYSTAGE);
end;
{ Here you should register others pipeline components }
end;
end;
В этой функции указывается, что
надо зарегистрировать в системе pipeline component, и что
этот компонент может принадлежать любому pipeline
stage.
На этом разработка закончена.
Осталось откомпилировать и зарегистриовать dll.
Это можно сделать через командную строку: regsvr32
testpipelines.dll