第21章 メニューの研究 その1

ユーザーインタフェースの中で最も重要なのは、たぶん、メニューであろう。これまで本稿では、 骨格プログラムでメニューを使用してきたが、 それは、2−3 メニューを装備する 節でコードにより動的に作成したものだった。メニューを作るには以下の三つの方法がある。

  1. メニューリソースを作成し、初期化時にロードして作成する。
  2. API関数を用いて、コードにより動的に作成する。
  3. メニューテンプレートをメモリ上に作成し、ロードして作成する。

最後の方法はあまり使われないが、前二方法の中間的なメニュー作成方法である。本章と次章では、リソースを用いる方法を簡単に説明し、あとはもっぱらAPI関数を用いてメニューを動的に作成し、制御する方法を調べることにする。

21−0 この章の内容

第21章 メニューの研究 その1
21−1 メニューリソース
21−2 複雑なメニューのリソース
21−3 メニューの構造
21−4 メニュー作成関数
21−5 メニュークラスをつくる
21−6 ラジオ項目、ビットマップ項目
21−7 メニューメッセージ
21−8 イベントハンドラを追加する


| arrw18c.gif 前の章へ | 総目次へ | 次の章へ arrw18d.gif

21−1 メニューリソース

本稿のような、大きなクラスライブラリやRAD開発環境を使わないコードベースのウィンドウズプログラミングでのメニュー作成の正統的な方法は、メニューリソースを使う方法である。 この節では、 メニューリソース入門として、メニューリソースをつかって Skeleton03 骨格プログラムと同じメニューを作成してみよう。

第19章 アイコン、ビットマップ、リソース の章では、 ビットマップやアイコンをリソースとして実行可能ファイル (*.exe) に取り込む方法を示した。 そこでは、テキストエディタでビットマップファイルやアイコンファイルをリソースとして定義する「リソース記述ファイル」(*.rc) を作成し、それを Delphi に付属しているリソースコンパイラでコンパイルしてリソースファイル (*.res) を作成し、それをコードの中で {$R *.res} のコンパイラ指令を使って実行可能ファイルに取り込んでいた。 Delphi5 では、リソース記述ファイルをプロジェクトに加えると自動的にコンパイルされるらしいが、基本的な手順は同じである。 メニューリソースも全く同様の手順により実行可能ファイルに取り込むことができる。 それでは早速試してみよう。

  1. エクスプローラを使って、新しいフォルダを 2101MenuResource01 という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 2004RadioGroup フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで MenuResource01.dpr という名前で保存する。
  5. コードエディタで以下のように編集する。
  6. [F.9]でコンパイルして実行する。

最初に、メニューリソース記述ファイルをつくる準備として、ユーザがメニュー項目を選んだとき WM_COMMAND メッセージの WParam の下位ワードとしてウィンドウ関数に送られるメニューIDの定数を各メニュー項目について定義しなければならない。この定数定義は、リソース記述ファイルからもプログラムコードからも見えなければならない。 それには、インクルードファイルを作ればよい。テキストエディタで以下のような内容のテキストファイルを作成する。

{       Name : MENUID.INC
   Description : Include file for Menu ID 
}
const
  M_EXIT = 300;
  M_TEST = 301;
  M_ABOUT= 302;

このファイルを、2101MenuResource01 フォルダに 'MenuID.inc' という名前でセーブする。 Skeleton03 骨格プログラムと同じメニューのリソース記述ファイルは以下のようになる。

/////////////////////////////////////////////////////
//        File: MyRes.RC
//     
// The resource for the MenuResource01 program
/////////////////////////////////////////////////////

#include "MENUID.inc"

// The Menu 
MyMenu MENU  DISCARDABLE
BEGIN
  POPUP "&File"
    BEGIN
      MENUITEM "E&xit",		M_EXIT
    END
  MENUITEM "&Test!",		M_TEST
  POPUP "&Help"
    BEGIN
      MENUITEM "&About..",		M_ABOUT
    END
END

このテキストファイルを 2101MenuResource01 フォルダに 'MyRes.rc' という名前でセーブする。 #includeMenuID.Inc ファイルを取り込んでいるので M_EXIT などの定数が使える訳である。 このリソース記述ファイルを 19−3 リソースコンパイラ 節で作った ResourceCompiler.exe でコンパイルして、MyRes.res というファイルができたらメニューリソースの完成である。 Skeleton03 のメニューと上記のリソース記述ファイルの内容を見較べると、メニューリソースの記述の仕方が理解できるだろう。 このリソースファイルを使用してメニューをつくるプログラムを MenuResource01.dpr に作ろう。 以下に全リストを示す。

program MenuResource01;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas';

  {$R MyRes.res}

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
  {$I MenuID.INC}

var
  MainWnd: TSDIMainWindow;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
  hM:HMENU;
begin
  hWindow := MainWnd.Handle;

  hM := LoadMenu(hInstance,'MyMenu');
  SetMenu(hWindow,hM);

  with MainWnd do begin
    ClientWidth := 360;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
begin
  MessageBeep($FFFFFFFF);
end;

procedure MainCommand(var Msg: TMessage);
var
  hWindow: HWND;
  s: string;
begin
  hWindow := MainWnd.Handle;;
  if (Msg.WParamHi+Msg.LParam)=0 then
    case Msg.WParamLo of
      M_EXIT: DestroyWindow(hWindow);
      M_TEST: OnTest(hWindow);
      M_ABOUT:begin
        s := 'OBJECT PASCAL API PROGRAM '+#13#13+
             '          By  Delphian Inc.,'+#13#13+
             '         Created by Delphi32    '+#13#13;
        MessageBox(hWindow,PChar(s),'About ...',MB_OK);
        end;
    end;
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifMenuResource01.zip(13.570kB)
MenuResource01.gif

実行すると右図のように、Skeleton03 と同じように動作する。

コードを見てみよう。uses 節のすぐ下で

{$R MyRes.res}

のコンパイラ指令でリソースをリンクしている。 その下には、

{$I MenuID.INC}

のコンパイラ指令により、MenuID.inc ファイルをインクルードしている。 この指令のある場所のところにインクルードファイルの内容そのものがコピーされると期待して良い。 メニューは、MainCreate( ) ハンドラの以下のコードで作られる。

 hM := LoadMenu(hInstance,'MyMenu');
 SetMenu(hWindow,hM);

最初のAPIは、リソースから指定の名前のメニューをロードしてハンドルを返す。

  function LoadMenu(hInstance: HINST; 
                   lpMenuName: PAnsiChar): HMENU; stdcall;

SetMenu( ) でメインウィンドウにメニューを設定する。 MenuID.inc で定義したメニューIDの値は、 今まで使ってきた UtilFunc.pas での定義と同じ値にしたので、メッセージ処理は同じコードで実行される。

以上、本節で見てきたように、ビットマップやアイコンと同じ手続きでメニューもリソースとすることができる。画像ファイルはともかく、プログラムコードと密接な関係を持つメニューの作成情報がリソースとしてコードと切り離して作成されるのは、少々奇異な感じがする。しかし、ユーザインタフェースの作成情報をコードと切り離すという思想は、プログラムのメンテナンスや国際化対応を考えると納得できるものだ。 後に示すように、ダイアログボックスでさえもリソースから作成されるのである。

redball.gifこの章の目次へ戻る

21−2 複雑なメニューのリソース

前節のメニューでは簡単すぎるので、ここでは、少々入り組んだメニューのリソース例を示す。 前節と同じ手続きを踏んで作られるリソースを 2102MenuResorce02 フォルダに作って、同じプログラムを MenuResource02.dpr として作成してほしい。

インクルードファイル MenuID.inc を示す。

{       Name : MENUID.INC
 Description : Include file for Menu ID
}

const
  M_EXIT = 300;
  M_TEST = 301;
  M_ABOUT= 302;

  M_NEW  = 303;
  M_OPEN = 304;
  M_SAVE = 305;

  M_ALPHA = 306;
  M_BETA  = 307;
  M_GAMMA = 308;
  M_DELTA = 309;
  
  M_FIRST = 310;
  M_SECOND = 311;

以下に、リソース記述ファイルを示す。

/////////////////////////////////////////////////////
//        File: MyRes.RC
//     
// The resource for the MenuResorce02 program
/////////////////////////////////////////////////////

#include "MENUID.inc"

// The Menu 
MyMenu MENU  DISCARDABLE
BEGIN
  POPUP "&File"
    BEGIN
      MENUITEM "新規作成(&N)",	 M_NEW
      MENUITEM "開く(&O)",       M_OPEN
      MENUITEM "保存(&S)",       M_SAVE
      MENUITEM SEPARATOR
      MENUITEM "E&xit",		 M_EXIT
    END
  MENUITEM "&Test!",		 M_TEST
  POPUP "&Option"
    BEGIN
      MENUITEM "&Alpha",         M_ALPHA
      POPUP "&Option2"
        BEGIN
          MENUITEM "&Beta",      M_BETA
          POPUP "&Option3"
            BEGIN
              MENUITEM "&Delta", M_DELTA
              MENUITEM "&Gamma", M_GAMMA
            END
          MENUITEM SEPARATOR
          MENUITEM "&Second",    M_SECOND
        END
      MENUITEM "&First",         M_FIRST
    END
  POPUP "&Help"
    BEGIN
      MENUITEM "&About..",       M_ABOUT
    END
END

DownloadExe.gifMenuResource02.zip(13.687kB)
MenuResource02.gif

実行時の様子を右図に示す。 作成されたメニューの構造と上のリソース記述ファイルを見較べてほしい。インデントは、ポップアップメニューの入れ子の関係と対応していることに気づくはずである。

redball.gifこの章の目次へ戻る

21−3 メニューの構造

本節以降の本章と次章では、 コードでメニューを動的に作成し、その機能をカプセル化してメニュークラスを新しく作る事を考える。 メニューが発行するメッセージの処理もこれらのクラスを使って行う。

骨格プログラム Skeleton03.dpr では、2−3 メニューを装備する 節のコードを関数にした UtilFunc.pas にある以下の関数でメニューを作成してきた。

procedure MakeInitMenu(hWindow: HWND);
var
  hM, hMp: HMENU;
begin
  hM := CreateMenu;

  hMp := CreateMenu;
  AppendMenu(hMp,MF_STRING,IDM_EXIT, 'E&xit');
  AppendMenu(hM,MF_POPUP,hMp,'&File');

  AppendMenu(hM, MF_STRING, IDM_TEST, '&Test!');

  hMp := CreateMenu;
  AppendMenu(hMp,MF_STRING,IDM_ABOUT, '&About..');
  AppendMenu(hM,MF_POPUP,hMp,'&Help');

  SetMenu(hWindow,hM);
end;

2−3 メニューを装備する でも述べたが、 CreateMenu で作った空のメニューに AppendMenu( ) で項目を追加してゆく方式を採っている。 本章の最初の節で作ったリソース記述ファイルと較べると、このコードで動的に作成する場合でも、特にタイピングの量が大幅に増えるわけではないことが分かる。 ここでの AppendMenu( ) は、第二パラメータに MF_STRING を指定してメニューに静的項目を追加する場合と、MF_POPUP を指定してポップアップメニューを追加する、二通りの使い方を示している。 上記の関数では、メニューハンドルを使い回しているので分かりづらいが、CreateMenu を3回使っていることが分かるだろう。 本節では、この関数を書き直してもっとすっきりした形にしてから、メニューの構造について簡単に考察する。

  1. エクスプローラを使って、新しいフォルダを 2103SkeletonMenu という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 2102MenuResource02 フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで SkeletonMenu.dpr という名前で保存する。
  5. コードエディタで以下のように編集する。
  6. [F.9]でコンパイルして実行する。

以下に SkeletonMenu.dpr の全リストを示す。

program SkeletonMenu;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  MainWnd: TSDIMainWindow;

  hMain,hFile,hHelp,hOption: HMENU;

const
  ID_EXIT = 200;
  ID_TEST = 201;
  ID_ABOUT = 202;
  ID_OPTION1 = 203;
  ID_OPTION2 = 204;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------
procedure AppendItem(hM:HMENU;mID:HMENU;s:string);
begin
  AppendMenu(hM,MF_STRING,mID,PChar(s));
end;

procedure AppendPopup(hM,hMpopup:HMENU;s:string);
begin
  AppendMenu(hM,MF_POPUP,hMpopup,PChar(s));
end;

