12 апреля 2012 г.

Пишем подключаемый модуль для ЛОЦМАН Клиент

Подключаемый модуль (плагин) для ЛОЦМАН Клиент предназначен для добавления новых функций в клиентское приложение.

Снаружи подключаемый модуль выглядит как несколько пунктов в меню клиентского модуля. Начиная с версии 10, эти пункты меню могут быть добавлены в контекстное меню и на панели инструментов.

Внутри подключаемый модуль — это DLL, реализующая специальные функции. Есть два основных вида интерфейсов для взаимодействия подключаемого модуля с клиентом Лоцман: PAS-интерфейс и COM-интерфейс. Описание PAS-интерфейса отсутствует в официальной документации, не рекомендовано к использованию и создает множество проблем при использовании новых версий Delphi, поэтому мы будем писать подключаемый модуль с COM-интерфейсом. Описание его вы найдете в папке SDK дистрибутива в файле LoodsmanClientApi.chm.

DLL подключаемого модуля должна экспортировать функции InitUserDLLCom, PgiCheckMenuItemCom и функции, которые будут вызываться при выборе пользователем пунктов меню. Вот прототипы этих функций:

// Используется ЛОЦМАН Клиент для построения меню,
// которое поддерживает модуль
function InitUserDLLCom(Value: Pointer): Integer; stdcall;

// Используется ЛОЦМАН Клиент для определения
// активности команд меню модуля
function PgiCheckMenuItemCom(stFunction: PAnsiChar;
    PluginCall: IPluginCall): Integer; stdcall;

// Функция, соответствующая команде меню
procedure MenuClick(PluginCall: IPluginCall); stdcall;

Модуль может быть подключен к одной или нескольким базам данных, либо являться общим (с версии 10), то есть использоваться при работе с любой из баз данных. Информация о подключенных модулях хранится либо в реестре в ветке HKCU\Software\ASCON\Loodsman\Client\PluginManager (с версии 10), либо для старых версий она хранится в файле Loodsman.ini.

При открытии окна базы данных, ЛОЦМАН Клиент получает информацию о подключенных для нее модулях из реестра или INI-файла. После чего подключаемый модуль загружается и из него вызывается функция получения меню — InitUserDLLCom. Функция PgiCheckMenuItemCom вызывается, когда ЛОЦМАН Клиент понадобится проверить доступность меню. Нужно отметить, что для пунктов меню, вынесенных на панели инструментов, PgiCheckMenuItemCom может вызываться очень часто, и если в ней будет происходить обращение к серверу приложений, то это может существенно замедлить работу клиента.

В старых версиях клиента DLL загружалась и выгружалась постоянно для вызова любой из функций. Так как DLL подключаемых модулей могут быть достаточно большими и иметь множество зависимостей, то постоянная их загрузка и выгрузка может отнимать заметное количество времени. В связи с тем, что в новых версиях ЛОЦМАН Клиент появилась возможность помещать кнопки вызова подключаемого модуля на панели инструментов, то вызываться функции подключаемого модуля стали чаще, и, чтобы ускорить работу, начиная с версии 10, подключаемый модуль по умолчанию выгружается только при закрытии клиента. Для целей отладки в версии 10 есть возможность вернуть режим, когда подключаемые модули постоянно выгружаются после вызова их функций. Для этого необходимо установить параметр системного реестра (типа DWORD) HKCU\Software\ASCON\Loodsman\Client\PluginManager\DebugMode = 1.

После загрузки подключаемого модуля клиент дважды вызывает функцию InitUserDLLCom. В первый раз, передавая в качестве Value значение nil, клиент запрашивает количество пунктов меню, которые хочет добавить подключаемый модуль. Затем клиент выделяет память для нужного количества пунктов меню и вызывает InitUserDLLCom во второй раз, передавая ей в качестве Value указатель на выделенную память.

Каждый добавляемый пункт меню описывается следующей структурой данных:

const
    MAX_TEXT_LENGTH = 255;
type
    TLoodsmanAddMenuCom = record
        stMenu: array [0..MAX_TEXT_LENGTH-1] of AnsiChar;
        stFunction: array [0..MAX_TEXT_LENGTH-1] of AnsiChar;
    end;
    PLoodsmanAddMenuCom = ^TLoodsmanAddMenuCom;


Где stFunction — это имя экспортируемой функции из DLL, которая будет вызвана, когда пользователь выберет данный пункт меню. А stMenu — это названия пунктов меню, разделенные символом '#'. Название первого пункта меню указывает, как меню подключаемого модуля будет расположено относительно собственного меню клиента. Возможные значения перечислены ниже:

Меню клиентаПередДочернееПосле
Файл BEFORE_MI_FILE MI_FILE AFTER_MI_FILE
Вид BEFORE_MI_VIEW MI_VIEW AFTER_MI_VIEW
Правка BEFORE_MI_EDIT MI_EDIT AFTER_MI_EDIT
Инструменты BEFORE_MI_TOOLS MI_TOOLS AFTER_MI_TOOLS
Окно BEFORE_MI_ZOOM MI_ZOOM AFTER_MI_ZOOM
Справка BEFORE_MI_HELP MI_HELP AFTER_MI_HELP

