СТАТЬЯ
13.06.02

Часть 1

Delphi и COM (Часть 2)

У Анатолий Тенцер
Статья была опубликована в "КомпьютерПресс", №5-2001

Интерфейсы и TComponent

В базовом классе VCL TComponent имеется полный набор методов, позволяющих реализовать интерфейс IUnknown, хотя сам класс данный интерфейс не реализует. Это позволяет наследникам TComponent реализовывать интерфейсы, не заботясь о реализации IUnknown. Однако методы TComponent._AddRef и TComponent._Release на этапе выполнения программы не реализуют механизм подсчета ссылок, и, следовательно, для классов-наследников TComponent, реализующих интерфейсы, не действует автоматическое управление памятью. Это позволяет запрашивать у них интерфейсы, не опасаясь, что объект будет удален из памяти при выходе переменной за область видимости. Таким образом, следующий код совершенно корректен и безопасен:

type
     IGetData = interface
          ['{B5266AE0-5E77-11D4-84DD-9153115ABFC3}']
          function GetData: String;
     end;

TForm1 = class(TForm, IGetData)
     private
          function GetData: String;
     end;



var
          I: Integer;
          GD: IGetData;
          S: String;
begin
          S := '';
          for I := 0 to Pred(Screen.FormCount) do begin
               if Screen.Forms[I].GetInterface(IGetData, GD) then
               S := S + GD.GetData + #13;
          end;
          ShowMessage(S);
end;

Этот код проверяет наличие у всех форм в приложении возможности реализации интерфейса IGetData и в случае, если форма реализует этот интерфейс, вызывает его метод.

Использование интерфейсов внутри программы

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

В качестве примера рассмотрим MDI-приложение, имеющее много различных форм и единую панель инструментов. Предположим, что на этой панели инструментов имеются команды «Сохранить», «Загрузить» и «Очистить», однако каждое из окон реагирует на эти команды по-разному.

Создадим модуль с объявлениями интерфейсов:

unit ToolbarInterface;

     interface

     type
          TCommandType = (ctSave, ctLoad, ctClear);
          TCommandTypes = set of TCommandType;
          TSaveType = (stSave, stSaveAS);

     IToolBarCommands = interface
     ['{B5266AE1-5E77-11D4-84DD-9153115ABFC3}']
          function SupportedCommands: TCommandTypes;
          function Save(AType: TSaveType): Boolean;
          procedure Load;
          procedure Clear;
     end;

implementation

end.

Интерфейс IToolBarCommands описывает набор методов, которые должны реализовать формы, поддерживающие работу с панелью кнопок. Метод SupportedCommands возвращает список поддерживаемых формой команд.

Создадим три дочерние формы — Form2, Form3 и Form4 — и установим им свойство FormStyle = fsMDIChild.

Form2 умеет выполнять все три команды:

type
     TForm2 = class(TForm, IToolBarCommands)
     private
          function SupportedCommands: TCommandTypes;
          function Save(AType: TSaveType): Boolean;
          procedure Load;
          procedure Clear;
     end;


{ TForm2 }


procedure TForm2.Clear;
     begin
          ShowMessage('TForm2.Clear');
     end;

procedure TForm2.Load;
     begin
          ShowMessage('TForm2.Load');
     end;

function TForm2.Save(AType: TSaveType): Boolean;
     begin
          ShowMessage('TForm2.Save');
          Result := TRUE;
     end;

function TForm2.SupportedCommands: TCommandTypes;
     begin
          Result := [ctSave, ctLoad, ctClear]
          end;

Form3 умеет выполнять только команду Clear:

type
     TForm3 = class(TForm, IToolBarCommands)
     private
          function SupportedCommands: TCommandTypes;
          function Save(AType: TSaveType): Boolean;
          procedure Load;
          procedure Clear;
     end;


{ TForm3 }

procedure TForm3.Clear;
     begin
          ShowMessage('TForm3.Clear');
     end;

procedure TForm3.Load;
     begin
          // Метод ничего не делает, но должен присутствовать
          // для корректной реализации интерфейса
     end;

function TForm3.Save(AType: TSaveType): Boolean;
          begin
     end;

function TForm3.SupportedCommands: TCommandTypes;
     begin
          Result := [ctClear]
     end;

И наконец, Form4 вообще не реализует интерфейс IToolBarCommands и не откликается ни на одну команду.

На главной форме приложения поместим ActionList и создадим три компонента TAction. Кроме того, разместим на ней TToolBar и назначим ее кнопкам соответствующие TAction.

type
     TForm1 = class(TForm)
          ToolBar1: TToolBar;
          ImageList1: TImageList;
          ActionList1: TActionList;
          acLoad: TAction;
          acSave: TAction;
          acClear: TAction;
          tbSave: TToolButton;
          tbLoad: TToolButton;
          tbClear: TToolButton;
          procedure acLoadExecute(Sender: TObject);
          procedure ActionList1Update(Action: TBasicAction;
          var Handled: Boolean);
          procedure acSaveExecute(Sender: TObject);
          procedure acClearExecute(Sender: TObject);
     end;