procedure MakeMenu(hParent:HWND);
begin
  hMain := CreateMenu;

    hFile := CreateMenu;
      AppendItem(hFile,ID_EXIT,'E&xit');
      AppendPopup(hMain,hFile,'&File');

    AppendItem(hMain,ID_TEST,'&Test!');

    hHelp := CreateMenu;
      AppendItem(hHelp,ID_ABOUT,'&About..');

      hOption := CreateMenu;
        AppendItem(hOption,ID_OPTION1,'Option&1');
        AppendItem(hOption,ID_OPTION2,'Option&2');
        AppendPopup(hHelp,hOption,'&Option');

    AppendPopup(hMain,hHelp,'&Help');

  SetMenu(hParent,hMain);
end;
//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;

  MakeMenu(hWindow);

  with MainWnd do begin
    ClientWidth := 360;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
begin
  MessageBeep($FFFFFFFF);
end;

procedure MainCommand(var Msg: TMessage);
begin
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifSkeletonMenu.zip(13.803kB)
SkeletonMenu.gif

実行すると、右図のようにメニュー項目が少し違うだけで Skeleton03 と同じ動作をする。

コードを見てみよう。グローバル変数として次のように宣言している。

  hMain,hFile,hHelp,hOption: HMENU;

const
  ID_EXIT = 200;
  ID_TEST = 201;
  ID_ABOUT = 202;
  ID_OPTION1 = 203;
  ID_OPTION2 = 204;

メニューハンドルとして、四つの変数を宣言している。 メニューに静的項目とポップアップ項目を追加する二つのヘルパ関数を見てみよう。

procedure AppendItem(hM:HMENU;mID:HMENU;s:string);
begin
  AppendMenu(hM,MF_STRING,mID,PChar(s));
end;

procedure AppendPopup(hM,hMpopup:HMENU;s:string);
begin
  AppendMenu(hM,MF_POPUP,hMpopup,PChar(s));
end;

これは、上述の AppendMenu( ) の二つの機能を書き直しただけだ。 これらの関数を使って、実際にメニューを作成する関数を見てみよう。

procedure MakeMenu(hParent:HWND);
begin
  hMain := CreateMenu;

    hFile := CreateMenu;
      AppendItem(hFile,ID_EXIT,'E&xit');
      AppendPopup(hMain,hFile,'&File');

    AppendItem(hMain,ID_TEST,'&Test!');

    hHelp := CreateMenu;
      AppendItem(hHelp,ID_ABOUT,'&About..');

      hOption := CreateMenu;
        AppendItem(hOption,ID_OPTION1,'Option&1');
        AppendItem(hOption,ID_OPTION2,'Option&2');
        AppendPopup(hHelp,hOption,'&Option');

    AppendPopup(hMain,hHelp,'&Help');

  SetMenu(hParent,hMain);
end;

このコードと作成されたメニューの構造を見較べて頂きたい。 CreateMenu を4回つかって空のメニューを四つ作成し、その各々に静的項目とポップアップ項目を追加し、最後に hMain をメインウィンドウに設置しているのが分かるだろう。 このように、メニューは、メニューハンドルで表されるポップアップメニューが入れ子になって構成されているのである。 筆者には、上の関数で作られるメニューは、hMain の下に hFile hHelp がぶらさがっており、hHelp にはさらに hOption がぶら下がっているように思える。 hMain は、ウィンドウに直接設置され、メニューバーに常時その項目内容が表示されている、ということが他の三つのポップアップメニューと違っている点である。 これをトップレベルメニューという。トップレベルメニュー以外は、その階層がどれだけ深くても、その本質には差がない。以上をまとめると、

  1. メニューは、メニューハンドルで識別されるポップアップメニューが入れ子になった構造をしている。
  2. ポップアップメニューは、直接ウィンドウにアタッチされるトップレベルメニューとそれ以外の二種類がある。

となるだろう。 以上のことから、自分のメニューハンドルを管理し、メニュー項目を追加・挿入・削除するメソッドと有効性やチェック状態を取得・設定するプロパティーやメソッドを備えた二種類のクラスを作成すれば、メニューの全機能を利用できると考えられる。 以降の節では、 実際にこのようなクラスを作成してゆくことにする。

redball.gifこの章の目次へ戻る

21−4 メニュー作成関数

空のメニューをつくってメニューハンドルを返すAPIは、以下の二つがある。

function CreateMenu: HMENU; stdcall;
function CreatePopupMenu: HMENU; stdcall;

CreateMenu は主に空のトップレベルメニューを作成する。しかし、これまで使ってきたように、 この関数が返すハンドルを他のポップアップメニューに追加・挿入するとポップアップメニューとなる。 CreatePopupMenu は、空のトップレベルメニュー以外のポップアップメニューをつくってハンドルを返す。ともに失敗するとゼロを返す。

メニューハンドルが手に入ったら、静的項目やポップアップ項目を追加・挿入して、メニューの内容を作成する。 これまで使ってきた AppendMenu( )InsertMenu( ) を使うことが出来るが、最新のヘルプを読むと、拡張機能を持った InsertMenuItem( ) 関数を使うようにという注意書きがある。ここでは、この InsertMenuItem( ) を使って、メニュー項目の追加・挿入を行う関数を作ることにしよう。 Windows.pas にある宣言では、

function InsertMenuItem(p1: HMENU; 
                        p2: UINT; 
                        p3: BOOL; 
                  const p4: TMenuItemInfo): BOOL; stdcall;

となっている。 p1 には追加・挿入するメニューのハンドルを設定する。 p2 には、挿入する位置を指定する。追加は、末尾への挿入と考える。 p3true なら、p2 に設定する値は、メニューの一番上をゼロとしたときの挿入位置の番号を指定する。p3false なら、現在、挿入する位置にあるメニュー項目のIDを指定する。TMenuItemInfo 型の p4 で追加・挿入するメニュー項目の内容を設定する。 このレコードは、あとで説明する GetMenuItemInfo( ) などのメニューに関する情報取得用APIにも使用される。

type
  PMenuItemInfoA = ^TMenuItemInfoA;
  PMenuItemInfo = PMenuItemInfoA;
  TMenuItemInfoA = packed record
    cbSize: UINT;            // このレコードのサイズを設定(必須)
    fMask: UINT;             // 取得設定項目のマスク
    fType: UINT;             { used if MIIM_TYPE}
    fState: UINT;            { used if MIIM_STATE}
    wID: UINT;               { used if MIIM_ID}
    hSubMenu: HMENU;         { used if MIIM_SUBMENU}
    hbmpChecked: HBITMAP;    { used if MIIM_CHECKMARKS}
    hbmpUnchecked: HBITMAP;  { used if MIIM_CHECKMARKS}
    dwItemData: DWORD;       { used if MIIM_DATA}
    dwTypeData: PAnsiChar;   { used if MIIM_TYPE}
    cch: UINT;               { used if MIIM_TYPE}
  end;
  TMenuItemInfo = TMenuItemInfoA;

fMask フィールドには、以下の定数のうち一つ以上を設定する。

const
  MIIM_STATE = 1;      // fState フィールドの取得・設定
  MIIM_ID = 2;         // wIDフィールドの設定・取得
  MIIM_SUBMENU = 4;    // hSubMenu フィールドの設定・取得
  MIIM_CHECKMARKS = 8; // hbmpChecked と hbmpunchecked フィールドの設定・取得
  MIIM_TYPE = $10;     // fType と dwTypeData フィールドの設定・取得
  MIIM_DATA = $20;     // dwItemData フィールドの設定・取得

fType はメニュー項目の「タイプ」を取得・設定する。以下の定数を使う。

const
  MFT_STRING = 0;         // 文字列項目。dwTypeData の下位ワードは文字列へのポインタ(PChar 型)、
                             cch は文字列のサイズ
  MFT_BITMAP = 4;         // ビットマップ項目。dwTypeData の下位ワードはビットマップハンドル、
                             cch は無視
  MFT_MENUBARBREAK = $20; // メニューブレイク(メニューバーでの改行、ポップアップでは改列)
  MFT_MENUBREAK = $40;    // ほぼ上と同じ。改列にともなう”しきり線”を表示しない。
  MFT_OWNERDRAW = $100;   // オーナードローメニュー項目。
  MFT_RADIOCHECK = $200;  // ラジオメニュー項目。
  MFT_SEPARATOR = $800;   // セパレータ項目。
  MFT_RIGHTORDER = $2000; // メニューは、右から左へ並べられる
  MFT_RIGHTJUSTIFY = $4000; // メニューバー項目で以降の項目を右揃えにする

fState はメニュー項目の「状態」を取得設定する。以下の定数を使う。

const
  MFS_GRAYED = 3;      // 項目はグレイ表示(選択不可をあらわす)
  MFS_DISABLED = MFS_GRAYED;
  MFS_CHECKED = 8;     // 項目はチェックされている
  MFS_HILITE = $80;    // 項目はハイライト表示されている
  MFS_ENABLED = 0;     // 項目は有効である(デフォルト)
  MFS_UNCHECKED = 0;   // 項目はチェックされていない(デフォルト)
  MFS_UNHILITE = 0;    // 項目はハイライト表示されていない(デフォルト)
  MFS_DEFAULT = $1000; // 項目はそのメニューのデフォルト項目である

wID は、メニュー項目のIDを取得・設定する。これにより、セパレータやポップアップ項目にもIDが設定できる。

hSubMenu は、ポップアップメニューのハンドルを取得・設定する。

hbmpCheckedhbmpUnchecked には、カスタムチェックマークのビットマップハンドルを取得・設定する。

dwItemData は、アプリケーション定義のメニュー項目データを取得・設定する。

dwTypeDatacch は、fType の設定によって異なる使われ方をする。

以上のように InsertMenuItem( ) API関数は、低レベルで強力なメニュー作成関数であるが、何でもできる関数は単機能の関数としては使いづらいので、InsertMenuItem( ) を使って機能を絞った関数を幾つか作成して試してみよう。 コーディングに入る前に一つだけ以下の関数例を説明する。

function InsertStringMenu(hM:HMENU;position:integer;
                               st:string;mID:UINT):Boolean;
var
  MII:TMenuItemInfo;
  i:integer;
begin
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_ID or MIIM_TYPE;
    fType := MFT_STRING;
    wID := mID;
    dwTypeData := PChar(st);
    cch := Length(st);
  end;
  if position = -1 then begin
    i := GetMenuItemCount(hM);
    result := Boolean(InsertMenuItem(hM,i,true,MII));
  end else
    result := Boolean(InsertMenuItem(hM,position,false,MII));
end;

この関数は、 文字列項目を挿入するものである。 コードを調べよう。 TMenuItemInfo 型のローカル変数 MII をゼロで初期化した後、各フィールドを設定している。 fMask には、メニューIDとタイプを設定するので MIIM_ID or MIIM_TYPE を指定している。 そして、dwTypeData には文字列を、cch にはそのサイズを設定している。 position パラメータが -1 のときは、GetMenuItemCount( ) APIで現在の項目数を取得し、末尾に「追加」する。それ以外は、挿入位置をメニューIDで指定して メニュー項目を挿入する。ここでは、メニューをコードで動的に作成・変更する事を前提にしているので、挿入のときには InsertMenuItem( )p3 パラメータを必ず false に設定して、挿入位置の指定にはメニューIDを使うことを厳守しなければならない。 メニュー項目の位置が動的に変更されるので、いつでも同じ位置にあることを前提条件に出来ないからである。 したがって、ポップアップ項目やセパレータにもメニューIDを必ず設定することを心がけるようにする。 この関数を使って文字列項目を追加する関数をつくると以下のようになる。

function AppendStringMenu(hM:HMENU;st:string;mID:UINT):Boolean;
begin
  result := InsertStringMenu(hM,-1,st,mID);
end;

以上のように、AppendMenu( )InsertMenu( ) を完全に InsertMenuItem( ) 関数で置き換える事が出来た。 他のタイプの項目についても追加・挿入関数を作って試してみよう。

  1. エクスプローラを使って、新しいフォルダを 2104InsertMenuItems という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 2103SkeletonMenu フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで InsertMenuItems.dpr という名前で保存する。
  5. [表示]メニューから[ユニット]を選んで表示されるダイアログから、UtilFunc を選ぶ。
  6. コードエディタで以下のように編集する。
  7. [F.9]でコンパイルして実行する。