Например, подключаемый модуль добавляет два пункта меню перед меню «Инструменты»:
'BEFORE_MI_TOOLS#Мои плагины#Тестовый#Список проектов' и
'BEFORE_MI_TOOLS#Мои плагины#Тестовый#Состав'.


Подключаемый модуль добавляет два пункта меню после меню «Инструменты»:
'AFTER_MI_TOOLS#Мои плагины#Тестовый#Список проектов' и
'AFTER_MI_TOOLS#Мои плагины#Тестовый#Состав'.


Подключаемый модуль добавляет два пункта меню внутрь меню «Инструменты»:
'MI_TOOLS#Мои плагины#Тестовый#Список проектов' и
'MI_TOOLS#Мои плагины#Тестовый#Состав'.


Пример реализации функции InitUserDLLCom:

function InitUserDLLCom(AValue: Pointer): Integer;
var
    LMenuItem: PLoodsmanAddMenuCom;
begin
    if AValue <> nil then
    begin
        LMenuItem := AValue;

        StrLCopy(LMenuItem.stMenu,
            'BEFORE_MI_TOOLS#LoodsmanPlugIn#Список проектов',
            SizeOf(LMenuItem.stMenu) - 1);
        StrLCopy(LMenuItem.stFunction,
            'ProjectList',
            SizeOf(LMenuItem.stFunction) - 1);
        Inc(LMenuItem);

        StrLCopy(LMenuItem.stMenu,
            'BEFORE_MI_TOOLS#LoodsmanPlugIn#Список',
            SizeOf(LMenuItem.stMenu) - 1);
        StrLCopy(LMenuItem.stFunction,
            'LinkedFast',
            SizeOf(LMenuItem.stFunction) - 1);
        Inc(LMenuItem);
    end;
    Result := 2;
end;

При необходимости, для пункта меню можно задать значок (как у пункта меню «Состав» на рисунке выше). Для этого в DLL нужно добавить ресурс типа RT_BITMAP, с именем как у экспортируемой функции, указанной в stFunction.
Проще всего это сделать, создав файл PluginIcon.rc и добавив его к проекту. Например:

LinkedFast BITMAP Images\LinkedFast.bmp

Для определения доступности пунктов меню подключаемого модуля в зависимости от выбранного объекта, клиент будет вызывать функцию PgiCheckMenuItemCom, экспортируемую клиентским модулем. В PgiCheckMenuItemCom передается имя функции для пункта меню (stFunction) и интерфейс IPluginCall, с помощью которого можно получить информацию о текущей базе данных и о выбранном объекте. На основании полученной информации подключаемый модуль должен принять решение, может он выполнить запрошенную функцию или нет, и вернуть соответствующий результат.

Пример реализации функции PgiCheckMenuItemCom:

function PgiCheckMenuItemCom(AFunction: PAnsiChar;
    APluginCall: IPluginCall): Boolean;
var
    LFuncName: String;
begin
    Result := False;
    try
        LFuncName := String(AFunction);

        if LFuncName = 'ProjectList' then
        begin
            Result := True;
        end;

        if LFuncName = 'LinkedFast' then
        begin
            Result := APluginCall.stType <> 'Документ';
        end;
    except
        on E: Exception do
        begin
            Application.ShowException(E);
        end;
    end;
end;

Если пункт меню доступен и пользователь выберет его, то клиент попытается найти среди экспортируемых из DLL функцию, указанную для данного пункта меню (в stFunction), и вызвать ее. В функцию будет передан интерфейс IPluginCall, с информацией о текущей базе данных и о выбранном объекте.

Пример реализации функции:

procedure LinkedFast(APluginCall: IPluginCall);
begin
    InitializeApplication(APluginCall.AppHandle);
    try
        ShowLinkedObjectsDialog(Application, APluginCall);
    except
        on E: Exception do
        begin
            Application.ShowException(E);
        end;
    end;
    FinalizeApplication;
end;

Обратите внимание, что функции, вызываемые из клиента, не должны выбрасывать за свои пределы исключения — в каждой такой функции должен быть блок try/except. Дело в том, что формат объекта исключений (Exception) изменялся в разных версия Delphi (например, в Delphi 2009 в нем произошли серьезные изменения связанные с поддержкой юникода и вложенных исключений).

Так же нельзя без дополнительных действий использовать Application.Handle, передаваемый из клиента в подключаемый модуль. Это опять же связано с тем, что в разных версиях Delphi объекты, передаваемые с помощью сообщений, будут отличаться.

Описанные выше проблемы решены в предлагаемом примере подключаемого модуля. Код предназначен для Delphi версии 7 и выше. Работоспособность примера проверялась в ЛОЦМАН Клиент версий 8.5, 10 и 11.

Исходный код примера подключаемого модуля.

Текущая версия кода на GitHub — github.com/achechulin/loodsman.

О том, как подключить модуль к клиенту написано в справке (файл Loodsman.chm), в разделе «Инструменты → Параметры → Подключаемые модули» или просто «Подключаемые модули», в зависимости от версии клиента.

Продолжение статьи: Подключаемые модули в новых версиях ЛОЦМАН Клиент.

Комментариев нет:

Отправить комментарий