第5回: イベントとイベントループ (後編)

やっぱり…という感じですが、第4回の1回分だけではイベントループの説明はちょっと無理でしたね。ということで、今回は前回の続きで、イベントループについてもう少し詳しいところを見ていきたいと思います。

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

1. イベントループのもう一つの書き方

1.1. BTRONプログラマの憂鬱

前回、イベントループ全体を書いてみたわけですが、これは骨組み自体はほとんど毎回一定です。wget_evtして、その上でswitch (wev0.s.cmd)するのは、どんなアプリケーションであれ、同じ事です。

ところが、先ほどのプログラムをサイズ変更可能にしてください、とか、スクロールバーをつけて、その上で正しく動くようにしてください、とか言うと、その処理は付け加えなければなりません。これは面倒です。頭痛の種になります。何か良い方法は無いでしょうか? こう、毎回イベントループを書かずに済むような。

1.2. evt_loop関数

ありがたいことに、そのような方法はパーソナル・メディア社が提供しているライブラリに用意されています。evt_loop関数というものです。仕様はこちらを見てください。これを使うことで、イベントループ全体を書く、という事は避けられます。

では、早速どのように使うか、見てみることにしましょう。前回(第4回)のプログラムを、そっくりそのままevt_loop関数を使って書き換えた物を用意しました。お手元にソースをダウンロードして、前回の物と比べながら、ゆっくり吟味して欲しいと思います。

まず、全体的にはほんの少しだけ行数が減っていることが分かりますが、見たこともないような処理が入ってきています。まず、こんな行がグローバルに定義されています。

extern WEVENT wevt;

static WINFOREC winfo[2];

static WFUNCREC wfunc = {
  NULL,        // bgfn
  idle_fn,     // idlefn
  NULL,        // msgfn
  NULL,        // menufn
  disp_fn,     // dspfn
  NULL,        // stsfn
  NULL,        // keyfn
  NULL,        // presfn
  finish_fn,   // finfn
  NULL,        // pastefn
  NULL,        // respfn
  NULL,        // scrlfn
  NULL,        // devfn
  NULL         // vobjfn
};

この部分に加えて、関数idle_fn, disp_fn, finish_fnが定義されているのが分かると思います。それで、wfunc(WFUNCREC構造体)の中に、関数を入れているように見えますね。これは、C/C++言語の経験があまり無いと知らないかもしれませんが、「関数ポインタ」というものです。暇なときに適当な参考書を調べてみてください。

で、このようにwfuncの中に対応する関数名を書いておくと、evt_loop関数を使ったときに、イベントが発生したときにwfuncの中に書いた、対応する関数をその都度呼び出してくれます。例えば、ここでは再描画処理時にdisp_fnが呼ばれ、終了時にfinish_fnが呼ばれ、アイドル処理時にidle_fnが呼ばれるようになっています。

それで、肝心のイベントループ部分は、たったこれだけになります。wfuncに書き込んだ情報を元に、後はevt_loopがそれぞれの関数に仕事を振り分けてくれます。

  do {
    ret = evt_loop(&wfunc, winfo);
  } while (ret == 0);

このように、evt_loopを使うことで、プログラム全体の見通しが良くなりますが、それだけではありません。ピクトグラムをダブルクリックしたら終了する、という処理はどこに書いてありますでしょうか? …見つかりませんね。では、どうして正しく動いているのでしょうか?

答えを先に述べると、そこら辺の処理はevt_loopの内部で行われている、ということです。evt_loopを使うことで、そのような"当たり前"の処理を省略する事が出来ることができます。また、イベントをより分かりやすい形に関数に分けて渡してくれます。WFUNCREC構造体で渡している関数と、ウィンドウイベントの種類(EV_BUTDWNとかEV_NULLとか)が一対一で対応していないことに注意してください。

現在のところ、あまりevt_loop関数を用いたソースコードが見あたらないのですが、こちらの方がずっと楽かつ安全かつ便利ですから、なるべくevt_loop関数を用いて見通しの良いプログラムを書いて欲しいと思います。

※ ただし、当たり前の処理をevt_loop内で行っているので、当たり前ではない動作をさせるのが困難になります。例えば、ピクトグラムをダブルクリックするとウィンドウが増えるとか…。そういった"怪しい"動作をさせたければ、イベントループを手書きで書く必要が出てきますが、BTRON作法を守る意味からも、なるべくevt_loopで記述できるようなプログラムにするよう心がけるべきでしょう。

