Delphi и COM (статья)

Материал из DRKB


Delphi и COM

Введение[править | править код]

COM (Component Object Model) или модель объектных компонентов - одна из основных технологий, на который основывается Windows. Более того, все новые технологии в Windows (Shell, Scripting, поддержка HTML и т.п.) реализуют свои API именно в виде COM интерфейсов. Таким образом, в настоящее время профессиональное программирование требует понимания модели COM и умения с ней работать. В этой главе мы рассмотрим основные понятия COM и особенности их поддержки в Delphi.

Базовые понятия[править | править код]

Основным понятием, на котором основана модель COM, является понятие интерфейса. Не имея четкого понимания о том, что такое интерфейс невозможно успешное программирование COM-объектов. Главными понятиями являются интерфейс и реализация интерфейса.

Интерфейс[править | править код]

Интерфейс является контрактом, между программистом и компилятором.

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

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

Объявление интерфейса включает в себя описание методов и их параметров, но не включает их реализации. Кроме этого, в объявлении может указываться идентификатор интерфейса - уникальное 16-байтовое число, сгенерированное по специальным правилам, гарантирующим его статистическую уникальность (GUID - global unique identifier).

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

Таким образом, необходимо понимать, что:

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

Реализация интерфейса - это непосредственно код, который реализует эти методы. При этом за несколькими исключениями, не накладывается никаких ограничений на то, каким образом будет выглядеть реализация. Физически реализация представляет собой массив указателей на методы, адрес которого и используется в клиенте для доступа к COM-объекту. Любая реализация интерфейса имеет метод QueryInterface, позволяющий запросить ссылку на конкретный интерфейс из числа реализуемых.

Автоматическое управление памятью и подсчет ссылок[править | править код]

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

Объявление интерфейсов[править | править код]

Для поддержки интерфейсов Delphi расширяет синтаксис языка Pascal дополнительными ключевыми словами. Объявление интерфейса в Delphi реализуется ключевым словом interface

type
  IMyInterface = interface
  ['{412AFF00-5C21-11D4-84DD-C8393F763A13}']
    procedure DoSomething(var I: Integer); stdcall;
    function DoSomethingAnother(S: String): Boolean;
  end;

  IMyInterface2 = interface(IMyInterface)
  ['{412AFF01-5C21-11D4-84DD-C8393F763A13}']
    procedure DoAdditional(var I: Integer); stdcall;
  end;

Для генерации нового значения GUID в IDE Delphi служит сочетание клавиш Ctrl+Shift+G.


IUnknown[править | править код]

Базовым интерфейсом в модели COM является IUnknown. Любой интерфейс наследуется от IUnknown и обязан реализовать объявленные в нем методы. IUnknown объявлен в модуле System.pas следующим образом:

type
  IUnknown = interface
    ['{00000000-0000-0000-C000-000000000046}']
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;


Рассмотрим назначение методов IUnknown более подробно.

Последние два метода предназначены для реализации механизма подсчета ссылок.

function _AddRef: Integer; stdcall;

Эта функция должна увеличить счетчик ссылок на интерфейс на 1 и вернуть новое значение счетчика.

function _Release: Integer; stdcall;

Эта функция должна уменьшить счетчик ссылок на интерфейс на 1 и вернуть новое значение счетчика. При достижении счетчиком значения 0 она должна освободить память, занятую реализацией интерфейса. Первый метод позволяет получить ссылку на реализуемый классом интерфейс.

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

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

  • возвращает ссылку на него в параметрt Obj
  • вызывает метод _AddRef полученного интерфейса
  • возвращает 0В противном случае, функция возвращает код ошибки E_NOINTERFACE.

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

В модуле System.pas объявлен класс TInterfacedObject, реализующий IUnknown и его методы. Рекомендуется использовать этот класс для создания реализаций своих интерфейсов.

Кроме этого, поддержка интерфейсов реализована в базовом классе TObject. Он имеет метод

function TObject.GetInterface(const IID: TGUID; out Obj): Boolean;

Если класс реализует запрошенный интерфейс, то функция:

  • возвращает ссылку на него в параметрt Obj
  • вызывает метод _AddRef полученного интерфейса
  • возвращает TRUE

В противном случае, функция возвращает FALSE.

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


Реализация интерфейсов[править | править код]

Реализацией интерфейса в Delphi всегда выступает класс. Для этого в объявлении класса необходимо указать, какие интерфейсы он реализует.

type
  TMyClass = class(TComponent, IMyInterface, IDropTarget)
    // Реализация методов
  end;


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

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

Рассмотрим более подробный пример.

type
  ITest = interface
  ['{61F26D40-5CE9-11D4-84DD-F1B8E3A70313}']
    procedure Beep;
  end;

  TTest = class(TInterfacedObject, ITest)
    procedure Beep;
    destructor Destroy; override;
  end;

{...}

procedure TTest.Beep;
begin
  Windows.Beep(0, 0);
end;

destructor TTest.Destroy;
begin
  inherited;
  MessageBox(0, 'TTest.Destroy', NIL, 0);
end;

Здесь класс TTest реализует интерфейс ITest. Рассмотрим использование интерфейса из программы.

procedure TForm1.Button1Click(Sender: TObject);
var
  Test: ITest;
begin
  Test := TTest.Create;
  Test.Beep;
end;


Данный код выглядит довольно странно, поэтому остановимся на нем подробнее.

Во-первых - оператор присваивания при приведении типа данных к интерфейсу неявно вызывает метод _AddRef. При этом количество ссылок на интерфейс увеличивается на 1.

Во-вторых - код не содержит никаких попыток освободить память, выделенную под объект TTest. Тем не менее, если выполнить эту программу, то на экран будет выведено сообщение о том, что деструктор был вызван. Это происходит потому, что при выходе переменной, ссылающейся на интерфейс за область видимости (либо, при присвоении ей другого значения) компилятор Delphi генерирует код для вызова метода _Release, информируя реализацию о том, что ссылка на неё больше не нужна.

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

Так, следующий код приведет к ошибке в момент выхода из функции:

var
  Test: ITest;
  T: TTest;
begin
  T := TTest.Create;
  Test := T;
  Test.Beep;
  T.Free;
end;   // в этот момент произойдет ошибка


Если вы хотите уничтожить реализацию интерфейса немедленно, не дожидаясь выхода переменной за область видимости - просто присвойте ей значение NIL

var
  Test: ITest;
  T: TTest;
begin
  T := TTest.Create;
  Test := T;
  Test.Beep;
  Test := NIL;  // Неявно вызывается IUnknown._Release;
end;


Обратите особое внимание, что вызовы методов IUnknown осуществляются Delphi неявно и автоматически. Поэтому - не вызывайте методов IUnknown самостоятельно. Это может нарушить нормальную работу автоматического подсчета ссылок и привести к не освобождению памяти, либо нарушениям защиты памяти при работе с интерфейсами. Во избежание этого - необходимо просто помнить, что:

1. При приведении типа объекта к интерфейсу, вызывается метод _AddRef.
2. При выходе переменной, ссылающейся на интерфейс за область видимости, либо при присвоении ей другого значения вызывается метод _Release
3. Единожды запросив у объекта интерфейс, Вы в дальнейшем не должны освобождать объект вручную. Лучше вообще, начиная с этого момента работать с объектом только через интерфейсные ссылки.

В рассмотренных примерах код для получения интерфейса у класса генерировался (с проверкой типов) на этапе компиляции. Если класс не реализует требуемого интерфейса, то программа не откомпилируется. Однако, существует возможность запросить интерфейс и во время выполнения программы. Для этого служит оператор as, который вызывает QueryInterface и, в случае успеха, возвращает ссылку на полученный интерфейс. В противном случае генерируется исключение.

