Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Реализация CallBack вызовов MapInfo

01.01.2007
Автор: Дмитрий Кузан

Доброе время суток !

Краткое примечание

Немного об отзывах - хочу сообщить и повторить снова в данных циклах статей не будет информации об ActiveX компоненте MapX (о работе с ней, отзывы о ней и т.п.) по причине отсутствия у меня оной (может кто поделится J).

Использование уведомляющих вызовов (Callbacks) для получения информации из Maplnfo - краткий учебный курс.

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

Такой тип уведомления известен как обратный вызов или уведомление (callback).

Уведомления используються в следующих случаях:

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

Пользователь выбирает команду меню. Например, предположим, что Ваше приложение настраивает "быстрое" меню MapInfo (меню, возникающее при нажатии правой кнопки мышки). Когда пользователь выбирает команду из этого меню, MapInfo может вызвать Вашу клиентскую программу, чтобы сообщить ей о выборе.

Изменяется окно Карты. Если пользователь изменяет содержание окна Карты (например, добавляя или передвигая слои), MapInfo может послать Вашей клиентской программе идентификатор этого окна.

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

Требования к функциям уведомления

Программа должна быть способна функционировать, как DDE-сервер или как сервер Автоматизации OLE.

Предопределенные процедуры SetStatusText, WindowContentsChanged.

Если Вы хотите имитировать строку состояния MapInfo, создайте метод, называемый SetStatusText. Определите этот метод так, чтобы у него был один аргумент: строка.

метод WindowContentsChanged, MapInfo посылает четырехбайтовое целое число (ID окна MapInfo), чтобы указать, какое из окон Карты изменилось. Напишите код, делающий необходимую обработку.

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

Переинсталяция компонента TKDMapInfoServer

Удалите старый компонент

Зарегистрируете в системе библиотеку MICallBack.dll , для этого откройте MICallBack.dpr и в меню Run Delphi выбирите Register ActiveX Server.После этого скопируйте саму DLL в каталог Windows

Установите пакет KDPack.dpk в Delphi

Вот в принципе и все.

Cервер автоматизации OLE для обработки CallBack

Данный сервер я разместил в ActiveX DLL.(данная DLL называется MICallBack.dll) в виде Automation Object.-а.

Что-бы вам просмотреть методы и свойства данногоAutomation Object.-а. откройте MICallBack.dpr и в меню Run Delphi выбирите TypeLibrary

Откроется окно - Где я реализовал CallBack методы MapInfo и создал сервер автоматизации MICallBack. Обратите внимание, что у данного сервера помимо присутствия интерфейса IMapInfoCallBack присутствует и еще интерфейс ImapInfoCallBackEvents (он нам нужен будет для перенаправления событий в компонент и далее в обработчик).

Листинг интерфейсного модуля

unit Call;
 
{$WARN SYMBOL_PLATFORM OFF}
 
interface
 
uses
 
ComObj, ActiveX, Dialogs, AxCtrls, Classes, MICallBack_TLB, StdVcl;
 
type
 
TMapInfoCallBack = class(TAutoObject, IConnectionPointContainer, IMapInfoCallBack)
 
private
   
{ Private declarations }
   
FConnectionPoints: TConnectionPoints;
   
FConnectionPoint: TConnectionPoint;
   
FEvents: IMapInfoCallBackEvents;
   
{ note: FEvents maintains a *single* event sink. For access to more
    than one
event sink, use FConnectionPoint.SinkList, and iterate
    through the list of sinks
. }
 
public
    procedure
Initialize; override;
 
protected
   
{ Protected declarations }
    property
ConnectionPoints: TConnectionPoints read FConnectionPoints
   
implements IConnectionPointContainer;
    procedure
EventSinkChanged(const EventSink: IUnknown); override;
    procedure
SetStatusText(const Status: WideString); safecall;
    procedure
WindowContentsChanged(ID: Integer); safecall;
    procedure
MyEvent(const Info: WideString); safecall;
 
end;
 
var
 
FDLLCall: THandle;
 
implementation
uses
ComServ;
 
procedure
TMapInfoCallBack.EventSinkChanged(const EventSink: IUnknown);
begin
 
FEvents := EventSink as IMapInfoCallBackEvents;
end;
 
procedure
TMapInfoCallBack.Initialize;
begin
  inherited
Initialize;
 
FConnectionPoints := TConnectionPoints.Create(Self);
 
if AutoFactory.EventTypeInfo <> nil then
   
FConnectionPoint := FConnectionPoints.CreateConnectionPoint(
   
AutoFactory.EventIID, ckSingle, EventConnect)
 
