ユーザーインタフェースの中で最も重要なのは、たぶん、メニューであろう。これまで本稿では、 骨格プログラムでメニューを使用してきたが、 それは、2−3 メニューを装備する 節でコードにより動的に作成したものだった。メニューを作るには以下の三つの方法がある。
最後の方法はあまり使われないが、前二方法の中間的なメニュー作成方法である。本章と次章では、リソースを用いる方法を簡単に説明し、あとはもっぱらAPI関数を用いてメニューを動的に作成し、制御する方法を調べることにする。
第21章 メニューの研究 その1 |
21−1 メニューリソース |
21−2 複雑なメニューのリソース |
21−3 メニューの構造 |
21−4 メニュー作成関数 |
21−5 メニュークラスをつくる |
21−6 ラジオ項目、ビットマップ項目 |
21−7 メニューメッセージ |
21−8 イベントハンドラを追加する |
本稿のような、大きなクラスライブラリやRAD開発環境を使わないコードベースのウィンドウズプログラミングでのメニュー作成の正統的な方法は、メニューリソースを使う方法である。 この節では、 メニューリソース入門として、メニューリソースをつかって Skeleton03 骨格プログラムと同じメニューを作成してみよう。
第19章 アイコン、ビットマップ、リソース の章では、 ビットマップやアイコンをリソースとして実行可能ファイル (*.exe) に取り込む方法を示した。 そこでは、テキストエディタでビットマップファイルやアイコンファイルをリソースとして定義する「リソース記述ファイル」(*.rc) を作成し、それを Delphi に付属しているリソースコンパイラでコンパイルしてリソースファイル (*.res) を作成し、それをコードの中で {$R *.res} のコンパイラ指令を使って実行可能ファイルに取り込んでいた。 Delphi5 では、リソース記述ファイルをプロジェクトに加えると自動的にコンパイルされるらしいが、基本的な手順は同じである。 メニューリソースも全く同様の手順により実行可能ファイルに取り込むことができる。 それでは早速試してみよう。
最初に、メニューリソース記述ファイルをつくる準備として、ユーザがメニュー項目を選んだとき 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' という名前でセーブする。 #include で MenuID.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.
実行すると右図のように、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 での定義と同じ値にしたので、メッセージ処理は同じコードで実行される。
以上、本節で見てきたように、ビットマップやアイコンと同じ手続きでメニューもリソースとすることができる。画像ファイルはともかく、プログラムコードと密接な関係を持つメニューの作成情報がリソースとしてコードと切り離して作成されるのは、少々奇異な感じがする。しかし、ユーザインタフェースの作成情報をコードと切り離すという思想は、プログラムのメンテナンスや国際化対応を考えると納得できるものだ。 後に示すように、ダイアログボックスでさえもリソースから作成されるのである。
前節のメニューでは簡単すぎるので、ここでは、少々入り組んだメニューのリソース例を示す。 前節と同じ手続きを踏んで作られるリソースを 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
実行時の様子を右図に示す。 作成されたメニューの構造と上のリソース記述ファイルを見較べてほしい。インデントは、ポップアップメニューの入れ子の関係と対応していることに気づくはずである。
本節以降の本章と次章では、 コードでメニューを動的に作成し、その機能をカプセル化してメニュークラスを新しく作る事を考える。 メニューが発行するメッセージの処理もこれらのクラスを使って行う。
骨格プログラム 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回使っていることが分かるだろう。 本節では、この関数を書き直してもっとすっきりした形にしてから、メニューの構造について簡単に考察する。
以下に 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.
実行すると、右図のようにメニュー項目が少し違うだけで 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 は、ウィンドウに直接設置され、メニューバーに常時その項目内容が表示されている、ということが他の三つのポップアップメニューと違っている点である。 これをトップレベルメニューという。トップレベルメニュー以外は、その階層がどれだけ深くても、その本質には差がない。以上をまとめると、
となるだろう。 以上のことから、自分のメニューハンドルを管理し、メニュー項目を追加・挿入・削除するメソッドと有効性やチェック状態を取得・設定するプロパティーやメソッドを備えた二種類のクラスを作成すれば、メニューの全機能を利用できると考えられる。 以降の節では、 実際にこのようなクラスを作成してゆくことにする。
空のメニューをつくってメニューハンドルを返す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 には、挿入する位置を指定する。追加は、末尾への挿入と考える。 p3 が true なら、p2 に設定する値は、メニューの一番上をゼロとしたときの挿入位置の番号を指定する。p3 が false なら、現在、挿入する位置にあるメニュー項目の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 は、ポップアップメニューのハンドルを取得・設定する。
hbmpChecked と hbmpUnchecked には、カスタムチェックマークのビットマップハンドルを取得・設定する。
dwItemData は、アプリケーション定義のメニュー項目データを取得・設定する。
dwTypeData と cch は、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( ) 関数で置き換える事が出来た。 他のタイプの項目についても追加・挿入関数を作って試してみよう。
最初に 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_ENABLED や MFS_UNCHECKED、MFS_UNHILITE などは、デフォルトのゼロと定義されているので、ビットマスクには使えないのである。 代わりに、MFS_GRAYED や MFS_CHECKED 、MFS_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.
実行して、Test! メニューを選択すると Test! 項目のあとに Option 項目が挿入される。エクスプローラから適当なビットマップファイルをドラグドロップすると、Option ポップアップメニューにビットマップ項目が追加される。 Delphi に付属する Image Editor でつくった 80x30 のビットマップを二つドロップしたときの様子を右図に示す。 Option1 と Option2 項目を選択するとチェック状態が反転する。また、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;
グローバル変数の flag が true のとき ID_HELP の位置に hOption を挿入し、false のときは削除している。
function RemoveMenu(hMenu: HMENU; uPosition, uFlags: UINT): BOOL; stdcall;
この関数は、hMenu のメニューから uPosition の項目を取り外す。取り外すだけでメニューハンドルは有効のままである。 uFlags に MF_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.WParamLo を case 文のセレクタにして、メニューIDを使って分岐している。 チェック状態の反転は分かり易いコードだろう。 ラジオ項目に排他的にチェックマークを付けるには、CheckMenuRadioItem( ) APIを使う。
function CheckMenuRadioItem(hMenu: HMENU; First, Last, Check, Flags: UINT): BOOL; stdcall;
Flags の意味は、上の RemoveMenu( ) のときと同じである。 ここでは、MF_BYCOMMAND を指定して位置をIDで指定している。 First と Last にはラジオ項目のグループの始まりと終わりのIDを指定する。したがって、同じグループのラジオ項目は、一続きのIDを設定する必要がある。 Check には、チェックマークを付ける項目のIDを指定する。
CreateMenu や CreatePopupMenu で作ったメニューは、ウィンドウに設置されている限りアプリケーションの終了時にシステムが廃棄してくれる。 ウィンドウに設置されていないときは、
function DestroyMenu(hMenu: HMENU): BOOL; stdcall;
で廃棄する。 hMenu がメニュー項目としてポップアップメニュー項目を含んでいるときは、その下のすべてのポップアップメニューも廃棄される。 ここでは、MainDestroy( ) ハンドラで、 flag の値を参照して、hOption が取り付けられていないときはこのAPIで廃棄している。
以上で、API関数をカプセル化した関数によりメニューをコードで作成できるようになった。
本章の 21−3 メニューの構造 で調べたように、メニューは一つのトップレベルメニューと幾つかのポップアップメニューが入れ子の階層を作って成り立っている事が分かった。 また前節では、 メニュー作成にかかわるAPI関数をカプセル化した関数を UtilFunc.pas に作った。 この節以後の本章では、 この二つの種類のメニューをカプセル化したクラスを作成することを考える。 本節では手始めに、文字列項目とセパレータだけからなるメニューの機能をカプセル化したクラスを作成してテストする。
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.
実行すると右図のようになる。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;
これらのメソッドのように、取り付けられるほうのメソッドではなく、取り付ける方の(つまり「ぶら下がる」方の)メソッドとした方が、 ハンドルを管理しやすいのは明らかだろう。 ぶら下がる方にとって、上の階層のメニューは常に一つしかないからだ。
以上で、 文字列項目とセパレータだけからなるメニュー作成のための基本的なクラスが出来たので、 次節以降で、これらにメソッドやプロパティー追加して、機能を充実させていこう。
前節で作った TAPopupMenu クラスにラジオ項目、ビットマップ項目を追加・挿入するメソッドを装備し、それらを制御するメソッドとプロパティーを追加しよう。
最初に、 MenuClass.pas の TAPopupMenu クラスを以下のように書き換える。 宣言部を以下に示す。
//--------------- 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.
実行して、Test! メニューで Option メニューを設置すると右図のようになる。 Option1 と Option2 項目を選択するとチェック状態が反転する。 Alpha 項目から Gamma 項目までは、ラジオチェックが排他的に付けられる。 ビットマップ項目を選択するとシステムブザーが鳴る。 まだ、このクラスはイベントを持っていないので、これらの WM_COMMAND メッセージ処理は MainCommand( ) ハンドラで行っている。
メニューは、MakeMenu( ) で作成している。 リソースからビットマップをロードしていること以外は、 前とほとんど同じである。
ここまでで、メニュー作成の機能は整ったので、次節ではメニューに関連するメッセージを調べ、 イベントとして公開しよう。
骨格プログラム 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_COMMAND、WM_ENTERMENULOOP、WM_EXITMENULOOP、WM_INITMENU、WM_INITMENUPOPUP メッセージを調べよう。
・WM_COMMAND wNotifyCode = HIWORD(wParam); // メニューではゼロ wID = LOWORD(wParam); // メニューID hwndCtl = (HWND) lParam; // メニューではゼロ
このメッセージは、すでにおなじみである。 コントロールや後に説明するアクセレレータキーもこのメッセージを発行するが、WParamHi と LParam がともにゼロのときだけメニューからのものである事が確かめられる。
・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); // ゼロ以外ならシステムメニューからである
ポップアップメニューがハイライト表示されてドロップダウンメニューが表示される直前に送られる。 ポップアップメニュー項目の初期化に使用される。
以上のメニューメッセージを捕まえて試してみよう。
以下に、 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.
実行すると、右図のようになる。プログラムの大部分は前節の MenuClass02.dpr と同じであるが、 メインウィンドウの Trap( ) メソッドを使って WM_ENTERMENULOOP、WM_EXITMENULOOP、WM_INITMENUPOPUP メッセージを捕まえてイベントハンドラで処理している。 [ALT} キーを押したり、マウスでポップアップメニューを選択して各メッセージが届くタイミングを調べてほしい。 また、システムメニューをクリックしてみて、ハンドルの値を確認したり、どんなメッセージが届くか試してみてほしい。
前節で調べたメニュー関連のメッセージを TAMainMenu と TAPopupMenu クラスで捕捉してイベントとして公開しよう。
WM_INITMENU や WM_ENTERMENULOOP や WM_EXITMENULOOP メッセージは、メニュー全体に関わるものなので TAMainMenu で処理するのが妥当だろう。 また、WM_COMMAND メッセージも特定のメニュー項目が選択された事に対する処理を一カ所のハンドラにまとめて処理するためには、やはり TAMainMenu クラスでイベントにしたほうがよいだろう。 WM_INITMENUPOPUP メッセージは、ポップアップメニューが表示される直前に送られる。 このタイミングで、メニュー項目の有効性を確認するために使われる。 メモ帳などで試してみると分かるが、クリップボードにテキストデータがないときは「貼り付け」メニューは使用禁止になっていなければならない。 また、テキストが選択されていないときには、「切り取り」「コピー」「削除」の項目は使用禁止になっている。このように、アプリケーションの状態により、使用可・不可状態を変更すべきであるメニュー項目を持つポップアップメニューは、WM_INITMENUPOPUP メッセージを捉えて状態変更をするべきである。 したがって、WM_INITMENUPOPUP メッセージは TAPopupMenu クラスで処理すべきであろう。 以上の事を考慮して、 前節までにつくってきた TAMainMenu と TAPopupMenu クラスにイベントを追加しよう。
最初に、MenuClass.pas の TAMainMenu と TAPopupMenu クラスを以下のように変更する。ここでは、このユニットファイルの全リストを以下に示す。
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 クラスでは、OnCommand、OnInitMenu、OnEnterLoop、OnExitLoop のイベントが追加された。対応するメッセージを処理するために 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.bmp と B2Gray.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.
実行すると、右図のようになる。前節のプログラムとほとんど同じであるが、各メッセージの処理をメニュークラスのイベントハンドラで行っている。 また、チェックグループのチェック状態と同期して、Option メニュー項目の使用可能状態が変更される。 これは、OnInitPopup イベントに同期して行われる。
以上で、一般的なメニューの機能を包み込んだクラスは一応完成した。次章では、ショートカットメニュー、システムメニュー、アクセレレータについて調べることにする。
ここまでのプログラムファイル