4 мая 2012 г.

Разработка системы плагинов, часть 6: UI в плагинах

В прошлый раз я подробно рассказал про обработку ошибок в системе плагинов. Теперь, когда эта тема позади, настало время поговорить об организации визуального интерфейса из плагинов. Наиболее частое применение этого - диалог настроек плагинов, вызываемый прямо из программы, но это могут быть и диалоги параметров операции и многое другое.

Упреждающее замечание (даже два): во-первых, я не эксперт в GUI, поэтому к моим утверждениям этой части стоит относиться с опаской. Желательно их проверять самостоятельно. Во-вторых, всё обсуждение и все примеры ниже рассчитаны на Delphi 2009+. Это касаемо момента с Application.Handle <> Application.MainFormHandle (MainFormOnTaskbar). У меня нет никакого желания писать весь код в нескольких вариантах. Адаптировать приведённые решения для динозаврических версий Delphi я оставляю вам в качестве домашнего задания.

Оглавление

  1. Матчасть
  2. Тестовое приложение
  3. "Неправильное" решение
  4. Базовая поддержка
  5. Реализация
  6. Ограничения системы и подводные камни
  7. MDI
  8. Выводы
  9. Заключение

Матчасть

Когда мы говорим о визуальном интерфейсе - нужно понимать, что это подразумевает, что вам нужно будет стыковать друг с другом элементы, созданные и управляемые на разных языках программирования. Откуда легко сделать вывод, что любые конструкции будут работать на уровне и по правилам операционной системы, а не библиотеки VCL. Поэтому ваших знаний и умений "работать с формочками" будет совершенно недостаточно.

Итого, первое, что вам нужно сделать, - изучить, как работают окна на уровне системы. И для этого вам нужно прочитать следующие подготовленные мной заранее материалы:
  1. Об окнах Windows
  2. Возможности окон Windows

Помимо основ окон в Windows вам также понадобятся знания о том, как свойства и возможности форм VCL проецируются на возможности окон Windows. Я постарался кратко изложить эти сведения по ссылкам выше в примечаниях.

Самое главное, что вам нужно понять из вышесказанного: отношения Owner-Owned и Parent-Child в смысле системы, а также (уже для Delphi) особенности взаимодействия окна Application, главной формы приложения и прочих окон, а также основы реализации модальных окон. Кроме того, необходимо представлять ключевые точки, в которых вы можете повлиять на конечные свойства создаваемых окон - т.е. нужно представлять себе процесс создания окон в Delphi, быть в курсе цепочки вызовов служебных методов и их назначения.

(да, это довольно много всего)

Далее, когда вы уложите в голове все эти новые сведения, вам понадобится применять их на практике. Для этого, во-первых, вам понадобятся инструменты. Я предлагаю использовать в первую очередь программу Spy++, которая входит в комплект Visual Studio и в примеры (Sample) Windows SDK. Мы уже установили Visual Studio Express на свою машину в первой части этой серии статей (для генерации заголовочников). К сожалению, Spy++ не входит в редакцию Express, так что вам придётся поставить полную версию Visual Studio (можно попробовать Trial). Или поискать её в Windows SDK.

Вообще-то в Delphi есть аналог этой утилиты - WinSight32 (она расположена в \bin\ws32.exe). Но интерфейс у неё... угх, страшен, короче.

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

Тестовое приложение

Наконец, вам нужно будет тестовое приложение, на котором вы будете проверять код. Идея следующая: нужно создать пустое VCL приложение, на котором мы будем тестировать наш код. Смысл тут в том, чтобы сначала проверить код в тепличных условиях, а уж потом нести его в систему плагинов. В этом тестовом приложении вам нужно сделать несколько кнопок, каждая из которых будет вызывать другое окно различными способами.

Я уже создал за вас такое приложение, которое состоит из двух форм и демонстрирует различные способы показа окон.

Первая кнопка показывает модальное окно. Вторая - полностью независимое окно. Третья и четвёртое показывают плавающее окно в двух вариантах (с кнопкой на панели задач и без неё). Плавающее окно отличается от независимого наличием связи Owner-Owned, что означает размещение owned окна всегда поверх owner-а. Кроме того, оба связанных окна будут сворачиваться и разворачиваться одновременно, в то время как независимые окна... ну, независимы друг от друга. Впрочем, всё это подробно разжёвано в статьях из мат-части. Последняя кнопка демонстрирует отношения Parent-Child.

Дополнительно приложение демонстрирует разницу в режимах APPMODAL и TASKMODAL для системного MessageBox. Вы можете увидеть эту разницу (и сравнить оба режима с классическим ShowModal из Delphi) при нажатии второй кнопки (Modalless).

Вам также может пригодится порядок вызова событий для окна. Он будет одинаковый при любом способе вызова:
  1. OnCreate
  2. OnShow
  3. OnActivate
  4. (Работа)
  5. OnCloseQuery
  6. OnClose
  7. OnHide
  8. OnDestroy
События OnActivate, OnDeactivate, OnMouseActivate, OnMouseEnter, OnMouseLeave и OnPaint могут происходить в любой последовательности - в том числе, влезать между вышеуказанными событиями.

Обратите внимание, что в тестовом приложении мы работаем с окном средствами Delphi.

"Неправильное" решение

Относительно часто при реализации интерфейса в плагинах можно увидеть код вроде такого:
library Plugin1;

...

procedure ShowForm(App: TApplication); stdcall;
begin
  OldApp := Application;
  try
    Application := App;

    MyForm := TMyForm.Create(nil);
    try
      MyForm.ShowModal;
    finally
      FreeAndNil(MyForm);
    end;
  finally
    Application := OldApp;
  end;
end;

exports
  ShowForm;

end.
Или даже такого:
function CreateForm(App, Scr: integer): BOOL;
begin
  Result := False;
  try
    DLLScr := Screen;
    Screen := TScreen(Scr);
    DLLApp := Application;
    Application := TApplication(App);

    TfrmTest.Create(Application);
  finally
    Result := True;
  end;
end;
Что не так с этим кодом? А то не так, что он передаёт объекты между границами модулей (exe и DLL плагина). Помните наши правила? Никаких объектов. Иначе - как же этим подходом воспользуется код на другом языке? А никак. И вопрос тут не только в коде на другом языке - если DLL и exe будут собраны в разных версиях компилятора Delphi (где объекты имеют разную структуру), то при попытке выполнить этот код вы получите вылет программы в лучшем случае, а в худшем - просто неверную работу без вывода сообщений об ошибках, но зато с порчей памяти. И потом будем удивляться, а чего это программа нестабильно работает и вылетает в самых странных местах.

Нет, если вам действительно нужно передавать объекты между модулями - нет проблем. Но при этом:
  • Забудьте про другие языки программирования
  • Компилируйте все модули строго в одной версии Delphi
  • Пересобирайте все модули при смене/обновления компилятора
А из этих пунктов следует неизбежный вывод: DLL вам не надо. Она не даст вам никаких плюсов, а, наоборот, только усложнит разработку. Что вам действительно нужно, так это - пакеты. Пакеты:
  • Привязаны к Delphi
  • Позволяют расшаривать объекты
  • Не имеют проблем с разделением памяти
  • Уменьшают код каждого модуля за счёт выноса общего кода в пакеты общего назначения
Поэтому код из примеров выше бессмыслен в любом случае - хотели вы обмениваться объектами или нет.

Примечание: код вида
library Plugin1;

...

procedure ShowForm(App: HWND); stdcall;
begin
  Application.Handle := App;
  try
    MyForm := TMyForm.Create(nil);
    try
      MyForm.ShowModal;
    finally
      FreeAndNil(MyForm);
    end;
  finally
    Application.Handle := 0;
  end;
end;

exports
  ShowForm;

end.
Напротив, имеет смысл. Потому что в этом примере кода мы не передаём объекты. Мы передаём описатель окна - т.е. число. И ниже мы увидим, почему и зачем это делается.

