第9回: メニューを使用してみる

今回はBTRONのGUIにおいて不可欠といっても良い、"メニュー"の使い方について説明したいと思います。メニューが実装できるようになると、なんとなくBTRONらしいアプリケーションが出来るようになります。

ところでイベントループを書く際に、手書きでイベントループを書く方法・ライブラリを用いて中身だけを書く方法の二通りの方法があることは、もうすでにご存じのことと思います。(第4, 5回を参照してください。) メニューを使用する場合も同様で、二通りの方法があります。それぞれ長所・短所がありますから、両方の使い方を書いてみたいと思います。

今回のプログラム全体を固めたものはこちらです: lecture9.tar.gz

1. 大まかに処理の流れを考えてみる

いわゆる"BTRONらしい"動作をするメニューを作ろうと思ったときに、やらなければならないことは以下の通りです。

  1. メニュー項目の準備と初期化
  2. ウィンドウがアクティブな状態での処理
    1. マウスの右クリックが起きたときにメニュー表示 → 選択されたメニュー項目の処理実行
    2. キーボードショートカット(Ctrl+Wとか)に対応する処理実行
  3. メニューの終了時の解放処理

結構やることは多いように見えますが、それほど悲惨なわけではありません。以下、細かくやることを見ていきましょう。

2. システムコールでメニューを使ってみる

まずは、何より始めに今回のソースコードを見てもらうのが早いと思います。例によって前回のソースコードをベースに手を加えたものですから、前回のものと比較しながら読み進めてもらうと良いと思います。

2.1. メニュー項目の用意

まず、何より始めに、メニュー項目の内容を用意する必要があります。例を見ながら説明していきたいと思います。以下のように用意します。

// メニュー関連の変数
static TC m_finish[] = {MC_STR|MC_KEY1, L"E終了"};
static TC m_disp[]   = {MC_STR, L"表示" | MC_STR, L"再表示"};
static TC m_color[] = {MC_STR, L"色選択" | MC_STR, L"赤" | MC_STR, L"青"};
static TC m_clear[] = {MC_STR, L"すべて消去"};

static MENUITEM mitems[] = {
  {0L, 0L, 0, 0, m_finish},
  {0L, 0L, 0, 0, m_disp},
  {0L, 0L, 0, 0, m_color},
  {0L, 0L, 0, 0, m_clear},  
  {0L, 0L, 0, 0, NULL}, // ウィンドウ
  {0L, 0L, 0, 0, NULL}, // 小物
};

上の指定では、"表示"のサブ項目として"再表示"が、"色選択"のサブ項目として"赤"・"青"の二つが表示されます。他の項目は全てトップレベルのメニューとなっています。

分かりにくくて恐縮なのですが、

サブ項目を持たないメニュー項目
(TC[]){MC_STR, L"項目名"}
サブ項目のあるメニュー項目
(TC[]){MC_STR, L"親項目名" | MC_STR, L"一つ目の子項目名" | MC_STR, L"二つ目の…" | ...(以下同様)... }

のようにして、トップレベルの項目を作っておきます。上の例では、m_finish, m_disp, m_color, m_clearがそれです。

なお、MC_KEY1MC_STRにOR結合しておくと、項目名の中の一つ目の文字がショートカットキー扱いされます。上の例では、メニュー項目として"E終了"と表示されるわけではなくて、"終了"とだけ表示され、Ctrl+Eを押したときにメニュー項目が実行されるようになります。

作ったトップレベル項目は、MENUITEMという構造体の配列に順に格納しておきます。格納順序がそのまま表示順序になると考えておいてください。

ところで、「ウィンドウ」・「小物」の項目の所は、なぜかNULLが代入されています。これは、メニュー項目がプログラムを書いている時点では決定できないからです。使用者がどんなウィンドウを開いているか、あるいはどんな小物をシステムにインストールしているか、分かるはずがありませんね。この部分については、メニューを表示する際に動的に設定することになります。

2.2. メニューの初期化・解放処理

基本的には、イベントループの前にメニューの登録処理を行います。mcre_men関数の第一引数としては、親項目数(トップレベル項目数)を渡します。

  // メニューの登録
  mid = mcre_men(n_mitems, mitems, NULL);
  if (mid < 0){
    printf("mcre_men: %d\n", mid);
    ext_prc(1);
  }

イベントループの後ではメニューの解放処理を行います。データボックスの解放関数などとも同じような、よくあるシステムコールですね。

  // メニューの削除
  mdel_men(mid);

これだけです。簡単ですよね。

2.3. イベントループでのメニュー処理

ここが今回の山場となります。やることが多く混乱してしまいそうですが、頭の中を整理しながら読み進めてもらえれば、と思います。

まず、もう一度メニュー項目の起動方法を整理しておくと、

  1. マウスの右クリックでメニュー表示 → 項目が選択されたら、その処理を実行
  2. ショートカットキーでのメニュー項目実行

の二種類の方法があります。一緒にすると紛らわしいので、分けて説明したいと思います。(Divide and Conquerですね。)

2.3.1. マウスの右クリックでのメニュー項目起動処理

まず、一つ目の、マウスの右クリックでメニューが使われる場合について説明します。

マウスの右クリックが行われると、ウィンドウイベントとしてEV_MENUというイベントが飛んできます。そのEV_MENUイベントを処理する関数は、wfuncの中では

  NULL,        // menufn

