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