Заметьте, как второй ("неправильный") пример в этом разделе пытается скопировать последний ("правильный") пример: "ну, смотри, вот я передаю числа, а не объекты, так что я - правильный код, да? Да?!". Я, надеюсь, вам понятно, что если на клетку льва повесили табличку "слон", то от этого лев не превратится волшебным образом в слона. Так же и здесь. Если вы сконвертируете указатели на объекты в числа, то от этого объекты никуда не исчезнут - они всё равно будут передаваться от одного модуля другому, и не важно, передаёте ли вы их как указатели, числа, или даже как набор Boolean-ов.

Базовая поддержка

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

Итого, я предлагаю начать реализацию с такого кода:
PluginAPI.pas
type
  IApplicationWindows = interface
  ['{2E3A7D92-4E59-4C63-B0CB-4752C089C970}']
  // private
    function GetApplicationWnd: HWND; safecall;
    function GetMainWnd: HWND; safecall;
    function GetActiveWnd: HWND; safecall;
    function GetClientWnd: HWND; safecall;
    function GetPopupCtrlWnd: HWND; safecall;
  // public
    property ApplicationWnd: HWND read GetApplicationWnd;
    property MainWnd: HWND read GetMainWnd;
    property ActiveWnd: HWND read GetActiveWnd;
    property ClientWnd: HWND read GetClientWnd;
    property PopupCtrlWnd: HWND read GetPopupCtrlWnd;
  end;
PluginManager.pas
...

uses
  ... 
  Forms;

...

type
  TPlugin = class(TCheckedInterfacedObject, ..., IApplicationWindows)
  ...
  protected
    ...
    // IApplicationWindows
    function GetApplicationWnd: HWND; safecall;
    function GetMainWnd: HWND; safecall;
    function GetActiveWnd: HWND; safecall;
    function GetClientWnd: HWND; safecall;
    function GetPopupCtrlWnd: HWND; safecall;
  ...
  end;

...

function TPlugin.GetApplicationWnd: HWND;
begin
  Result := Application.Handle;
end;

function TPlugin.GetMainWnd: HWND;
begin
  Result := Application.MainFormHandle;
end;

function TPlugin.GetActiveWnd: HWND;
begin
  Result := Application.ActiveFormHandle;
end;

function TPlugin.GetClientWnd: HWND;
begin
  Result := Application.MainForm.ClientHandle; 
end;

function TPlugin.GetPopupCtrlWnd: HWND;
begin
  Result := Application.PopupControlWnd;
end;

...
Или, если вы не хотите вводить зависимость системы плагинов от визуального UI, то:
remain.pas
...

  TMainFormProvider = class(TBasicProvider, ..., IApplicationWindows)
  ...
  protected
    ...
    // IApplicationWindows
    function GetApplicationWnd: HWND; safecall;
    function GetMainWnd: HWND; safecall;
    function GetActiveWnd: HWND; safecall;
    function GetClientWnd: HWND; safecall;
    function GetPopupCtrlWnd: HWND; safecall;
  ...
  end;

...

function TMainFormProvider.GetApplicationWnd: HWND;
begin
  Result := Application.Handle;
end;

function TMainFormProvider.GetMainWnd: HWND;
begin
  Result := Application.MainFormHandle;
end;

function TMainFormProvider.GetActiveWnd: HWND;
begin
  Result := Application.ActiveFormHandle;
end;

function TMainFormProvider.GetClientWnd: HWND;
begin
  Result := Application.MainForm.ClientHandle; 
end;

function TMainFormProvider.GetPopupCtrlWnd: HWND;
begin
  Result := Application.PopupControlWnd;
end;

...
(во втором случае дополнительно ничего делать не нужно, т.к. вся регистрация у нас уже готова)

Тут следует сразу уточнить цель введения таких свойств. Их задача - дать плагину информацию об окнах, относительно которых он может создавать свои собственные элементы пользовательского интерфейса. Например, плагин может показывать окно, указывая главное окно в качестве владельца (в терминах системы). Данные описатели не предназначены для того, чтобы плагин оперировал бы ими (скажем, скрывал, минимизировал и менял свойства). Если вы хотите дать плагину возможность влиять на указанные ключевые окна вашей программы, то вам нужно будет дополнительно ввести методы по управлению ими. Скажем, HideMainWindow и т.п. Плагин должен будет вызывать их, а не пытаться манипулировать окнами самостоятельно. Пример, когда это может быть необходимо: ваша программа размещается в трее и вы хотите дать плагину возможность "сворачивать" и "разворачивать" программу из трея до и после выполнения своих задач.

Реализация

Я предлагаю создать новый плагин и на его примере показать работу с различными типами окон.

Модальные окна

Модальные окна являются самыми простыми окнами в нашей системе плагинов. Потому что модальное окно в смысле Delphi реализуется просто отключением (disable) всех прочих окон в приложении. Итого, чтобы показать модальное окно, нам нужно:
  1. Отключить все окна приложения.
  2. Показать наше окно.
  3. Запустить цикл сообщений, пока окно не закроется.
  4. Включить все окна приложения (см. также).
  5. Удалить наше окно.
Опционально, вы можете сделать модальное окно owned-окном к вызывающему окну. А в качестве хорошего тона - вы можете уведомить приложение о начале и конце модального диалога.

Вы можете делать все эти действия вручную или позволить VCL сделать их за вас. Практически все они уже реализованы в методе ShowModal.

С учётом сказанного, у нас получается такой код:
type
  TPlugin = class(TCheckedInterfacedObject, IPlugin, IDestroyNotify, IUnknown)
  private
    FCore: ICore;
    FWnds: IApplicationWindows;
    FMenuItem1: TPluginMenuItem; // пример: MessageBox
    FMenuItem2: TPluginMenuItem; // пример: модальное окно
    FMenuItem3: TPluginMenuItem; // пример: автономное окно
    FMenuItem4: TPluginMenuItem; // пример: плавающее окно
    FMenuItem5: TPluginMenuItem; // пример: дочернее окно
    procedure MenuItem1Click(Sender: TObject);
    procedure MenuItem2Click(Sender: TObject);
    procedure MenuItem3Click(Sender: TObject);
    procedure MenuItem4Click(Sender: TObject);
    procedure MenuItem5Click(Sender: TObject);

  protected
    // Обёртки для удобства работы:
    property Core: ICore read FCore;
    property Wnds: IApplicationWindows read FWnds;
    function MessageBox(const AText, ACaption: String; const AFlags: DWORD): Integer;

    // IPlugin
    ...
    // IDestroyNotify
    ...
  public
    constructor Create(const ACore: ICore);
    destructor Destroy; override;
  end;

{ TPlugin }

constructor TPlugin.Create(const ACore: ICore);
begin
  inherited Create;
  FCore := ACore;
  Assert(FCore.Version >= 1);
  if not Supports(FCore, IApplicationWindows, FWnds) then
    raise EInvalidCoreVersion.Create('Этому плагину нужна поддержка окон');

  Application.Handle := FWnds.ApplicationWnd;

  FMenuItem1 := TPluginMenuItem.Create(FCore);
  FMenuItem2 := TPluginMenuItem.Create(FCore);
  FMenuItem3 := TPluginMenuItem.Create(FCore);
  FMenuItem4 := TPluginMenuItem.Create(FCore);
  FMenuItem5 := TPluginMenuItem.Create(FCore);

  FMenuItem1.Item.Caption := 'UIDemo: MessageBox';
  FMenuItem1.Item.Hint := 'Показать пример системного MessageBox';
  FMenuItem1.OnClick := MenuItem1Click;

  FMenuItem2.Item.Caption := 'UIDemo: Модальный диалог';
  FMenuItem2.Item.Hint := 'Показать пример модального диалога';
  FMenuItem2.OnClick := MenuItem2Click;

  FMenuItem3.Item.Caption := 'UIDemo: Автономное окно';
  FMenuItem3.Item.Hint := 'Показать пример автономного окна';
  FMenuItem3.OnClick := MenuItem3Click;

  FMenuItem4.Item.Caption := 'UIDemo: Плавающее окно';
  FMenuItem4.Item.Hint := 'Показать пример плавающего окна';
  FMenuItem4.OnClick := MenuItem4Click;

  FMenuItem5.Item.Caption := 'UIDemo: Дочернее окно';
  FMenuItem5.Item.Hint := 'Показать пример дочернего окна';
  FMenuItem5.OnClick := MenuItem5Click;
