Lesson 7:メニューを付けてみる


 前回はテキスト編集を行ってみましたが、全ての操作をボタンから行いました。ただ一般的なアプリケーションは、こういった操作はメニューから行います。そこで、今回は、メニュー操作に挑戦してみます。(テキスト編集との結び付けまでは、今回はやりません。次回からはじめるテキストエディタの作成で行います。)

 例によって、メニュー操作に使用するクラスをBeBookから探してみましょう。
 まず目に付くのが「BMenu」クラスでしょう。このクラスの説明を流し読みすると、BMenuは「BMenuItem」や他の「BMenu」を登録してメニュー構成を管理するクラスのようです。
 あと必要なものは、このメニューを画面に表示するためのクラスです。このクラスに「BMenu」を登録していく事で、メニューを表示させる事ができます。(BeOSにはポップアップメニューを表示するためのクラスも準備されていますが、今回は使用頻度の高いメニューバーで話を進めていきます。)

 まずはメニューを登録する土台となるメニューバーを表示してみましょう。
 いつものようにBaseAppを雛型にプロジェクトを作成したら、APPLICATION_SIGNATURE等を書き換えてから、BAppMainViewクラスのコンストラクタを次のように書き換えて見てください。

/**** ファイル名 : MainWindow.cpp ****/

//---------------------------------------------------------------------
BAppMainView::BAppMainView(BRect frame)
    :BView(frame,"bappmainview",B_FOLLOW_ALL,B_WILL_DRAW)
{
    BRect viewrect(Bounds());

    BMenuBar *mainmenu=new BMenuBar(BRect(0,0,viewrect.right,24),
                                    "mainmenubar");
    AddChild(mainmenu);

    BMenu *menu1=new BMenu("Menu1");
    mainmenu->AddItem(menu1);
}
//---------------------------------------------------------------------

 今更説明する必要も無い簡単なコンストラクタですね。実行したところで、ウィンドウのタイトルの下にちょっと幅の広い棒が一本表示されて、「Menu1」というメニューが表示されます。

 次は、このメニューに項目を追加していくわけですが、メニューを選択した事を知るために、MainWindow.hにMSG_MENUSELECTメッセージを用意しておきます。

/**** ファイル名 : MainWindow.h ****/

//---------------------------------------------------------------------
#define MSG_MENUSELECT  'msel'
//---------------------------------------------------------------------

 とりあえず、ウィンドウにストリングビューを用意しておいて、何かメニューを選択したら、そのメニューのタイトルをストリングビューに表示してみます。

 まずは、メニュー項目の登録ですね。
 BMenuにメニュー項目を追加するには、メンバ関数のAddItemを使用します。このAddItem関数には追加したい項目を指定するのですが、項目としては、BMenuItemクラスとBMenuクラスが使用できます。BMenuItemクラスは単一のメニュー項目を追加するのに使用し、BMenuクラスは階層構造になるようサブメニューを追加するのに使用します。
 試しに、次のような構成のメニューを作ってみましょう。

Menu1Menu2
Test1_1Test2_1
Test1_2Test2_2

Test2_3
Test2_4
Test2_4_1
Test2_4_2

 この構成ですと、Menu1、Menu2は更にメニュー項目を持つので、BMenuクラスを使用します。Menu1は、Test1_1とTest1_2を持ち、これらはBMenuItemクラスを使用します。Menu2は、Test2_1、Test2_2、Test2_3、Test2_4の四つのメニューを持ち、更にTest2_2とTest2_3の間にはセパレータを、Test2_4はサブメニューを持っています。

 まずは簡単なMenu1を作ってみましょう。
 先ほどのソースで作ったmenu1に、Test1_1と、Test1_2を追加するだけです。BMenuのAddItem関数は、引数にBMenuItemクラスかBMenuクラスを渡せば項目の追加を行います。BMenuItemクラスのコンストラクタは、メニューの名前とメッセージを渡せば良いようなので、メッセージは先ほど定義したMSG_MENUSELECTを使用するようにして、
        menu1->AddItem(new BMenuItem("Test1_1",new BMessage(MSG_MENUSELECT)));
