Главная »Статьи »AutoCAD и Delphi » |
Поддержка реакции на события в AutoCAD не вызывает трудностей при использовании раннего связывания и библиотеки типов. Компонент-обертка TAcadDocument содержит соответствующую реализацию, делающую работу с событиями документа AutoCAD такой же тривиальной, как и для родных Delphi-компонентов. Однако этот вариант во-первых не дает доступа к событиям приложения, а во-вторых, как уже указывалось в статье Подключаемся… привязан к конкретной библиотеке типов. Поэтому здесь рассматривается вариант подключения к событиям AutoCAD при позднем связывании и без использования библиотеки типов (точнее, с использованием только определений констант из нее). ВведениеПринцип организации возможности отслеживания событий сервера клиентом описан во многих достойных произведениях и потому здесь представлен лишь схематично. Сервер, интерфейсы событий которого доступны для клиентов, содержит реализацию интерфейсов IConnectionPointContainer (контейнер точек подключения) и IConnectionPoint (точка подключения), соединенный с источником событий. Клиент реализует класс-приемник событий (sink), который реализует интерфейсы IDispatch и IUnknown. Процесс установления связи приемника и источника заключается в том, что приемник:
При успешном вызове 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); Параметры:
Метод InterfaceConnect запрашивает IConnectionPointContainer. Если этот интерфейс найден, вызывается его метод FindConnectionPoint, которому передается идентификатор событийного интерфейса. Метод возвращает интерфейс IConnectionPoint. Вызов Advise завершает подключение к источнику событий. Так как мы решили не привязываться к конкретной библиотеке типов, этот идентификатор требуется получить для конкретного объекта. Для этого воспользуемся методом EnumConnectionPoints и получим перечислитель точек подключения, который имеет полезный метод Next: function Next(celt: Longint; out elt; pceltFetched: PLongint): HResult; stdcall; Параметры:
И 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. Внимание! Запрещается воспроизведение
данной статьи или ее части без согласования с автором. Если вы желаете разместить
эту статью на своем сайте или издать в печатном виде, свяжитесь с автором. |