Например, следующий код будет успешно откомпилирован, но вызовет ошибку «Interface not supported» при выполнении:

var
  Test: ITest;
begin
  Test := TInterfacedObject.Create as ITest;
  Test.Beep;
end;

В то же время код:

var
  Test: ITest;
begin
  Test := TTest.Create as ITest;
  Test.Beep;
end;

Будет успешно компилироваться и исполняться.


Реализация интерфейсов - расширенное рассмотрение[править | править код]

Рассмотрим вопросы реализации интерфейсов подробнее.

Объявим два интерфейса:

type
  ITest = interface
  ['{61F26D40-5CE9-11D4-84DD-F1B8E3A70313}']
    procedure Beep;
  end;

  ITest2 = interface
  ['{61F26D42-5CE9-11D4-84DD-F1B8E3A70313}']
    procedure Beep;
  end;


Теперь создадим класс, который будет реализовывать оба этих интерфейса

  TTest2 = class(TInterfacedObject, ITest, ITest2)
    procedure Beep1;
    procedure Beep2;
    procedure ITest.Beep = Beep1;
    procedure ITest2.Beep = Beep2;
  end;


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

Если реализация методов TTest2.Beep1 и TTest2.Beep2 идентична, то можно не создавать двух разных методов, а объявить класс следующим образом:

  TTest2 = class(TInterfacedObject, ITest, ITest2)
    procedure MyBeep;
    procedure ITest.Beep = MyBeep;
    procedure ITest2.Beep = MyBeep;
  end;


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

type
  TBeeper = class
    procedure Beep;
  end;

  TMessager = class
    procedure ShowMessage(const S: String);
  end;

  TTest3 = class(TInterfacedObject, ITest, IAnotherTest)
  private
    FBeeper: TBeeper;
    FMessager: TMessager;
    property Beeper: TBeeper read FBeeper implements ITest;
    property Messager: TMessager read FMessager implements IAnotherTest;
  public
    constructor Create;
    destructor Destroy; override;
  end;


Для делегирования реализации интерфейса другому классу служит ключевое слово implements

{ TBeeper }
procedure TBeeper.Beep;
begin
  Windows.Beep(0, 0);
end;

{ TMessager }
procedure TMessager.ShowMessage(const S: String);
begin
  MessageBox(0, PChar(S), NIL, 0);
end;

{ TTest3 }
constructor TTest3.Create;
begin
  inherited;
  // Создаем экземпляры дочерних классов
  FBeeper := TBeeper.Create;
  FMessager := TMessager.Create;
end;

destructor TTest3.Destroy;
begin
  // Освобождаем экземпляры дочерних классов
  FBeeper.Free;
  FMessager.Free;
  inherited;
end;


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

Обращаться к полученному классу можно точно так же, как и к любому классу, реализующему интерфейсы:

var
  Test: ITest;
  Test2: IAnotherTest;
begin
  Test2 := TTest3.Create;
  Test2.ShowMessage('Hi');
  Test := Test2 as ITest;
  Test.Beep;
end;


Интерфейсы и 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;


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

Clip0058.png

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

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


Использование интерфейсов для реализации 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=IpTXT.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-Proc сервер), так и в виде самостоятельного процесса (Out-Of-Proc сервер) или даже на другом компьютере (Distributed COM). COM автоматически разрешает вопросы, связанные с передачей параметров (Marshalling) и согласованием потоковых моделей клиента и сервера.

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

COM-сервер[править | править код]

COM-сервер - это специальным образом оформленное и зарегистрированное приложение, которое позволяет клиентам запрашивать у себя создание реализованных в нем объектов. Сервер может быть выполнен в виде динамической библиотеки, либо исполнимого файла.

Сервер в виде DLL[править | править код]

Такой сервер всегда выполняется в адресном пространстве активизировавшего его приложения (In-Process). За счет этого, как правило, снижаются накладные расходы на вызов методов сервера. В то же время такой сервер менее надежен, поскольку его память не защищена от ошибок в вызывающем приложении. Кроме этого он не может выполняться на удаленной машине без исполнимого модуля-посредника, который бы создал процесс, в который может быть загружена DLL. Примером такого модуля может служить Microsoft Transaction Server.

Сервер в виде исполнимого файла[править | править код]

Этот сервер представляет собой обычный исполнимый файл Windows в котором реализована возможность создания COM-объектов по запросу других приложений. Примером такого сервера является пакет Microsoft Office, приложения которого являются COM-серверами.

Регистрация сервера[править | править код]

COM реализует механизм автоматического поиска серверов по запросу клиента. Каждый COM-объект имеет уникальный идентификатор, Class Identifier (CLSID). Windows ведет в реестре базу данных зарегистрированный объектов, индексированную при помощи CLSID. Она расположена в ветке реестра HKEY_CLASSES_ROOT\CLSID.

Clip0059.png


Для каждого сервера прописывается информация, необходимая для нахождения и загрузки его модуля. Таким образом, клиентское приложение не должно беспокоиться о поиске сервера, достаточно зарегистрировать его на компьютере и COM автоматически найдет и загрузит нужный модуль. Кроме этого, объект может зарегистрировать свое «дружественное» имя, или Programmatic Identifier (PROGID). Обычно оно формируется как комбинация имени сервера и имени объекта, например «Word.Application». Это имя содержит ссылку на CLSID объекта. Когда он создается с использованием PROGID - COM просто берет связанное с ним значение CLSID и получает из него всю необходимую информацию.

Серверы в виде EXE автоматически регистрируются при первом запуске программы на компьютере. Для регистрации серверов DLL служит программа Regsvr32, поставляемая в составе Windows, либо TRegSvr из поставки DELPHI.

Потоки и Комнаты[править | править код]

Windows - многозадачная и многопоточная среда с вытесняющей многозадачностью. Применительно к COM это обозначает, что клиент и сервер могут оказаться в различных процессах или потоках приложения, что к серверу могут обращаться множество клиентов, причем в непредсказуемые моменты времени. Технология COM решает эту проблему при помощи концепции комнат (Apartments), в которых и выполняются COM клиенты и серверы. Комнаты бывают однопоточные (Single Threaded Apartment, STA) и многопоточные (Multiple Threaded Apartment, MTA).

STA[править | править код]

При создании однопоточной «комнаты» COM неявно создает окно, и при вызове любого метода COM-сервера в этой «комнате» посылает этому окну сообщение при помощи функции PostMessage. Таким образом, организуется очередь вызовов методов и каждый из них обрабатывается только после того, как будут обработаны все предшествующие вызовы. Основные достоинства однопоточной «комнаты»:

  1. Программист может не заботиться о синхронизации методов. Гарантируется, что до окончания выполнения текущего метода не будет вызван никакой другой метод объекта.
  2. Программист может не заботиться о синхронизации доступа к полям класса, реализующего объект. Поскольку одновременно может выполняться только один метод - одновременный доступ к полю из двух методов невозможен.

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

Недостатки STA напрямую вытекают из её реализации:

  1. Дополнительные (и иногда излишние) затраты на синхронизацию при вызове методов.
  2. Невозможность отклика на вызов метода, пока не исполнен предыдущий. Например, если в настоящее время выполняется метод, требующий одну минуту на исполнение - то до его завершения COM-объект будет недоступен.

Тем не менее, STA как правило является наиболее подходящим выбором для реализации COM-сервера. Использовать MTA есть смысл только в том случае, если STA не подходит для конкретного сервера.

MTA[править | править код]