としてメニューを追加します。Test1_2も同じように作ります。
 この作業で、BAppMainViewクラスのコンストラクタは次のようになっているはずです。

/**** ファイル名 : MainWindow.cpp ****/

//---------------------------------------------------------------------
BAppMainView::BAppMainView(BRect frame)
    :BView(frame,"bappmainview",B_FOLLOW_ALL,B_WILL_DRAW)
{
    BRect viewrect(Bounds());

    BMenuBar *mainmenu=new BMenuBar(BRect(0,0,viewrect.right,24),
                                    "mainmenubar");
    AddChild(mainmenu);

    BMenu *menu1=new BMenu("Menu1");
    menu1->AddItem(new BMenuItem("Test1_1",new BMessage(MSG_MENUSELECT)));
    menu1->AddItem(new BMenuItem("Test1_2",new BMessage(MSG_MENUSELECT)));
    mainmenu->AddItem(menu1);
}
//---------------------------------------------------------------------

 実行すれば、Menu1にTest1_1とTest1_2が追加されている事が確認できます。 Test1_1やTest1_2を選択しても、メッセージを受信してからの処理を作っていないので、何も動作はしません。

 次はMenu2ですが、こちらは少し複雑で、セパレータを入れることと、 Test2_4のサブメニューとしてTesdt2_4_1とTest2_4_2を作ってやる必要があります。
 セパレータは簡単で、BMenuはセパレータを追加するためのメンバ関数として、AddSeparatorItemがありますので、それを使用します。
 Test2_2とTest2_3の間で、
menu2->AddSeparatorItem();
としてやるだけです。
 2_4は更にサブメニューを持ちますので、まずは2_4のタイトルでBMenuを作成して、サブメニューのメニュー項目、2_4_1と2_4_2を追加しておきます。あとは、Test2_3の後ろに、AddItemで追加するだけです。

/**** ファイル名 : MainWindow.cpp ****/

//---------------------------------------------------------------------
BAppMainView::BAppMainView(BRect frame)
    :BView(frame,"bappmainview",B_FOLLOW_ALL,B_WILL_DRAW)
{
    BMenu *menu1=new BMenu("Menu1");
    menu1->AddItem(new BMenuItem("Test1_1",new BMessage(MSG_MENUSELECT)));
    menu1->AddItem(new BMenuItem("Test1_2",new BMessage(MSG_MENUSELECT)));
    mainmenu->AddItem(menu1);

    BMenu *menu2=new BMenu("Menu2");
    menu2->AddItem(new BMenuItem("Test2_1",new BMessage(MSG_MENUSELECT)));
    menu2->AddItem(new BMenuItem("Test2_2",new BMessage(MSG_MENUSELECT)));
    menu2->AddSeparatorItem();
    menu2->AddItem(new BMenuItem("Test2_3",new BMessage(MSG_MENUSELECT)));

    BMenu *menu2_2=new BMenu("Menu2_4");
    menu2_2->AddItem(new BMenuItem("Test2_4_1",new BMessage(MSG_MENUSELECT)));
    menu2_2->AddItem(new BMenuItem("Test2_4_2",new BMessage(MSG_MENUSELECT)));

    menu2->AddItem(menu2_2);        

    mainmenu->AddItem(menu2);
}
//---------------------------------------------------------------------

 実際に動かして見ると、上の構成通りのメニューとなっているはずです。

 次にメニューを選択された際の処理を書いてみましょう。
 いつものように、BAppMainWindowクラスのMessageReceived関数にswitch(msg->what)としてMSG_MENUSELECTを処理してやるだけです。ストリングビューを一つ用意しておき、メニューが選択された際に、そのストリングビューにメニューのタイトルを表示してみましょう。
 さて、ここで問題になるのは、メッセージを一種類しか用意していないのに、どうやって選択されたメニューを判別するかですよね。メニューの数だけメッセージを作るのも手ですが、それではあまりにも芸が無いので、ここはBeOSのメッセージならではの事をしてみましょう。
 MS-Windowsのプログラムを組んだ事がある方は、WindowsのメッセージにはWParam、LParamといったパラメータを持っている事をご存知でしょう。BeOSのメッセージもやはりパラメータを持つ事ができます。それも、Windowsのメッセージのような限られた個数の数値情報しか持てないような使いにくいものではなく、数値や文字列等を複数個自由に持たす事ができるのです。

 それでは、今回の処理ですが、MessageReceived関数では選択されたメニューのタイトルを表示する処理を行うわけですから、それぞれのメッセージにそれぞれのタイトルの情報を持たせてしまいましょう。
 それぞれのメニューに登録されたBMessageを参照するには、BMenuクラスの、ItemAt関数を使用してメニューを参照して、更にMessage関数で、メニューに登録されたBMessageを参照します。
 BMessageに文字列情報を付加するには、AddString関数を使用します。BMessageは情報を複数個持つ事ができるので、AddString関数には情報の名称と内容の二つのパラメータを渡してやります。