end;

procedure TPlugin.Delete;
begin
  FreeAndNil(FMenuItem5);
  FreeAndNil(FMenuItem4);
  FreeAndNil(FMenuItem3);
  FreeAndNil(FMenuItem2);
  FreeAndNil(FMenuItem1);
  Application.Handle := 0;
  FWnds := nil;
  FCore := nil;
end;

destructor TPlugin.Destroy;
begin
  Delete;
  inherited;
end;

function TPlugin.MessageBox(const AText, ACaption: String; const AFlags: DWORD): Integer;
begin
  Wnds.ModalStarted;
  try
    Result := Windows.MessageBox(Wnds.MainWnd, // 0 или окно - по вкусу
                                 PChar(AText), 
                                 PChar(ACaption), 
                                 (AFlags and (not MB_SYSTEMMODAL)) or MB_TASKMODAL);
  finally
    Wnds.ModalFinished;
  end;
end;

procedure TPlugin.MenuItem1Click(Sender: TObject);
begin
  MessageBox('Привет из плагина!', 'Заголовок MessageBox', MB_OK or MB_ICONINFORMATION);
end;
Как видите, я сразу ввёл обёртки для удобного доступа к MessageBox, ядру и интерфейсу управления окнами, так что теперь вы в любом месте можете легко их использовать.

Заметьте, что у не-Delphi плагинов также нет никаких проблем: они ровно так же могут уведомлять приложение о начале и конце показа модального окна, и ровно так же могут вызывать системную функцию MessageBox.

Далее, нам нужно уметь показать модально произвольную форму в плагине. Поскольку нам нужно сейчас реализовать некоторые вспомогательные действия, то я предлагаю ввести промежуточный служебный класс, который возьмёт на себя всю работу поддержки. Для этого создайте новый модуль и введите в него такой код:
unit PluginForm;

interface

uses
  Windows,
  SysUtils,
  Forms,
  PluginAPI;

type
  TForm = class(Forms.TForm)
  private
    FCore: ICore;
    FWnds: IApplicationWindows;
    procedure SetupWndIcon;
  protected
    property Core: ICore read FCore;
    property Wnds: IApplicationWindows read FWnds;
    function MessageBox(const AText, ACaption: String; const AFlags: DWORD): Integer;

    procedure DoShow; override;
  public
    constructor Create(const ACore: ICore); reintroduce;
    function ShowModal: Integer; override;
  end;

implementation

uses
  Messages;

constructor TForm.Create(const ACore: ICore);
begin
  FCore := ACore;
  if not Supports(FCore, IApplicationWindows, FWnds) then
    Assert(False);
  inherited Create(nil);
end;

function TForm.ShowModal: Integer;
begin
  Wnds.ModalStarted;
  try
    Result := inherited ShowModal;
  finally
    Wnds.ModalFinished;
  end;
end;

function TForm.MessageBox(const AText, ACaption: String; const AFlags: DWORD): Integer;
begin
  Wnds.ModalStarted;
  try
    Result := Windows.MessageBox(Wnds.MainWnd, PChar(AText), PChar(ACaption), (AFlags and (not MB_SYSTEMMODAL)) or MB_TASKMODAL);
  finally
    Wnds.ModalFinished;
  end;
end;

end.
Сохраните этот код в файл \PluginAPI\Plugins\PluginForm.pas - поскольку этот файл будет использоваться только плагинами.

Примечание: кстати, неплохо бы вынести в отдельный файл ещё код по управлению пунктом меню в плагине (который сейчас у нас дублируется в каждом плагине):
unit PluginSupport;

interface

uses
  Windows,
  SysUtils,
  PluginAPI,
  Classes;

type
  TPluginMenuItem = class(TObject, IUnknown, INotifyEvent)
  private
    FManager: IMenuManager;
    FItem: IMenuItem;
    FClick: TNotifyEvent;
  protected
    // IUnknown
    function QueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    // INotifyEvent
    procedure Execute(Sender: IInterface); safecall;
  public
    constructor Create(const ACore: ICore);
    destructor Destroy; override;
    property Item: IMenuItem read FItem;

    procedure Click; virtual;
    property OnClick: TNotifyEvent read FClick write FClick;

    function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override;
  end;

implementation

uses
  Helpers;

{ TPluginMenuItem }

constructor TPluginMenuItem.Create(const ACore: ICore);
begin
  inherited Create;
  if not Supports(ACore, IMenuManager, FManager) then
    Assert(False);

  FItem := FManager.CreateMenuItem;

  FItem.Caption := 'PluginMenuItem';
  FItem.Hint := '';
  FItem.Enabled := True;
  FItem.Checked := False;
  FItem.RegisterExecuteHandler(Self);
end;

destructor TPluginMenuItem.Destroy;
begin
  FManager.DeleteMenuItem(FItem);
  FManager := nil;
  inherited;
end;

procedure TPluginMenuItem.Execute(Sender: IInterface);
begin
  Click;
end;

procedure TPluginMenuItem.Click;
begin
  if FItem.Enabled then
  begin
    if Assigned(FClick) then
      FClick(Self);
  end;
end;

function TPluginMenuItem.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := S_OK
  else
    Result := E_NOINTERFACE;
end;

function TPluginMenuItem._AddRef: Integer;
begin
  Result := -1;   // -1 indicates no reference counting is taking place
end;

function TPluginMenuItem._Release: Integer;
begin
  Result := -1;   // -1 indicates no reference counting is taking place
end;

function TPluginMenuItem.SafeCallException(ExceptObject: TObject;
  ExceptAddr: Pointer): HResult;
begin
  Result := HandleSafeCallException(Self, ExceptObject, ExceptAddr);
end;

end.
Поместите его в файл \PluginAPI\Plugins\PluginSupport.pas.

Ладно, возвращаясь к нашему PluginForm.pas: я использовал метод Geo для модификации базовой формы. Я добавил хранение ссылки на ядро плагина, интерфейс управление окнами, а также скопировал обёртку для MessageBox и обернул вызов ShowModal в нотификацию ядра.

Теперь, когда у нас есть такая обёртка, вы легко можете превратить любую форму в форму плагина. Для этого достаточно просто вписать модуль PluginForm последним в uses секции interface произвольной формы:
unit FormUIDemo;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, PluginForm; // <- последним!

type
  TUIDemoForm = class(TForm)
    btClose: TButton;
    edText: TEdit;
    procedure btCloseClick(Sender: TObject);
  end;

implementation

{$R *.dfm}

procedure TUIDemoForm.btCloseClick(Sender: TObject);
begin
  Close;
end;

end.
Теперь простая TUIDemoForm стала формой для плагина, умеющей общаться с ядром (конкретно в данном случае - уведомлять его о модальном показе).

Использовать её не менее просто:
procedure TPlugin.MenuItem2Click(Sender: TObject);
var
  UIDemoForm: TUIDemoForm;
begin
  UIDemoForm := TUIDemoForm.Create(FCore);
  try
    UIDemoForm.ShowModal;
  finally
    FreeAndNil(UIDemoForm);
  end;
end;
Это практически ничем не отличается от использования обычной формы в обычном приложении.

Это что касается Delphi. Программисты же на других языках будут вынуждены реализовывать модальность вручную - по алгоритму, описанному в начале пункта. Т.е. вызвать EnumWindows, перебрать окна, отобрать из них принадлежащие нашей программе, отключить их, показать своё окно, организовать модальный цикл выборки сообщений и так далее. Или, может быть, в их библиотеке уже есть аналогичная функциональность, так что они смогут воспользоваться ей.

Сразу замечу, что наш базовый класс для формы реализует некоторые возможности - те, который мы захотели. При желании, мы можем его расширить и добавить в него больше возможностей. Например...

Немодальные окна

Не модальные автономные окна ещё проще модальных по реализации. Потому что они вообще никак не связаны с окнами приложения. Вам просто нужно его показать - и всё:
type
  TCreateMode = (cmDefault, cmStandalone);

  TForm = class(Forms.TForm)
  private
    ...
    FMode: TCreateMode;
    ...
  protected
    ...
    property Mode: TCreateMode read FMode;
    ...
    procedure CreateParams(var Params: TCreateParams); override;
  public
    ...
    constructor CreateStandalone(const ACore: ICore);
    ...
  end;