最初に UtilFunc.pas に各タイプのメニュー項目を追加・挿入する関数を作ろう。 FindOtherInstance( ) の宣言の下に以下の宣言を追加する。

function InsertPopupMenu(hM:HMENU;position:integer;
                         st:string;hMpop:HMENU;mID:UINT):Boolean;
function InsertStringMenu(hM:HMENU;position:integer;
                              st:string;mID:UINT):Boolean;
function InsertSeparatorMenu(hM:HMENU;position:integer;mID:UINT):Boolean;
function InsertRadioMenu(hM:HMENU;position:integer;
                              st:string;mID:UINT):Boolean;
function InsertBitmapMenu(hM:HMENU;position:integer;
                               hB:HBITMAP;mID:UINT):Boolean;

function AppendPopupMenu(hM:HMENU;st:string;hMpop:HMENU;mID:UINT):Boolean;
function AppendStringMenu(hM:HMENU;st:string;mID:UINT):Boolean;
function AppendSeparatorMenu(hM:HMENU;mID:UINT):Boolean;
function AppendRadioMenu(hM:HMENU;st:string;mID:UINT):Boolean;
function AppendBitmapMenu(hM:HMENU;hB:HBITMAP;mID:UINT):Boolean;

function GetMenuItemState(hM:HMENU;mID:UINT):UINT;
function IsMenuChecked(hM:HMENU;mID:UINT):Boolean;
function SetMenuChecked(hM:HMENU;mID:UINT;value:Boolean):Boolean;
function IsMenuEnabled(hM:HMENU;mID:UINT):Boolean;
function SetMenuEnabled(hM:HMENU;mID:UINT;value:Boolean):Boolean;

ここでは、ポップアップ、文字列、セパレータ、ラジオ、ビットマップのメニュー項目の挿入と追加のための関数を宣言している。 最後の五つの関数は、メニュー項目のチェック状態や使用可・不可状態を取得・設定する関数である。 対応する実装部のところに以下のコードを書く。

function InsertPopupMenu(hM:HMENU;position:integer;
                        st:string;hMpop:HMENU;mID:UINT):Boolean;
var
  MII:TMenuItemInfo;
  i:integer;
begin
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_ID or MIIM_TYPE or MIIM_SUBMENU;
    fType := MFT_STRING;
    wID := mID;
    hSubMenu := hMpop;
    dwTypeData := PChar(st);
    cch := Length(st);
  end;
  if position = -1 then begin
    i := GetMenuItemCount(hM);
    result := Boolean(InsertMenuItem(hM,i,true,MII));
  end else
    result := Boolean(InsertMenuItem(hM,position,false,MII));
end;

function InsertStringMenu(hM:HMENU;position:integer;
                               st:string;mID:UINT):Boolean;
var
  MII:TMenuItemInfo;
  i:integer;
begin
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_ID or MIIM_TYPE;
    fType := MFT_STRING;
    wID := mID;
    dwTypeData := PChar(st);
    cch := Length(st);
  end;
  if position = -1 then begin
    i := GetMenuItemCount(hM);
    result := Boolean(InsertMenuItem(hM,i,true,MII));
  end else
    result := Boolean(InsertMenuItem(hM,position,false,MII));
end;

function InsertSeparatorMenu(hM:HMENU;position:integer;mID:UINT):Boolean;
var
  MII:TMenuItemInfo;
  i:integer;
begin
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_ID or MIIM_TYPE;
    fType := MFT_SEPARATOR;
    wID := mID;
  end;
  if position = -1 then begin
    i := GetMenuItemCount(hM);
    result := Boolean(InsertMenuItem(hM,i,true,MII));
  end else
    result := Boolean(InsertMenuItem(hM,position,false,MII));
end;

function InsertRadioMenu(hM:HMENU;position:integer;
                              st:string;mID:UINT):Boolean;
var
  MII:TMenuItemInfo;
  i:integer;
begin
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_ID or MIIM_TYPE;
    fType := MFT_STRING or MFT_RADIOCHECK;
    wID := mID;
    dwTypeData := PChar(st);
    cch := Length(st);
  end;
  if position = -1 then begin
    i := GetMenuItemCount(hM);
    result := Boolean(InsertMenuItem(hM,i,true,MII));
  end else
    result := Boolean(InsertMenuItem(hM,position,false,MII));
end;

function InsertBitmapMenu(hM:HMENU;position:integer;
                               hB:HBITMAP;mID:UINT):Boolean;
var
  MII:TMenuItemInfo;
  i:integer;
begin
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_ID or MIIM_TYPE;
    fType := MFT_BITMAP;
    wID := mID;
    dwTypeData := PChar(hB);
  end;
  if position = -1 then begin
    i := GetMenuItemCount(hM);
    result := Boolean(InsertMenuItem(hM,i,true,MII));
  end else
    result := Boolean(InsertMenuItem(hM,position,false,MII));
end;

function AppendPopupMenu(hM:HMENU;st:string;hMpop:HMENU;mID:UINT):Boolean;
begin
  result := InsertPopupMenu(hM,-1,st,hMpop,mID);
end;

function AppendStringMenu(hM:HMENU;st:string;mID:UINT):Boolean;
begin
  result := InsertStringMenu(hM,-1,st,mID);
end;

function AppendSeparatorMenu(hM:HMENU;mID:UINT):Boolean;
begin
  result := InsertSeparatorMenu(hM,-1,mID);
end;

function AppendRadioMenu(hM:HMENU;st:string;mID:UINT):Boolean;
begin
  result := InsertRadioMenu(hM,-1,st,mID);
end;

function AppendBitmapMenu(hM:HMENU;hB:HBITMAP;mID:UINT):Boolean;
begin
  result := InsertBitmapMenu(hM,-1,hB,mID);
end;

function GetMenuItemState(hM:HMENU;mID:UINT):UINT;
var
  MII:TMenuItemInfo;
begin
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_STATE;
  end;
  GetMenuItemInfo(hM,mID,false,MII);
  result := MII.fState;
end;

function IsMenuChecked(hM:HMENU;mID:UINT):Boolean;
var
  ItemState:UINT;
begin
  ItemState := GetMenuItemState(hM,mID);
  result := Boolean((ItemState and MFS_CHECKED)<>0);
end;

function SetMenuChecked(hM:HMENU;mID:UINT;value:Boolean):Boolean;
var
  MII:TMenuItemInfo;
  ItemState:UINT;
begin
  ItemState := GetMenuItemState(hM,mID);
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_STATE;
  end;
  if value then
    MII.fState := ItemState or MFS_CHECKED
  else
    MII.fState := ItemState and (not MFS_CHECKED);
  result := Boolean(SetMenuItemInfo(hM,mID,false,MII));
end;

function IsMenuEnabled(hM:HMENU;mID:UINT):Boolean;
var
  ItemState:UINT;
begin
  ItemState := GetMenuItemState(hM,mID);
  result := not Boolean((ItemState and MFS_GRAYED)<>0);
end;

function SetMenuEnabled(hM:HMENU;mID:UINT;value:Boolean):Boolean;
var
  MII:TMenuItemInfo;
  ItemState:UINT;
begin
  ItemState := GetMenuItemState(hM,mID);
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_STATE;
  end;
  if value then
    MII.fState := ItemState and (not MFS_GRAYED)
  else
    MII.fState := ItemState or MFS_GRAYED;
  result := Boolean(SetMenuItemInfo(hM,mID,false,MII));
end;

これらのメニュー項目の作成関数は、すでに説明した InsertMenuItem( ) 関数を単機能化したものである。 GetMenuItemState( )GetMenuItemIfo( ) APIを単機能化したものである。

  function GetMenuItemInfo(p1: HMENU; 
                           p2: UINT; 
                           p3: BOOL; 
                       var p4: TMenuItemInfo): BOOL; stdcall;

また、SetMenuChecked( )SetMenuEnabled( )SetMenuItemInfo( ) APIを単機能化したものである。

                       
  function SetMenuItemInfo(p1: HMENU; 
                           p2: UINT; 
                           p3: BOOL; 
                     const p4: TMenuItemInfo): BOOL; stdcall;

いずれも、パラメータは InsertMenuItem( ) と同じである。 これらのメニュー項目の状態を取得・設定するとき、TMenuItemInfo 型レコードの fState フィールドは、チェック状態や有効性、デフォルト項目、ハイライト表示などの複数のビットフラグになっていることには注意が必要である。 各フラグの状態を取得・設定するときに MFS_ENABLEDMFS_UNCHECKEDMFS_UNHILITE などは、デフォルトのゼロと定義されているので、ビットマスクには使えないのである。 代わりに、MFS_GRAYEDMFS_CHECKEDMFS_HILITE を使う必要がある。

それでは、これらの関数をためすプログラムを InsertMenuItems.dpr に書こう。 以下に全リストを示す。

program InsertMenuItems;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  MainWnd: TSDIMainWindow;

  hMain,hFile,hHelp,hOption: HMENU;

  flag:Boolean = true;

  DD:TAPIDragDrop;

const
  ID_EXIT = 200;
  ID_TEST = 201;
  ID_ABOUT = 202;

  ID_OPTION1 = 203;
  ID_OPTION2 = 204;

  ID_RADIOA = 205;
  ID_RADIOB = 206;
  ID_RADIOC = 207;

  ID_SEP1 = 208;
  ID_SEP2 = 209;

  ID_FILE = 300;
  ID_HELP = 301;
  ID_OPTION = 302;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------
procedure MakeMenu(hParent:HWND);
begin
  hMain := CreateMenu;

    hFile := CreateMenu;
      AppendStringMenu(hFile,'E&xit',ID_EXIT);
      AppendPopupMenu(hMain,'&File',hFile,ID_FILE);

    AppendStringMenu(hMain,'&Test!',ID_TEST);

    hHelp := CreateMenu;
      AppendStringMenu(hHelp,'&About..',ID_ABOUT);
      AppendPopupMenu(hMain,'&Help',hHelp,ID_HELP);

  SetMenu(hParent,hMain);
  
  hOption := CreateMenu;
    AppendStringMenu(hOption,'Option&1',ID_OPTION1);
    AppendStringMenu(hOption,'Option&2',ID_OPTION2);
    AppendSeparatorMenu(hOption,ID_SEP1);
    AppendRadioMenu(hOption,'Radio&A',ID_RADIOA);
    AppendRadioMenu(hOption,'Radio&B',ID_RADIOB);
    AppendRadioMenu(hOption,'Radio&C',ID_RADIOC);
    AppendSeparatorMenu(hOption,ID_SEP2);
end;

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------
procedure DDDrop(var Msg: TMessage);
var
  hBit: HBITMAP;
begin
  hBit := LoadImage(0,PChar(DD.Files[0]),IMAGE_BITMAP,0,0,LR_LOADFROMFILE);
  if hBit > 0  then begin
    AppendBitmapMenu(hOption,hBit,500);
  end;
end;

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;
  MakeMenu(hWindow);

  DD := TAPIDragDrop.Create(hWindow);
  DD.OnDrop := DDDrop;

  with MainWnd do begin
    ClientWidth := 360;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
begin
  if flag then
    InsertPopupMenu(hMain,ID_HELP,'&Option',hOption,ID_OPTION)
  else
    RemoveMenu(hMain,ID_OPTION,MF_BYCOMMAND);
  DrawMenuBar(hWindow);
  flag := not flag;
end;

procedure MainCommand(var Msg: TMessage);
begin
  if (Msg.WParamHi+Msg.LParam)=0 then
    case Msg.WParamLo of
      ID_OPTION1,ID_OPTION2:
        if IsMenuChecked(hOption,Msg.WParamLo) then
          SetMenuChecked(hOption,Msg.WParamLo,false)
        else
          SetMenuChecked(hOption,Msg.WParamLo,true);
      ID_RADIOA..ID_RADIOC:
        CheckMenuRadioItem(hOption,ID_RADIOA,
                         ID_RADIOC,Msg.WParamLo,MF_BYCOMMAND);
    end;
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  if flag then DestroyMenu(hOption);
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifInsertMenuItems.zip(16.486kB)
InsertMenuItems.gif

実行して、Test! メニューを選択すると Test! 項目のあとに Option 項目が挿入される。エクスプローラから適当なビットマップファイルをドラグドロップすると、Option ポップアップメニューにビットマップ項目が追加される。 Delphi に付属する Image Editor でつくった 80x30 のビットマップを二つドロップしたときの様子を右図に示す。 Option1Option2 項目を選択するとチェック状態が反転する。また、RadioA から RadioC 項目を選択すると、チェック状態が排他的に変化するのを見ることができる。 再び、Test! メニューを選択すると Option 項目が取り外される。