menu1->ItemAt(0)->Message()->AddString("title",menu1->ItemAt(0)->Label());
 これでmenu1の一番目(インデックスは0)のメニュー項目のメッセージにtitleという名称でメニュー項目のタイトル(ラベル)を設定しています。
同じように他のメニュー項目にもメッセージを設定してやると、次のようになります。
 ついでに、ストリングビューも用意しておきましょう。(ヘッダに宣言を追加するのを忘れちゃだめですよ。)

/**** ファイル名 : MainWindow.cpp ****/

//---------------------------------------------------------------------
BAppMainView::BAppMainView(BRect frame)
    :BView(frame,"bappmainview",B_FOLLOW_ALL,B_WILL_DRAW)
{
    BRect viewrect(Bounds());

    BMenuBar *mainmenu=new BMenuBar(BRect(0,0,viewrect.right,24),
                                    "mainmenubar");
    AddChild(mainmenu);

    BMenu *menu1=new BMenu("Menu1");
    menu1->AddItem(new BMenuItem("Test1_1",new BMessage(MSG_MENUSELECT)));
    menu1->ItemAt(0)->Message()->AddString(
			"title",menu1->ItemAt(0)->Label());
    menu1->AddItem(new BMenuItem("Test1_2",new BMessage(MSG_MENUSELECT)));
    menu1->ItemAt(1)->Message()->AddString(
			"title",menu1->ItemAt(1)->Label());
    mainmenu->AddItem(menu1);

    BMenu *menu2=new BMenu("Menu2");
    menu2->AddItem(new BMenuItem("Test2_1",new BMessage(MSG_MENUSELECT)));
    menu2->ItemAt(0)->Message()->AddString(
			"title",menu2->ItemAt(0)->Label());
    menu2->AddItem(new BMenuItem("Test2_2",new BMessage(MSG_MENUSELECT)));
    menu2->ItemAt(1)->Message()->AddString(
			"title",menu2->ItemAt(1)->Label());
    menu2->AddSeparatorItem();
    menu2->AddItem(new BMenuItem("Test2_3",new BMessage(MSG_MENUSELECT)));
    menu2->ItemAt(3)->Message()->AddString(
			"title",menu2->ItemAt(3)->Label());

    BMenu *menu2_2=new BMenu("Menu2_4");
    menu2_2->AddItem(new BMenuItem("Test2_4_1",new BMessage(MSG_MENUSELECT)));
    menu2_2->ItemAt(0)->Message()->AddString(
			"title",menu2_2->ItemAt(0)->Label());
    menu2_2->AddItem(new BMenuItem("Test2_4_2",new BMessage(MSG_MENUSELECT)));
    menu2_2->ItemAt(1)->Message()->AddString(
			"title",menu2_2->ItemAt(1)->Label());

    menu2->AddItem(menu2_2);        

    mainmenu->AddItem(menu2);

    strview=new BStringView(BRect(8,38,viewrect.right-8,58),"strview",
                            "NoData",
                            B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP);
    AddChild(strview);
}
//---------------------------------------------------------------------

 メッセージを用意したら、受信する部分も作らないとですね。
 受信はMessageReceived関数ですから、そこに、switch文でMSG_MENUSELECTを処理するようにします。 あとは、メッセージに付加した文字列データの取得をして、それをストリングビューに表示するだけですね。
 文字列データの取得には、BMessageのFindString関数の引数に、付加したデータの名称を渡して行います。FindStringは文字列のポインタを返しますので、それをストリングビューのSetText関数に渡してやります。