implementation

...

constructor TForm.CreateStandalone(const ACore: ICore);
begin
  FMode := cmStandalone;
  Create(ACore);
end;

procedure TForm.CreateParams(var Params: TCreateParams);
begin
  inherited;
  if Mode = cmStandalone then
    Params.WndParent := 0;
end;
Для создания автономного окна нам просто не нужно делать его дочерним или owned-окном и не нужно отключать или как-то иначе трогать другие окна - вот и всё. И тогда:
procedure TPlugin.MenuItem3Click(Sender: TObject);
var
  UIDemoForm: TUIDemoForm;
begin
  UIDemoForm := TUIDemoForm.CreateStandalone(FCore);
  UIDemoForm.Show;
end;
Правда в этом примере объекты форм у нас не удаляются. На практике подобного вида окна реализуются в приложениях типа SDI. Как правило они создаются во множественном числе - по одному на... что-то. Документ, файл и т.п. Поэтому в реальной программе вы должны будете завести массив из форм для отслеживания подобных окон. В данном примере мне это делать просто лениво. Так что учтите, что при включении поиска утечек - он будет вам указывать на не удалённые формы и объекты, с ними связанные. Если вы не хотите использовать объект окна после закрытия самого окна, то можете удалять его при закрытии (Action = caFree).

Примечание: возможно, что желаемого можно достичь установкой свойств формы - типа PopupMode и аналогичных. Откровенно, мне не хочется в этом копаться. Свойства VCL работают отлично, когда вам нужно просто задать поведение окна, но если вам нужны конкретные свойства системного окна, а не формы, то может оказаться, что будет проще их просто установить, чем подбирать необходимую комбинацию свойств формы. Вот почему в данном случае я предпочёл просто заместить свойства окна своими значениями.

Ну а с другими языками тут тоже всё прозрачно - просто создай окно, не указывая родителя/владельца, и покажи его.

Плавающие окна

Плавающие окна используются для не модальных диалогов, плавающих панелей инструментов и аналогичных вещей. Характеризуются они привязкой к какому-то окну - как правило, главному. Соответственно, для создания плавающего окна нам нужно создать окно, указав ему владельца:
type
  TCreateMode = (cmDefault, cmStandalone, cmPopup, cmPopupTB);

  TForm = class(Forms.TForm)
  private
    ...
    FOwnerWnd: HWND;
    ...
  public
    ...
    // Без кнопки в панели задач
    constructor CreatePopup(const ACore: ICore; const AOwnerWnd: HWND);
    // С кнопкой в панели задач
    constructor CreatePopupTB(const ACore: ICore; const AOwnerWnd: HWND);
    ...
  end;

implementation

...

constructor TForm.CreatePopup(const ACore: ICore; const AOwnerWnd: HWND);
begin
  FMode := cmPopup;
  FOwnerWnd := AOwnerWnd;
  Create(ACore);
end;

constructor TForm.CreatePopupTB(const ACore: ICore; const AOwnerWnd: HWND);
begin
  FMode := cmPopupTB;
  FOwnerWnd := AOwnerWnd;
  Create(ACore);
end;

procedure TForm.CreateParams(var Params: TCreateParams);
begin
  inherited;
  if Mode = cmStandalone then
    Params.WndParent := 0
  else
  if (Mode = cmPopup) or
     (Mode = cmPopupTB) then
  begin
    Params.WndParent := FOwnerWnd;
    if Mode = cmPopupTB then
      Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW
    else
      Params.ExStyle := Params.ExStyle and (not WS_EX_APPWINDOW);
  end;
end;
procedure TPlugin.MenuItem4Click(Sender: TObject);
var
  UIDemoForm: TUIDemoForm;
begin
  UIDemoForm := TUIDemoForm.CreatePopup(FCore, Wnds.MainWnd); // или CreatePopupTB
  UIDemoForm.Show;
end;
Здесь аналогичные замечания: вы можете создать произвольное количество окон и, вообще-то, вам хорошо бы их отслеживать в каком-нибудь массиве. В примере для простоты я это делать не буду.

Также замечу, что окна этого типа, если это не диалог (не модальный), а именно плавающая панель инструментов, так вот, эти окна хорошо бы снабдить стилем WS_EX_TOOLWINDOW: такое окно не показывается в панели задач и списке Alt+Tab, имеет уменьшенный заголовок и не имеет значка. Ещё одним полезным стилем является WS_EX_NOACTIVATE, включение которого не даёт окну активироваться. Это полезная возможность для вспомогательных окон, которым не нужно угонять фокус ввода (например, окна-уведомления).

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

Бонус-пример: окна FireMonkey

В качестве дополнительного бонуса я хотел бы рассмотреть аналогичные возможности окон, но для FireMonkey. FireMonkey - это векторная кросс-платформенная библиотека UI, которая появилась в Delphi XE2 как замена VCL. Пример с FireMonkey я хочу дать по двум причинам:
  1. Во-первых, это хорошо покажет состыковку двух разных систем. А то когда с двух сторон VCL - так не интересно. А вот когда со стороны плагинов принципиально несовместимая библиотека - вот это уже другое дело. Это как-бы аналог плагина на другом языке.
  2. Во-вторых, FireMonkey - это кросс-платформенная библиотека. Это значит, что она оперирует общими вещами, не спускаясь до конкретики платформы. Иными словами, все платформенно-зависимые вещи (оконные классы, стили, описатели и т.п.) в ней скрыты. Вот заодно и покажем, как с таким работать.

Для демонстрации я предлагаю создать новый плагин, который будет дублировать наш плагин-пример для VCL-окон, только теперь все окна будут на базе FireMonkey.

Проблема номер 1 - само помещение FireMonkey в DLL. Подробно вы можете изучить этот вопрос в этой статье. Кратко, суть заключается в следующем: FireMonkey использует GDI+. GDI+ нуждается в инициализации. Но инициализацию нельзя проводить в DLLMain - откуда следует, что её нельзя проводить в initialization/finalization модулей (поскольку у нас DLL). А если инициализацию не делать, то мы получим Access Violation при попытке создания формы FireMonkey в DLL.

У этой проблемы есть четыре решения:
  1. Инициализацию может производить приложение.
  2. Мы можем инициализировать GDI+ в специальных функциях инициализации. Вот зачем в наш набор правил мы ввели требование явных Init/Done для плагина.
  3. Можно оборачивать в GdiplusStartup/GdiplusShutdown каждый вызов функции плагина.
  4. Мы можем обойти проблему с DLLMain, собрав плагин с FireMonkey в виде пакета, как описано в моей старой серии статей про плагины.
Как несложно угадать, я выбрал способ номер два:
library UIDemo;

uses
  ...
  Winapi.GDIPOBJ,
  Winapi.GDIPAPI,
  ...

...

function Init(const ACore: ICore): IPlugin; safecall;
begin
  StartupInput.DebugEventCallback := nil;
  StartupInput.SuppressBackgroundThread := False;
  StartupInput.SuppressExternalCodecs   := False;
  StartupInput.GdiplusVersion := 1;
  GdiplusStartup(gdiplusToken, @StartupInput, nil);

  Result := TPlugin.Create(ACore);
end;

procedure Done; safecall;
begin
  if Assigned(GenericSansSerifFontFamily) then
    GenericSansSerifFontFamily.Free;
  if Assigned(GenericSerifFontFamily) then
    GenericSerifFontFamily.Free;
  if Assigned(GenericMonospaceFontFamily) then
    GenericMonospaceFontFamily.Free;
  if Assigned(GenericTypographicStringFormatBuffer) then
    GenericTypographicStringFormatBuffer.free;
  if Assigned(GenericDefaultStringFormatBuffer) then
    GenericDefaultStringFormatBuffer.Free;
  GdiplusShutdown(gdiplusToken);
end;
Окей, теперь мы можем использовать FireMonkey в DLL.

