戻る ホーム 上へ 進む

MDIの不具合対処

Tip

Delphi 2/3/4/5(5以前) で作成した MDI アプリケーションには、私が知っている範囲で3個の問題があります。

  1. 子フォームのシステムメニューからメニュー項目「閉じる」を削除すると、クローズボタンも淡色化して押せなくなるが、子フォームを最大化するとクローズボタンは淡色化したままだが押すことが出来る。
    tips13-1
  2. 子フォームがメインメニューを持つ場合、子フォームを最大化して、Window メニューで子フォームを切り替えると、子フォームのクローズボタンが淡色化する(但し押すことは出来る)。
    tips13-2
  3. 子フォームが最大化している時にメニューの項目を実行時に追加/削除/変更すると、フレームメニュー内に有る子フォームのシステムメニューやボタン類が消えてしまう。
    tips13-3

これらの問題の内 1 は子フォームの OnClose ハンドラで閉じないように 指定することで簡単に対処できますが、2 と 3 は厄介です。

そこで、2と3に対処するコードを含んだメインフォームのコードを紹介します

unit MainF;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs,  Menus, StdCtrls;

type
  TMainForm = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure ActiveFormChange(Sender: TObject);
  private
    FixMenuMessage: UINT;
  protected
    procedure DefaultHandler(var Message); override;
  public
    procedure FixMenu;
  end;

var
  MainForm: TMainForm;

implementation


{$R *.DFM}

// FixMenu 起動用メッセージの登録
// Screen 変数の OnActiveFormChange ハンドラの登録
procedure TMainForm.FormCreate(Sender: TObject);
begin
  FixMenuMessage := RegisterWindowMessage(PCHAR('FixMenuMessage'));
  Screen.OnActiveFormChange := ActiveFormChange;
end;

// ActiveForm の変更を検出したら FixMenu メッセージをポスト
procedure TMainForm.ActiveFormChange(Sender: TObject);
begin
  // アプリケーション終了時もアクティブフォームの変更が
  // 起きるが、この時既にメインフォームは破棄されているので
  // メッセージは送らない。
  if not Application.Terminated then
    PostMessage(Handle, FixMenuMessage, 0, 0);
end;

// アクティブフォーム変更時、アクティブフォームが子フォーム
// で最大化しているなら、メニューを修復する。
procedure TMainForm.DefaultHandler(var Message);
begin
  inherited DefaultHandler(Message);

  if (FixMenuMessage <> 0) and
     (TMessage(Message).Msg = FixMenuMessage) and
     (ActiveMDIChild = Screen.ActiveForm) then
    FixMenu;
end;

// メニューの修復
procedure TMainForm.FixMenu;
var Size: LongInt;
begin
  // 偽の WM_SIZE をアクティブ子フォームに送って
  // Windows をだます。
  if (ActiveMDIChild <> Nil) and
     (ActiveMDIChild.WindowState = wsMaximized) then begin
      Size := ActiveMDIChild.ClientWidth +
              (Longint(ActiveMDIChild.ClientHeight) shl 16);
      SendMessage(ActiveMDIChild.Handle, WM_SIZE, SIZE_RESTORED, Size);
      SendMessage(ActiveMDIChild.Handle, WM_SIZE, SIZE_MAXIMIZED, Size);
  end;
end;

end.

このメインフォームを使えば、自動的に 2 の問題は解決します。3 の問題は下記のようにメニューの変更の後にメインフォームのメソッド FixMenu を呼べばメニューが修復されます。

procedure TMainForm.NewItem1Click(Sender: TObject);
var NewItem: TMenuItem;
begin
  // ファイルメニューに「プリント」メニューを追加。
  NewItem := TMenuItem.Create(File1);
  NewItem.Caption := 'Print';
  File1.Add(NewItem);

  // メニューの修復
  FixMenu;
end;

尚、本問題のうち 2, 3 は Delphi 6 では直っています。1は依然として対処が必要です。

また、WIndows NT系列のWindows では 1 の問題は起きません。

解説

MDI の 問題 2 の原因は、メニューのマージ処理が子フォームの最大化を考慮していないことに起因しています。

子フォームが最大化すると、MDI アプリケーションのクライアントウィンドウは自動的にフレームフォームのメニューに子フォームのシステムメニューやボタン類を挿入します。一方、アクティブな子フォームが切り替わると VCL は親フォームのメインメニューと新たにアクティブになった子フォームのメインメニューをマージします。

子フォームが最大化していない時は問題はないのですが、マージ処理ではフレームフォームの全てのメニュー項目をいったん削除し作り直すので、クライアントウィンドウが挿入した子フォームのシステムメニューやボタン類も削除されてしまいます。

VCL はこの消えたメニューを偽の WM_SIZE メッセージを子フォームに送ることによって復活させようとしますが、Windows 95 では WM_MDIACTIVATE のメッセージ処理中にこの処置を行うとクローズボタンの表示に不具合が出るようです。Tip での処置は、偽の WM_SIZE メッセージを WM_MDIACTIVATE の処理の「外」で送るように改良したものです。

3 の原因は 2と同じなのですが、VCL はメニューを修復する努力を 3 の場合は 全く行わないためメニューの表示が完全におかしくなってしまいます。つまりマージ処理により子フォームのシステムメニューやボタン類が削除されたメニューがそのまま表示されてしまうのです。3 の場合は 2の様にメインフォーム側で対処するのは難しいので FixMenu メソッドを明示的に呼び出してもらうことで対処することにしました。

戻る ホーム 上へ 進む

inserted by FC2 system