コードを見てみよう。 メニューIDの定数とメニューハンドルを格納するための変数は、グローバルに宣言している。

var
  hMain,hFile,hHelp,hOption: HMENU;
  
const
  ID_EXIT = 200;
  ID_TEST = 201;
  ID_ABOUT = 202;

  ID_OPTION1 = 203;
  ID_OPTION2 = 204;

  ID_RADIOA = 205;
  ID_RADIOB = 206;
  ID_RADIOC = 207;

  ID_SEP1 = 208;
  ID_SEP2 = 209;

  ID_FILE = 300;
  ID_HELP = 301;
  ID_OPTION = 302;

このように、全てのメニューハンドルとIDを管理しておくと項目の制御やメッセージの処理が簡単になる。特に、ポップアップ項目やセパレータにもIDを設定しておくと、あとあと便利に使える。メニューは、MainCreate( ) から呼び出される以下の関数で作っている。

procedure MakeMenu(hParent:HWND);
begin
  hMain := CreateMenu;

    hFile := CreateMenu;
      AppendStringMenu(hFile,'E&xit',ID_EXIT);
      AppendPopupMenu(hMain,'&File',hFile,ID_FILE);

    AppendStringMenu(hMain,'&Test!',ID_TEST);

    hHelp := CreateMenu;
      AppendStringMenu(hHelp,'&About..',ID_ABOUT);
      AppendPopupMenu(hMain,'&Help',hHelp,ID_HELP);

  SetMenu(hParent,hMain);

  hOption := CreateMenu;
    AppendStringMenu(hOption,'Option&1',ID_OPTION1);
    AppendStringMenu(hOption,'Option&2',ID_OPTION2);
    AppendSeparatorMenu(hOption,ID_SEP1);
    AppendRadioMenu(hOption,'Radio&A',ID_RADIOA);
    AppendRadioMenu(hOption,'Radio&B',ID_RADIOB);
    AppendRadioMenu(hOption,'Radio&C',ID_RADIOC);
    AppendSeparatorMenu(hOption,ID_SEP2);
end;

SetMenu( ) までで Skeleton03 のメニューを作っている。 hOption のメニューは、作っているだけで、ここではどこにもアタッチしていない。 この項目をトップレベルメニューに挿入または削除する OnTest( ) ハンドラを見てみよう。

procedure OnTest(hWindow: HWND);
begin
  if flag then
    InsertPopupMenu(hMain,ID_HELP,'&Option',hOption,ID_OPTION)
  else
    RemoveMenu(hMain,ID_OPTION,MF_BYCOMMAND);
  DrawMenuBar(hWindow);
  flag := not flag;
end;

グローバル変数の flagtrue のとき ID_HELP の位置に hOption を挿入し、false のときは削除している。

  function RemoveMenu(hMenu: HMENU; 
                  uPosition, 
                     uFlags: UINT): BOOL; stdcall;

この関数は、hMenu のメニューから uPosition の項目を取り外す。取り外すだけでメニューハンドルは有効のままである。 uFlagsMF_BYCOMMAND を指定したときは uPosition には取り外す項目のIDを指定する。MF_BYPOSITION を指定したときは、uPosition にゼロベースの相対位置の番号を指定する。 この例のように、トップレベルメニューの表示に変更が加えられたとき、DrawMenuBar( ) によって再描画する。パラメータはメニューハンドルではなく、メニューが設定されているウィンドウのハンドルである。 ビットマップファイルがドロップされたときに実行される DDDrop( ) ハンドラを見てみよう。

procedure DDDrop(var Msg: TMessage);
var
  hBit: HBITMAP;
begin
  hBit := LoadImage(0,PChar(DD.Files[0]),IMAGE_BITMAP,0,0,LR_LOADFROMFILE);
  if hBit > 0  then begin
    AppendBitmapMenu(hOption,hBit,500);
  end;
end;

ここでは、19−4 ビットマップの表示 節で使った LoadImage( ) によりビットマップハンドルを取得して、hOption メニューにビットマップ項目を追加している。もちろん、ビットマップをメニュー項目にするときは、 19−5 ビットマップリソース 節で説明した手順でビットマップをリソースにしておき、初期化時に LoadBitmap( ) でハンドルを手に入れてメニューに項目を設定するのがよい。 WM_COMMAND を処理する MainCommand( ) を見てみよう。

procedure MainCommand(var Msg: TMessage);
begin
  if (Msg.WParamHi+Msg.LParam)=0 then
    case Msg.WParamLo of
      ID_OPTION1,ID_OPTION2:
        if IsMenuChecked(hOption,Msg.WParamLo) then
          SetMenuChecked(hOption,Msg.WParamLo,false)
        else
          SetMenuChecked(hOption,Msg.WParamLo,true);
      ID_RADIOA..ID_RADIOC:
        CheckMenuRadioItem(hOption,ID_RADIOA,
                         ID_RADIOC,Msg.WParamLo,MF_BYCOMMAND);
    end;
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

最初の if 文は、このメッセージがメニューからのものであることを確かめている。 Msg.WParamLocase 文のセレクタにして、メニューIDを使って分岐している。 チェック状態の反転は分かり易いコードだろう。 ラジオ項目に排他的にチェックマークを付けるには、CheckMenuRadioItem( ) APIを使う。

  function CheckMenuRadioItem(hMenu: HMENU; 
                              First, 
                               Last, 
                              Check, 
                              Flags: UINT): BOOL; stdcall;

Flags の意味は、上の RemoveMenu( ) のときと同じである。 ここでは、MF_BYCOMMAND を指定して位置をIDで指定している。 FirstLast にはラジオ項目のグループの始まりと終わりのIDを指定する。したがって、同じグループのラジオ項目は、一続きのIDを設定する必要がある。 Check には、チェックマークを付ける項目のIDを指定する。

CreateMenuCreatePopupMenu で作ったメニューは、ウィンドウに設置されている限りアプリケーションの終了時にシステムが廃棄してくれる。 ウィンドウに設置されていないときは、

  function DestroyMenu(hMenu: HMENU): BOOL; stdcall;

で廃棄する。 hMenu がメニュー項目としてポップアップメニュー項目を含んでいるときは、その下のすべてのポップアップメニューも廃棄される。 ここでは、MainDestroy( ) ハンドラで、 flag の値を参照して、hOption が取り付けられていないときはこのAPIで廃棄している。

以上で、API関数をカプセル化した関数によりメニューをコードで作成できるようになった。

redball.gifこの章の目次へ戻る

21−5 メニュークラスをつくる

本章の 21−3 メニューの構造 で調べたように、メニューは一つのトップレベルメニューと幾つかのポップアップメニューが入れ子の階層を作って成り立っている事が分かった。 また前節では、 メニュー作成にかかわるAPI関数をカプセル化した関数を UtilFunc.pas に作った。 この節以後の本章では、 この二つの種類のメニューをカプセル化したクラスを作成することを考える。 本節では手始めに、文字列項目とセパレータだけからなるメニューの機能をカプセル化したクラスを作成してテストする。

  1. エクスプローラを使って、新しいフォルダを 2105MenuClass01 という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 2104InsertMenuItems フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで MenuClass01.dpr という名前で保存する。
  5. [ファイル]メニューから[新規作成...]を選んで表示されるダイアログから、「ユニット」を選んで新しくユニットファイルを作成し、 これを MenuClass.pas として保存する。
  6. コードエディタで以下のように編集する。
  7. [F.9]でコンパイルして実行する。

UtilClass.pas は大きくなりすぎたので、新しいクラスを作成するにはスクロールを頻繁に行わねばならず不便なので、新規作成した MenuClass.pas にメニュークラスを作成し、完成したら UtilClass.pas に移すことにしよう。 メニュークラスは、メニューを作成するだけでなく、メニューの使用に伴うメッセージをイベントとして公開したいので、メッセージを受け取るために、TSubClass から派生して作ることにしよう。 二種類のクラスは、トップレベルメニューの TAMainMenu と ポップアップメニューの TAPopupMenu クラスであるが、両方に共通するメソッドやプロパティーもあるので、ここでは、TSubClass から派生して TMenuBase クラスを作成し、これから上の二つのクラスを派生して作ることにしよう。

以下に、MenuClass.pas の全リストを示す。

unit MenuClass;

interface

uses
  windows,
  messages,
  UtilFunc,
  UtilClass;

type

//-------------------- TMenuBase ------------------------------
TMenuBase = class(TSubClass)
private
  FHandle: HMENU;
protected
public
  function AppendString(st:string;mID:UINT):Boolean;
  function InsertString(position:integer;st:string;mID:UINT):Boolean;
  function Remove(mID:UINT):Boolean;
  property Handle: HMENU read FHandle;
end;

//--------------- TAMainMenu ----------------------------------
TAMainMenu = class(TMenuBase)
public
  constructor Create(hParent:HWND);override;
  destructor Destroy; override;
  function SetToParent:Boolean;
end;

//--------------- TAPopupMenu ----------------------------------
TAPopupMenu = class(TMenuBase)
private
  FID: UINT;
  FAttachedMenu: HMENU;
protected
public
  constructor Create(hParent:HWND);override;
  destructor Destroy; override;
  function AppendSeparator(mID:UINT):Boolean;
  function InsertSeparator(position:integer;mID:UINT):Boolean;
  function AttachMenu(Menu:TMenuBase;position:integer;st:string):Boolean;
  function DetachMenu: Boolean;
  property ID: UINT read FID write FID;
  property AttachedMenu: HMENU read FAttachedMenu;
end;

//---------------------------------------------------------------
function MakeMainMenu(hParent:HWND): TAMainMenu;
function MakePopupMenu(hParent:HWND;ID:UINT): TAPopupMenu;

implementation

//-------------------- TMenuBase ------------------------------
function TMenuBase.AppendString(st:string;mID:UINT):Boolean;
begin
  result := AppendStringMenu(FHandle,st,mID);
end;

function TMenuBase.InsertString(position:integer;st:string;mID:UINT):Boolean;
begin
  result := InsertStringMenu(FHandle,position,st,mID);
end;

function TMenuBase.Remove(mID:UINT):Boolean;
begin
  result := Boolean(RemoveMenu(FHandle,mID,MF_BYCOMMAND));
end;

//--------------- TAMainMenu ----------------------------------
constructor TAMainMenu.Create(hParent:HWND);
begin
  inherited Create(hParent);

  FHandle := CreateMenu;
end;

destructor TAMainMenu.Destroy;
var
  hM:HMENU;
begin
  hM := GetMenu(Parent);
  if hM <> FHandle then DestroyMenu(FHandle);

  inherited Destroy;
end;

function TAMainMenu.SetToParent:Boolean;
begin
  result := Boolean(SetMenu(Parent,FHandle));
end;

//--------------- TAPopupMenu ----------------------------------
constructor TAPopupMenu.Create(hParent:HWND);
begin
  inherited Create(hParent);

  FHandle := CreatePopupMenu;
end;

destructor TAPopupMenu.Destroy;
begin
  if FAttachedMenu = 0 then DestroyMenu(FHandle);

  inherited Destroy;
end;

function TAPopupMenu.AppendSeparator(mID:UINT):Boolean;
begin
  result := AppendSeparatorMenu(FHandle,mID);
end;

function TAPopupMenu.InsertSeparator(position:integer;mID:UINT):Boolean;
begin
  result := InsertSeparatorMenu(FHandle,position,mID);
end;

function TAPopupMenu.AttachMenu(Menu:TMenuBase;position:integer;st:string):Boolean;
begin
  if FAttachedMenu <> 0 then begin
    result := false;
    exit;
  end else begin
    result := InsertPopupMenu(Menu.FHandle,position,st,FHandle,FID);
    DrawMenuBar(Parent);
  end;
  if result then FAttachedMenu := Menu.FHandle;
end;

function TAPopupMenu.DetachMenu: Boolean;
begin
  if FAttachedMenu = 0 then begin
    result := false;
    exit;
  end else begin
    result := Boolean(RemoveMenu(FAttachedMenu,FID,MF_BYCOMMAND));
    DrawMenuBar(Parent);
  end;
  if result then FAttachedMenu := 0;
end;

