Главная »Статьи »AutoCAD и Delphi »Подключаемся к событиям
Подключаемся к событиям

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

Введение

Принцип организации возможности отслеживания событий сервера клиентом описан во многих достойных произведениях и потому здесь представлен лишь схематично. Сервер, интерфейсы событий которого доступны для клиентов, содержит реализацию интерфейсов IConnectionPointContainer (контейнер точек подключения) и IConnectionPoint (точка подключения), соединенный с источником событий. Клиент реализует класс-приемник событий (sink), который реализует интерфейсы IDispatch и IUnknown.

Процесс установления связи приемника и источника заключается в том, что приемник:

  • запрашивает у объекта сервера интерфейс IConnectionPointContainer.
  • с помощью метода FindConnectionPoint IConnectionPointContainer находит конкретную точку подключения IConnectionPoint.
  • вызывает метод IConnectionPoint.Advise для регистрации указателя интерфейса приемника. В метод Advise передается интерфейсный указатель приемника для точки соединения. Метод возвращает идентификатор соединения, который затем используется при отключении обработчика.

При успешном вызове Advise сервер, в случае возникновении события, вызывает методы переданного ему интерфейса приемника. Для отключения отслеживания событий приемник разрывает подключение с помощью метода IConnectionPoint.Unadvise, передавая ему идентификатор соединения.

Практика

Применительно к AutoCAD схема взаимодействия между источником событий (AutoCAD) и приемником (Приложение) выглядит так:

В AutoCAD два источника событий: Application и Document. Начнем с Document. Для начала создадим интерфейсный указатель приемника, который передается в метод IConnectionPoint.Advise. Открываем AutoCAD_TLB.pas, находим там TAcadDocument и выше него определения типов обработчиков событий. Замечаем, что многие их них имеют одинаковый набор параметров, но с разными именами. Создаем новый модуль (для простоты), например AcadEvents.pas и на основе увиденного в AutoCAD_TLB.pas определяем свои типы обработчиков событий:

type
  TAcadWindowMovedEvent = procedure(Sender: TObject; AHandle: Integer; AMoved: WordBool) of object;
  TAcadBeginCloseEvent = procedure(Sender: TObject; var ACancel: WordBool) of object;
  TAcadCommandEvent = procedure(Sender: TObject; const ACommand: WideString) of object;
  TAcadPointEvent = procedure(Sender: TObject; APoint: OleVariant) of object;
  TAcadObjectEvent = procedure(Sender: TObject; AObject: OleVariant) of object;
  TAcadEditEvent = procedure(Sender: TObject; AObject: OleVariant; AParam: OleVariant) of object;
  TAcadEditCommandEvent = procedure(Sender: TObject; AObject: OleVariant; const ACommand: WideString) of object;
  TAcadErasedEvent = procedure(Sender: TObject; AObjectID: Integer) of object;
  TAcadWindowChangedEvent = procedure(Sender: TObject; AState: TOleEnum) of object;
  TAcadUnknownEvent = procedure(Sender: TObject; ADispID: TDispID) of object;

Далее в AutoCAD_TLB.pas создаем определение класса-приемника для точки соединения:

  TAcadDocumentEvents = class(TObject, IUnknown, IDispatch)
  private
    FOwner: TObject;
    FOnBeginSave: TAcadCommandEvent;
    FOnEndSave: TAcadCommandEvent;
    FOnBeginCommand: TAcadCommandEvent;
    FOnEndCommand: TAcadCommandEvent;