Многопоточная «комната» не реализует автоматического сервиса по синхронизации и не имеет его ограничений. Внутри неё может быть создано сколько угодно потоков и объектов, причем каждый объект не привязан к какому-то конкретному потоку. Это означает, что любой метод объекта может быть вызван в любом из потоков в MTA. В это же самое время в другом потоке может быть вызван любой другой (либо тот же самый) метод COM-объекта по запросу другого клиента. COM автоматически ведет пул потоков внутри MTA и, при вызове со стороны клиента, находит свободный поток, и в нем вызывает метод требуемого объекта. Таким образом, даже если выполняется метод, требующий длительного времени - для другого клиента он может быть вызван без задержки в другом потоке. Очевидно, что COM-сервер, работающий в MTA обладает потенциально более высоким быстродействием и доступностью для клиентов, однако он значительно сложнее в разработке, поскольку даже локальные данные объектов не защищены от одновременного доступа и требуют синхронизации.

Передача интерфейсов и параметров[править | править код]

Таким образом, клиент и сервер COM могут выполняться как в одной «комнате», так и в разных, расположенных в различных процессах или даже на разных компьютерах. Встает вопрос - как же клиент может вызывать методы сервера, если они находятся в общем случае в другом адресном пространстве?

Эту работу берет на себя COM. Для доступа к серверу в другой «комнате» клиент должен запросить у COM создание в своей «комнате» представителя, реализующего запрошенный интерфейс. Такой представитель в терминах COM называется proxy и представляет собой объект, экспортирующий запрошенный интерфейс. Одновременно, COM создает в «комнате» сервера объект-заглушку, принимающий вызовы от proxy и транслирующий их в вызовы сервера. Таким образом, клиент в своей комнате может рассматривать proxy в качестве сервера и работать с ним так, как будто сервер создан в его «комнате». В то же время, сервер может рассматривать stub как расположенного с ним в одной «комнате» клиента. Всю работу по организации взаимодействия proxy и stub берет на себя COM. При вызове со стороны клиента Proxy получает от него параметры, упаковывает их во внутреннюю структуру и передает в комнату сервера. Stub получает параметры, распаковывает их и производит вызов метода сервера. Аналогично осуществляется передача параметров обратно. Этот процесс называется Marshalling. При этом «комнаты» клиента и сервера могут иметь разные потоковые модели и физически находиться где угодно. Разумеется, такой вызов вносит значительные накладные расходы, по сравнению с вызовом сервера в «своей» комнате, однако это единственный способ обеспечить корректную работу любых клиентов и серверов. Если необходимо избежать накладных расходов - сервер надо создавать в той же комнате, где расположен клиент.

Для обеспечения возможности корректного создания Proxy в клиентской комнате COM должен узнать «устройство» сервера. Сделать это можно двумя способами:

  1. Реализовать на сервере интерфейс IMarshal и, при необходимости proxy-DLL, которая будет загружена на клиенте для реализации proxy. Подробности реализации описаны в документации COM и MSDN
  2. Описать интерфейс на IDL (Interface Definition Language) и при помощи компилятора MIDL фирмы Microsoft сгенерировать proxy-stub DLL
  3. Сделать сервер совместимым с Ole Automation. В этом случае COM сам создаст proxy, используя описание сервера из его библиотеки типов - специального двоичного ресурса, описывающего COM-интерфейс. При этом в интерфейсе можно использовать только типы данных, совместимые с Ole Automation.

Инициализация COM[править | править код]

Каким же образом клиенты и серверы COM могут создавать комнаты в соответствии со своими требованиями? Для этого они должны соблюдать одно правило: каждый поток, который желает использовать COM должен создать комнату, при помощи вызова функции CoInitializeEx. Она объявлена в модуле ActiveX.pas следующим образом:

const
  COINIT_MULTITHREADED      = 0;  // OLE calls objects on any thread.
  COINIT_APARTMENTTHREADED  = 2;  // Apartment model

function CoInitializeEx(pvReserved: Pointer;
                        coInit: Longint): HResult; stdcall;


Параметр pvReserved зарезервирован для будущего использования и должен быть равен NIL, а параметр coInit определяет потоковую модель создаваемой комнаты. Он может принимать значения:

  • COINIT_APARTMENTTHREADED - Для потока создается STA. Каждый поток может иметь (или не иметь) свою STA
  • COINIT_MULTITHREADED - Если в текущем процессе еще не создана MTA - создается новая MTA, если она уже создана другим потоком - поток «подключается» к ранее созданной. Иными словами, каждый процесс может иметь только одну MTA

Функция возвращает S_OK в случае успешного создания комнаты.

По завершении работы с COM (или перед завершением работы) поток должен уничтожить «комнату» при помощи вызова процедуры CoUninitialize, также описанной в модуле ActiveX

procedure CoUninitialize; stdcall;

Каждый вызов CoInitializeEx должен иметь соответствующий вызов CoUninitialize, т.е. если Вы используете COM в приложении, вы должны вызвать CoInitializeEx до первого использования функций COM и CoUninitialize в перед завершением работы приложения. VCL реализует автоматическую инициализацию COM при использовании модуля ComObj. По умолчанию создается STA. Если Вы хотите использовать другую потоковую модель, Вы должны установить флаг инициализации COM до оператора Application.Initialize

program Project1;

uses
  Forms,
  ComObj,
  ActiveX,
  Unit1 in 'Unit1.pas' {Form1};

{$R *.RES}

begin
  CoInitFlags := COINIT_MULTITHREADED;
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

Если COM используется в потоке, то эти функции должны быть вызваны в методе Execute

procedure TMyThread.Execute;
begin
  CoInitializeEx(NIL, COINIT_MULTITHREADED);
  {...}
  CoUninitialize
end;


Инициализация COM требуется также для вызова любых функций Windows API, связанных с COM, за исключением CoGetMalloc, CoTaskMemAlloc, CoTaskMemFree и CoTaskMemReAlloc.

Отдельного обсуждения заслуживает инициализация потоковой модели COM для сервера, расположенного в DLL. Дело в том, что DLL может быть загружена любым потоком, который уже ранее создал свою «комнату». Поэтому, сервер в DLL не может сам проинициализировать требуемую ему потоковую модель. Вместо этого, сервер при регистрации прописывает в реестре параметр «ThreadingModel», который и указывает - в какой потоковой модели способен работать данный сервер. При создании сервера COM анализирует значение этого параметра и потоковой модели «комнаты» запросившего создание сервера потока и при необходимости создает для сервера «комнату» с требуемой потоковой моделью.

Embim1877.png


Параметр ThreadingModel может принимать следующие значения:

  • Apartment - Сервер может работать только в STA. Если он создается из STA, то он будет создан в «комнате» вызывающего потока, если из MTA - COM автоматически создаст для него «комнату» c STA и proxy в «комнате» клиента.
  • Free - Сервер может работать только в MTA. Если он создается из MTA, то он будет создан в «комнате» вызывающего потока, если из STA - COM автоматически создаст для него «комнату» c MTA и proxy в «комнате» клиента.
  • Both - Сервер может работать как в STA, так и MTA. Объект всегда создается в вызывающей комнате.

Если этот параметр не задан - сервер имеет потоковую модель Single. В этом случае он создается в Primary STA (т.е. в STA потока, который первым вызвал CoInitialize), даже если создание сервера запрошено из потока, имеющего свою отдельную STA.

Активация сервера[править | править код]

Для активации COM-сервера клиент должен вызвать функцию CreateComObject, описанную в модуле ComObj.pas

function CreateComObject(const ClassID: TGUID): IUnknown;

функция получает в качестве параметра CLSID требуемого объекта и возвращает ссылку на его интерфейс IUnknown. Далее клиент может запросить требуемый интерфейс и работать с ним.

var
  COMServer: IComServer;

