Ktouth Brand. on Web

け〜くんこと K.Ktouth のだらだらした日常と突発的に作るプログラムや読み物とかの雑多サイト



MDIプログラムで、子フォームを.DLLに分割したい。その1

2005年09月05日 19:55更新 筆者:K.Ktouth

MDI形式のアプリケーションで、大量のフォームを持っている場合や、プラグインなどの形式で追加フォームをサポートしたい場合など、外部DLLからMDI子フォームを作成、表示したいことがあります。
しかし、そのまま作成した場合、MDI子フォームとして動作してくれません。

こんな際、Delphi2やC++Builder1で分割したい場合は、以下の方法をとります。
Delphi3,C++Builder3以降では、より簡潔な方法であるパッケージを用いた手法をご利用ください。

原因

MDIアプリに限らず、フォームを表示する際にTFormクラスは下記の3つのプロパティを参照しています。

  • Application.Handle
  • Application.MainForm
  • Screen.OnActiveFormChange

ですが、Applicationインスタンス・Screenインスタンスなどこれらの固有インスタンスは各モジュールで自動で個別に作成されるため、同じではありません
※ .DLLモジュールの中ではこの二つのインスタンスは生成されないようです。
そのため、フォームのインスタンスが生成される際に「親ウィンドウがない」と判断されてしまうのです。

対処方法

つまり、これら統合に必要なインスタンスを一致させると問題は解決します。

この場合、MDI親フォームのある.exeファイルと子フォームのある.dllファイルで、子フォームを生成する前にメインとなるモジュールの固有インスタンスをコピーしてやる必要があります。

その際には……

  1. .DLLを解放する前にコピーしたインスタンスを元に戻す。
    ※.DLL側でインスタンスが解放されないようにするため
  2. メニューハンドルリストなど、VCL外部から参照できない固有インスタンスもある。
    ※ 必要に応じてそれらを利用するインスタンスを.exeファイル側で再生成することになります。

……ということに注意する必要があります。

また、この方法をとると、『子フォームのメインメニューをマージした場合、メニューがうまくTMenuItemを参照出来ない』のという問題があります。
TMenuクラスはWindowsのメニュー機能のラッパークラスであり、その動作もWindowメッセージを使って実装されています。メニューに対する操作はフォームのメッセージ機構にメニューIDと対となって届きます。
このメニューIDは重複しないようVCLランタイムの中で一元管理されているわけですが、それはすなわち.exe/.dllの各々で別々に管理されてしまう、という意味でもあります。
この管理機構も統一しなければ別々のメニュー項目に同じIDが割り振られるという事になるわけですが、統一しようにもVCLの外からは隠蔽されているため、Application/Screenインスタンスのように簡単にはいきません。

下記のサンプルでは、この問題に対して『親フォーム(.exe)側でメニューを再生成する』事でこの問題を回避しています。
TMDIParent.RecreateComponentメソッドは未検証です。
バグや改善点がありましたら是非ご連絡ください。

{--- 子フォームのあるモジュール ---}

library samDll

uses samMDIChild is 'samMDIChild.pas';  // MDIチャイルドフォーム

Exports

InitInstance name 'InitInstance',
GetMDIChildClass name 'GetMDIChildClass';

begin
end.
{--- 子フォームのユニット ---}

unit samMDIChild;

type
  TMDIChild = class(TForm)
    (省略)
  end;

procedure InitInstance(AApp: TApplication; AScr: TScreen);
function GetMDIChildClass: TFormClass;

imprimentation

var
  // もともとのApplicationインスタンス
  vOldApp: TApplication;
  // もともとのScreenインスタンス
  vOldScr: TScreen;

procedure InitInstance(AApp: TApplication; AScr: TScreen);
begin
  // まだ待避を行っていなかったなら…
  if not Assigned(vOldApp) then
  begin
    vOldApp:=Application;
    vOldScr:=Screen;
    Application:=AApp;
    Screen:=AScr;
  end;
end;

function GetMDIChildClass: TFormClass;
begin
  Result:=TMDIChild;
end;

initialization
  // 判定で使用する為、初期化の必要がある。
  vOldApp:=nil;

finalization
  if Assigned(vOldApp) then // 待避を行っているなら…
  begin
    Application:=vOldApp;
    Screen:=vOldScr;
  end;

end.
{--- 親フォームのユニット ---}

unit samMDIParent;

type
  TMDIParent = class(TForm)
    (省略)
  private:
    // 子フォームライブラリのモジュールハンドル
    hModule: THandle;
    // 子フォームのクラス参照型(Formsユニットで定義済み)
    fChildClass: TFormClass;
    procedure CreateChild(Title: string);
    proceudre LoadDll;
    // 子フォームの特定コンポーネントを再生するメソッド
    function RecreateComponent(
                         ACompo: TComponent): TComponent;
  end;

imprimentation

uses Menus;

procedure TMDIParent.CreateChild(Title: string);
begin
  // 初回のみライブラリを参照する。
  if not Assigned(fChildClass) then LoadDll;
  with fChildClass.Create(Application) do
  begin
    Caption:=Title;
    RecreateComponent(Menu); // メニューを再生成する
    {その他の処理}
  end;
end;

procedure TMDIParent.LoadDll;
var
  vInit: procedure(AApp: TApplication; AScr: TScreen);
  vClass: function: TFormClass;
begin
  if hModule = 0 then
  begin
    hModule:=LoadLibrary('samDll.dll');
    if hModule = 0 then
      raise Exception.Create(
                'ライブラリの読み込みに失敗しました');
    // インスタンスの初期化
    vInit:=GetProcAddress(hModule, 'InitInstance');
    if Assigned(vInit) then
      vInit(Application, Screen)    // インスタンス引き渡し
    else
      raise Exception.Create(
                'ライブラリの初期化に失敗しました');
    // 子フォームのクラス参照型の取得
    vClass:=GetProcAddress(hModule, 'GetMDIChildClass');
    if Assigned(vClass) then
      fChildClass:=vClass;    // クラス取得
    else
      raise Exception.Create(
                '子フォームクラスの取得に失敗しました');
    end;
  end;
end;

function TMDIParent.RecreateComponent(
                         ACompo: TComponent): TComponent;
var
  i: Integer;
begin
   if Assigned(ACompo) then
   begin
     Result:=TComponentClass(
                ACompo.ClassType).Create(ACompo.Owner);
     Result.Assign(ACompo); // プロパティのコピー
     if Result is TMenuItem then
       when Result as TMenuItem do
       begin
         Clear; // 子メニューをいったん削除
         for i:=0 to TMenuItem(ACompo).Count - 1 do
           // 子メニューを再生成
           Add(RecreateComponent(TMenuItem(ACompo).Items[i]));
       end;
     end;
   else
     Result:=nil;
   end;
end;
本日のリンク元
その他のリンク元
検索