. . .
    FOnUnknownEvent: TAcadUnknownEvent;
  protected
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    function GetTypeInfoCount(out Count: Integer): HResult; virtual; stdcall;
    function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; virtual; stdcall;
    function GetIDsOfNames(const IID: TGUID; Names: Pointer;
      NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; virtual; stdcall;
    function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
      Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; virtual; stdcall;
    procedure InvokeEvent(DispID: TDispID; var Params: TVariantArray);
  public
    constructor Create(Owner: TObject);
    property OnBeginSave: TAcadCommandEvent read FOnBeginSave write FOnBeginSave;
    property OnEndSave: TAcadCommandEvent read FOnEndSave write FOnEndSave ;
    property OnBeginCommand: TAcadCommandEvent read FOnBeginCommand write FOnBeginCommand;
    property OnEndCommand: TAcadCommandEvent read FOnEndCommand write FOnEndCommand;
. . .
    property OnUnknownEvent: TAcadUnknownEvent read FOnUnknownEvent write FOnUnknownEvent;
  end;

Определяем переменную-идентификатор событийного интерфейса:

var
  IID_AcadDocumentEvents: TIID;

Реализация метода QueryInterface:

// Метод возвращает экземпляр только в случае если запрашиваемым интерфейсом
// является IUnknown, IDispatch или IID_AcadDocumentEvents
function TAcadDocumentEvents.QueryInterface(const IID: TGUID;
  out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result:= S_OK
  else if IsEqualIID(IID, IID_AcadDocumentEvents) then
    Result:= QueryInterface(IDispatch, Obj)
  else
    Result:= E_NOINTERFACE;
end;

Реализация метода Invoke. Открываем OleServer.pas, находим реализацию TServerEventDispatch.Invoke и копируем себе весь код, заменив лишь строки:

  // Invoke Server proxy class
  if FServer <> nil then
    FServer.InvokeEvent(DispID, vVarArray);

на

InvokeEvent(DispID, vVarArray);

Реализация метода InvokeEvent. Открываем AutoCAD_TLB.pas, находим реализацию TAcadDocument.InvokeEvent и копируем себе, добавив обработку неизвестного события:

  case DispID of
   -1: Exit;  // DISPID_UNKNOWN
    1: if Assigned(FOnBeginSave) then
         FOnBeginSave(Self, Params[0] {const WideString});
. . .
    34: if Assigned(FOnBeginDocClose) then
         FOnBeginDocClose(Self, WordBool((TVarData(Params[0]).VPointer)^) {var WordBool});
. . .
  else
    if Assigned(FOnUnknownEvent) then
      FOnUnknownEvent(Self, DispID);
  end; {case DispID}

Кроме того все приведения к интерфейсным типам (IAcadPopupMenu, IAcadSelectionSet) необходимо заменить на приведения к IDispatch. Вообще в методе InvokeEvent полезно изучить методику передачи параметров.

Константы DispID в конструкции case определяются в диспинтерфейсе _DAcadDocumentEvents (см. AutoCAD_TLB.pas).

Реализация остальных методов тривиальна, см. ее в прилагаемых файлах.

Для подключения (отключения) к источнику модуль ComObj содержит две удобные процедуры:

procedure InterfaceConnect(const Source: IUnknown; const IID: TIID;
  const Sink: IUnknown; var Connection: Longint);
procedure InterfaceDisconnect(const Source: IUnknown; const IID: TIID;
  var Connection: Longint);

Параметры:

  • Source — интерфейс AutoCAD, получаемый при подключении, например методом GetActiveOleObject.
  • IID — идентификатор событийного интерфейса.
  • Sink — описанный выше класс-приемник.
  • Connection — идентификатор соединения.

Метод InterfaceConnect запрашивает IConnectionPointContainer. Если этот интерфейс найден, вызывается его метод FindConnectionPoint, которому передается идентификатор событийного интерфейса. Метод возвращает интерфейс IConnectionPoint. Вызов Advise завершает подключение к источнику событий.

Так как мы решили не привязываться к конкретной библиотеке типов, этот идентификатор требуется получить для конкретного объекта. Для этого воспользуемся методом EnumConnectionPoints и получим перечислитель точек подключения, который имеет полезный метод Next:

function Next(celt: Longint; out elt; pceltFetched: PLongint): HResult; stdcall;

Параметры:

  • celt — количество точек подключения, которые мы хотим получить, в нашем случае 1.
  • elt — возвращаемая точка подключения.
  • pceltFetched — количество реально возвращенных точек – должно быть равно 1.
И Application и Document имеют по одной событийной точке подключения.

На основании изложенного, процедура подключения такова:

. . .
var
  Acad: OleVariant;
  AcadDocEvents: TAcadDocumentEvents;
. . .
var
  CPC: IConnectionPointContainer;
  CP: IConnectionPoint;
  Cookie: Longint;
  IID_AcadDocumentEvents: TIID;

function AcadConnect: Boolean;
var
  Enum: IEnumConnectionPoints;
  N: Longint;
begin
  Result:= False;
  // Присоединяемся к AutoCAD. Предполагаем, что AutoCAD уже запущен!
  Acad:= GetActiveOleObject('AutoCAD.Application');
  // Присоединяемся к событиям
  if not VarIsClear(Acad) then
  begin
    // Запрашиваем IConnectionPointContainer
    if Succeeded(IUnknown(Acad.ActiveDocument).QueryInterface(IConnectionPointContainer, CPC)) then
    begin
      // Запрашиваем EnumConnectionPoints
      if Succeeded(CPC.EnumConnectionPoints(Enum)) then
      begin
        Enum.Next(1, CP, @N); // Запрашиваем IConnectionPoint
        if N = 1 then         // Есть точка подключения!
        begin
          // Запрашиваем идентификатор интерфейса найденной точки подключения
          if Succeeded(CP.GetConnectionInterface(IID_AcadDocumentEvents)) then
            // Подключаемся к источнику событий
            Result:= Succeeded(CP.Advise(AcadDocEvents, Cookie));
        end;
      end;
    end;
  end;
end;

При отключении от AutoCAD не забываем отключить приемник от сервера:

procedure AcadRelease;
begin
  if not VarIsClear(Acad) then
  begin
    // Отключаемся от источника событий
    if Cookie <> 0 then
      CP.Unadvise(Cookie);
    Acad:= UnAssigned;
  end;
end;

Теперь можно создавать обработчики событий. Пример в приложении выдает в объект TMemo сообщения при наступлении всех событий Document.

Для отслеживания событий Application необходимо аналогичным образом создать класс-приемник для точки соединения и произвести подключение. Константы DispID для метода InvokeEvent см. в AutoCAD_TLB.pas, диспинтерфейс _DAcadApplicationEvents.

В прилагаемом примере построено простейшее приложение-монитор событий и Application и Document. Запустив его и присоединившись к событиям можно наблюдать за многими скрытыми от глаз пользователя действиями AutoCAD.

К статье прилагаются примеры на Delphi 7.


Внимание! Запрещается воспроизведение данной статьи или ее части без согласования с автором. Если вы желаете разместить эту статью на своем сайте или издать в печатном виде, свяжитесь с автором.
Автор статьи: Вершинин И.В.

 
Используются технологии uCoz