Следующий вопрос - как повлиять на свойства создаваемого окна. И тут нас ждёт засада. Во-первых, свойств у формы FireMonkey - мало. Вам не удастся подобрать их так, чтобы сформировать нужное окно. Во-вторых, библиотека FireMonkey продумана существенно хуже VCL. Поэтому внедриться в точку создания окна у вас не получится (по крайней мере - чистыми методами).

Итого, вам придётся делать обходной путь, который выглядит следующим образом:
unit PluginFormFMX;

interface

uses
  Winapi.Windows,
  System.SysUtils,
  System.UITypes,
  System.Types,
  FMX.Types,
  FMX.Forms,
  FMX.Platform,
  FMX.Platform.Win,
  PluginAPI;

type
  TCreateMode = (cmDefault, cmStandalone, cmPopup, cmPopupTB);

  TCreateParams = record
    Style: DWORD;
    ExStyle: DWORD;
    X, Y: Integer;
    Width, Height: Integer;
    WndParent: HWnd;
    Param: Pointer;
  end;

  TForm = class(FMX.Forms.TForm)
  private
    FCore: ICore;
    FWnds: IApplicationWindows;
    FMode: TCreateMode;
    FOwnerWnd: HWND;
  protected
    property Core: ICore read FCore;
    property Wnds: IApplicationWindows read FWnds;
    property Mode: TCreateMode read FMode;

    function CreateWindow: TFmxHandle; virtual;
    procedure CreateParams(var Params: TCreateParams); virtual;

    procedure CreateHandle; override;
  public
    constructor Create(const ACore: ICore); reintroduce;
    constructor CreateStandalone(const ACore: ICore);
    constructor CreatePopup(const ACore: ICore; const AOwnerWnd: HWND);
    constructor CreatePopupTB(const ACore: ICore; const AOwnerWnd: HWND);
    function ShowModal: TModalResult;
  end;

implementation

uses
  Winapi.Messages;

constructor TForm.Create(const ACore: ICore);
begin
  FCore := ACore;
  if not Supports(FCore, IApplicationWindows, FWnds) then
    Assert(False);
  inherited Create(nil);
end;

constructor TForm.CreateStandalone(const ACore: ICore);
begin
  FMode := cmStandalone;
  Create(ACore);
end;

constructor TForm.CreatePopup(const ACore: ICore; const AOwnerWnd: HWND);
begin
  FMode := cmPopup;
  FOwnerWnd := AOwnerWnd;
  Create(ACore);
end;

constructor TForm.CreatePopupTB(const ACore: ICore; const AOwnerWnd: HWND);
begin
  FMode := cmPopupTB;
  FOwnerWnd := AOwnerWnd;
  Create(ACore);
end;

procedure TForm.CreateHandle;
var
  P: Pointer;
begin
  // TCommonCustomForm.CreateHandle
  Handle := CreateWindow;
  if TFmxFormState.fsRecreating in FormState then
    Platform.SetWindowRect(Self, RectF(Left, Top, Left + Width, Top + Height));

  // TCustomForm.CreateHandle
  if DefaultCanvasClass <> nil then
  begin
    P := @Self.Canvas;
    NativeInt(P^) := NativeInt(DefaultCanvasClass.CreateFromWindow(Handle, ClientWidth, ClientHeight));
  end;
end;

function TForm.CreateWindow: TFmxHandle;
var
  Params: TCreateParams;
  Wnd: HWND;
begin
  Result := Platform.CreateWindow(Self);

  Wnd := FmxHandleToHWND(Result);
  CreateParams(Params);
  SetWindowLong(Wnd, GWL_EXSTYLE, NativeInt(Params.ExStyle));
  SetWindowLong(Wnd, GWL_HWNDPARENT, NativeInt(Params.WndParent));
end;

procedure TForm.CreateParams(var Params: TCreateParams);

  procedure DefaultCreateParams(var Params: TCreateParams);
  begin
    FillChar(Params, SizeOf(Params), 0);
    Params.X := Integer(CW_USEDEFAULT);
    Params.Y := Integer(CW_USEDEFAULT);
    Params.Width := Integer(CW_USEDEFAULT);
    Params.Height := Integer(CW_USEDEFAULT);

    // TPlatformWin.CreateWindow
    if Transparency then
    begin
      Params.Style := Params.Style or WS_POPUP;
      Params.ExStyle := Params.ExStyle or WS_EX_LAYERED;
      if (Application.MainForm <> nil) and (Self <> Application.MainForm) then
        Params.ExStyle := Params.ExStyle or WS_EX_TOOLWINDOW; // disable taskbar
    end
    else
    begin
      case Self.BorderStyle of
        TFmxFormBorderStyle.bsNone:
          begin
            Params.Style := Params.Style or WS_POPUP or WS_SYSMENU;
            Params.ExStyle := Params.ExStyle { or WS_EX_TOOLWINDOW }; // disable taskbar
          end;
        TFmxFormBorderStyle.bsSingle, TFmxFormBorderStyle.bsToolWindow:
          Params.Style := Params.Style or (WS_CAPTION or WS_BORDER);
        TFmxFormBorderStyle.bsSizeable, TFmxFormBorderStyle.bsSizeToolWin:
          Params.Style := Params.Style or (WS_CAPTION or WS_THICKFRAME);
      end;
      if Self.BorderStyle in [TFmxFormBorderStyle.bsToolWindow, TFmxFormBorderStyle.bsSizeToolWin] then
      begin
        Params.ExStyle := Params.ExStyle or WS_EX_TOOLWINDOW;
        if (Application.MainForm = nil) or (Application.MainForm = Self) then
          Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
      end;
      if Self.BorderStyle <> TFmxFormBorderStyle.bsNone then
      begin
        if TBorderIcon.biMinimize in Self.BorderIcons then
          Params.Style := Params.Style or WS_MINIMIZEBOX;
        if TBorderIcon.biMaximize in Self.BorderIcons then
          Params.Style := Params.Style or WS_MAXIMIZEBOX;
        if TBorderIcon.biSystemMenu in Self.BorderIcons then
          Params.Style := Params.Style or WS_SYSMENU;
      end;
    end;

    // modal forms must have an owner window
    if TFmxFormState.fsModal in Self.FormState then
      Params.WndParent := GetActiveWindow
    else
      Params.WndParent := GetDesktopWindow;
  end;

begin
  DefaultCreateParams(Params); // inherited;

  if Mode = cmStandalone then
    Params.WndParent := 0
  else
  if (Mode = cmPopup) or
     (Mode = cmPopupTB) then
  begin
    Params.WndParent := FOwnerWnd;
    if Mode = cmPopupTB then
      Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW
    else
      Params.ExStyle := Params.ExStyle and (not WS_EX_APPWINDOW);
  end;
end;

function TForm.ShowModal: TModalResult;
begin
  Wnds.ModalStarted;
  try
    Result := inherited ShowModal;
  finally
    Wnds.ModalFinished;
  end;
end;

end.
Итак, это - вспомогательный модуль со служебным классом. Как и ранее, вам достаточно подключить его в форму FireMonkey, чтобы сделать эту форму формой плагина. Суть же телодвижений в коде заключается в том, что мы сначала создаём окно (уж как создастся), а потом заменяем ему свойства - пост-фактум. Связано это с тем, что мы не можем получить доступ к точке создания окна. Более того, не можем мы её и скопировать к себе - потому что код создания окна оперирует со скрытыми объектами - коллекцией окон, внутренними классами и (что самое главное) закрытой оконной процедурой. Поэтому приходится идти в обход. Кстати, даже при этом нам пришлось использовать хак, чтобы заместить свойство Canvas.

В итоге же использование форм FireMonkey выглядит точно так же, как и форм VCL:
procedure TPlugin.MenuItem1Click(Sender: TObject);
var
  UIDemoForm: TUIDemoForm;
begin
  UIDemoForm := TUIDemoForm.Create(FCore);
  try
    UIDemoForm.ShowModal;
  finally
    FreeAndNil(UIDemoForm);
  end;
end;

procedure TPlugin.MenuItem2Click(Sender: TObject);
var
  UIDemoForm: TUIDemoForm;
begin
  UIDemoForm := TUIDemoForm.CreateStandalone(FCore);
  UIDemoForm.Show;
end;

procedure TPlugin.MenuItem3Click(Sender: TObject);
var
  UIDemoForm: TUIDemoForm;