/**** ファイル名 : MainWindow.cpp ****/

//---------------------------------------------------------------------
void BAppMainWindow::MessageReceived(BMessage *msg)
{
    switch(msg->what)
    {
        case MSG_MENUSELECT:
            mainview->strview->SetText(msg->FindString("title"));
            break;
        default:
            BWindow::MessageReceived(msg);
    }
}
//---------------------------------------------------------------------

 実際に動かして、メニューを選択すると、ストリングビューの内容が書き換わる事を確認してみてください。

 今回はもう一つ、メニューにチェックを付けるところまでやって終わりにしましょう。
 メニューにチェックを付けるのは、難しくもなんともありません。設定にはSetMarked関数を、状態を確認するには、IsMarked関数を使用します。
 今回、Menu2_4として作ったサブメニュー、Menu2_4_1とMenu2_4_2を、選択する毎にチェックがOn/Offされるようにしてみましょう。

 まずは、MSG_MENUSELECTと動作を区別するために、MainWindow.hに別のメッセージ(MSG_SETMARKEDとしておきます)を用意しておきます。AddItem関数でMenu2_4_1とMenu2_4_2をメニューに登録するときのメッセージも、MSG_SETMARKEDにしておきます。
 メッセージに付加する情報も、メニューのラベルを文字列情報として持たせておいて、MessageReceivedの中で文字列比較して選択されたメニューを判断しても良いのですが、それだとちょっと芸が無いので、今回は、メッセージにメニュー自体の情報を付加してしまいましょう。情報を付加するといっても、メニュー項目のポインタを渡してやるだけですけどね。
 文字列を付加するにはAddString関数を使用しましたが、ポインタを付加するには、AddPointer関数を使用します。あとは、情報を識別するための名前と、文字列情報のかわりに、メニュー項目のポインタを渡すだけです。
menu2_2->ItemAt(0)->Message()->AddPointer("bmenuitem",menu2_2->ItemAt(0));
といった感じになります。

 今回はここまで。次回からは、少しづつテキストエディタを作っていきましょう。


☆2000年10月2日追記☆

 最後に行っているMenu2_4_1とMenu2_4_2のチェック状態をトグル処理する部分で、メッセージにメニュー自体のポインタを付加していますが、これは余計な処理であるとの御指摘を受けました。
 BMenuItemから送られるメッセージには、BMenuItem自身のメッセージが"source"という名前で付加されます。ですから、上記で付加しています"bmenuitem"という情報は不要なものとなります。MessageReceived関数のMSG_SETMARKEDメッセージを処理している部分も、FindPointerで探すのは"bmenuitem"ではなく、"source"となります。

ソースリスト
圧縮ファイル
R5 Intel環境で確認
Be5thApp20000131.zip
ソースファイル
BaseApp.h
main.cpp
MainWindow.cpp
MainWindow.h

次の項目へ

トップページへ戻る