//---------------------------------------------------------------
function MakeMainMenu(hParent:HWND): TAMainMenu;
begin
  result := TAMainMenu.Create(hParent);
end;

function MakePopupMenu(hParent:HWND;ID:UINT): TAPopupMenu;
begin
  result := TAPopupMenu.Create(hParent);
  result.FID := ID;
end;

end.

コードを調べよう。 TMenuBase クラスは、メニューハンドルを管理する Handle プロパティーと文字列項目の追加・挿入のためのメソッドと項目を削除する Remove( ) メソッドを持っている。 これから派生した TAMainMenu クラスは、コンストラクタで CreateMenu を実行して空のメニューを作成している。

constructor TAMainMenu.Create(hParent:HWND);
begin
  inherited Create(hParent);

  FHandle := CreateMenu;
end;

デストラクタでは、親ウィンドウのメニューハンドルが自分のハンドルと違っていたらメニューを廃棄する。

destructor TAMainMenu.Destroy;
var
  hM:HMENU;
begin
  hM := GetMenu(Parent);
  if hM <> FHandle then DestroyMenu(FHandle);

  inherited Destroy;
end;

また、親ウィンドウに SetToParent メソッドにより自分をアタッチすることができる。

function TAMainMenu.SetToParent:Boolean;
begin
  result := Boolean(SetMenu(Parent,FHandle));
end;

ポップアップメニュー TAPopupMenu クラスのコンストラクタでは、CreatePopupMenu APIを使って空のポップアップメニューを作成している。

constructor TAPopupMenu.Create(hParent:HWND);
begin
  inherited Create(hParent);

  FHandle := CreatePopupMenu;
end;

上の階層のポップアップメニューやトップレベルメニューにアタッチするには AttachMenu メソッドをつかう。

function TAPopupMenu.AttachMenu(Menu:TMenuBase;position:integer;st:string):Boolean;
begin
  if FAttachedMenu <> 0 then begin
    result := false;
    exit;
  end else begin
    result := InsertPopupMenu(Menu.FHandle,position,st,FHandle,FID);
    DrawMenuBar(Parent);
  end;
  if result then FAttachedMenu := Menu.FHandle;
end;

ここで、position−1 にすると追加、挿入位置のメニューIDを設定すると挿入する事になる。 自分がアタッチしているメニューのハンドルは AttachedMenu プロパティーで管理する。 取り外すには、DetachMenu メソッドを使う。

function TAPopupMenu.DetachMenu: Boolean;
begin
  if FAttachedMenu = 0 then begin
    result := false;
    exit;
  end else begin
    result := Boolean(RemoveMenu(FAttachedMenu,FID,MF_BYCOMMAND));
    DrawMenuBar(Parent);
  end;
  if result then FAttachedMenu := 0;
end;

最後に、デストラクタでは、AttachedMenu プロパティーを調べて、もしどこにも設置されていなければここで廃棄する。

destructor TAPopupMenu.Destroy;
begin
  if FAttachedMenu = 0 then DestroyMenu(FHandle);

  inherited Destroy;
end;

以上で、二つのクラスが完成した。 MakeMainMenu( )MakePopupMenu( ) 関数は、これらのクラスのインスタンス作成関数である。

これらのクラスをテストするプログラムを MenuClass.dpr に書く。 以下に全リストを示す。

program MenuClass01;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas',
  MenuClass in 'MenuClass.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  MainWnd: TSDIMainWindow;

  MMain: TAMainMenu;
  PFile, PHelp, POption: TAPopupMenu;

  flag:Boolean = true;

const
  ID_EXIT = 200;
  ID_TEST = 201;
  ID_ABOUT = 202;

  ID_OPTION1 = 203;
  ID_OPTION2 = 204;

  ID_ALPHA = 205;
  ID_BETA = 206;
  ID_GAMMA = 207;

  ID_SEP1 = 208;
  ID_SEP2 = 209;

  ID_FILE = 300;
  ID_HELP = 301;
  ID_OPTION = 302;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------
procedure MakeMenu(hParent:HWND);
begin

  MMain := MakeMainMenu(hParent);

    PFile := MakePopupMenu(hParent,ID_FILE);
      PFile.AppendString('E&xit',ID_EXIT);
      PFile.AttachMenu(MMain,-1,'&File');

    MMain.AppendString('&Test!',ID_TEST);

    PHelp := MakePopupMenu(hParent,ID_HELP);
      PHelp.AppendString('&About..',ID_ABOUT);
      PHelp.AttachMenu(MMain,-1,'&Help');

  MMain.SetToParent;

  POption := MakePopupMenu(hParent,ID_OPTION);
    POption.AppendString('Option&1',ID_OPTION1);
    POption.AppendString('Option&2',ID_OPTION2);
    POption.AppendSeparator(ID_SEP1);
    POption.AppendString('&Alpha',ID_ALPHA);
    POption.AppendString('&Beta',ID_BETA);
    POption.AppendString('&Gamma',ID_GAMMA);
    POption.AppendSeparator(ID_SEP2);
end;

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;
  MakeMenu(hWindow);


  with MainWnd do begin
    ClientWidth := 360;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
begin
  if flag then
    POption.AttachMenu(MMain,ID_HELP,'&Option')
  else
    POption.DetachMenu;
  flag := not flag;
end;

procedure MainCommand(var Msg: TMessage);
begin
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifMenuClass01.zip(15.734kB)
MenuClass01.gif

実行すると右図のようになる。Test! メニューを選ぶと Option メニューの挿入と削除が交互に起こる。 ここではまだイベントを公開していないので、Optionメニューのメッセージ処理は行っていない。
コードを調べよう。 メニューは MainCreate( ) から呼ばれる MakeMenu( ) ヘルパハンドラで作成している。

procedure MakeMenu(hParent:HWND);
begin

  MMain := MakeMainMenu(hParent);

    PFile := MakePopupMenu(hParent,ID_FILE);
      PFile.AppendString('E&xit',ID_EXIT);
      PFile.AttachMenu(MMain,-1,'&File');

    MMain.AppendString('&Test!',ID_TEST);

    PHelp := MakePopupMenu(hParent,ID_HELP);
      PHelp.AppendString('&About..',ID_ABOUT);
      PHelp.AttachMenu(MMain,-1,'&Help');

  MMain.SetToParent;

  POption := MakePopupMenu(hParent,ID_OPTION);
    POption.AppendString('Option&1',ID_OPTION1);
    POption.AppendString('Option&2',ID_OPTION2);
    POption.AppendSeparator(ID_SEP1);
    POption.AppendString('&Alpha',ID_ALPHA);
    POption.AppendString('&Beta',ID_BETA);
    POption.AppendString('&Gamma',ID_GAMMA);
    POption.AppendSeparator(ID_SEP2);
end;

これは、前節のプログラムの MakeMenu( ) とほぼ同じ構造のメニューを作成している。 筆者には、裸の関数をつかった前節の関数より、クラスのインスタンスとそのメソッドを使ってメニューを作成している上の関数の方が美しく感じる。 インデントしているのでその階層構造も分かり易いだろう。 OnTest( ) ハンドラでは、AttachMenu( )DetachMenu( ) メソッドをテストしている。

procedure OnTest(hWindow: HWND);
begin
  if flag then
    POption.AttachMenu(MMain,ID_HELP,'&Option')
  else
    POption.DetachMenu;
  flag := not flag;
end;

これらのメソッドのように、取り付けられるほうのメソッドではなく、取り付ける方の(つまり「ぶら下がる」方の)メソッドとした方が、 ハンドルを管理しやすいのは明らかだろう。 ぶら下がる方にとって、上の階層のメニューは常に一つしかないからだ。

以上で、 文字列項目とセパレータだけからなるメニュー作成のための基本的なクラスが出来たので、 次節以降で、これらにメソッドやプロパティー追加して、機能を充実させていこう。

redball.gifこの章の目次へ戻る

21−6 ラジオ項目、ビットマップ項目

前節で作った TAPopupMenu クラスにラジオ項目、ビットマップ項目を追加・挿入するメソッドを装備し、それらを制御するメソッドとプロパティーを追加しよう。

  1. エクスプローラを使って、新しいフォルダを 2106MenuClass02 という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 2105MenuClass01 フォルダの Skeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pasMenuClass.pas をコピーする。
  3. コピーした Skeleton03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに Skeleton03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで MenuClass02.dpr という名前で保存する。
  5. [プロジェクト]メニューから[プロジェクトに追加...]を選んで表示されるダイアログから、MenuClass.pas を選択する。
  6. コードエディタで以下のように編集する。
  7. [F.9]でコンパイルして実行する。

最初に、 MenuClass.pasTAPopupMenu クラスを以下のように書き換える。 宣言部を以下に示す。

//--------------- TAPopupMenu ----------------------------------
TAPopupMenu = class(TMenuBase)
private
  FID: UINT;
  FAttachedMenu: HMENU;
  function GetChecked(mID:UINT):Boolean;
  procedure SetChecked(mID:UINT;value:Boolean);
protected
public
  constructor Create(hParent:HWND);override;
  destructor Destroy; override;
  function AppendSeparator(mID:UINT):Boolean;
  function InsertSeparator(position:integer;mID:UINT):Boolean;
  function AppendRadio(st:string;mID:UINT):Boolean;
  function InsertRadio(position:integer;st:string;mID:UINT):Boolean;
  function AppendBitmap(hB:HBITMAP;mID:UINT):Boolean;
  function InsertBitmap(position:integer;hB:HBITMAP;mID:UINT):Boolean;
  function AttachMenu(Menu:TMenuBase;position:integer;st:string):Boolean;
  function DetachMenu: Boolean;
  function CheckRadio(First,Last,Check:UINT):Boolean;
  property ID: UINT read FID write FID;
  property AttachedMenu: HMENU read FAttachedMenu;
  property Checked[mID:UINT]:Boolean read GetChecked write SetChecked;
end;

ここでは、ラジオ項目とビットマップ項目を追加・挿入する四つのメソッドとラジオ項目にチェックマークを排他的に付ける CheckRadio( ) メソッドと一般の項目のチェック状態を取得・設定する Checked プロパティーを追加している。 実装部を以下に示す。

//--------------- TAPopupMenu ----------------------------------
constructor TAPopupMenu.Create(hParent:HWND);
begin
  inherited Create(hParent);

  FHandle := CreatePopupMenu;
end;

destructor TAPopupMenu.Destroy;
begin
  if FAttachedMenu = 0 then DestroyMenu(FHandle);

  inherited Destroy;
end;

function TAPopupMenu.AppendSeparator(mID:UINT):Boolean;
begin
  result := AppendSeparatorMenu(FHandle,mID);
end;

function TAPopupMenu.InsertSeparator(position:integer;mID:UINT):Boolean;
begin
  result := InsertSeparatorMenu(FHandle,position,mID);
end;

function TAPopupMenu.AppendRadio(st:string;mID:UINT):Boolean;
begin
  result := AppendRadioMenu(FHandle,st,mID);
end;

function TAPopupMenu.InsertRadio(position:integer;st:string;mID:UINT):Boolean;
begin
  result := InsertRadioMenu(FHandle,position,st,miD);
end;

function TAPopupMenu.AppendBitmap(hB:HBITMAP;mID:UINT):Boolean;
begin
  result := AppendBitmapMenu(FHandle,hB,mID);
end;

function TAPopupMenu.InsertBitmap(position:integer;hB:HBITMAP;mID:UINT):Boolean;
begin
  result := InsertBitmapMenu(FHandle,position,hB,mID);
end;

function TAPopupMenu.AttachMenu(Menu:TMenuBase;position:integer;st:string):Boolean;
begin
  if FAttachedMenu <> 0 then begin
    result := false;
    exit;
  end else begin
    result := InsertPopupMenu(Menu.FHandle,position,st,FHandle,FID);
    DrawMenuBar(Parent);
  end;
  if result then FAttachedMenu := Menu.FHandle;
end;

function TAPopupMenu.DetachMenu: Boolean;
begin
  if FAttachedMenu = 0 then begin
    result := false;
    exit;
  end else begin
    result := Boolean(RemoveMenu(FAttachedMenu,FID,MF_BYCOMMAND));
    DrawMenuBar(Parent);
  end;
  if result then FAttachedMenu := 0;
end;

function TAPopupMenu.CheckRadio(First,Last,Check:UINT):Boolean;
begin
  result := Boolean(CheckMenuRadioItem(FHandle,First,Last,Check,MF_BYCOMMAND));