2. イベントループをいじる

2.1. マウスイベントを書く

イベントループをいじる手順は、反応させたいイベント処理関数を書いて、その名前で、wfuncの中身を入れているところのNULLを置き換える、という手順になります。NULLが入っているところは、何もしない(あるいはデフォルト動作)、というイベント応答になっていると思ってください。

前回のsig_buz(0);だけではさすがにつまらないでしょうから、今度はちょっとだけ発展させてみましょう。ウィンドウ作業領域でマウスをクリックするとその場所に点を描画するような、そういうアプリケーションを書いてみましょう。ちょっと難しくなりますが、頑張ってください。(ただし、今回はちょっと間違いを含めてあります。) とりあえず、全体のソースはこちらに用意しておきましたが、ぜひ自分で元のソースから書き加えてみてください。

それで、肝心の"元のソースからいじった場所"ですが、以下の通りです。

まず、マウスボタンがクリックされたときの処理関数を付け加えます。rectは円を書く座標、patは円を書くときの色や塗りつぶしかたを指定するための変数で、gfil_ovlする時に渡しています。gfil_ovlの行で実際に小さな円を書いています。また、画面に書き込む際にwget_gidを用いて"ウィンドウ描画環境"というものを取り出すひつようがありりますが、ここでは詳細についての説明は略します。

// PDプレス処理 (今回の追加部分です)
W pdpress_fn(W wno)
{
  RECT rect = {{ wevt.s.pos.x - 10, wevt.s.pos.y - 10,
                 wevt.s.pos.x + 10, wevt.s.pos.y + 10 }};
  PAT pat = {{ 0, 1, 1, 0x100000ff, 0x10ffffff, FILL75 }};

  GID gid = wget_gid(winfo[wno].wid);
  gfil_ovl(gid, rect, &pat, 0, G_STORE);

  return 0;
}

ここで、注目して欲しいのが、wevtという謎の変数からボタンプレスされた座標を取り出していることです。実は、これはevt_loop関数が内部でwget_evtしているときにイベントを格納しているWEVENT型の値です。これを利用することで、evt_loopに変形させられていない"生の"イベントを取り出すことが出来ます。

詳しくは仕様書の対応部分を見る必要がありますが、EV_BUTDWNの時には、wevt.s.posにクリックされた場所の座標値が入るので、それに基づいて点の描画場所を計算すれば良いわけです。

処理関数がかけたら、wfuncの代入の中に入れて、evt_loopと関連づけを行えば大丈夫です。以下のようにpresfnの行の値を、NULLから書き換えます。

static WFUNCREC wfunc = {
  ....
  pdpress_fn,  // presfn <-- 今回はここを追加
  ....
};

変更はこれだけで十分です。試しに変更・コンパイル・実行してみてください。

2.2. しかし微妙に不具合が…

しかし、上のプログラム、試してみると微妙な問題を抱えています。ずっとそのプログラムのウィンドウ自身が表に出ている時は良いのですが、他のウィンドウの裏に隠れてしまったときに、表側にもう一度ウィンドウを出してくると、ウィンドウの中身が消えてしまいます。また、ウィンドウが画面からはみ出した場合、それをもう一度画面の中に戻しても同じ事が起きます。

例によって、この問題の解決は次回に行いたいと思いますが、ぜひ考えてみてください。

3. 描画環境と描画関数

2.ではgfil_ovl関数を用いて、円を書く処理を行ってみました。絵が描けるといろいろと楽しいと思いますので、ここで簡単に描画環境と描画関数についての説明を行っておきましょう。

3.1. 描画環境とは?

描画環境とは、対象となる「キャンバス」のようなものの事だとまず考えておいてください。2.で書いた

  GID gid = wget_gid(winfo[wno].wid);

では、winfo[wno].widをウィンドウIDとして持つウィンドウの作業領域の"キャンバス"を取り出してきます。そうすることで、このgid(描画環境ID)を介してウィンドウ上に線を書いたり文字を書いたり…といった処理を書くことが出来ます。

ところで、ウィンドウIDと描画環境IDを分けることによるメリットというのは何かあるのでしょうか。ウィンドウ=キャンバス、という等式が成り立つならば、そんな物は不要ですね。