begin
  UIDemoForm := TUIDemoForm.CreatePopup(FCore, Wnds.MainWnd);
  UIDemoForm.Show;
end;
Ну вот, собственно, и всё. Мы привели пример использования не-VCL для создания UI в качестве плагинов. Кстати говоря, использовать FireMonkey таким образом вы можете из любой Delphi - хоть из Delphi 2 (правда, сделать это для неё будет существенно сложнее, чем для Delphi 7).

Ограничения системы и подводные камни

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

Дело тут вот в чём. Некоторые возможности VCL реализуются не на базе системных механизмов, а за счёт прямого взаимодействия компонентов VCL. К примеру, взять хотя бы кнопку Esc для закрытия окна. Или свойство KeyPreview. Для того, чтобы это работало, необходимо взаимодействие компонентов, которые могут сообщать друг другу о различных событиях. Работать это будет, понятно, до тех пор, пока с обоих сторон у нас находится VCL. Как только с одной стороны (в нашем случае - со стороны плагинов) мы VCL убираем - эти возможности работать перестают.

В целом, это небольшие мелочи, которые, тем не менее, часто могут быть раздражающими. К примеру, помимо упомянутого выше примера, из этой же серии будет проход по элементам формы через Tab. Вместо использования системных механизмов Delphi и здесь использует своё решение.

Частный случай этой же проблемы - модальные циклы. Здесь дело в том, что цикл выборки сообщений в Delphi выполняет кучу работы. До окончательной диспетчеризации сообщений он выполняет ряд проверок и может вызывать различные события и перенаправлять сообщение. Но если ядро вызывает плагин, а тот, в свою очередь, делает свой модальный диалог (а следовательно - и свой собственный цикл выборки сообщений), то весь дополнительный обвес VCL не вызывается.

Итого, если вы видите проблему в поведении вашего приложения и плагинов - в первую очередь вы должны взяться за Spy++ (ну или WinSight32) и тестовое приложение. Воспроизведите эту же ситуацию в тестовом приложении и с помощью утилиты найдите отличия между двумя ситуациями "работает как надо" и "работает не как надо". Если различия - в системных вещах (не задали родителя, не тот стиль у окна и т.п.), то это - баг. Просто исправьте его. Если же таковой разницы не видно, то это - не баг, а недостаток дизайна.

В целом, проблемы дизайна - проблемы это решаемые. Вам, по сути, нужно продублировать работу, выполняемую VCL. Например, для модальных циклов: просто пусть плагин вызывает функцию "прокачки" сообщений ядра. При этом будут вызываться все доп. действия VCL. Понятно, что это возможно только в том случае, если модальный цикл под вашим контролем. Тот же системный MessageBox цикл выборки сообщений менять вам не даст. Теоретически, эту проблему можно решить глобально - простой установкой локального хука на WH_GETMESSAGE. Но задача эта достаточно сложна. Нужно хорошо знать внутренности VCL, чтобы правильно согласовать обработку сообщений. Лично у меня сейчас нет возможности досконально исследовать этот вопрос.

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

MDI

Несколько особняком стоят в стороне MDI-Child окна. Дело в том, что для создания MDI-Child окна вам потребуется мастер-форма (MDIForm). Именно форма, а не окно. Хотя сами по себе MDI-окна - это обычное отношение Parent-Child, но Delphi требует объектов для выполнения дополнительной работы по организации взаимодействия. Вам не удастся решить эту проблему "в лоб".

Я не буду подробно сейчас разбирать MDI-окна. Хотя бы потому, что наше демо-приложение не является MDI-приложением. Вместо этого я лишь кратко намечу возможные подходы, а подробно разберу пример с MDI в следующей статье - где я хотел продемонстрировать именно универсальность того, что мы написали, то, что систему плагинов можно просто взять и прикрепить к новому приложению. Вот в качестве такого приложения я и возьму MDI-приложение и посвящу 1 раздел примерам работы с MDI.

Окей, а сейчас - краткие решения:
  1. Использовать пакеты. Это простейшее решение, но оно не применимо к плагинам на других языках программирования.
  2. Попытаться скопировать работу VCL, подсунув MDI-Child fake-объект MDIForm (привет, "Матрица"). Это достаточно сложный путь. И не факт, что это удастся сделать. Но если удастся - это будет идеальным решением.
  3. Отказаться в плагинах от VCL и писать на чём-нибудь другом, что не будет требовать объекта для родительского окна.
  4. Частично отказаться в плагинах от VCL. Суть в том, что MDI-Child форму нужно создать на WinAPI. Либо же (что лучше) - попросить ядро создать нам MDI-Child форму. После чего на эту заготовку "плюхнуть" форму из плагина - уже обычную, не MDI-Child. Убрать заголовок и растянуть по заготовке. Получится форма-в-форме, а визуально это будет выглядеть как MDI-Child форма из плагина. В целом тут будет куча мелких сложностей с синхронизацией двух форм.
В будущей статье я рискну попробовать вариант 2. А если мне это не удастся - то вариант 4.

Резюме по UI в приложениях с плагинами

Итак, как вы уже поняли, основной момент при разработке UI в приложениях с плагинами - изоляция. Вам нужно максимально изолировать ядро и плагины. Отдельное окно, показываемое плагином - это хорошее решение. Элемент управления, созданный плагином, размещаемый в окне программы-ядра - это крайне плохая идея.

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

Если вам нужно размещать в окне программы-ядра элементы управления для плагинов, то вы должны поступать так же, как мы делали ранее с меню программы: т.е. ядро должно предоставлять способ создания и интерфейс управления для... кнопок, полей ввода, панелей, переключателей, изображений, списков выбора, главных и контекстных меню и так далее. Да, вам придётся создавать интерфейсы, делать оболочки - для каждого типа! Уфф, надеюсь это заставит вас проголосовать за перевод VCL (а теперь уже - FMX) на интерфейсы.

Как компромиссный вариант - размещение элементов управления плагина в отдельном окне и встаивание этого окна (контейнера) в окно программы-ядра. Я попробую рассмотреть этот вариант, когда я буду говорить про MDI окна в плагинах.

Заключение

Итак, в этой статье мы рассмотрели основы реализации визуального интерфейса в системе плагинов. Хочу заметить, что эта статья - лишь начало пути, т.к. тема эта, как уже было сказано, достаточно объёмна. Вам придётся самостоятельно подбирать решения для желаемого вами поведения на стыке ядра и плагинов. Основы я продемонстрировал, а дальше - вам придётся взять в руки утилиты, закопаться в недра VCL и писать код.

Скачать пример к этому моменту можно тут.

В следующих статьях серии: основы расширения и обратной совместимости, демонстрация универсальности полученной системы плагинов.