end;

function TAPopupMenu.GetChecked(mID:UINT):Boolean;
begin
  result := IsMenuChecked(FHandle,mID);
end;

procedure TAPopupMenu.SetChecked(mID:UINT;value:Boolean);
begin
  SetMenuChecked(FHandle,mID,value);
end;

これで完成したので、さっそく試してみよう。その前に、メニュー項目にするビットマップをリソースにしよう。リソース記述ファイルを以下に示す。

//
//  MyRes.rc
//

B1 BITMAP "B1.bmp"
B2 BITMAP "B2.bmp"

二つのビットマップファイルは、 第4節で使ったものと同じである。 これを 19−3 リソースコンパイラ 節で作った ResourceCompiler.exe でコンパイルして、MyRes.res というファイルができたらメニューリソースの完成である。 MenuClass02.dpr の全リストを以下に示す。

program MenuClass02;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas',
  MenuClass in 'MenuClass.pas';

  {$R MyRes.res}

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  MainWnd: TSDIMainWindow;

  MMain: TAMainMenu;
  PFile, PHelp, POption: TAPopupMenu;

  flag:Boolean = true;

  hB1,hB2: HBITMAP;

const
  ID_EXIT = 200;
  ID_TEST = 201;
  ID_ABOUT = 202;

  ID_OPTION1 = 203;
  ID_OPTION2 = 204;

  ID_ALPHA = 205;
  ID_BETA = 206;
  ID_GAMMA = 207;

  ID_SEP1 = 208;
  ID_SEP2 = 209;

  ID_FILE = 300;
  ID_HELP = 301;
  ID_OPTION = 302;

  ID_BITMAP1 = 1000;
  ID_BITMAP2 = 1001;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------
procedure MakeMenu(hParent:HWND);
begin

  hB1 := LoadBitmap(HInstance,'B1');
  hB2 := LoadBitmap(HInstance,'B2');

  MMain := MakeMainMenu(hParent);

    PFile := MakePopupMenu(hParent,ID_FILE);
      PFile.AppendString('E&xit',ID_EXIT);
      PFile.AttachMenu(MMain,-1,'&File');

    MMain.AppendString('&Test!',ID_TEST);

    PHelp := MakePopupMenu(hParent,ID_HELP);
      PHelp.AppendString('&About..',ID_ABOUT);
      PHelp.AttachMenu(MMain,-1,'&Help');

  MMain.SetToParent;

  POption := MakePopupMenu(hParent,ID_OPTION);
    POption.AppendString('Option&1',ID_OPTION1);
    POption.AppendString('Option&2',ID_OPTION2);
    POption.AppendSeparator(ID_SEP1);
    POption.AppendRadio('&Alpha',ID_ALPHA);
    POption.AppendRadio('&Beta',ID_BETA);
    POption.AppendRadio('&Gamma',ID_GAMMA);
    POption.AppendSeparator(ID_SEP2);
    POption.AppendBitmap(hB1,ID_BITMAP1);
    POption.AppendBitmap(hB2,ID_BITMAP2);
end;

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;
  MakeMenu(hWindow);

  with MainWnd do begin
    ClientWidth := 360;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
begin
  if flag then
    POption.AttachMenu(MMain,ID_HELP,'&Option')
  else
    POption.DetachMenu;
  flag := not flag;
end;

procedure MainCommand(var Msg: TMessage);
begin
  if (Msg.WParamHi+Msg.LParam)=0 then
    case Msg.WParamLo of
      ID_OPTION1,ID_OPTION2:
        POption.Checked[Msg.WParamLo] :=
                          not POption.Checked[Msg.WParamLo];
      ID_ALPHA..ID_GAMMA:
        POption.CheckRadio(ID_ALPHA,ID_GAMMA,Msg.WParamLo);
      ID_BITMAP1,ID_BITMAP2:MessageBeep($FFFFFFFF);
    end;
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifMenuClass02.zip(17.276kB)
MenuClass02.gif

実行して、Test! メニューで Option メニューを設置すると右図のようになる。 Option1Option2 項目を選択するとチェック状態が反転する。 Alpha 項目から Gamma 項目までは、ラジオチェックが排他的に付けられる。 ビットマップ項目を選択するとシステムブザーが鳴る。 まだ、このクラスはイベントを持っていないので、これらの WM_COMMAND メッセージ処理は MainCommand( ) ハンドラで行っている。

メニューは、MakeMenu( ) で作成している。 リソースからビットマップをロードしていること以外は、 前とほとんど同じである。

ここまでで、メニュー作成の機能は整ったので、次節ではメニューに関連するメッセージを調べ、 イベントとして公開しよう。

redball.gifこの章の目次へ戻る

21−7 メニューメッセージ

骨格プログラム Skeleton03 では、メニューが発行するメッセージとして WM_COMMAND しか処理していなかった。メニュー項目の選択によって実行されるコードを書くだけなら、WM_COMMAND だけ処理すればよいが、 それ以外にも重要なメニューメッセージがある。ここでは、メニューメッセージを調べて、これらを前節までで作成してきたメニュークラスで捕捉し、イベントとして公開する事を考えよう。 メニュー関連の主なメッセージを以下に示す。

const
  WM_COMMAND          = $0111; // メニュー項目が選択された
  WM_CONTEXTMENU      = $007B; // ウィンドウで右クリックした。ショートカットメニューの表示
  WM_ENTERMENULOOP    = $0211; // メニューのモーダルループに入った
  WM_EXITMENULOOP     = $0212; // メニューのモーダルループから出た
  WM_INITMENU         = $0116; // メニューがアクティブになろうとしている
  WM_INITMENUPOPUP    = $0117; // ポップアップメニューが表示されようとしている
  WM_NEXTMENU         = $0213; // 矢印キーでメニュー項目を移動しようとしている

ここでは、WM_COMMANDWM_ENTERMENULOOPWM_EXITMENULOOPWM_INITMENUWM_INITMENUPOPUP メッセージを調べよう。

・WM_COMMAND
    wNotifyCode = HIWORD(wParam); // メニューではゼロ 
    wID = LOWORD(wParam);         // メニューID 
    hwndCtl = (HWND) lParam;      // メニューではゼロ

このメッセージは、すでにおなじみである。 コントロールや後に説明するアクセレレータキーもこのメッセージを発行するが、WParamHiLParam がともにゼロのときだけメニューからのものである事が確かめられる。

・WM_ENTERMUMENULOOP
    wParam = (BOOL) fIsTrackPopupMenu  // ゼロでなければショートカットメニューから 
    lParam = 0 ;                       // not used; must be zero 

メニューの選択がモーダルループであることは意外に知られていない。 筆者は、Petzold 氏の著書を参考にして、無限ループでランダムに四角形をクライアント領域に描くプログラムを作ったとき、メニューを選択すると描画が停止することに気づいた。

・WM_EXITMENULOOP
    wParam = (BOOL) fIsTrackPopupMenu    // ゼロでなければショートカットメニューから 
    lParam = 0 ;                         // not used; must be zero 

これは、ループから出るときに送られる。

・WM_INITMENU
     hmenuInit = (HMENU) wParam; // アクティブになろうとしているメニューのハンドル

このメッセージは、メニューをマウスで選択したとき、[ALT] キーなどのメニューキーを押したときに、一度だけ送られる。

・WM_INITMENUPOPUP
     hmenuPopup = (HMENU) wParam;         // 表示されようとしているポップアップメニューのハンドル
     uPos = (UINT) LOWORD(lParam);        // ゼロベースでの位置 
     fSystemMenu = (BOOL) HIWORD(lParam); // ゼロ以外ならシステムメニューからである

ポップアップメニューがハイライト表示されてドロップダウンメニューが表示される直前に送られる。 ポップアップメニュー項目の初期化に使用される。

以上のメニューメッセージを捕まえて試してみよう。

  1. エクスプローラを使って、新しいフォルダを 2107MenuMessage という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 2106MenuClass02 フォルダの MenuClass02.dprMyRes.resSkeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pasMenuClass.pas をコピーする。
  3. コピーした MenuClass02.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに MenuClass02.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで MenuMessage.dpr という名前で保存する。
  5. コードエディタで以下のように編集する。
  6. [F.9]でコンパイルして実行する。

以下に、 MenuMessage.dpr の全リストを示す。

program MenuMessage;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas',
  MenuClass in 'MenuClass.pas';

  {$R MyRes.res}

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  MainWnd: TSDIMainWindow;

  MMain: TAMainMenu;
  PFile, PHelp, POption: TAPopupMenu;
  L1,L2,L3: TAPILabel;

  flag:Boolean = true;

  hB1,hB2: HBITMAP;

const
  ID_EXIT = 200;
  ID_TEST = 201;
  ID_ABOUT = 202;

  ID_OPTION1 = 203;
  ID_OPTION2 = 204;

  ID_ALPHA = 205;
  ID_BETA = 206;
  ID_GAMMA = 207;

  ID_SEP1 = 208;
  ID_SEP2 = 209;

  ID_FILE = 300;
  ID_HELP = 301;
  ID_OPTION = 302;

  ID_BITMAP1 = 1000;
  ID_BITMAP2 = 1001;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------
procedure MainEnterMenuLoop(var Msg: TMessage);
begin
  L3.Text := 'Now in Menu Loop';
end;

procedure MainExitMenuLoop(var Msg: TMessage);
begin
  L2.Text := ' ';
  L3.Text := ' ';
end;

procedure MainInitMenuPopup(var Msg: TMessage);
begin
  L2.Text := 'OnInitPopUp'+#13+
             'Handle = '+AIntToStr(Msg.WParam)+#13+
             'position := '+AIntToStr(Msg.LParamLo);
  if Msg.LParamHi = 0 then
    L2.Text := L2.Text +#13+ 'not SysMenu'
  else
    L2.Text := L2.Text +#13+ 'SysMenu'
end;

procedure MakeMenu(hParent:HWND);
begin

  hB1 := LoadBitmap(HInstance,'B1');
  hB2 := LoadBitmap(HInstance,'B2');

  MMain := MakeMainMenu(hParent);

    PFile := MakePopupMenu(hParent,ID_FILE);
      PFile.AppendString('E&xit',ID_EXIT);
      PFile.AttachMenu(MMain,-1,'&File');

    MMain.AppendString('&Test!',ID_TEST);

    PHelp := MakePopupMenu(hParent,ID_HELP);
      PHelp.AppendString('&About..',ID_ABOUT);
      PHelp.AttachMenu(MMain,-1,'&Help');

  MMain.SetToParent;

  POption := MakePopupMenu(hParent,ID_OPTION);
    POption.AppendString('Option&1',ID_OPTION1);
    POption.AppendString('Option&2',ID_OPTION2);
    POption.AppendSeparator(ID_SEP1);
    POption.AppendRadio('&Alpha',ID_ALPHA);
    POption.AppendRadio('&Beta',ID_BETA);
    POption.AppendRadio('&Gamma',ID_GAMMA);
    POption.AppendSeparator(ID_SEP2);
    POption.AppendBitmap(hB1,ID_BITMAP1);
    POption.AppendBitmap(hB2,ID_BITMAP2);
end;

//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := MainWnd.Handle;
  MakeMenu(hWindow);

  L1 := CreateLabel(hWindow,10,50,150,150,'');
  L1.Text := 'hMain =   '+AIntToStr(MMain.Handle)+#13+
             'hFile =   '+AIntToStr(PFile.Handle)+#13+
             'hHelp =   '+AIntToStr(PHelp.Handle)+#13+
             'hOption = '+AIntToStr(POption.Handle)+#13#13+
             'SysMenu = '+AIntToStr(GetSystemMenu(MainWnd.Handle,false));

  L2 := CreateLabel(hWindow,170,50,220,90,'');

  L3 := CreateLabel(hWindow,170,150,220,40,'');

  with MainWnd do begin
    ClientWidth := 400;
    ClientHeight := 260;
    Center;
    Trap(WM_ENTERMENULOOP,MainEnterMenuLoop);
    Trap(WM_EXITMENULOOP,MainExitMenuLoop);
    Trap(WM_INITMENUPOPUP,MainInitMenuPopup);
  end;
end;

procedure OnTest(hWindow: HWND);
begin
  if flag then
    POption.AttachMenu(MMain,ID_HELP,'&Option')
  else
    POption.DetachMenu;
  flag := not flag;