Наиболее интересен метод ActionList1Update, в котором проверяются поддерживаемые активной формой команды и настраивается интерфейс главной формы. Если нет активной дочерней формы либо она не поддерживает интерфейс IToolBarCommands, все команды запрещаются, в противном случае — разрешаются только поддерживаемые формой команды.

procedure TForm1.ActionList1Update(Action: TBasicAction;
          var Handled: Boolean);
     var
          Supported: TCommandTypes;
     TC: IToolBarCommands;
begin
     if Assigned(ActiveMDIChild)
          and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then
          Supported := TC.SupportedCommands
     else
          Supported := [];
     acSave.Enabled := ctSave in Supported;
     acLoad.Enabled := ctLoad in Supported;
     acClear.Enabled := ctClear in Supported;
end;

При активизации команд проверяется наличие активной дочерней формы, у нее запрашивается интерфейс IToolBarCommands и вызывается соответствующий ему метод:

procedure TForm1.acLoadExecute(Sender: TObject);
     var
          TC: IToolBarCommands;
     begin
          if Assigned(ActiveMDIChild)
          and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then
          TC.Load;
     end;

procedure TForm1.acSaveExecute(Sender: TObject);
     var
          TC: IToolBarCommands;
     begin
          if Assigned(ActiveMDIChild)
               and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then
          if not TC.Save(stSaveAS) then
               ShowMessage(‘Not Saved !!!’);
     end;

procedure TForm1.acClearExecute(Sender: TObject);
     var
          TC: IToolBarCommands;
     begin
          if Assigned(ActiveMDIChild)
          and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then
     TC.Clear;
end;

Работа программы представлена на рисунке.

Того же эффекта можно добиться и другими методами (например, унаследовав все дочерние формы от единого предка либо обмениваясь с ними сообщениями), однако эти методы имеют ряд существенных недостатков.

Так, при обмене сообщениями мы теряем строгую типизацию и вынуждены передавать параметры через целые числа, а при визуальном наследовании мы привязываем себя к родительскому классу, что не всегда удобно. К тому же можно определить множество интерфейсов и реализовывать в каждой из дочерних форм лишь необходимые, а в случае наследования все формы должны будут реализовывать все общие методы.

Использование интерфейсов для реализации Plug-In

Еще более удобно использовать интерфейсы для реализации модулей расширения программы (Plug-In). Как правило, такой модуль экспортирует ряд известных главной программе методов, которые могут быть из него вызваны. В то же время зачастую ему необходимо обращаться к каким-либо функциям вызывающей программы. И то и другое легко реализуется при помощи интерфейсов.

В качестве примера реализуем несложную программу, использующую Plug-In для загрузки данных.

Объявим интерфейсы модуля расширения и внутреннего API программы.

unit PluginInterface;

     interface

     type
          IAPI = interface
          ['{64CFF1E0-61A3-11D4-84DD-B18D6F94141F}']
               procedure ShowMessage(const S: String);
          end;


ILoadFilter = interface
     ['{64CFF1E1-61A3-11D4-84DD-B18D6F94141F}']
          procedure Init(const FileName: String; API: IAPI);
          function GetNextLine(var S: String): Boolean;
     end;

implementation
end.

Этот модуль должен использоваться как в Plug-In, так и в основной программе и гарантирует использование ими идентичных интерфейсов.

Plug-In представляет собой DLL, экспортирующую функцию CreateFilter, возвращающую ссылку на интерфейс ILoadFilter. Главный модуль сначала должен вызвать метод Init, передав в него имя файла и ссылку на интерфейс внутреннего API, а затем вызывать метод GetNextLine до тех пор, пока он не вернет FALSE.

Рассмотрим код модуля расширения:

library ImpTxt;

     uses
          ShareMem, SysUtils, Classes, PluginInterface;

     {$R *.RES}

type
     TTextFilter = class(TInterfacedObject, ILoadFilter)
     private
          FAPI: IAPI;
          F: TextFile;
          Lines: Integer;
          InitSuccess: Boolean;
          procedure Init(const FileName: String; API: IAPI);
          function GetNextLine(var S: String): Boolean;
     public
          destructor Destroy; override;
     end;
{ TTextFilter }

procedure TTextFilter.Init(const FileName: String; API: IAPI);
     begin
          FAPI := API;
          {$I-}
          AssignFile(F, FileName);
          Reset(F);
          {$I+}
          InitSuccess := IOResult = 0;
          if not InitSuccess then
               API.ShowMessage('Ошибка инициализации загрузки');
     end;

Метод Init выполняет две задачи: сохраняет ссылку на интерфейс API главного модуля для дальнейшего использования и пытается открыть файл с данными. Если файл открыт успешно – выставляется внутренний флаг InitSuccess.

function TTextFilter.GetNextLine(var S: String): Boolean;
     begin
               if InitSuccess then begin
                    Inc(Lines);
                    Result := not Eof(F);
                    if Result then begin
                         Readln(F, S);
                         FAPI.ShowMessage('Загружено ' + IntToStr(Lines) + ' строк.');
                    end;
               end else
                    Result := FALSE;
     end;

