![]() |
| Главная »Статьи »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. Внимание! Запрещается воспроизведение
данной статьи или ее части без согласования с автором. Если вы желаете разместить
эту статью на своем сайте или издать в печатном виде, свяжитесь с автором. |