{...}
  // Создаем COM объект и запрашиваем у него интерфейс
  ComServer := CreateComObject(IComServer) as IComServer;
  // Работаем с интерфейсом
  ComServer.DoSomething;
  // Освобождаем интерфейс
  ComServer := NIL;

Что же делает COM при запросе на создание сервера?

  1. В реестре, по запрошенному CLSID, ищется запись регистрации сервера.
  2. Из этой записи получается имя исполнимого модуля сервера.
  3. Если это исполнимый файл - он запускается на выполнение. Любое приложение, реализующее COM-сервер при старте регистрирует в системе интерфейс «фабрики объектов». После запуска и регистрации COM получает ссылку на "фабрику объектов"
  4. Если это DLL - она загружается в адресное пространство вызвавшего процесса и вызывается её функция DllGetClassObject, возвращающая ссылку на реализованную в DLL "фабрику объектов"
  5. Фабрика объектов - это COM-сервер, реализующий интерфейс IClassFactory. Ключевым методом этого интерфейса является метод CreateInstance, который и создает экземпляр требуемого объекта.
  6. COM вызывает метод CreateInstance и передает полученный интерфейс клиенту.

По завершении работы с COM-объектом клиент освобождает ссылку на него (что приводит к вызову метода Release). В этот момент COM-сервер проверяет, есть ли еще ссылки на созданные им объекты. Если все объекты освобождены, то COM-сервер завершает свою работу. В случае, если он реализован в виде DLL он должен экспортировать функцию DllCanUnloadNow, которая вызывается COM по таймеру или при вызове функции API CoFreeUnusedLibraries. Если все объекты из этой DLL освобождены, то она выгружается из памяти.

Вся работа по созданию и регистрации фабрики объектов и экспорту соответствующих функций из DLL в Delphi уже реализована в составе стандартных библиотек и создание COM-сервера в действительности является очень простой задачей.


Создание COM-сервера[править | править код]

Для создания COM-сервера Delphi предоставляет широкий набор мастеров, автоматизирующих выполнение рутинных задач и позволяющих программисту сконцентрироваться на реализации функциональности. Мастера доступны при помощи команды меню File->New, на закладке ActiveX

Чтобы сделать COM-сервером EXE-файл, Вы должны просто добавить в него модуль с COM-объектом. Для создания COM-сервера в виде DLL Вы должны сначала создать библиотеку, оформленную с учетом требований COM. Это делается при помощи мастера «ActiveX Library». При его выборе будет создан новый проект, реализующий DLL и сгенерирован следующий код:

library Project1;

uses
  ComServ;

exports
  DllGetClassObject,
  DllCanUnloadNow,
  DllRegisterServer,
  DllUnregisterServer;

{$R *.RES}

begin
end.


Созданная DLL экспортирует функции, необходимые для работы COM и Вы можете не заботиться о рутинной работе и сразу приступить к реализации COM-сервера.

Для этого выберите мастер «COM-объект». Image 2021-12-12 134631.png

От заполнения полей этой формы зависит реализация создаваемого COM-объекта

  • ClassName - Имя класса Delphi, реализующего COM-сервер. Мастер создаст заготовку класса с этим именем. Под этим же именем COM-сервер будет зарегистрирован в реестре.
  • Instansing - Определяет режим создания COM-объектов. Параметр может принимать следующие значения:
    • Internal - Объект может использоваться только внутри этого приложения.
    • Single Instance - Создание каждого экземпляра объекта приводит к запуску нового экземпляра приложения-сервера. После создания объекта "фабрика объектов" приложения удаляет информацию о себе из системного списка зарегистрированных фабрик, что заставляет COM при создании нового объекта запустить приложение сервер в новом процессе
    • Multiple Instance - После создания экземпляра объекта "фабрика" не удаляет себя из списка зарегистрированных. При создании запросе на создание нового объекта COM обнаружит её в этом списке и запросит создание у той же фабрики и новый экземпляр объекта будет создан в том же приложении. Другими словами для создания всех объектов данного типа будет запущено не более одного экземпляра сервера. Этот параметр имеет смысл только для EXE-серверов, для DLL он игнорируется.
  • Threading Model - Потоковая модель сервера. Действие этого параметра зависит от типа сервера (EXE или DLL)
    • Single - Нет никакой поддержки потоков. Для DLL-сервера при регистрации не будет создан параметр ThreadingModel Для EXE-сервера указание этого параметра (в отличие от любого другого) не приведет к установке флага IsMultiThread, будет создана STA. Обычно эта модель используется для Internal серверов
    • Apartment - Для DLL сервера в реестре будет создан параметр ThreadingModel=Apartment, для EXE - создана STA
    • Free - Для DLL сервера в реестре будет создан параметр ThreadingModel=Free, для EXE - создана MTA
    • Both - Для DLL сервера в реестре будет создан параметр ThreadingModel=Both, для EXE - создана MTA
  • Include Type Library - Установка этого флажка приводит к включению в сервер библиотеки типов - специального двоичного ресурса, описывающего реализуемые сервером интерфейсы, их методы и параметры вызова. COM предоставляет стандартные средства работы с библиотеками типов. В частности, Delphi может импортировать имеющуюся в сервере библиотеку типов и автоматически построить по ней интерфейсный модуль для работы с ним. При использовании библиотеки типов интерфейсы описываются при помощи Type Library Editor. Объект наследуется от TTypedComObject Если этот флажок выключен, то объект наследуется от TComObject. Это более легковесная реализация сервера.
  • Implemented Interfaces - Это поле разрешено, только если объект не использует библиотеку типов. В этом случае Вы должны сами описать интерфейсы в коде своей программы и перечислить их в этом поле, например "ITest, IAnotherTest"
  • Mark interface Oleautomation - Установка этого флага делает COM-сервер совместимым с OLE Automation. Вы должны использовать в методах интерфейса только совместимые с OLE Automation типы данных. Это необходимо, если Вы хотите передавать ссылку на интерфейс между разными «комнатами». Такая операция, называемая маршаллингом интерфейсов требует написания специальной proxy/stub DLL. Однако, если интерфейс помечен как Oleautomation, эту работу выполняет маршаллер OLE и Вы будете избавлены от лишней работы.
Внимание!
Для поддержки Oleautomation маршаллинга необходимо, чтобы:
  • сервер был унаследован от TTypedComObject (реализация IDispatch не обязательна)
  • все методы интерфейса должны быть объявлены как safecall.

Если Вы создаете интерфейс унаследованный от IUnknown, то по умолчанию все его методы объявляются как stdcall. Чтобы создать safecall методы необходимо в диалоге Tools->Environment Options на закладке Type Library установить переключатель Safecall function mapping в значение All v-table interfaces


Сервер без библиотеки типов[править | править код]

Такой сервер, если он не реализует интерфейс IMarshall, может работать только в одной "комнате" с клиентом, поэтому его следует использовать только для In-Process серверов с потоковой моделью идентичной клиенту.

При создании сервера, не включающего библиотеку типов Вы должны указать мастеру реализуемые им интерфейсы. Укажем имя интерфейса ITest. По завершении работы мастера будет создан следующий модуль:

unit Unit1;

interface

uses
  Windows, ActiveX, Classes, ComObj;

type
  TTest = class(TComObject, ITest)
  protected
  end;

const
  Class_Test: TGUID = '{1302FB00-703F-11D4-84DD-825B45DBA617}';

implementation

uses ComServ;

initialization
  TComObjectFactory.Create(ComServer, TTest, Class_Test,
    'Test', '', ciMultiInstance, tmApartment);
end.


Внимание!
Если Вы создаете COM-сервер, который может использоваться различными клиентами (а не только в рамках конкретного проекта, в котором спецификации клиентов жестко заданы) не рекомендуется делать серверы без поддержки маршаллинга данных, поскольку в этом случае невозможно обеспечить гарантированное нахождение его в одной «комнате» с клиентом. Если вы все же делаете такой сервер - необходимо отразить требуемые спецификации клиента в документации на него.