となっていますから、まずここを、

  menu_fn,     // menufn

と書き換えた上で、W menu_fn()の中身を書いていくことになります。

それでは、menu_fnの中身を順に見ていきましょう。

まず、メニュー項目のうち、ウィンドウ・小物の項目の中身はとりあえずNULLを代入してあるはずです。なので、そのNULLになっている場所をまず埋めることになります。(これは決まり文句だと思っていただいて差し支えないでしょう。)

  // ウィンドウ・小物のメニュー項目を補充
  wget_dmn(&(mitems[n_mitems-2].ptr));
  mset_itm(mid, n_mitems-2, &(mitems[n_mitems-2]));
  oget_men(0, NULL, &(mitems[n_mitems-1].ptr), NULL, NULL);
  mset_itm(mid, n_mitems-1, &(mitems[n_mitems-1]));

次に、メニューを表示して、どの項目が選択されたか、その項目番号を取得する必要がありますが、これらの一連の流れをmsel_menという関数がすべてまとめて処理してくれます。なので、

  // メニューを表示し、選択動作を行い、選択されたメニュー番号を得る
  sel = msel_men(mid, wevt.s.pos);

たったのこれだけで十分です。この時点で、何が選択されたかの項目番号がselの中に入っています。

項目番号の表現は少し分かりにくいのですが、selの中の右から15〜8ビット目の部分に親項目番号(0から始まります)、7〜0ビット目に子項目番号が入っています。トップレベル項目、つまり子項目がない場合、子項目番号は1になります。

なので、親項目番号は(sel >> 8) & 0xffとして、子項目番号はsel & 0xffとして取り出せます。これらの中身に応じて処理を行い、menu_fnを終了すればOKです。全体としてはこんな感じになります。

  if (sel < 0){
    return 0; // 何も選択されなかった
  }

  // 選択されたメニューに応じた処理を行う
  switch (sel >> 8){
  case 0: // 終了
    return -1;
  case 1: // 再表示
    wreq_dsp(winfo[0].wid);
    break;
  case 2: // 色選択
    switch (sel & 0xff){ // 子項目を取り出す
    case 1: // 赤
      pdsp_msg((TC[]){L"赤が選択されました。"});
      break;
    case 2: // 青
      pdsp_msg((TC[]){L"青が選択されました"});
      break;
    default:
      break;
    }
    break;
  case 3: // 全て消去
    pdsp_msg((TC[]){L"全て消去"});
    break;
  case 4: // ウィンドウ
    wexe_dmn(sel);
    break;
  case 5: // 小物
    oexe_apg(0, sel);
    break;
  default:
    break;
  }

ウィンドウ・小物項目が選択したときに、wexe_dmn, oexe_apgをそれぞれ実行していますが、これは決まり文句だと思ってください。よほど特殊な動作をさせない限りはここをいじることはないでしょう。

2.3.1. ショートカットキーでのメニュー項目実行処理

ショートカットキーが押された時のイベントは、EV_KEYDWNで受け取ることが出来ます。evt_loop関数を使うと、keyfnで受け取ることになるので、そのエントリを定義します。(くどいようですが、そこら辺は仕様書を見て対応関係を把握しておきましょう。) key_fnのエントリ定義は、menu_fnの定義と同じやり方で問題ありませんから、ここでは説明を省略します。

EV_KEYDWNが起きただけでは、単にキーが押された、ということしか分かりませんから、key_fnの中では、それが本当にメニュー項目だったか、などを調べ、もしメニュー項目だった場合にはそれに応じた動作を行う必要があります。

  if (wevt.s.stat & ES_CMD){
    W i = mfnd_key(mid, wevt.s.wid);
    if (i > 0){
      return select_menu(i);
    }
  }

これもほぼ決まり文句なので、そのまま自分のプログラムに流用してもらって大丈夫です。やっていることは、Ctrlキーが押されているかどうかを調べ、もしそうならmfnd_keyでそのメニュー項目番号を取り出す、という感じの流れになっています。(したがってiには項目番号が入ります。

以上で、とりあえずメニューの使い方の説明は終わりです。実際に動かして動作を確認してみると良いと思います。今回は特に、いじる部分が広範囲に及んでいてなかなか紛らわしいですが、仕様書を参照しつつ、頑張ってみてください。

演習問題

いかがでしたでしょか? ライブラリ関数とデータボックスを用いてメニューを扱う方法は今回は説明できなかったので、また別の機会に紹介したいと思います。

  1. 好きなようにメニュー項目を書き換えてみてください。(子項目を増やしてみるとか、項目全体を増やしてみるとか。)
  2. メニュー項目には、インジケータ(ON/OFFを切り替えられるメニュー項目)という項目もあります。(具体的には、全画面表示切り替えで使われていることが多いです。) どのようにすればインジケータを表示できるでしょうか? 仕様書を調べて、実際に試してみましょう。
  3. menu_fnで返り値として0を返しているのはなぜでしょうか? これを仕様書を見て調べてください。0以外の値を返すと何が起こるでしょうか?
  4. 実際に、メニューで「赤」を選択したら、ペンの描画色が赤になるようにしてみましょう。同様に、「全て消去」で画面が真っ白になるようにしてみましょう。

[ 戻る ]

Last modified: Sun Oct 3 16:39:22 2010
Valid XHTML 1.0!