Метод GetNextLine считывает следующую строку данных и возвращает либо TRUE, если это удалось, либо FALSE — в случае окончания файла. Кроме того, при помощи API, предоставляемого главным модулем, данный метод информирует пользователя о ходе загрузки.

destructor TTextFilter.Destroy;
     begin
          FAPI := NIL;
          if InitSuccess then
          CloseFile(F);
     inherited;
end;

В деструкторе мы обнуляем ссылку на API главного модуля, уничтожая его, и закрываем файл.

function CreateFilter: ILoadFilter;
begin
Result := TTextFilter.Create;
end;

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

exports
     CreateFilter; // Функция должна быть экспортирована из DLL

          begin
     end.

Теперь полученную DLL можно использовать из основной программы.

type
     TAPI = class(TInterfacedObject, IAPI)
          procedure ShowMessage(const S: String);
     end;



{ TAPI }

procedure TAPI.ShowMessage(const S: String);
begin
     with (Application.MainForm as TForm1).StatusBar1 do begin
          SimpleText := S;
          Update;
     end;
end;

Класс TAPI реализует API, предоставляемый модулю расширения. Функция ShowMessage выводит сообщения модуля в Status Bar главной формы приложения.

type
          TCreateFilter = function: ILoadFilter;

     procedure TForm1.LoadData(FileName: String);
     var
          PluginName: String;
          Ext: String;
          hPlugIn: THandle;
          CreateFilter: TCreateFilter;
          Filter: ILoadFilter;
          S: String;
     begin

Подготавливаем TMemo к загрузке данных:

Memo1.Lines.Clear;
     Memo1.Lines.BeginUpdate;

Получаем имя модуля с фильтром для выбранного расширения файла. Описания модулей хранятся в файле plugins.ini в секции Filters в виде строк формата:

<расширение> = <имя модуля>, например:

[Filters]
TXT=ImpTXT.DLL

try
     Ext := ExtractFileExt(FileName);
     Delete(Ext, 1, 1);
     with TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'plugins.ini') do
     try
          PlugInName := ReadString('Filters', Ext, '');
     finally
          Free;
     end;

Теперь попытаемся загрузить модуль и найти в нем функцию CreateFilter:

hPlugIn := LoadLibrary(PChar(PluginName));
     try
          CreateFilter := GetProcAddress(hPlugIn, 'CreateFilter');
          if Assigned(CreateFilter) then begin

Функция найдена, создаем экземпляр фильтра и инициализируем его. Поскольку внутренний API реализован тоже как интерфейс — нет необходимости сохранять ссылку на него.

Filter := CreateFilter;
     try
          Filter.Init(FileName, TAPI.Create);

Загружаем данные при помощи созданного фильтра:

while Filter.GetNextLine(S) do
     Memo1.Lines.Add(S);

Перед выгрузкой DLL из памяти необходимо обязательно освободить ссылку на интерфейс Plug-In, иначе это произойдет по выходе из функции и вызовет Access Violation.

finally
               Filter := NIL;
          end;
     end else raise Exception.Create('Не могу загрузить фильтр');

Выгружаем DLL и обновляем TMemo:

finally
               FreeLibrary(hPlugIn);
          end;
     finally
          Memo1.Lines.EndUpdate;
     end;
end;

Таким образом, реализовать модули расширения для загрузки данных из форматов, отличающихся от текстового, и единообразно работать с ними несложно.

Преимущества данного способа становятся особенно очевидными при реализации сложных модулей расширения, интерфейс с которыми состоит из многих методов.

Внимание! Поскольку в EXE и DLL используются длинные строки, не забудьте включить в список uses обоих проектов модуль ShareMem. Другим вариантом решения проблемы передачи строк является использование типа данных WideString. Для них распределением памяти занимается OLE, причем делает это независимо от модуля, из которого была создана строка.

COM-сервер, структура и использование

Модель COM предоставляет возможность создания многократно используемых компонентов, независимых от языка программирования. Такие компоненты называются COM-серверами и представляют собой исполняемые файлы (EXE) или динамические библиотеки (DLL), специальным образом оформленные для обеспечения возможности их универсального вызова из любой программы, написанной на поддерживающем COM языке программирования. При этом COM-сервер может выполняться как в адресном пространстве вызывающей программы (In-Process-сервер), так и в виде самостоятельного процесса (Out-Of-Process-сервер) или даже на другом компьютере (Distributed COM). COM автоматически разрешает вопросы, связанные с передачей параметров (Marshalling) и согласованием потоковых моделей клиента и сервера.

Далее будут рассмотрены некоторые архитектурные вопросы, знание которых необходимо для работы с COM.

Продолжение

Дополнительную информацию Вы можете получить в компании Interface Ltd.

Обсудить на форуме Borland
Отправить ссылку на страницу по e-mail


Interface Ltd.
Тel/Fax: +7(095) 105-0049 (многоканальный)
Отправить E-Mail
http://www.interface.ru
Ваши замечания и предложения отправляйте автору
По техническим вопросам обращайтесь к вебмастеру
Документ опубликован: 13.06.02