end;

procedure MainCommand(var Msg: TMessage);
begin
  if (Msg.WParamHi+Msg.LParam)=0 then
    case Msg.WParamLo of
      ID_OPTION1,ID_OPTION2:
        POption.Checked[Msg.WParamLo] :=
                          not POption.Checked[Msg.WParamLo];
      ID_ALPHA..ID_GAMMA:
        POption.CheckRadio(ID_ALPHA,ID_GAMMA,Msg.WParamLo);
      ID_BITMAP1,ID_BITMAP2:MessageBeep($FFFFFFFF);
    end;
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifMenuMessage.zip(19.525kB)
MenuMessage.gif

実行すると、右図のようになる。プログラムの大部分は前節の MenuClass02.dpr と同じであるが、 メインウィンドウの Trap( ) メソッドを使って WM_ENTERMENULOOPWM_EXITMENULOOPWM_INITMENUPOPUP メッセージを捕まえてイベントハンドラで処理している。 [ALT} キーを押したり、マウスでポップアップメニューを選択して各メッセージが届くタイミングを調べてほしい。 また、システムメニューをクリックしてみて、ハンドルの値を確認したり、どんなメッセージが届くか試してみてほしい。

redball.gifこの章の目次へ戻る

21−8 イベントハンドラを追加する

前節で調べたメニュー関連のメッセージを TAMainMenuTAPopupMenu クラスで捕捉してイベントとして公開しよう。

WM_INITMENUWM_ENTERMENULOOPWM_EXITMENULOOP メッセージは、メニュー全体に関わるものなので TAMainMenu で処理するのが妥当だろう。 また、WM_COMMAND メッセージも特定のメニュー項目が選択された事に対する処理を一カ所のハンドラにまとめて処理するためには、やはり TAMainMenu クラスでイベントにしたほうがよいだろう。 WM_INITMENUPOPUP メッセージは、ポップアップメニューが表示される直前に送られる。 このタイミングで、メニュー項目の有効性を確認するために使われる。 メモ帳などで試してみると分かるが、クリップボードにテキストデータがないときは「貼り付け」メニューは使用禁止になっていなければならない。 また、テキストが選択されていないときには、「切り取り」「コピー」「削除」の項目は使用禁止になっている。このように、アプリケーションの状態により、使用可・不可状態を変更すべきであるメニュー項目を持つポップアップメニューは、WM_INITMENUPOPUP メッセージを捉えて状態変更をするべきである。 したがって、WM_INITMENUPOPUP メッセージは TAPopupMenu クラスで処理すべきであろう。 以上の事を考慮して、 前節までにつくってきた TAMainMenuTAPopupMenu クラスにイベントを追加しよう。

  1. エクスプローラを使って、新しいフォルダを 2108MenuClass03 という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 2107MenuMessage フォルダの MenuMessage.dprMyRes.resSkeleton03.dprUtilFunc.pasGDIObject.pasUtilClass.pasAPIControl.pasAPIWindow.pasMenuClass.pas をコピーする。
  3. コピーした MenuMessage.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに MenuMessage.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで MenuClass03.dpr という名前で保存する。
  5. [表示]メニューから[ユニット]を選んで表示されるダイアログから、MenuClass を選ぶ。
  6. コードエディタで以下のように編集する。
  7. [F.9]でコンパイルして実行する。

最初に、MenuClass.pasTAMainMenuTAPopupMenu クラスを以下のように変更する。ここでは、このユニットファイルの全リストを以下に示す。

unit MenuClass;

interface

uses
  windows,
  messages,
  UtilFunc,
  UtilClass;

type

//-------------------- TMenuBase ------------------------------
TMenuBase = class(TSubClass)
private
  FHandle: HMENU;
protected
public
  function AppendString(st:string;mID:UINT):Boolean;
  function InsertString(position:integer;st:string;mID:UINT):Boolean;
  function Remove(mID:UINT):Boolean;
  property Handle: HMENU read FHandle;
end;

//--------------- TAMainMenu ----------------------------------
TAMainMenu = class(TMenuBase)
private
  FOnCommand: TNotifyMessage;
  FOnInitMenu: TNotifyMessage;
  FOnEnterLoop: TNotifyMessage;
  FOnExitLoop: TNotifyMessage;
public
  constructor Create(hParent:HWND);override;
  destructor Destroy; override;
  procedure DefaultHandler(var AMessage); override;
  function SetToParent:Boolean;
  property OnCommand: TNotifyMessage read FOnCommand write FOnCommand;
  property OnInitMenu: TNotifyMessage read FOnInitMenu write FOnInitMenu;
  property OnEnterLoop: TNotifyMessage read FOnEnterLoop write FOnEnterLoop;
  property OnExitLoop: TNotifyMessage read FOnExitLoop write FOnExitLoop;
end;

//--------------- TAPopupMenu ----------------------------------
TAPopupMenu = class(TMenuBase)
private
  FID: UINT;
  FAttachedMenu: HMENU;
  FOnInitPopup: TNotifyMessage;
  function GetChecked(mID:UINT):Boolean;
  procedure SetChecked(mID:UINT;value:Boolean);
  function GetEnabled(mID:UINT):Boolean;
  procedure SetEnabled(mID:UINT;value:Boolean);
protected
public
  constructor Create(hParent:HWND);override;
  destructor Destroy; override;
  procedure DefaultHandler(var AMessage); override;
  function AppendSeparator(mID:UINT):Boolean;
  function InsertSeparator(position:integer;mID:UINT):Boolean;
  function AppendRadio(st:string;mID:UINT):Boolean;
  function InsertRadio(position:integer;st:string;mID:UINT):Boolean;
  function AppendBitmap(hB:HBITMAP;mID:UINT):Boolean;
  function InsertBitmap(position:integer;hB:HBITMAP;mID:UINT):Boolean;
  function AttachMenu(Menu:TMenuBase;position:integer;st:string):Boolean;
  function DetachMenu: Boolean;
  function CheckRadio(First,Last,Check:UINT):Boolean;
  function ChangeBitmap(mID:UINT;hB:HBITMAP):Boolean;
  property ID: UINT read FID write FID;
  property AttachedMenu: HMENU read FAttachedMenu;
  property Checked[mID:UINT]:Boolean read GetChecked write SetChecked;
  property Enabled[mID:UINT]:Boolean read GetEnabled write SetEnabled;
  property OnInitPopup: TNotifyMessage read FOnInitPopup write FOnInitPopup;
end;

//---------------------------------------------------------------
function MakeMainMenu(hParent:HWND;sOnCommand:TNotifyMessage): TAMainMenu;
function MakePopupMenu(hParent:HWND;ID:UINT;
                                sOnInitPopup:TNotifyMessage): TAPopupMenu;

implementation

//-------------------- TMenuBase ------------------------------
function TMenuBase.AppendString(st:string;mID:UINT):Boolean;
begin
  result := AppendStringMenu(FHandle,st,mID);
end;

function TMenuBase.InsertString(position:integer;st:string;mID:UINT):Boolean;
begin
  result := InsertStringMenu(FHandle,position,st,mID);
end;

function TMenuBase.Remove(mID:UINT):Boolean;
begin
  result := Boolean(RemoveMenu(FHandle,mID,MF_BYCOMMAND));
end;

//--------------- TAMainMenu ----------------------------------
constructor TAMainMenu.Create(hParent:HWND);
begin
  inherited Create(hParent);

  FHandle := CreateMenu;
  Dispatcher.TrapMessage(WM_COMMAND,self);
  Dispatcher.TrapMessage(WM_INITMENU,self);
  Dispatcher.TrapMessage(WM_ENTERMENULOOP,self);
  Dispatcher.TrapMessage(WM_EXITMENULOOP,self);
end;

destructor TAMainMenu.Destroy;
var
  hM:HMENU;
begin
  hM := GetMenu(Parent);
  if hM <> FHandle then DestroyMenu(FHandle);

  inherited Destroy;
end;

procedure TAMainMenu.DefaultHandler(var AMessage);
var
  AMsg: TMessage;
begin
  AMsg := TMessage(AMessage);
  case AMsg.Msg of

    WM_COMMAND:begin
      if (AMsg.WParamHi+AMsg.LParam)=0 then begin
        AMsg.Msg := UINT(self);  // sender
        if Assigned(FOnCommand) then OnCommand(AMsg);
      end;
    end;

    WM_INITMENU:begin
      if AMsg.WParam = WPARAM(FHandle) then begin
        AMsg.Msg := UINT(self);  // sender
        if Assigned(FOnInitMenu) then OnInitMenu(AMsg);
      end;
    end;

    WM_ENTERMENULOOP:begin
      AMsg.Msg := UINT(self);  // sender
      if Assigned(FOnEnterLoop) then OnEnterLoop(AMsg);
    end;

    WM_EXITMENULOOP:begin
      AMsg.Msg := UINT(self);  // sender
      if Assigned(FOnExitLoop) then OnExitLoop(AMsg);
    end;
    
  end;
end;

function TAMainMenu.SetToParent:Boolean;
begin
  result := Boolean(SetMenu(Parent,FHandle));
end;

//--------------- TAPopupMenu ----------------------------------
constructor TAPopupMenu.Create(hParent:HWND);
begin
  inherited Create(hParent);

  FHandle := CreatePopupMenu;
  Dispatcher.TrapMessage(WM_INITMENUPOPUP,self);
end;

destructor TAPopupMenu.Destroy;
begin
  if FAttachedMenu = 0 then DestroyMenu(FHandle);

  inherited Destroy;
end;

procedure TAPopupMenu.DefaultHandler(var AMessage);
var
  AMsg: TMessage;
begin
  AMsg := TMessage(AMessage);
  if AMsg.Msg = WM_INITMENUPOPUP then
    if AMsg.WParam = WPARAM(FHandle) then begin
      AMsg.Msg := UINT(self);  // sender
      if Assigned(FOnInitPopup) then OnInitPopup(AMsg);
    end;
end;

function TAPopupMenu.AppendSeparator(mID:UINT):Boolean;
begin
  result := AppendSeparatorMenu(FHandle,mID);
end;

function TAPopupMenu.InsertSeparator(position:integer;mID:UINT):Boolean;
begin
  result := InsertSeparatorMenu(FHandle,position,mID);
end;

function TAPopupMenu.AppendRadio(st:string;mID:UINT):Boolean;
begin
  result := AppendRadioMenu(FHandle,st,mID);
end;

function TAPopupMenu.InsertRadio(position:integer;st:string;mID:UINT):Boolean;
begin
  result := InsertRadioMenu(FHandle,position,st,miD);
end;

function TAPopupMenu.AppendBitmap(hB:HBITMAP;mID:UINT):Boolean;
begin
  result := AppendBitmapMenu(FHandle,hB,mID);
end;

function TAPopupMenu.InsertBitmap(position:integer;hB:HBITMAP;mID:UINT):Boolean;
begin
  result := InsertBitmapMenu(FHandle,position,hB,mID);
end;

function TAPopupMenu.AttachMenu(Menu:TMenuBase;position:integer;st:string):Boolean;
begin
  if FAttachedMenu <> 0 then begin
    result := false;
    exit;
  end else begin
    result := InsertPopupMenu(Menu.FHandle,position,st,FHandle,FID);
    DrawMenuBar(Parent);
  end;
  if result then FAttachedMenu := Menu.FHandle;
end;

function TAPopupMenu.DetachMenu: Boolean;
begin
  if FAttachedMenu = 0 then begin
    result := false;
    exit;
  end else begin
    result := Boolean(RemoveMenu(FAttachedMenu,FID,MF_BYCOMMAND));
    DrawMenuBar(Parent);
  end;
  if result then FAttachedMenu := 0;
end;

function TAPopupMenu.CheckRadio(First,Last,Check:UINT):Boolean;
begin
  result := Boolean(CheckMenuRadioItem(FHandle,First,Last,Check,MF_BYCOMMAND));
end;

function TAPopupMenu.ChangeBitmap(mID:UINT;hB:HBITMAP):Boolean;
var
  MII:TMenuItemInfo;
begin
  FillChar(MII,SizeOf(TMenuItemInfo),0);
  with MII do begin
    cbSize := 44; // Required for Windows 95
    fMask := MIIM_TYPE;
    fType := MFT_BITMAP;
    dwTypeData := PChar(hB);
  end;
  result := Boolean(SetMenuItemInfo(FHandle,mID,false,MII));