Посмотрим на сгенерированный код подробнее. Особый интерес вызывает секция Initialization. В ней создается экземпляр «фабрики объектов» - COM-сервера, реализующего интерфейс IClassFactory2. К нему COM будет обращаться для создания экземпляра объекта Test. VCL автоматически реализует всю рутинную работу по взаимодействию с COM.

Для реализации сервера требуется написать интерфейсный модуль с описанием реализуемого интерфейса. Кроме этого, вынесем в него описание константы Class_Test и добавим его в строку uses модуля Unit1

unit TestInterface;

interface

const
  Class_Test: TGUID = '{1302FB00-703F-11D4-84DD-825B45DBA617}';

type
  ITest = interface
  ['{1C986802-6D6D-11D4-84DD-996A491CE716}']
    procedure ShowIt(S: String);
  end;

implementation

end.


Этот модуль содержит всю необходимую информацию для использования сервера и должен использоваться при компиляции клиента.

Дополним код COM-объекта реализацией методов реализуемого интерфейса:

unit Unit1;

interface

uses
  Windows, ActiveX, Classes, ComObj, TestInterface;

type
  TTest = class(TComObject, ITest)
  protected
    procedure ShowIt(S: String);
  end;

implementation

uses ComServ;

{ TTest }

procedure TTest.ShowIt(S: String);
begin
  MessageBox(0, PChar(S), NIL, 0);
end;

initialization
  TComObjectFactory.Create(ComServer, TTest, Class_Test,
    'Test', '', ciMultiInstance, tmApartment);
end.


Откомпилировав проект, мы получим файл Project1.dll

Последним шагом является регистрация COM-сервера.

Введем в командной строке: regsvr32 project1.dll

Если все было проделано правильно, на экране должно появиться сообщение об успешной регистрации: "DllRegisterServer in Project1.dll succeeded".

Можно приступать к написанию клиента. Для этого создадим новый проект, добавим в модуль с его главной формой строку uses TestInterface и напишем следующий код:

uses TestInterface, ComObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  Test: ITest;
begin
  Test := CreateComObject(Class_Test) as ITest;
  Test.ShowIt('Hi');
end;


Как видно из этого примера создание и использование COM-сервера не сложнее, чем работа с обычными классами Delphi. Сервер без библиотеки типов является хорошим выбором для реализации COM-серверов, используемых внутри проекта, поскольку для его использования нужен интерфейсный модуль. При передаче сервера другим разработчикам Вам придется передать им этот модуль и, при необходимости, перевести его на другой язык (например, С).

Сервер с библиотекой типов[править | править код]

Библиотека типов - это специальный двоичный ресурс, описывающий интерфейсы и методы, реализуемые COM-сервером. Кроме наличия библиотеки типов сервер должен поддерживать интерфейс IProvideClassInfo. В Delphi такой сервер реализуется путем наследования его от TTypedComObject. Для этого оставьте флажок Include Type Library в мастере создания COM-объекта включенным.

Создадим COM-сервер в виде EXE (разумеется, он может быть также создан и виде DLL).

Сначала создадим новый проект (File-New Application), а затем добавим в него COM-объект.

Если не отключать флажок Include Type Library, то мастер создаст уже не один, а два модуля. Первый из них напоминает созданный ранее.

unit Unit1;

interface

uses
  Windows, ActiveX, Classes, ComObj, Project1_TLB, StdVcl;

type
  TTest1 = class(TTypedComObject, ITest1)
  protected
    {Declare ITest1 methods here}
  end;

implementation

uses ComServ;

initialization
  TTypedComObjectFactory.Create(ComServer, TTest1, Class_Test1,
    ciMultiInstance, tmApartment);
end.


Наиболее интересна строка: uses Project1_TLB. Это автоматически сгенерированный интерфейсный модуль к нашему COM-объекту (аналогично TestInterface.pas в предыдущем примере). Он содержит описание всех необходимых для работы с сервером интерфейсов. В отличие от предыдущего примера Вы не должны редактировать его вручную. Для этого Delphi откроет редактор библиотеки типов

Image 2021-12-12 135946.png

Это специализированный редактор для описания интерфейсов COM-объектов. Вы должны описать все требуемые интерфейсы, методы и т.п. в этом редакторе, после чего можно нажать кнопку "обновить" и изменения будут автоматически внесены во все требуемые модули. Вам останется лишь дописать реализацию методов.

Добавим описание нового метода. Для этого щелкнем правой кнопкой на интерфейсе ITest и выберем в контекстном меню New->Method. Введем имя метода "ShowIt".

Embim1878.png


На закладке Parameters зададим параметр S и тип BSTR. После этого нажмем кнопку обновить и посмотрим, что случилось с исходными текстами нашей программы. В модуле Project1_TLB в описании интерфейса ITest1 появился метод ShowIt

  ITest1 = interface(IUnknown)
    ['{1302FB06-703F-11D4-84DD-825B45DBA617}']
    function ShowIt(const S: WideString): HResult; stdcall;
  end;


А в модуле Unit1:

type
  TTest1 = class(TTypedComObject, ITest1)
  protected
    function ShowIt(const S: WideString): HResult; stdcall;
  end;

implementation

uses ComServ;

function TTest1.ShowIt(const S: WideString): HResult;
begin

end;


Нам остается лишь написать реализацию метода:

function TTest1.ShowIt(const S: WideString): HResult;
begin
  MessageBoxW(0, PWideChar(S), NIL, 0)
  Result := S_OK; // Стандартный код успешного завершения
end;


Для регистрации сервера достаточно один раз запустить его на компьютере клиента.


Создание приложения клиента[править | править код]

Перейдем к написанию приложения клиента. Если у нас есть модуль Project_TLB, то оно ничем не будет отличаться от предыдущего примера. Более интересен случай, когда мы имеем только исполнимый файл с сервером. Зарегистрируем этот сервер и выберем в меню Delphi IDE команду Project -> Import Type Library В открывшемся окне найдем строку с описанием библиотеки типов требуемого сервера:

Clip0234.gif


Если включен флажок Generate Component Wrappers, то в импортированный модуль будет добавлен код для создания компонента Delphi, который можно бросить на форму и он автоматически создаст требуемый COM-сервер и позволит обращаться к его методам. В противном случае будет сгенерирован модуль, содержащий описание всех имеющихся в библиотеке типов интерфейсов.

Далее Вы должны определить, что Вы собираетесь сделать с выбранной библиотекой

  • Install - Создает модуль с описанием интерфейсов и автоматически регистрирует требуемые компоненты в IDE. После этого Вам остается лишь бросить их на форму.
  • Create Unit - Создает интерфейсный модуль, но не устанавливает его в IDE. Это удобно, если Вам нужны только описания интерфейсов, либо если Вы хотите вручную установить его в package, отличающуюся от используемой по умолчанию

Таким образом, для распространения и использования сервера не нужно ничего, кроме его исполнимого модуля. Но самое главное даже не это. Гораздо более важно, что Вы можете импортировать и использовать в своей программе любой из имеющихся на компьютере COM-серверов. Естественно, что при передаче своей программы клиенту Вы должны установить на его компьютере соответствующий COM-сервер.

Clip0235.gif


Для примера, используем в своем приложении процессор регулярных выражений VBScript. Импортируем библиотеку типов "Microsoft VBScript Regular Expressions"

При этом будет создан файл VBScript_RegExp_TLB.pas

Создадим форму и добавим следующий код для проверки вхождения текста в Exit1 в текст Edit2:

uses
  VBScript_RegExp_TLB;

procedure TForm1.Button1Click(Sender: TObject);
var
  RE: IRegExp;
begin
  RE := CoRegExp.Create;
  RE.Pattern := Edit1.Text;
  if RE.Test(Edit2.Text) then
    Caption := 'TRUE'
  else
    Caption := 'FALSE';
end;


Это все! Мы получили в своем приложении поддержку регулярных выражений такую же, которая включена в скриптовые языки Microsoft (VBScript и JScript).


Создание Plug-In в виде COM-Сервера[править | править код]

Попробуем теперь реализовать Plug-In к своей программе, в виде COM-сервера и сравним код, полученный в этом случае с кодом, полученным при «ручном» программировании. В начале создадим модуль с описанием интерфейсов:

unit PluginInterface;

interface

const
  Class_TAPI: TGUID = '{A132D1A1-721C-11D4-84DD-E2DEF6359A17}';

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);
    function GetNextLine(var S: String): Boolean;
  end;

implementation

end.


Обратите внимание, что метод ILoadFilter.Init больше не получает ссылки на внутренний API программы - он будет реализован в виде COM-объекта.

Создадим DLL c COM-сервером, реализующим ILoadFilter. Для этого создадим новую ActiveX library и добавим в неё COM-объект TLoadFilter. Установим ThreadingModel в Single, поскольку использования сервера в потоках не предполагается. После этого реализуем методы интерфейса ILoadFilter.

unit Unit3;

interface

uses
  Windows, ActiveX, Classes, ComObj, PluginInterface;

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

const
  Class_LoadFilter: TGUID = '{A132D1A2-721C-11D4-84DD-E2DEF6359A17}';


implementation

uses ComServ, SysUtils;


Деструктор и метод GetNextLine аналогичны предыдущему примеру:

destructor TLoadFilter.Destroy;
begin
  if InitSuccess then
    CloseFile(F);
  inherited;
end;

function TLoadFilter.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;


В методе Init имеется существенное отличие - теперь ссылку на внутреннее API программы мы получаем при помощи COM. Это освобождает нас от необходимости передавать ссылку в модуль расширения.

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


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

initialization
  TComObjectFactory.Create(ComServer, TLoadFilter, Class_LoadFilter,
    'LoadFilter', '', ciMultiInstance, tmSingle);
end.


Компилируем DLL и регистрируем её при помощи regsvr32.

Поскольку программа может поддерживать множество различных фильтров, организуем их подключение через INI файл следующего вида:

[Filters]
TXT={A132D1A2-721C-11D4-84DD-E2DEF6359A17}

Параметром строки служит CLSID сервера, реализующего фильтр. В нашем случае это будет содержание константы Class_LoadFilter. Для подключения дополнительных фильтров необходимо создать DLL с сервером, реализующим ILoadFilter, зарегистрировать её в системе и добавить CLSID сервера в INI-файл.

Теперь можно приступать к написанию программы-клиента. Она аналогична используемой в предыдущем примере. Добавим в неё COM-сервер, реализующий внутреннее API.

За исключением кода, сгенерированного COM объект полностью аналогичен объекту, приведенному ранее. Константу Class_TAPI вынесем в модуль PluginInterface, чтобы сделать её доступной для модулей расширения.

unit Unit2;

interface

uses
  Windows, ActiveX, Classes, ComObj, PluginInterface;

type
  TTAPI = class(TComObject, IAPI)
  protected
    procedure ShowMessage(const S: String);
  end;

implementation

uses Forms, ComServ, Unit1;

{ TTAPI }

procedure TTAPI.ShowMessage(const S: String);
begin
  (Application.MainForm as TForm1).StatusBar1.SimpleText := S;
end;

initialization
  TComObjectFactory.Create(ComServer, TTAPI, Class_TAPI,
    'TAPI', '', ciMultiInstance, tmSingle);
end.


Теперь все готово к реализации функциональности клиента. Для экономии места приведем лишь метод LoadData

procedure TForm1.LoadData(FileName: String);
var
  PlugInName: String;
  Filter: ILoadFilter;
  S, Ext: String;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;
  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;
    Filter := CreateComObject(StringToGUID(PlugInName)) as ILoadFilter;
    Filter.Init(FileName);
    while Filter.GetNextLine(S) do
      Memo1.Lines.Add(S);
  finally
    Memo1.Lines.EndUpdate;
  end;
end;


Очевидно, что код метода стал гораздо короче и читабельнее. COM взял на себя всю черновую работу по поиску загрузке и выгрузке DLL, поиску и созданию объектов.

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


Автоматическая регистрация серверов из своей программы[править | править код]

Удобно в своей программе автоматически регистрировать все необходимые серверы. Это можно сделать при помощи следующей процедуры:

procedure CheckComServerInstalled(const CLSID: TGUID; const DllName: String);
var
  Size: Integer;
  DllHandle: THandle;
  FileName: String;
begin
  Size := MAX_PATH;
  SetLength(FileName, Size);
  try
    if RegQueryValue(HKEY_CLASSES_ROOT,
         PChar(Format('CLSID\%s\InProcServer32',
        [GUIDToString(CLSID)])), PChar(FileName), Size) = ERROR_SUCCESS then
    begin
      SetLength(FileName, Size);
      DllHandle := LoadLibrary(PChar(FileName));
      FreeLibrary(DllHandle);
      if DllHandle = 0 then
      begin
        RegDeleteKey(HKEY_CLASSES_ROOT,
          PChar(Format('CLSID\%s',[GUIDToString(CLSID)])));
        RegisterComServer(DllName);
      end;
    end
    else
    begin
      RegisterComServer(DllName);
    end;
  except
    raise Exception.CreateFmt('Не могу зарегистрировать %s.', [DllName]);
  end;
end;


В процедуре осуществляется дополнительная проверка наличия на диске файла с зарегистрированным сервером. Если файл не найден по указанному в реестре месту - данные о регистрации удаляются и предпринимается попытка зарегистрировать сервер заново. Такая проверка очень полезна при переносе DLL с сервером в другую папку на диске.


OLE Automation[править | править код]

Стандарт COM основан на едином для всех поддерживающих его языков формате таблицы, описывающей ссылки на методы объекта, реализующего интерфейс. Однако, вызов методов при помощи этой таблицы доступен только для компилирующих языков программирования. В то же время, очень удобно было бы иметь доступ к богатству возможностей, предоставляемых COM из интерпретирующих языков, таких, как VBScript. Для поддержки этих языков была разработана технология под названием OLE Automation, позволяющая приложениям делать свою функциональность доступной для гораздо большего числа клиентов. Automation базируется на COM и является его подмножеством, однако накладывает на COM-серверы ряд дополнительных требований:

  1. Интерфейс, реализуемый COM-сервером должен наследоваться от IDispatch
  2. Должны использоваться типы данных, из числа поддерживаемых OLE Automation
Тип данных OLE Automation Тип данных Delphi Примечание
Boolean WordBool
Unsigned Char Byte
Double Double
Float Single
Int SYSINT Машинно-зависимый целый тип данных, в настоящее время объявлен как Integer, однако, в будущем может иметь другую разрядность
Long Integer
Short SmallInt
BSTR WideString В Automation нельзя использовать строки Delphi
Currency Currency
Date TDateTime
SAFEARRAY PSafeArray Массив из элементов любого поддерживаемого типа
Decimal TDecimal 96 битное десятичное число.
Interface IDispatch * IDispatch Ссылка на IDispatch или любой унаследованный от него интерфейс
Interface IUnknown * IUnknown Ссылка на произвольный интерфейс
VARIANT OleVariant Вариант, совместимый с OLE

