メニューバーを作成してみる ACT-1

今になって考えてみると、前回まで作成してきたプログラムは Win32 API プログラミングの中でもかなり高度な部類に入るものだったような気もします。今回からは、スタンダードなものをやっていくことにしましょう。

メニューバー

Windowsアプリケーションなら当たり前というぐらいに、いろいろなアプリケーションがウィンドウにメニューバーを持っています。しかし、HSPの標準機能だけではメニューを実現させることができませんでした。そこで今回は、Win32 APIとメッセージ取得機能を用いてメニューを使ってみようと思います。

今回作成するもは、右のようなメニューバーです。メニューのアイテムにサブメニューを持たせていないので、メニューアイテムをクリックしても新しいメニューが表示されてきたりはしません。


Visual C++ などのプログラミング言語では、メニューを作成するのに『メニューリソース』と呼ばれるものを使うことができるため、メニューを作るのは簡単なのですが、HSPでメニューを作ろうとすると、一からAPIで作成していかなければならないため、やや煩雑になってしまいます。とりあえずは簡単なところからやっていきましょう。

メニューの作成

今回は階層が1つだけの簡単なメニューを作成します。作成する手順は以下のようになります。

  1. メニューを作成する。
  2. メニューアイテムを追加する。アイテムが複数あれば必要なだけ行なう。
  3. メニューをウィンドウに割り当てる。
  4. メニューを再描画する。
  5. WM_COMMAND メッセージ(コード0x0111)を待つ。

とりあえず順番に見ていきましょう。

メニューバーの作成

まずはメニューバーを作成します。メニューバーを作成するには CreateMenu 関数を呼び出します。

HMENU CreateMenu(VOID);

この関数は、メニューバーとしてウィンドウに表示するためのメニューを作成して、そのハンドルを返します。このハンドルはメニューを識別するための値で、以後、このメニューを操作するにはこのハンドルを用いることになります。


余談になりますが、メニューもまた、Windowsオブジェクトの1つです。すなわち、『メニューオブジェクト』であるということです。したがって、他のWindowsオブジェクトと同じように、ハンドルによって識別されています。

メニューなどのようなユーザーインターフェースの機能は、主に USER32.DLL によって提供されているものです。つまり、以前に紹介したWindowsオブジェクトの3つの分類から考えると、メニューは「ユーザーオブジェクト」に属するというわけです。

メニューアイテムの追加

メニューを作成したら、そのメニューにメニューアイテムを追加していきます。メニューアイテムを追加するには AppendMenu 関数を呼び出します。

BOOL AppendMenuA(
    HMENU  hMenu,      // メニューハンドル
    UINT   uFlags,     // オプションフラグ
    UINT   uIDNewItem, // アイテム ID またはサブメニューのハンドル
    PCTSTR pNewItem    // 表示する文字列
);

この関数は、指定されたメニューの最後尾に新しいメニューアイテムを追加します。追加するメニューアイテムの数だけ呼び出します。

第3引数(uIDNewItem パラメータ)には、追加するメニューアイテムのIDを指定するようになっています。ユーザーがメニューアイテムを選択したとき、Windowsはそのメニューを所有するウィンドウに WM_COMMAND メッセージを送信します。このメッセージには付加情報として、 wParam パラメータの下位ワードに、このパラメータで指定されたアイテムIDが含まれています。アイテムごとに違うIDを指定していくことで、メッセージが送られたときにどのアイテムが選択されたのかを判別することができます。

第4引数(pNewItem パラメータ)には、メニューアイテムに表示する文字列のアドレスを指定します。この文字列に、「終了(&Q)」というように、アルファベットの直前にアンパサンド(&)を指定すると、[ALT]キーとそのアルファベットのキーを同時に押すことでそのメニューアイテムを選択することができるようになります。アンパサンド自体はメニューには表示されず、アルファベットに下線が引かれます。

ウィンドウへの割り当てと再描画

さて、メニューの準備ができたら、ウィンドウにそのメニューを割り当てて、さらに再描画を行ないます。これによってウィンドウにメニューバーが表示されます。

ウィンドウにメニューを割り当てるには SetMenu 関数を使います。

BOOL SetMenu(
    HWND  hWnd,    // ウィンドウハンドル
    HMENU hMenu    // メニューハンドル
);

続いて、メニューの再描画を行なうために DrawMenuBar 関数を呼び出します。

BOOL DrawMenuBar(
    HWND  hWnd   // ウィンドウハンドル
);

ウィンドウにメニューを割り当ててから、メニューアイテムを追加していくこともできます。この場合は、アイテムを追加し終わった後で必ず DrawMenuBar 関数を呼び出して、メニューを再描画するようにしてください。

メニューオブジェクトの削除について

以前まで扱ってきたWindowsオブジェクトは、必要なくなった時点で削除してきましたよね。ということは、メニューもオブジェクトである以上は削除しておく必要があるのでしょうか。

結果から言うと、今回の場合は削除する必要がありません。本来ならメニューも DestroyMenu 関数を呼び出して、メニューオブジェクトの削除をしなければならないのですが、ウィンドウに割り当てられているメニュー(およびそのサブメニュー)は、ウィンドウが破棄されるときに自動的に削除されるようになっているためです。