34 комментария :

  1. "Матчасть" по правилам русского языка пишется слитно.

    ОтветитьУдалить
  2. Для слова "модуль" множественное число будет "модули", а не "модуля".

    МодулЯ, указателЯ, дескрипторА... вы что, литературных негров наняли?

    ОтветитьУдалить
  3. Александр, интересует вопрос кросплатформы.
    Будут ли эти решения, в которых не COM но есть интерфейсы с гуидами работать на Маке
    (ну естественно если заменить VCL на FM)?

    ОтветитьУдалить
  4. Про модулЯ, указателЯ и мат-часть
    Ну бюрократы... и здесь от вас покоя нет. Вот для меня в программировании, как переменную не назови - результат не изменится. Хоть всю программу на матерных переменных пиши.... Если на логику слово в предложении не влияет и смысл понятен, зачем такое пристальное внимание на это обращать.
    Извиняюсь за небольшой флуд.

    Александру, большое спасибо за продолжение темы.

    ОтветитьУдалить
  5. > Будут ли эти решения, в которых не COM но есть интерфейсы с гуидами работать на Маке (ну естественно если заменить VCL на FM)?

    Не могу сказать наверняка: у меня нет Apple-ских устройств.

    В целом подозреваю, что работать должно без проблем - основываясь на этой цитате: "IUnknown serves as the base for Mac OS X's Core Foundation CFPlugIn framework."

    ОтветитьУдалить
  6. > вы что, литературных негров наняли

    Достаточно выделить ошибку и нажать Ctrl + Enter.

    ОтветитьУдалить
  7. Я как-то думал о том, как вставить MDI-child форму в плагин. И для себя решил, что если когда-то буду такое делать, то скорее всего вообще откажусь от создания самой формы в плагине. Вместо этого, пусть плагин создаёт все контролы на TPanel или на TFrame, а саму форму пусть создаёт хост и передаёт плагину хэндл. Правда тогда теряется контроль над событиями формы. Но для самых важных событий, можно сделать свои обёртки, о неважнымии можно и пожертвовать.

    ОтветитьУдалить
  8. Немного про "подводные камни".
    В оконную процедуру TApplication передаются такие сообщения, как CM_ACTIONEXECUTE и CM_ACTIONUPDATE. Если использовать
    Application.Handle := FWnds.ApplicationWnd;
    и писать код приложения и плагина на разных версиях Delphi, то вернется проблема передачи объектов между приложением и плагином. При сильно разных версиях Delphi приложение сразу упадет с ошибкой Access Violation.
    Можно использовать TApplication.HookMainWindow и не пропускать эти сообщения, но это повлияет на функциональность основного приложения.

    ОтветитьУдалить
  9. Chaa, насколько я понимаю, Application приложения эти сообщения будут отправляться только если ActionList в DLL не смог обработать Action.

    ОтветитьУдалить
  10. Последовательность такая:
    ActionList.UpdateAction(Self)
    Application.UpdateAction(Self)
    OnUpdate
    SendAppMessage(CM_ACTIONUPDATE, 0, Longint(Self))

    Проблема больше не в Execute, а в Update. Если для Action-а не установлен обработчик OnUpdate, то будет послано сообщение. А это не такой уж редкий случай, если действие всегда доступно.
    Кроме того, для выполнения Action-ов есть еще второй механизм: HandlesTarget - UpdateTarget - ExecuteTarget. Именно он и запускается в обработчике CM_ACTIONEXECUTE в TApplication.

    ОтветитьУдалить
  11. Тогда (как я понял) достаточно присвоить обработчик OnActionUpdate/OnActionExecute объекту Application в плагинах и устанавливать там Handled в True?

    ОтветитьУдалить
  12. Есть еще много недостатков использования VCL в длл, на вскидку:

    Например проблемы с использованием TThread.Synchronize, который не будет работать внутри длл. В длл придется либо писать свой аналог через SendMessage нашему окну, либо выводить подобный метод в интерфейсе приложения.

    Так же проблема размещения некоторых контролов на чужой форме (не всегда плагину нужно создавать форму, иногда просто пару контролов кинуть). В особенности это касается стандартных классов windows, типа BUTTON, TRACKBAR. Например эта особенность является подводным камнем, т.к. все стандартные контролы посылают WM_COMMAND родительскому окну. И дельфийский код ищет дочерний контрол и посылает ему соответствующее CM_ уведомление. А если контрол из длл то понятное дело он его не найдет.

    Возникнут проблемы с VCL-ным Drag&Drop-ом. Я думаю это очевидно.

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

    Другие недостатки - размер получаемых длл. От мегабайта и выше. Когда я разрабатывал систему для себя, я сначала поступил примерно так же как описано в статье. В последствии мне пришлось отказаться от этого подхода, т.к. я хотел чтобы каждый мой плагин имел строго определенный набор функционала. Один - это всплывающие окошки, другой - звуковые уведомления, третий - менеджер загрузок, четрветый - сам загрузчик с докачами и прочими плюшками, пятый - сниффер, шестой седьмой и восьмой - парсеры соснифанного траффика, девятый - хранилище (в котором хранят опции сами плагины) и так далее... когда посчитал, у меня вышло что весь проект в скомпилированном виде будет весить от 30 мбайт чисто код. При этом если убрать "дубликаты" VCL то там и 10 мбайт не набегало. В качестве выхода из этой ситуации пришлось делать интерфейсный врапперы для вцл начиная от IPersistent. Задача значительно облегчается начиная с д2010, которая со своими возможностями RTTI позволяет написать код, полностью генерирующий такие врапперы.

    ОтветитьУдалить
  13. > Так же проблема размещения некоторых контролов на чужой форме (не всегда плагину нужно создавать форму, иногда просто пару контролов кинуть). В особенности это касается стандартных классов windows, типа BUTTON, TRACKBAR. Например эта особенность является подводным камнем, т.к. все стандартные контролы посылают WM_COMMAND родительскому окну. И дельфийский код ищет дочерний контрол и посылает ему соответствующее CM_ уведомление. А если контрол из длл то понятное дело он его не найдет.
    > Как например - проблематично добавить свой пункт скажем в попап меню. Проблематично воспользоваться имейджлистом, чтобы наш плагин не дублировал ресурсы.

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

    > когда посчитал, у меня вышло что весь проект в скомпилированном виде будет весить от 30 мбайт чисто код. При этом если убрать "дубликаты" VCL то там и 10 мбайт не набегало

    Можно было плагины собрать как DLL с пакетами. Пакет можно было делать стандартный rtl/vcl или же собрать свой собственный. При таком подходе общий код будет содержаться в пакетах и не дублироваться.

    ОтветитьУдалить
  14. Сам себе ещё пометку сделаю, чтобы не забыть - про потоки отдельно сказать хотел.

    ОтветитьУдалить
  15. Лично я очень негативно отношусь к идее двух VCL в одном приложении. Всякие костыли типа предложенных для MDI если и будут работать, то только в простейших случаях. Как только потребуется что-нибудь посложнее, типа скинов, все сразу посыпется.
    Так что только создание VCL-объектов на стороне хоста. Это может быть простая передача dfm + кастомный механизм привязки событий. Либо более продвинутый вариант: передача dfm и скрипта, который будет дергать интерфейс плагина (эдакий MVC). Кстати у некоторых скриптовых движков имеется встроенная поддержка работы с dfm.

    ОтветитьУдалить
  16. куда-то исчез мой предыдущий пост :(
    ну да ладно

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

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


    >Можно было плагины собрать как DLL с пакетами. Пакет можно было делать стандартный rtl/vcl или же собрать свой собственный. При таком подходе общий код будет содержаться в пакетах и не дублироваться.

    От плагинов хотелось бы еще некоторой языковой переносимости, чтобы можно было и из плюсцов, и из васиков там всяких писать. Кроме того существуют некоторые проблемы при использовании стандартных модулей в пакетах, например модуль ComServ нельзя включать в пакет.

    Кроме того, с возможностями RTTI Delphi2010 и выше не сильная проблема написать программу генерирующую код с врапперами для этой цели. Возможно я как-нибудь соберусь с духом, и напишу статью на основе своих разработок.

    Кстати давно хотел спросить, как вы находите мотивацию писать такие подробные статьи. Для меня когда я что-то изучил/сделал, писать становится сложно, ибо оно тривиально и понятно.

    ОтветитьУдалить
  17. > куда-то исчез мой предыдущий пост :(

    BlogSpot решил, что это спам. Вернул.

    > Кроме того ведь в VCL можно удобно на форму например назначит попапменю... если форма в длл а меню в приложении - выкручиваться через винапи.

    Почему-же. Никто же не запрещает плагину попросить ядро создать popupmenu и/или пункты в нём.

    > От плагинов хотелось бы еще некоторой языковой переносимости, чтобы можно было и из плюсцов, и из васиков там всяких писать.

    Сборка DLL с пакетами не привносит проблем совместимости с другими языками - потому что протокол плагинов не использует пакеты. Пакеты используются только сугубо внутри двух десятков плагинов Delphi для разделения кода между ними. Больше никаких бонусов с них мы не получаем. Даже менеджер памяти разделяемый не используем.

    Пожалуй, надо будет мне примеры расширить.

    > Кстати давно хотел спросить, как вы находите мотивацию писать такие подробные статьи.

    Честно - не знаю :) Я рассматриваю это как хобби/отдых. Кроме того, изложение материала на бумаге иногда помогает и самому упорядочить всё в голове.

    ОтветитьУдалить
  18. > Проблематично воспользоваться имейджлистом, чтобы наш плагин не дублировал ресурсы.

    Можно сделать как-то так:
    ImageList1.ShareImages := True;
    ImageList1.Handle := ACore.ImageListHandle;

    > Достаточно присвоить обработчик OnActionUpdate/OnActionExecute объекту Application в плагинах и устанавливать там Handled в True?

    Нет, потому что тогда OnUpdate/OnExecute у самого TAction не будет вызываться. Можно для всех Action-ов назначить обработчики OnUpdate и OnExecute, но это только частичное решение проблемы.

    Дело в том, что есть три способа работы с Action-ми.

    1. Обработчики событий OnUpdate/OnExecute у объектов TActionList, TApplication или TAction.

    2. Метод UpdateAction/ExecuteAction у контролов (см. TCustomStatusBar.ExecuteAction).

    3. Вызовы HandlesTarget/UpdateTarget/ExecuteTarget у объектов-наследников TCustomAction (см. StdActns).

    Пример кода и вообще про реализацию плагинов в одной системе отечественного производства можно посмотреть здесь.

    Плагины там работают только в модальном режиме, и при вызове плагина я подменял оконную процедуру для TApplication и фильтровал в ней сообщений CM_ACTIONUPDATE/CM_ACTIONEXECUTE.

    ОтветитьУдалить
  19. Ооо, ЛОЦМАН... пример того, как не надо делать плагины :)

    ANSI нуль-терминированные строки совместно с интерфейсами, пересечение границ модулей исключениями, отдельные функции работы для разных языков,... не говоря уже об обычных ошибках реализации. Типа, неверных сигнатур функций :)

    Но это всё равно уже лучше многих других вариантов.

    ОтветитьУдалить
  20. > Нет, потому что тогда OnUpdate/OnExecute у самого TAction не будет вызываться.

    Вот этого не понял. CM_ACTIONEXECUTE отправляется из единственного места - TContainedAction.Execute. И делается это в последнюю очередь. А до этого вызывается ActionList.ExecuteAction, Application.ExecuteAction и свой Execute.

    ОтветитьУдалить
  21. > Ооо, ЛОЦМАН... пример того, как не надо делать плагины

    Приходится работать с тем, что есть. Импортные решения на порядок дороже.

    Сейчас посмотрел внимательнее, на первый взгляд можно сделать обработчик Application.OnActionExecute примерно таким:

    procedure TForm1.ApplicationActionExecute(Action: TBasicAction; var Handled: Boolean);
    begin
    Handled := not Assigned(Action.OnExecute);
    end;

    Тогда все Action-ы, для которых не задан обработчик OnExecute, будут игнорироваться и не попадут из плагина в главное приложение.

    Получается еще одно ограничение VCL в плагине - не будут работать Action-ы из StdActns и подобные.

    ОтветитьУдалить
  22. Александр, возможно ли эту систему плагинов сделать в виде компонента? На данный момент вижу значительные преимущества по сравнению с другими системами и хотелось бы дальнейшего развития и желательно с регулярными обновлениями, чтобы, так сказать, морально не устаревала.

    ОтветитьУдалить
  23. Не очень понятно, что здесь будет делать компонент. Использование такой системы плагинов в программе подразумевает, что вы объявите новые интерфейсы под свою функциональность и зарегистрируете их в менеджере. Тут нет свойств и событий, которые мог бы выставлять наружу компонент. Поэтому, если сделать это в виде компонента, то получится вроде TXPManifest из старых Delphi: вся задача такого компонента - подключить модуль системы в uses.

    ОтветитьУдалить
  24. Пускай будет вроде TXPManifest, но при этом хотя бы будет понятно по версии компонента, когда происходили изменения. А то сейчас в процессе написания статей исходники системы меняются...
    А насчёт событий, то неплохо было бы добавить например такие события как onLoadAll и onUnLoadAll, для того чтобы показать процесс загрузки/выгрузки плагинов в прогрессбаре. Это достаточно необходимые события, при наличии большого количества плагинов.

    >Пожалуй, надо будет мне примеры расширить.

    Поддерживаю, размер плагина имеет весомое значение, особенно когда этих плагинов больше сотни...

    ОтветитьУдалить
  25. > Получается еще одно ограничение VCL в плагине - не будут работать Action-ы из StdActns и подобные.

    Chaa, а можно демку на проблемы при использовании Action?

    ОтветитьУдалить
  26. прежде всего , спасибо за очень интересную серию статей.
    у меня тут возник вопрос по поводу встроенных форм / фреймов. как офрмляется встраивание дочерней формы из плагина в основную форму программы?

    ОтветитьУдалить
  27. Добрый день,
    цикл статей интересный, с созданием формы проблем нет, а вот как сделать чтобы при выгрузке плагина форма удалялась? Намекните плиз. Спасибо.

    ОтветитьУдалить
  28. Вы хотите сделать выгрузку плагинов по запросу? Это довольно непросто сделать. В нашей модели выгрузку по запросу сделать можно, но при этом предполагается, что к моменту выгрузки все ссылки на плагин уже отпущены (читай: все ресурсы плагина освобождены). В целом, это самый простой для реализации способ, хотя не всегда может быть очевидно как освободить ресурсы плагина перед его выгрузкой.

    В некоторых случаях вы можете захотеть сделать форсированную выгрузку плагина. Это существенно сложнее, т.к. ссылку на ваш ресурс (в вашем примере - форму) может держать кто угодно. В таком случае плагин должен вести явный или неявный список всех ресурсов (читай: интерфейсов), отданных наружу. В момент, когда его попросят выгрузиться, плагин должен пройтись по всем этим ресурсам и попросить их выгрузить. Если все ресурсы были отпущены - выгружаемся. Соответственно, форма будет удалена вместе с отпусканием последней ссылки на неё.

    Само собой, чтобы это работало, необходимо, чтобы любой кто запрашивает ресурс, имел бы способ сказать ему "отдай вот этот". Это нетривиальная задача. И если вы пойдёте этим путём, то здорово осложните себе жизнь. Потому что в этом случае вам для каждого объекта придётся реализовывать метод "отпусти ресурс", в котором для каждого поля объекта реализовать "если это - тот ресурс, то поле - очистить". Да ещё при этом придумать что делать, если вашему объекту этот ресурс необходим. Короче, муть полная.

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

    ОтветитьУдалить
  29. >>Вы хотите сделать выгрузку плагинов по запросу?

    Да, чтобы плагин можно было выгрузить по ходу работы программы, при этом он корректно освободил ресурсы и удалил свою форму.

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

    ОтветитьУдалить
  30. Добрый день!
    Ссылка для скачивания примера - битая, файл там не найден.
    Как как можно получить его другим способом7

    ОтветитьУдалить
  31. Добрый день! Уже 2020 год на дворе.
    Дополнение к "части 6: UI в плагинах". По пункту MDI.

    Краткие решения, 4 пункта не рассматривал, как сложные и не вписываются в систему плагинов.
    Мое решение 5:
    - использовать Главную форму проекта (MainForm.FormStyle = fsMDIForm) с плагинами,
    - создавать дочерние MDI-формы (Form.FormStyle = fsMDIChildForm) в плагинах.
    Всё вписывается в систему плагинов.
    Такой вариант я реализовал, проверено на нескольких реальных плагинах. Всё работает.
    Дополнений получилось для комментария многовато. Составил изменения по юнитам ~300 строк, смогу отправить по почте для включения в Систему разработки плагинов, если такой вариант подойдет. GunSmoker, куда я могу отправить е-письмо?

    Николай

    ОтветитьУдалить
    Ответы
    1. У меня нет сейчас возможности/времени посмотреть подробно. Вы можете описать словами сам принцип того что вы делаете.

      Удалить
    2. Совсем кратко, это так.
      В PluginAPI.pas добавляются 2 интерфейса:
      + IMdiForm: Интерфейс главной MDI-формы
      + IMDIChild: Интерфейс дочерней MDI-формы

      В юните главной формы добавляется реализация IMdiForm в MainForm или MainFormProvider.
      Плагин создается как любой плагин по Разработке системы плагинов с IMDIChild. В плагине делается вызов и создание дочерней MDI-формы.
      Я такой вариант сделал, работает.

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.