Возможна поддержка пользовательских типов данных, для чего необходимо реализовать интерфейс IRecordInfo

  1. Все методы должны быть процедурами или функциями, возвращающими значение типа HRESULT
  2. Все методы должны иметь соглашение о вызовах safecall

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

IDispatch[править | править код]

Центральным элементом технологии OLE Automation является интерфейс IDispatch. Ключевыми методами этого интерфейса являются методы GetIdsOfNames и Invoke, которые позволяют клиенту запросить у сервера, поддерживает ли он метод с указанным именем, а затем, если метод поддерживается - вызвать его. Подробно реализация и работа IDispatch рассматривается в главе, посвященной работе с Mcrosoft Scripting Control, здесь же мы лишь вкратце опишем основной алгоритм вызова методов при помощи IDispatch.

Когда клиенту требуется вызвать метод, он вызывает GetIdsOfNames, передавая ему имя запрошенного метода. Если сервер поддерживает такой метод, он возвращает его идентификатор - целое число, уникальное для каждого метода. После этого клиент упаковывает параметры в массив переменных типа OleVariant и вызывает Invoke, передавая ему массив параметров и идентификатор метода.

Таким образом, все, что должен знать клиент - это строковое имя метода. Такой алгоритм позволяет работать с наследниками IDispatch из скриптовых языков.

Методы GetTypeInfo и GetTypeInfoCount являются вспомогательными и обеспечивают поддержку библиотеки типов объекта. Реализация методов GetIdsOfNames и Invoke, предоставляемая COM по умолчанию базируется на библиотеке типов объекта.

Поддержка IDispatch, тип данных Variant[править | править код]

Delphi имеет встроенную поддержку работы в качестве клиента Automation. Тип данных Variant может содержать ссылку на интерфейс IDispatch и использоваться для вызова его методов.

uses ComObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  V: Variant;
begin
  V := CreateOleObject('InternetExplorer.Application');
  V.Toolbar := FALSE;
  V.Left := (Screen.Width - 600) div 2;
  V.Width := 600;
  V.Top := (Screen.Height - 400) div 2;
  V.Height := 400;
  V.Visible := TRUE;
  V.Navigate(URL := 'file://C:\config.sys');
  V.StatusText := V.LocationURL;
  Sleep(10000);
  V.Quit;
end;


Приведенный выше код весьма необычен и заслуживает внимательного рассмотрения.

  1. Переменная V не является классом и, очевидно не имеет ни одного из используемых свойств и методов.
  2. Вызываемые свойства и методы нигде не описаны, однако это не вызывает ошибки компиляции.
  3. Объект создается не по CLSID, а по информативному имени, функцией CreateOleObjectВсе это непривычно и выглядит довольно странно. На самом деле - ничего странного нет. Компилятор Delphi просто запоминает в коде программы строковые описания обращений к серверу автоматизации, а на этапе выполнения передает их его интерфейсу IDispatch, который и производит синтаксический разбор и выполнение. Исправим третью строку функции на:
V.Left1 := (Screen.Width - 600) div 2;

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

Такое обращение к серверу называется поздним связыванием, что означает, что связывание имен свойств и методов объекта с их кодом происходит не на этапе компиляции, а на этапе выполнения программы. Достоинства позднего связывания очевидны - не нужна библиотека типов, написание несложных программ упрощается. Столь же очевидны недостатки - не производится контроль вызовов и передаваемых параметров на этапе компиляции, работа несколько медленнее, чем при раннем связывании.

Внимание!
Если COM-сервер находится в другой "комнате" - затраты на позднее связывание пренебрежимо малы, по сравнению с затратами на маршаллинг вызовов. Разница в скорости между ранним и поздним связыванием становится ощутимой (десятки и сотни раз) при нахождении клиента и сервера в одной "комнате", что возможно только для In-Proc сервера при совместимой с клиентом потоковой модели. Для Out-Of-Proc сервера (размещенного в отдельном исполнимом файле) затраты на вызов метода практически равны.

В связи с этим главным преимуществом раннего связывания является строгий контроль типов на этапе компиляции. Для разрешения проблемы нестрогого контроля типов COM предлагает несколько дополнительных возможностей

Dispinterface[править | править код]

Dispinterface - это декларация методов, доступных через интерфейс IDispatch. Объявляется он следующим образом:

type
  IMyDisp = dispinterface
    ['{EE05DFE2-5549-11D0-9EA9-0020AF3D82DA}']
    property Count: Integer dispid 1
    procedure Clear dispid 2;
  end;


Самих методов может физически и не существовать (например, они реализуются динамически в Invoke). Рассмотрим использование dispinterface на простом примере. Объявим диспинтерфейс объекта InternetExplorer и используем его в своей программе:

type
  IIE = dispinterface
  ['{0002DF05-0000-0000-C000-000000000046}']
    property Visible: WordBool dispid 402;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  II: IIE;
begin
  II := CreateOleObject('InternetExplorer.Application') as IIE;
  II.Visible := TRUE;
end;


Эта программа успешно компилируется и работает, несмотря на то, что в интерфейсе объявлено только одно из множества имеющихся свойств и методов. Это возможно благодаря тому, что Delphi не вызывает методы диспинтерфейса напрямую и, поэтому, не требует полного описания всех методов в правильном порядке. При вызове метода диспинтерфейса Delphi просто вызывает Invoke соответствующего IDispatch, передавая ему идентификатор метода, указанный в dispid. В результате программисту становятся доступна возможность строгого контроля типов при вызове методов IDispatch и вызов методов, реализованных в IDispatch без формирования сложных структур данных для вызова Invoke. Необходимо лишь описать (или импортировать из библиотеки типов сервера) описание диспинтерфейса.

В описании диспинтерфейса допустимо использовать только OLE-совместимые типы данных.

Dual Interfaces[править | править код]

Идея двойных интерфейсов очень проста. Сервер реализует одновременно некоторый интерфейс, оформленный по стандартам COM (VTable) и диспинтерфейс, доступный через IDispatch. При этом интерфейс VTable должен быть унаследован от IDispatch и иметь идентичный с диспинтерфейсом набор методов. Такое оформление сервера позволяет клиентам работать с ним наиболее удобным для каждого клиента образом.

Клиенты, использующие VTable вызывают методы интерфейса напрямую, а клиенты, использующие позднее связывание - через методы IDispatch.

Большинство OLE-серверов реализуют двойной интерфейс.

Создание Automation серверов[править | править код]

Чтобы создать при помощи Delphi сервер, совместимый с OLE Automation необходимо включить в свое приложение Automation Object. Мастер для его создания запускается при выборе File -> New -> ActiveX -> Automation Object

В поле CoClassName вводится имя создаваемого объекта. Поля Instancing и Threading Model аналогичны рассмотренным выше при создании COM сервера. Наибольший интерес представляет собой флаг Generate Event Support code. В случае, если он задан, генерируется дополнительный код, позволяющий серверу реализовать интерфейс событий. Этот интерфейс описывает события, которые может генерировать сервер. Клиент может зарегистрировать себя в качестве подписчика на эти события и получать уведомления о них. Для того, чтобы понять механизм этого процесса отвлечемся от создания ActiveX сервера и рассмотрим событийную модель COM.

События в COM[править | править код]

При возникновении события в COM-сервере, которое он должен передать клиенту, сервер должен вызвать какой-либо из методов клиента. Фактически, в этот момент клиент с сервером меняются местами. Обращение к клиенту осуществляется при помощи стандартных механизмов COM. Основная идея заключается в том, что сервер, генерирующий события декларирует интерфейс их обработчика. Клиент, подписывающийся на события, должен реализовать этот интерфейс (т.е. фактически должен быть или включать в себя COM-объект, реализующий интерфейс). Кроме этого сервер должен реализовать стандартные интерфейсы COM, позволяющие зарегистрировать на нем обработчик событий. Клиент, используя эти интерфейсы, регистрирует на сервере интерфейс обработчика событий, позволяя серверу вызывать свои методы. Рассмотрим основные интерфейсы, используемые в этом процессе.