ところが、描画環境IDというのはウィンドウに対してだけではなくて、もっといろいろな物をキャンバスとして持つことが出来ます。例えばビットマップとか、プリンタとか…です。そういったデバイス(などの)間の違いを無視して、全てをキャンバスのように見なして、描画出来るようにしてあるインタフェースが描画環境です。

とりあえずしばらくはウィンドウに対応する描画環境をwget_gidして取り出して作業することがメインになると思います。

3.2. 描画関係のパラメータ

画面描画関数は奥が深くて、かなりいろいろな機能を持っていたりします。ところが、大体の部分はほとんど共通になっているので、ここで説明してしまいましょう。

3.2.1. RECT型とPNT型

まず、大体の関数では、RECT型なりPNT型なりを引数としてとります。RECT型の値をrect, PNT型の値をptとすると、以下のような感じで代入・参照が出来ます。

PNT型
点の座標を表しています。pt.xがX座標、pt.yはY座標
RECT型
rect.c.left, rect.c.right, rect.c.top, rect.c.bottomとしてそれぞれの値にアクセスできる。
(left, top) - (right, bottom)を領域とする四角形を表現しています。

また、PNT pt = { x座標, y座標 }, RECT rect = {{ left, top, right, bottom }}のように、初期値を代入することも出来ます。

4.2.2. PAT型

PAT型は描画パターンを指定するための型で、具体的には、塗りつぶし方法、色などを設定するものです。ちょっとここでは詳しい説明を省きますが、以下のように初期値を指定してください。

  PAT pat = {{ 0, 1, 1, 前景色, 背景色, 塗りつぶしパターン }};

前景色、背景色は、次のCOL型の項目で、具体的な値の設定を説明します。塗りつぶしパターンは、とりあえずFILL100, FILL87, FILL75, FILL50, FILL25, FILL12, FILL0のいずれかを指定すると考えておいてください。これはどの程度の密度で前景色を用いて塗りつぶすかを示すものです。

4.2.3. COL型

COL型は色を指定するための値で、実体は4バイトの整数値で表されます。とりあえずここではやはり説明を端折りますが、色の指定は 0x10rrggbbのように行います。つまり、RGBそれぞれの色に対して0x00から0xffまでの値を決め、それを上のルールに従って組み合わせれば良いわけです。

例として、例えば0x1000ff00ならば緑色、0x10ffffffならば白、0x10888888ならば灰色になります。

4.3. 実際に描画関数を使う

4.2.で説明したパラメータを用いれば、大体の描画関数は使いこなせるはずです。あとは、その都度仕様書を見て必要な物を取り出して使ってください。

例えば、紫色の破線をウィンドウ上の (10,10)-(100,100)の範囲に引きたいときは、上のような感じで書けば良いですね。(線属性に関しては説明しなかったので、そこら辺は調べてもらう必要がありますね。)

PNT p1 = { 10, 10 };
PNT p2 = { 100, 100 };
PAT pat = {{ 0, 1, 1, 0x10ff00ff, 0x10000000, FILL100 }};
gdra_lin(gid, p1, p2, LN_SOLID, pat, G_STORE);

あと、特に理由がない場合は、DCM型の値の部分にはG_STOREを入れるようにしましょう。本当はXOR描画とか便利なのですが、とりあえずは、ということで。

演習問題

一番最後の問題は難しいので、分からなくても大丈夫です。次回説明するつもりです。

  1. 青い点の代わりに、赤い点を描画するように変更を加えてください。
  2. pdpress_fnを好きなように書き換えてみてください。点の代わりに線を書くとか、そんな感じで十分です。
  3. キーボードイベントに対応するイベント処理関数を定義し、wfuncにその関数を加え、その関数の中でsig_buz(0);してください。 (ヒント: イベント処理関数の引数と返り値の型は仕様書に定義されています。決められたとおりにあわせて定義しましょう。)
  4. [難] 2.2.に書いた問題、すなわち、特定の状況でウィンドウの中身が消えてしまう問題の原因はどこにあるでしょうか。また、どのように解決したら良いでしょうか。 (ヒント: ウィンドウの中身が消されるのは、再描画要求イベントを受け取ったとき、すなわちdisp_fnが呼ばれる時です。つまりはそこに問題があるわけですが…)

[ 戻る ]

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