end;

function TAPopupMenu.GetChecked(mID:UINT):Boolean;
begin
  result := IsMenuChecked(FHandle,mID);
end;

procedure TAPopupMenu.SetChecked(mID:UINT;value:Boolean);
begin
  SetMenuChecked(FHandle,mID,value);
end;

function TAPopupMenu.GetEnabled(mID:UINT):Boolean;
begin
  result := IsMenuEnabled(FHandle,mID);
end;

procedure TAPopupMenu.SetEnabled(mID:UINT;value:Boolean);
begin
  SetMenuEnabled(FHandle,mID,value);
end;

//---------------------------------------------------------------
function MakeMainMenu(hParent:HWND;sOnCommand:TNotifyMessage): TAMainMenu;
begin
  result := TAMainMenu.Create(hParent);
  result.FOnCommand := sOnCommand;
end;

function MakePopupMenu(hParent:HWND;ID:UINT;
                                sOnInitPopup:TNotifyMessage): TAPopupMenu;
begin
  result := TAPopupMenu.Create(hParent);
  result.FID := ID;
  result.FOnInitPopup := sOnInitPopup;
end;

end.

TAMainMenu クラスでは、OnCommandOnInitMenuOnEnterLoopOnExitLoop のイベントが追加された。対応するメッセージを処理するために DefaultHandler( ) をオーバーライドしている。 コンストラクタでは、Dispatcher にこれらのメッセージを送ってくれるように依頼している。

constructor TAMainMenu.Create(hParent:HWND);
begin
  inherited Create(hParent);

  FHandle := CreateMenu;
  Dispatcher.TrapMessage(WM_COMMAND,self);
  Dispatcher.TrapMessage(WM_INITMENU,self);
  Dispatcher.TrapMessage(WM_ENTERMENULOOP,self);
  Dispatcher.TrapMessage(WM_EXITMENULOOP,self);
end;

そして、DefaultHandler( ) で処理している。

procedure TAMainMenu.DefaultHandler(var AMessage);
var
  AMsg: TMessage;
begin
  AMsg := TMessage(AMessage);
  case AMsg.Msg of

    WM_COMMAND:begin
      if (AMsg.WParamHi+AMsg.LParam)=0 then begin
        AMsg.Msg := UINT(self);  // sender
        if Assigned(FOnCommand) then OnCommand(AMsg);
      end;
    end;

    WM_INITMENU:begin
      if AMsg.WParam = WPARAM(FHandle) then begin
        AMsg.Msg := UINT(self);  // sender
        if Assigned(FOnInitMenu) then OnInitMenu(AMsg);
      end;
    end;

    WM_ENTERMENULOOP:begin
      AMsg.Msg := UINT(self);  // sender
      if Assigned(FOnEnterLoop) then OnEnterLoop(AMsg);
    end;

    WM_EXITMENULOOP:begin
      AMsg.Msg := UINT(self);  // sender
      if Assigned(FOnExitLoop) then OnExitLoop(AMsg);
    end;
    
  end;
end;

WM_COMMAND の処理では if 文を使って、このメッセージがメニューからのものであることを確かめている。したがってコントロールからのメッセージではイベントが発生しない。 WM_INITMENU ではハンドルの値を確認して自分からのものであることを確かめている。

TAPopupMenu クラスでは、OnInitPopup イベントが追加された。 DefaultHandler( ) を見てみよう。

procedure TAPopupMenu.DefaultHandler(var AMessage);
var
  AMsg: TMessage;
begin
  AMsg := TMessage(AMessage);
  if AMsg.Msg = WM_INITMENUPOPUP then
    if AMsg.WParam = WPARAM(FHandle) then begin
      AMsg.Msg := UINT(self);  // sender
      if Assigned(FOnInitPopup) then OnInitPopup(AMsg);
    end;
end;

ここでも、ハンドルの値を使って自分からのものだけでイベントが発生するようにしている。 さらに、 TAPopupMenu クラスには ChangeBitmap( ) メソッドと Enabled[mID:UINT] プロパティーが追加されている。 Enabled プロパティーは、メニューIDを指定して各メニュー項目の使用可・不可状態を取得・設定するのに使われる。 ビットマップ項目は、使用不可の状態でも文字列項目と違って外観が変化しないので ChangeBitmap( ) メソッドでビットマップを変更して使用不可の状態をユーザに知らせる。インスタンス生成関数もイベントハンドラを指定できるように変更してある。

それでは、これらのクラスのイベントハンドラをテストしてみよう。まず、リソース記述ファイルを以下のようにして、'MyRes.res' として保存し、コンパイルする。

//
//  MyRes.rc
//

B1 BITMAP "B1.bmp"
B1Gray BITMAP "B1Gray.bmp"
B2 BITMAP "B2.bmp"
B2Gray BITMAP "B2Gray.bmp"

B1Gray.bmpB2Gray.bmp は対応するビットマップのモノクロ画像である。 MenuClass03.dpr の全リストを以下に示す。

program MenuClass03;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  UtilClass in 'UtilClass.pas',
  GDIObject in 'GDIObject.pas',
  APIWindow in 'APIWindow.pas',
  APIControl in 'APIControl.pas',
  MenuClass in 'MenuClass.pas';

  {$R MyRes.res}

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  MainWnd: TSDIMainWindow;

  MMain: TAMainMenu;
  PFile, PHelp, POption: TAPopupMenu;
  L1,L2,L3: TAPILabel;
  CG: TCheckGroup;

  hB1,hB1Gray,hB2,hB2Gray: HBITMAP;

const
  ID_EXIT = 200;
  ID_TEST = 201;
  ID_ABOUT = 202;

  ID_OPTION1 = 203;
  ID_OPTION2 = 204;

  ID_ALPHA = 205;
  ID_BETA = 206;
  ID_GAMMA = 207;

  ID_SEP1 = 208;
  ID_SEP2 = 209;

  ID_FILE = 300;
  ID_HELP = 301;
  ID_OPTION = 302;

  ID_BITMAP1 = 1000;
  ID_BITMAP2 = 1001;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------

//------------------------------------------------------------
//             Object Event Handlers
//------------------------------------------------------------
procedure MenuCommand(var Msg: TMessage);
begin
  if (Msg.WParamHi+Msg.LParam)=0 then
    case Msg.WParamLo of
      ID_OPTION1,ID_OPTION2:
        POption.Checked[Msg.WParamLo] :=
                          not POption.Checked[Msg.WParamLo];
      ID_ALPHA..ID_GAMMA:
        POption.CheckRadio(ID_ALPHA,ID_GAMMA,Msg.WParamLo);
      1000,1001:MessageBeep($FFFFFFFF);
    end;
  L1.Text := 'WM_COMMAND : ID = '+AIntToStr(Msg.WParamLo);
end;

procedure OptionInitPopup(var Msg: TMessage);
begin
  POption.Enabled[ID_OPTION1] := CG.Checked[0];
  POption.Enabled[ID_OPTION2] := CG.Checked[1];
  POption.Enabled[ID_ALPHA] := CG.Checked[2];
  POption.Enabled[ID_BETA] := CG.Checked[3];
  POption.Enabled[ID_GAMMA] := CG.Checked[4];
  POption.Enabled[ID_BITMAP1] := CG.Checked[5];
  POption.Enabled[ID_BITMAP2] := CG.Checked[6];
  if CG.Checked[5] then
    POption.ChangeBitmap(ID_BITMAP1,hB1)
  else
    POption.ChangeBitmap(ID_BITMAP1,hB1Gray);
  if CG.Checked[6] then
    POption.ChangeBitmap(ID_BITMAP2,hB2)
  else
    POption.ChangeBitmap(ID_BITMAP2,hB2Gray);
end;

procedure MenuEnterLoop(var Msg: TMessage);
begin
  L2.Text := ' Now in MenuLoop';
end;

procedure MenuExitLoop(var Msg: TMessage);
begin
  L2.Text := '';
  L3.Text := '';
end;

procedure MenuInit(var Msg: TMessage);
begin
  L3.Text := 'InitMenu Received!';
end;

procedure MakeMenu(hParent:HWND);
begin

  hB1 := LoadBitmap(HInstance,'B1');
  hB1Gray := LoadBitmap(HInstance,'B1Gray');
  hB2 := LoadBitmap(HInstance,'B2');
  hB2Gray := LoadBitmap(HInstance,'B2Gray');

  MMain := MakeMainMenu(hParent,MenuCommand);
  MMain.OnEnterLoop := MenuEnterLoop;
  MMain.OnExitLoop := MenuExitLoop;
  MMain.OnInitMenu := MenuInit;

    PFile := MakePopupMenu(hParent,ID_FILE,nil);
      PFile.AppendString('E&xit',ID_EXIT);
      PFile.AttachMenu(MMain,-1,'&File');

    MMain.AppendString('&Test!',ID_TEST);

    PHelp := MakePopupMenu(hParent,ID_HELP,nil);
      PHelp.AppendString('&About..',ID_ABOUT);
      PHelp.AttachMenu(MMain,-1,'&Help');

    POption := MakePopupMenu(hParent,ID_OPTION,OptionInitPopup);
      POption.AppendString('Option&1',ID_OPTION1);
      POption.AppendString('Option&2',ID_OPTION2);
      POption.AppendSeparator(ID_SEP1);
      POption.AppendRadio('&Alpha',ID_ALPHA);
      POption.AppendRadio('&Beta',ID_BETA);
      POption.AppendRadio('&Gamma',ID_GAMMA);
      POption.AppendSeparator(ID_SEP2);
      POption.AppendBitmap(hB1,ID_BITMAP1);
      POption.AppendBitmap(hB2,ID_BITMAP2);
      POption.AttachMenu(MMain,ID_HELP,'&Option');

  MMain.SetToParent;

end;
//------------------------------------------------------------
//             Main Window Event Handlers
//------------------------------------------------------------
procedure MainCreate(var Msg: TMessage);
var
  hWindow: HWND;
  i:integer;
begin
  hWindow := MainWnd.Handle;

  MakeMenu(hWindow);

  L1 := CreateLabel(hWindow,10,110,250,30,'');
  L2 := CreateLabel(hWindow,10,150,250,30,'');
  L3 := CreateLabel(hWindow,10,190,250,30,'');

  CG := CreateCheckGroup(hWindow,true,300,50,120,160,7,nil);
  CG.Captions[0] := 'Option1';
  CG.Captions[1] := 'Option2';
  CG.Captions[2] := 'Alpha';
  CG.Captions[3] := 'Beta';
  CG.Captions[4] := 'Gamma';
  CG.Captions[5] := 'Bitmap1';
  CG.Captions[6] := 'Bitmap2';
  for i := 0 to 6 do CG.Checked[i] := true;

  with MainWnd do begin
    ClientWidth := 400;
    ClientHeight := 260;
    Center;
  end;
end;

procedure OnTest(hWindow: HWND);
begin
  MessageBeep($FFFFFFFF);
end;

procedure MainCommand(var Msg: TMessage);
begin
  Msg.Msg := MainWnd.Handle;
  InitMenuCommand(Msg,OnTest);
end;

procedure MainDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MainWnd := TSDIMainWindow.Create(0,GetExeName);
  with MainWnd do begin
    OnCreate := MainCreate;
    OnDestroy := MainDestroy;
    OnCommand := MainCommand;
  end;
  MainWnd.DoCreate;

  Halt(MessageLoopNormal);

end.

DownloadExe.gifMenuClass03.zip(21.140kB)
MenuClass03.gif

実行すると、右図のようになる。前節のプログラムとほとんど同じであるが、各メッセージの処理をメニュークラスのイベントハンドラで行っている。 また、チェックグループのチェック状態と同期して、Option メニュー項目の使用可能状態が変更される。 これは、OnInitPopup イベントに同期して行われる。

以上で、一般的なメニューの機能を包み込んだクラスは一応完成した。次章では、ショートカットメニューシステムメニューアクセレレータについて調べることにする。

redball.gifこの章の目次へ戻る


ここまでのプログラムファイル

  1. Skeleton03.dpr (2 kB)
  2. UtilFunc.pas (18 kB)
  3. UtilClass.pas (62 kB)
  4. GDIObject.pas (21 kB)
  5. APIControl.pas (61 kB)
  6. APIWindow.pas (19 kB)



  1. MenuClass.pas(9 kB)


redball.gifこの章の目次へ戻る