type
  IConnectionPointContainer = interface
    ['{B196B284-BAB4-101A-B69C-00AA00341D07}']
    function EnumConnectionPoints(out Enum: IEnumConnectionPoints): HResult;
      stdcall;
    function FindConnectionPoint(const iid: TIID;
      out cp: IConnectionPoint): HResult; stdcall;
  end;


Этот интерфейс должен реализовываться каждым COM-объектом, который позволяет подключаться к своим событиям. Ключевой метод FindConnectionPoint, который получает GUID интерфейса-обработчика и возвращает указатель на соответствующую этому обработчику «точку подключения». Такой подход позволяет серверу иметь несколько интерфейсов для обработки событий и позволять клиентам подключаться к ним по необходимости. В случае успеха метод возвращает S_OK, иначе - код ошибки.

Точка подключения также представляет собой интерфейс:

type
  IConnectionPoint = interface
    ['{B196B286-BAB4-101A-B69C-00AA00341D07}']
    function GetConnectionInterface(out iid: TIID): HResult; stdcall;
    function GetConnectionPointContainer(out cpc: IConnectionPointContainer):
      HResult; stdcall;
    function Advise(const unkSink: IUnknown; out dwCookie: Longint): HResult;
      stdcall;
    function Unadvise(dwCookie: Longint): HResult; stdcall;
    function EnumConnections(out Enum: IEnumConnections): HResult; stdcall;
  end;


Ключевые методы этого интерфейса - Advise и Unadvise.

function Advise(const unkSink: IUnknown; out dwCookie: Longint): HResult; stdcall;

Этот метод регистрирует на сервере клиентский интерфейс обработчика событий, который передается в параметре unkSink. Метод возвращает dwCookie - идентификатор подключения, который должен использоваться при отключении обработчика событий. Начиная с этого момента, сервер, при возникновении события, вызывает методы переданного ему интерфейса-обработчика.

function Unadvise(dwCookie: Longint): HResult; stdcall;

Метод Unadvise отключает обработчик от сервера. Теперь, когда мы имеем базовое понимание, как COM реализует обработчики событий, можно продолжить работу над нашим сервером.

Продолжаем создание Automation сервера[править | править код]

Если флаг Generate Event Support code включен, то Delphi автоматически добавляет в библиотеку типов сервера интерфейс IXXXEvents, где XXX - имя Automation объекта

Clip0236.gif


В этот интерфейс Вы должны добавить методы, которые должен реализовать обработчик событий Вашего сервера

Создадим интерфейс обработчика событий с методом TestEvent и метод FireEvent интерфейса IAutoTest.

В сгенерированном файле с реализацией сервера добавим код для вызова обработчика события в метод FilreEvent

procedure TAutoTest.FireEvent;
begin
  if FEvents <> NIL then
    FEvents.TestEvent;
end;


Здесь FEvents - автоматически добавленный Delphi в код сервера интерфейс IAutoTestEvents.

Компилируем и регистрируем сервер аналогично любому другому COM-серверу. Теперь его можно использовать из любого Automation клиента, например из скрипта на Web-странице

<HTML>
 <HEAD>
  <TITLE>Test Page</TITLE>
 </HEAD>
 <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
  <CENTER>
    <OBJECT CLASSID="clsid:344E2D50-7B91-11D4-84DD-97E4E55E3E05" ID=Ctrl1>
    </OBJECT>
    <INPUT TYPE = TEXT NAME = Textbox SIZE=20>
  </CENTER>
  <SCRIPT LANGUAGE = VBScript>
   Sub Page_Initialize
    Ctrl1.FireEvent
   End Sub
   Sub Ctrl1_TestEvent
    MsgBox("Event Fired")
    Textbox.Value = "Hi !"
   End Sub
  </SCRIPT>
 </BODY>
</HTML>

Здесь в качестве Clsid элемента OBJECT необходимо указать содержание константы CLASS_AutoTest из файла Project1_TLB, сгенерированного Delphi. Загрузив эту страницу в Internet Explorer Вы получите сообщение при загрузке страницы.

Создание обработчика событий COM[править | править код]

Для лучшего понимания механизма обработки событий COM создадим программу, обрабатывающую события от нашего сервера. Для этого создадим проект с одной формой и добавим в него объект, реализующий интерфейс IAutoTestEvents. Этот объект реализуется в виде Automation Object

Clip0237.gif


После этого, в редакторе библиотеки типов необходимо произвести следующие действия:

  1. Для созданного объекта вводим все методы, имеющиеся в интерфейсе IAutoTestEvents
  2. В поле GUID заменяем автоматически сгенерированный идентификатор на содержимое константы DIID_IAutoTestEvents из библиотеки типов объекта IAutoEvents. Если этого не сделать, наш обработчик не удастся зарегистрировать в объекте IAutoEvents.

Нажимаем кнопку «Обновить» и в сгенерированном модуле пишем код обработчика события

procedure TEventSink.TestEvent;
begin
  MessageBox(0, 'Event Fired', NIL, 0);
end;


Обработчик готов, теперь надо добавить в проект код для его использования

Clip0238.gif


Добавляем к классу формы поля для хранения необходимых данных - ссылки на экземпляр обработчика событий, экземпляр объекта, точку подключения и идентификатор подключения.

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    EventSink: IEventSink;
    AutoTest: IAutoTest;
    ConnectionPoint: IConnectionPoint;
    Cookie: Integer;
  end;


При создании формы создаем COM-сервер AutoTest и COM-объект обработчика событий

procedure TForm1.FormCreate(Sender: TObject);
var
  Container: IConnectionPointContainer;
begin
  AutoTest := CreateOleObject('Project1.AutoTest') as IAutoTest;
  EventSink := TEventSink.Create as IEventSink;
  // Запрашиваем у COM-сервера интерфейс IConnectionPointContainer
  Container := AutoTest as IConnectionPointContainer;
  // Получаем ссылку на точку подключения
  OleCheck(Container.FindConnectionPoint(IEventSink, ConnectionPoint));
  // И регистрируем в ней свой обработчик
  OleCheck(ConnectionPoint.Advise(EventSink, Cookie));
end;

По окончании работы - отключаем обработчик

procedure TForm1.FormDestroy(Sender: TObject);
begin
  ConnectionPoint.UnAdvise(Cookie);
end;


Все. Теперь можно вызвать метод объекта и убедиться, что обработчик реагирует на события в нем:

procedure TForm1.Button1Click(Sender: TObject);
begin
  AutoTest.FireEvent;
end;


Clip0239.gif


Хорошая новость - проделывать все эти сложные манипуляции не обязательно. Мы сделали это в основном для демонстрации механизмов работы COM. Можно пойти другим, более простым путем. Для этого Вы можете просто импортировать библиотеку типов сервера, поддерживающего события и в мастере импорта библиотеки типов нажать кнопку Install.

Clip0240.gif


После этого на закладку ActiveX палитры компонентов будет помещен компонент для работы с этим сервером, который можно просто положить на форму

Clip0241.gif


При этом сгенерированный компонент Delphi будет иметь обработчики событий для всех событий, объявленных в COM-объекте. Вам останется лишь написать для них свой код. Всю работу по созданию объекта-обработчика, подключению к серверу и трансляции его событий в события компонента VCL Delphi возьмет на себя.


Author: Тенцер А. Л. (ICQ UIN 15925834) tolik@katren.nsk.ru
ID: 04317