else
   
FConnectionPoint := nil;
end;
 
 
procedure
TMapInfoCallBack.SetStatusText(const Status: WideString);
begin
 
if FEvents <> nil then
   
FEvents.OnChangeStatusText(Status);
end;
 
procedure
TMapInfoCallBack.WindowContentsChanged(ID: Integer);
begin
 
if FEvents <> nil then
   
FEvents.OnChangeWindowContentsChanged(ID);
end;
 
procedure
TMapInfoCallBack.MyEvent(const Info: WideString);
begin
 
if FEvents <> nil then
   
FEvents.OnChangeMyEvent(Info);
end;
 
initialization
 
TAutoObjectFactory.Create(ComServer, TMapInfoCallBack, Class_MapInfoCallBack,
  ciMultiInstance
, tmApartment);
 
end.

Обратите внимание на присутствие двух предопределенных методов MapInfo SetStatusText и WindowContentsChanged.

Метод MyEvent я пока зарезервировал для реализации своих сообщений (более подробно будет изложено в 3 части цикла)

И так что мы видим.

// если есть обработчик
if FEvents <> nil then
begin
 
// Отправка сообщения далее - в данном случае в компонент
 
FEvents.OnChangeStatusText(Status);

Как заставить MapInfo пересылать CallBack данному OLE серверу и как нам обрабатывать сообщения в компоненте от OLE сервера.

Итак представляю переработанный компонент -

unit KDMapInfoServer;
 
interface
 
uses
 
Stdctrls, Dialogs, ComObj, Controls, Variants, ExtCtrls, Windows, ActiveX,
 
Messages, SysUtils, Classes, MICallBack_TLB; // - сгенерировано из DLL
 
type
 
// запись "типа" Variant
 
TEvalResult = record
   
AsVariant: OLEVariant;
   
AsString: string;
   
AsInteger: Integer;
   
AsFloat: Extended;
   
AsBoolean: Boolean;
 
end;
 
  type
 
// Событие на изменение SetStatusText // генерируется при обратном вызове
 
TSetStatusTextEvent = procedure(Sender : TObject; StatusText: WideString) of object;
 
// WindowContentsChanged
 
TWindowContentsChanged = procedure(Sender : TObject; ID : Integer) of object;
 
// Для собственных событий
 
TMyEvent = procedure(Sender : TObject; Info : WideString) of object;
 
 
TEvent = class(TInterfacedObject,IUnknown,IDispatch)
 
private
   
FAppConnection : Integer;
   
FAppDispatch : IDispatch;
   
FAppDispIntfIID : TGUID;
 
protected
   
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
   
function _AddRef: Integer; stdcall;
   
function _Release: Integer; stdcall;
   
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
   