WM_COMMAND メッセージの処理

ここまでやればウィンドウにメニューバーが表示されていると思います。あとはメッセージが送られているかを監視していればOKです。メニューアイテムが選択されると、ウィンドウに WM_COMMAND メッセージが送られます。

#define  WM_COMMAND    0x0111

WM_COMMAND
    wID         =  wParam & 0xFFFF;
    wNotifyCode = (wParam >> 16) & 0xFFFF;
    hwndControl =  lParam;

このメッセージの wParam パラメータの下位ワード(wID パラメータ)は、選択されたメニューアイテムのIDになります。この値からどのアイテムが選択されたのを知ることができます。

ただし、この WM_COMMAND メッセージは、メニュー以外でもさまざまな目的で使われているため、メッセージを受け取ったからといって、メニューアイテムが選択されたのだとは限らないのです。

例えば、エディットコントロール(mesbox 命令や input 命令で作成されるHSPオブジェクト)やコンボボックス、リストボックスなどの『Windows標準コントロール』と呼ばれるものは、何らかのイベントが発生したときに、親ウィンドウに対して WM_COMMAND メッセージを送信します。また、ツールバーなどの『コモンコントロール』にも、 WM_COMMAND メッセージを送信するものがあります。

したがって、 mesbox 命令を使ってウィンドウにエディットコントロールを作成してしまっていると、例えばエディットコントロールをクリックしたりしただけでも、親ウィンドウは WM_COMMAND メッセージを受け取ってしまうわけです。

そこで、メニューから送られたのか、それ以外のコントロールから送られたのかを判別する必要があります。コントロールから送られると、 WM_COMMAND メッセージの lParam パラメータの値(hwndControl パラメータ)がコントロールのハンドルになるのですが、メニューから送られた場合にはこの値が 0 になるのです。そこで、 lParam パラメータが 0 であるかどうかで判別することにしましょう。

サンプルスクリプト

さて、実際にスクリプトを書いてみます。メニューアイテムIDの値を最初に定数名として #define で定義しておくと、スクリプトがよりわかりやすくなると思います。途中でウィンドウハンドルが必要になりますが、メッセージ取得設定の際に set_subclass 命令を使うと stat にウィンドウハンドルが格納されるので、これを用いています。

    #include "llmod.as"
    #include "hsgetmsg.as"

    #define WM_COMMAND    0x0111

    ; メニューアイテムIDを定義
    #define CMD_MESSAGE    1      ; 「表示」アイテムのID
    #define CMD_QUIT       2      ; 「終了」アイテムのID

    ; メニューを作成
    dllproc "CreateMenu", pm, 0, D_USER
    hmenu = dllret                ; メニューハンドル

    ; メニューアイテムを追加
    mesbuf = "表示(&M)"           ; 表示する文字列
    pm.0 = hmenu                  ; メニューハンドル
    pm.1 = 0                      ; オプション(デフォルト)
    pm.2 = CMD_MESSAGE            ; コマンドID
    getptr pm.3, mesbuf           ; 文字列へのポインタ
    dllproc "AppendMenuA", pm, 4, D_USER

    mesbuf = "終了(&Q)"
    pm.0 = hmenu
    pm.1 = 0
    pm.2 = CMD_QUIT
    getptr pm.3, mesbuf
    dllproc "AppendMenuA", pm, 4, D_USER

    ; ウィンドウのサブクラス化
    set_subclass
    hwnd = stat                   ; HSPウィンドウのハンドル
    set_message WM_COMMAND        ; 取得メッセージ設定

    ; メニューをウィンドウに割り当てる
    pm.0 = hwnd                   ; ウィンドウハンドル
    pm.1 = hmenu                  ; メニューハンドル
    dllproc "SetMenu", pm, 2, D_USER

    ; メニューを再描画
    pm.0 = hwnd                   ; メニューハンドル
    dllproc "DrawMenuBar", pm, 1, D_USER

    ; メッセージパラメータ用変数
    dup msg,  msgval.1            ; メッセージが格納される変数
    dup wprm, msgval.2            ; wParamパラメータが格納される変数
    dup lprm, msgval.3            ; lParamパラメータが格納される変数

*mainloop
    get_message msgval
    if msgval == hwnd {
        if msg == WM_COMMAND {
            gosub *lb_on_command
        }
    } else {
        wait 10
    }
    goto *mainloop

*lb_on_command
    ; WM_COMMAND が送られたときの処理
    ; メニュー以外から送られた場合は何もしない
    if lprm != 0 : return

    ; 選択されたメニューアイテムID
    itemid = wprm & $FFFF

    if itemid == CMD_MESSAGE {
        dialog "アイテムが選択されました", 0, "メニューテスト"
    }
    if itemid == CMD_QUIT {
        dialog "終了します", 0, "メニューテスト"
        end
    }
    return

このスクリプトを実行させてみましょう。メニューアイテムをクリックすると、それぞれの処理を行なっています。また、[Alt]+[M] や [Alt]+[Q] でもアイテムを選択することができるようになっています。