function GetTypeInfo(index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
   
function GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount,
   
LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
   
function Invoke(dispid: Integer; const IID: TGUID; LocaleID: Integer;
   
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
 
public
   
constructor Create( AnAppDispatch : IDispatch; const AnAppDispIntfIID : TGUID);
    destructor
Destroy ; override;
 
end;
 
 
 
TKDMapInfoServer = class(TComponent)
 
private
   
{ Private declarations }
   
FOwner : TWinControl; // Владелец
   
Responder : Variant;  // Для OLE Disp
 
   
FServer : Variant;
   
FHandle : THandle;    // Зарезервировано
   
FActive : Boolean;    // Запущен/незапущен
   
FPanel : TPanel;      // Панель вывода
 
    srv_OLE
: OLEVariant;
    srv_disp
: IMapInfoCallBackDisp;
    srv_vTable
: IMapInfoCallBack;
 
   
FEvent : TEvent;
 
   
FSetStatusTextEvent : TSetStatusTextEvent; // события компонента
   
FWindowContentsChanged : TWindowContentsChanged;
   
FMyEvent : TMyEvent;
 
   
Connected : Boolean; // Установлено ли соединение
   
MapperID : Cardinal; // ИД окна
 
    procedure
SetActive(const Value: Boolean);
    procedure
SetPanel(const Value: TPanel);
    procedure
CreateMapInfoServer;
    procedure
DestroyMapInfoServer;
 
protected
   
{ Protected declarations }
 
public
   
{ Public declarations }
   
constructor Create(AOwner: TComponent); override;
    destructor
Destroy; override;
   
// Данная процедура выполеняет метод сервера MapInfo - Do
    procedure
ExecuteCommandMapBasic(Command: string; const Args: array of const);
   
function Eval(Command: string; const Args: array of const): TEvalResult; virtual;
    procedure
WindowMapDef;
    procedure
OpenMap(Path : string);
    procedure
RepaintWindowMap;
   
// Дополнил для генерации события SetStatus при изменении строки состояния
   
// в MapInfo
    procedure
DoSetStatus(StatusText: WideString);
   
// Дополнил.для генерации события WindowContentsChanged при изменении окна
   
// в MapInfo
    procedure
DoWindowContentsChanged(ID : Integer);
   
// Дополнил для генерации собственно события в MapInfo
    procedure
DoMyEvent(Info: WideString);
  published
   
{ Published declarations }
   
// Создает соединение с сервером MapInfo
    property
Active: Boolean read FActive write SetActive;
    property
PanelMap : TPanel read FPanel write SetPanel;
 
   
// Событие возникающее при изменении строки состояния MapInfo
    property
StatusTextChange : TSetStatusTextEvent read FSetStatusTextEvent
    write
FSetStatusTextEvent;
    property
WindowContentsChanged : TWindowContentsChanged read FWindowContentsChanged
    write
FWindowContentsChanged;
 
    property
MyEventChange : TMyEvent read FMyEvent write FMyEvent;
 
end;
 
var
 
// О это вообще хитрость - используеться для определения созданного компонента
 
// TKDMapInfoServer (см. SetStatusText и Create
 
KDMapInfoServ : TKDMapInfoServer;
 
procedure
register;
 
implementation
 
// Вот тут то и хитрость если сервер создан то тогда и вызываем SetStatus
//// IF KDMapInfoServ <> nil Then
/// KDMapInfoServ.SetStatus(StatusText);
 
procedure
register;
begin
 
RegisterComponents('Kuzan', [TKDMapInfoServer]);
end;
 
{ TKDMapInfoServer }
 
constructor TKDMapInfoServer.Create(AOwner: TComponent);
begin
  inherited
Create(AOwner);
 
FOwner := AOwner as TWinControl;
 
 
KDMapInfoServ := Self; // **** Вот тут и указываеться созданный компонент
 
// TKDMapInfoServer
 
FHandle := 0;
 
FActive := False;
 
Connected := False;
end;
 
destructor
TKDMapInfoServer.Destroy;
begin
 
DestroyMapInfoServer;
  inherited
Destroy;
end;
 
procedure
TKDMapInfoServer.CreateMapInfoServer;
begin
 
try
   
FServer := CreateOleObject('MapInfo.Application');
 
except
   
FServer := Unassigned;
 
end;
 
// Скрываем панели управления MapInfo
 
ExecuteCommandMapBasic('Alter ButtonPad ID 4 ToolbarPosition (0, 0) Show Fixed', []);
 
ExecuteCommandMapBasic('Alter ButtonPad ID 3 ToolbarPosition (0, 2) Show Fixed', []);
 
ExecuteCommandMapBasic('Alter ButtonPad ID 1 ToolbarPosition (1, 0) Show Fixed', []);
 
ExecuteCommandMapBasic('Alter ButtonPad ID 2 ToolbarPosition (1, 1) Show Fixed', []);
 
ExecuteCommandMapBasic('Close All', []);
 
ExecuteCommandMapBasic('Set ProgressBars Off', []);
 
ExecuteCommandMapBasic('Set Application Window %D', [FOwner.Handle]);
 
ExecuteCommandMapBasic('Set Window Info Parent %D', [FOwner.Handle]);
 
 
FServer.Application.Visible := True;
 
if IsIconic(FOwner.Handle)then ShowWindow(FOwner.Handle, SW_Restore);
 
BringWindowToTop(FOwner.Handle);
 
  srv_ole
:= CreateOleObject('MICallBack.MapInfoCallBack') as IDispatch;
  srv_vtable
:= CoMapInfoCallBack.Create;
  srv_disp
:= CreateComObject(CLASS_MapInfoCallBack) as IMapInfoCallBackDisp;
 
FEvent := TEvent.Create(srv_disp,IMapInfoCallBackEvents);
 
 
// Указываем MapInfo что нужно передовать обратные вызовы нашему OLE
 
// а тм далее по цепочке (см.начало)
 
FServer.SetCallBack(srv_disp);
end;
 
procedure
TKDMapInfoServer.DestroyMapInfoServer;
begin
 
ExecuteCommandMapBasic('End MapInfo', []);
 
FServer := Unassigned;
end;
 
procedure
TKDMapInfoServer.ExecuteCommandMapBasic(Command: string;
const Args: array of const);
begin
 
if Connected then
   
try
     
FServer.do(Format(Command, Args));
   
except
      on E
: Exception do MessageBox(FOwner.Handle,
     
PChar(Format('Ошибка выполнения () - %S', [E.message])),
     
'Warning', MB_ICONINFORMATION or MB_OK);
   
end;
end;
 
function TKDMapInfoServer.Eval(Command: string;
const Args: array of const): TEvalResult;
 
 
function IsInt(Str : string): Boolean;
 
var
   
Pos : Integer;
 
begin
   
Result := True;
   
for Pos := 1 to Length(Trim(Str)) do
   
begin
     
if (Str[Pos] <> '0') and (Str[Pos] <> '1') and
     
(Str[Pos] <> '2') and (Str[Pos] <> '3') and
     
(Str[Pos] <> '4') and (Str[Pos] <> '5') and
     
(Str[Pos] <> '6') and (Str[Pos] <> '7') and
     
(Str[Pos] <> '8') and (Str[Pos] <> '9') and
     
(Str[Pos] <> '.') then
     
begin
       
Result := False;
       
Exit;
     
end;
   
end;
 
end;
 
var
  ds_save
: Char;
begin
 
if Connected then
 
begin
   
Result.AsVariant := FServer.Eval(Format(Command, Args));
   
Result.AsString := Result.AsVariant;
   
Result.AsBoolean := (Result.AsString = 'T') or (Result.AsString = 't');
 
   
if IsInt(Result.AsVariant) then
   
begin
     
try
        ds_save
:= DecimalSeparator;
       
try
         
DecimalSeparator := '.';
         
Result.AsFloat := StrToFloat(Result.AsString);
       
finally
         
DecimalSeparator := ds_save;
       
end;
     
except
       
Result.AsFloat := 0.00;
     
end;
 
     
try
       
Result.AsInteger := Trunc(Result.AsFloat);
     
except
       
Result.AsInteger := 0;
     
end;
   
end
   
else
   
begin
     
Result.AsInteger := 0;
     
Result.AsFloat := 0.00;
   
end;
 
end;
end;
 
procedure
TKDMapInfoServer.SetActive(const Value: Boolean);
begin
 
FActive := Value;
 
 
if FActive then
 
begin
   
CreateMapInfoServer;
   
WindowMapDef;
   
Connected := True;
 
end
 
else
 
begin
   
if Connected then
   
begin
     
DestroyMapInfoServer;
     
Connected := False;
   
end;
 
end;
end;
 
procedure
TKDMapInfoServer.SetPanel(const Value: TPanel);
begin
 
FPanel := Value;
end;
 
procedure
TKDMapInfoServer.WindowMapDef;
begin
 
ExecuteCommandMapBasic('Set Next Document Parent %D Style 1', [FPanel.Handle]);
 
RepaintWindowMap;
end;
 
procedure
TKDMapInfoServer.OpenMap(Path: string);
begin
 
ExecuteCommandMapBasic('Run Application "%S"', [Path]);
 
MapperID := Eval('WindowInfo(FrontWindow(),%D)',[12]).AsInteger;
 
RepaintWindowMap;
end;
 
procedure
TKDMapInfoServer.DoSetStatus(StatusText: WideString);
begin
 
if Assigned(FSetStatusTextEvent) then
   
FSetStatusTextEvent(Self,StatusText);
end;
 
procedure
TKDMapInfoServer.DoWindowContentsChanged(ID: Integer);
begin
 
if Assigned(FWindowContentsChanged) then
   
FWindowContentsChanged(Self,ID);
end;
 
procedure
TKDMapInfoServer.DoMyEvent(Info: WideString);
begin
 
if Assigned(FWindowContentsChanged) then
   
FMyEvent(Self,Info);
end;
 
procedure
TKDMapInfoServer.RepaintWindowMap;
begin
 
with PanelMap do
   
MoveWindow(MapperID, 0, 0, FPanel.ClientWidth, FPanel.ClientHeight, True);
end;
 
{ TEvent }
 
function TEvent._AddRef: Integer;
begin
 
Result := 2; // Заглушка
end;
 
function TEvent._Release: Integer;
begin
 
Result := 1; // Заглушка
end;
 
constructor TEvent.Create(AnAppDispatch: IDispatch;
const AnAppDispIntfIID: TGUID);
begin
  inherited
Create;
 
FAppDispatch := AnAppDispatch;
 
FAppDispIntfIID := AnAppDispIntfIID;
 
// Передадим серверу
 
InterfaceConnect(FAppDispatch,FAppDispIntfIID,self,FAppConnection);
end;
 
destructor
TEvent.Destroy;
begin
 
InterfaceDisConnect(FAppDispatch,FAppDispIntfIID,FAppConnection);
  inherited
;
end;
 
function TEvent.GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount,
LocaleID: Integer; DispIDs: Pointer): HResult;
begin
 
// Заглушка не реализовано
 
Result := E_NOTIMPL;
end;
 
function TEvent.GetTypeInfo(index, LocaleID: Integer;
out TypeInfo): HResult;
begin
 
// Заглушка не реализовано
 
Result := E_NOTIMPL;
end;
 
function TEvent.GetTypeInfoCount(out Count: Integer): HResult;
begin
 
// Заглушка не реализовано
 
Count := 0;
 
Result := S_OK;
end;
 
function TEvent.Invoke(dispid: Integer; const IID: TGUID;
LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo,
ArgErr: Pointer): HResult;
var
 
Info,Status : string;
 
IDWin : Integer;
begin
 
case dispid of
   
1 :
   
begin
     
Status := TDispParams(Params).rgvarg^[0].bstrval;
     
if KDMapInfoServ <> nil then
       
KDMapInfoServ.DoSetStatus(Status);
   
end;
   
2 :
   
begin
     
IDWin := TDispParams(Params).rgvarg^[0].bval;
     
if KDMapInfoServ <> nil then
       
KDMapInfoServ.DoWindowContentsChanged(IDWin);
   
end;
   
3 :
   
begin
     
Info := TDispParams(Params).rgvarg^[0].bstrval;
     
if KDMapInfoServ <> nil then
       
KDMapInfoServ.DoMyEvent(Info);
   
end;
 
end;
 
Result := S_OK;
end;
 
function TEvent.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
 
Result := E_NOINTERFACE;
 
if GetInterface(IID,Obj) then
   
Result := S_OK;
 
if IsEqualGUID(IID,FAppDispIntfIID) and GetInterface(IDispatch,Obj) then
   
Result := S_OK;
end;
 
end.

И так что добавилось - Метод CreateMapInfoServer;

  // Создаем наш сервер OLE
  srv_ole
:= CreateOleObject('MICallBack.MapInfoCallBack') as IDispatch;
  srv_vtable
:= CoMapInfoCallBack.Create;
 
// Получаем Idispatch созданного сервера
  srv_disp
:= CreateComObject(CLASS_MapInfoCallBack) as IMapInfoCallBackDisp;
 
FEvent := TEvent.Create(srv_disp,IMapInfoCallBackEvents);
 
// Указываем MapInfo что нужно передовать обратные вызовы нашему OLE серверу
 
// а там далее по цепочке (см.начало)
 
FServer.SetCallBack(srv_disp);
end;

Здесь мы столкнулись с еще одним методом MapInfo помимо рассмотренных ранее методов Do и Eval - Метод SetCallBack(IDispatch). Описание - Регистрирует объект механизма-управления объектами OLE (OLE Automation) как получатель уведомлений, генерируемых программой MapInfo. Только одна функция уведомления может быть зарегистрирована в каждый данный момент. Параметр интерфейс Idispatch объекта OLE (COM)

Реализация FServer.SetCallBack(srv_disp); - данным кодом мы заставили MapInfo уведомлять наш OLE сервер.

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

Я сделал так: ввел класс отвечающий за принятие событий от COM(OLE) объекта

TEvent = class(TInterfacedObject,IUnknown,IDispatch)
private
 
FAppConnection : Integer;
 
FAppDispatch : IDispatch;
 
FAppDispIntfIID : TGUID;
protected
 
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
 
function _AddRef: Integer; stdcall;
 
function _Release: Integer; stdcall;
 
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
 
function GetTypeInfo(index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
 
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
 
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
 
function Invoke(dispid: Integer; const IID: TGUID; LocaleID: Integer;
 
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
public
 
constructor Create( AnAppDispatch : IDispatch;
 
const AnAppDispIntfIID : TGUID);
  destructor
Destroy ; override;
end;

создание этого класса в компоненте реализовано так

FEvent := TEvent.Create(srv_disp,IMapInfoCallBackEvents);

В методе Invoke и происходит прием и получение сообщений и пересылка их в обработчик моего компонента.

Еще раз на последующие вопросы касательно COM (OLE) серверов отвечу: данная тема выходит за рамки данной статьи - советую почитать книгу Александроского А.Д - Delphi 5 разработка корпоративных приложений.

Напоследок — модуль MICallBack_TLB.pas импортирован из DLL командой меню DELPHI Import Type Libray.

Примечание:

при импорте данный сервер инсталировать не нужно, нет смысла он нам нужен только для приема сообщений из MapInfo.

https://delphiworld.narod.ru/

DelphiWorld 6.0