たまにはVC++

『ウィンドウプロージャでswitch文を使うのはもう古い!』
2005.Feb.16

ウィンドウプロージャ

SDKの場合です。
MFCを利用する場合は気にしないでください。

さてさて、ウィンドウを利用するプログラムでは概ね以下のような流れになりますね。

ウィンドウクラス登録
  ↓
ウィンドウ作る
  ↓
メッセージループ
  ↓
ウィンドウプロージャで処理
  ↓
ウィンドウが閉じられたら終了

リソースからダイアログを表示させる場合は多少変わりますが、基本的な流れはこんな感じです。
この中でウィンドウプロージャに焦点を当ててみましょう。
通常はswitch文を利用して、メッセージにより処理を分岐させます。
各caseラベルにて、メッセージを受け取った場合の処理を書きます。
処理しない場合は、DefWindowProc関数なんかを呼んでデフォルトの処理を行います。

しかし、ここでメッセージの処理が増えるとswitch文がダラダラと長くなります。
WM_COMMANDやWM_NOTIFYなんかを処理する場合、さらにswitch文が必要となる場合もあります。
それを避ける為に、長い処理なんかは別の関数に分けてある程度構造化させるのはよくあります。
俺もそうやっていました。

それでも、もはやウィンドウプロージャでswitch文を利用するのは条件反射のごとく書いていました。
構造化プログラミング

構造化とは、ダラダラした長いコードを関数やモジュール単位に分割したり、逆にまとめたりすることです。
メインの処理部はそんな感じでやっていましたが、ウィンドウ周りは全くの無頓着でありました。
何で気付かなかったんでしょうか。
ウィンドウプログラムを覚える時から身に付いた習慣?

「おまじないコード」とはよく使われる言葉ですが、そんな言葉は入門レベルで卒業しときましょう!
ウィンドウプロージャでswitchを使うと、1つの関数がやたら誇大化しますよね。
ローカル変数をいくつも宣言して、更にそれらの使いまわしなんて経験ありませんか?
名前付けるのが面倒になって最後はtmp1とかtmp2とかになってませんか?
俺はあります。最悪ですね。

あと、あまり世間では強くは言われていないのですが、ネストがやたら深くなるのは好きじゃありません。
長いブロックの中にswitchがあってifがあってforがあってまたifが・・・。
正直なところネストが5段か6段を越えると異常事態だと思っています。
別な方法考えるべきです。

と、言いつつもウィンドウプロージャでは「まぁここは良いか」とスルー。
良くありません。

ウィンドウプロージャからswitch文を消し去るコードを書いてみましょう。
どんな感じにするか

とりあえず、「メッセージを識別して各イベントハンドラ(関数)へ処理を渡す」ことが出来ることが条件です。
イベントハンドラ、つまりメッセージを処理する関数をそれぞれ作るわけです。
この辺りはMFCと同じですね。
関数の名前もMFCになぞってOnXXXにしましょう。

ウィンドウプロージャではswitch文を使わず、且つメッセージハンドラが増えても変更の必要のない形にしたいですね。

これを実現するには、1つのメッセージIDとイベント処理関数へのポインタをメンバとする構造体を作ります。
さらにそれを配列として定義することで複数のメッセージに対応できるようにします。

ま、とりあえず構造体と関数の宣言を見てみましょう。

// メッセージ関数型
typedef BOOL (*MSG_FUNC)(HWND, WPARAM, LPARAM, LRESULT*);
// メッセージ関数構造体
typedef struct {
    UINT     msg_id;
    MSG_FUNC msg_func;
} MSG_TABLE, *LPMSG_TABLE;


まず、メッセージハンドラ用の関数の説明です。
引数は、
ウィンドウハンドル、WPARAM、LPARAM、ウィンドウプロージャの戻り値
となります。
戻り値は処理を行った場合はTRUE、行っていない場合はFALSEを返します。
関数は全て静的関数として定義します。
隠蔽することでカプセル化となりますね。

次にメッセージテーブルの定義部を見てみます。
MFCでいうところのメッセージマップです。

static const MSG_TABLE msg_handler[] = {
    {WM_DESTROY       , OnDestroy},
    {WM_CLOSE         , OnClose},
    {WM_CREATE        , OnCreate},
    {WM_PAINT         , OnPaint},
    {WM_COMMAND       , OnCommand},
    {WM_CTLCOLORSTATIC, OnColorStatic},
};


これはモジュール内でのみアクセスを許すグローバル変数として宣言しておきます。
関数の名前は分かりやすいようにメッセージIDの定義名と同じにしときます。
これでメッセージIDと各ハンドラ関数の関連付けが出来ました。

次はウィンドウプロージャを見てみましょう。
switchスタイルを捨てる!

とりあえず今までswitch文で書いていたスタイルは捨てます。
処理は同じですが、書き方は全く変わります。


さもswitch文が悪の如く書いていますが、別にswitchが悪いわけじゃありません。
使い方、使い処が悪いのです。
処理が大きくなると予想されるウィンドウ処理の場合に限ってはswitchで分岐させると、ぐちゃぐちゃコードになる可能性が高いのだと思います。
逆に1つか2つしか処理しないのであればswitchで充分です。
この辺を念頭に置いておいてください。

んじゃ見てみましょう。

LRESULT WINAPI WndMainProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)

    // ウィンドウプロージャ用の戻り値格納する変数
    LRESULT result=0;

    // ハンドラテーブル配列の要素数
    static int table_max = sizeof(msg_handler) / sizeof(msg_handler[0]);

    // メッセージハンドラ
    for(int i=0; i < table_max; i++)
    {
        if( msg_handler[i].msg_id == msg )
        {
            // メッセージ関数へ
            if( msg_handler[i].msg_func &&
                msg_handler[i].msg_func(hwnd, wp, lp, &result) )
                return result;  // 処理を行ったらresultを返す
            break;
        }
    }
    // デフォルトウィンドウ処理
    return DefWindowProc(hwnd, msg, wp, lp);
}


ここで行っている事を簡単に説明します。
forループを利用して、構造体で定義されているIDとウィンドウプロージャが受け取ったメッセージIDとを比較します。
マッチすればIDに関連付けられている関数を呼び出します。
その結果が真なら、resultを戻り値としてセットします。
FALSEまたはIDがマッチしなかった場合は、デフォルトの処理とします。

配列の要素数はプログラム実行時に計算されますので、気にしなくて良いです。(=気軽にテーブルに追加できる)
使わないメッセージはテーブルのところを一行丸々コメントアウト。
ウィンドウプロージャでは何もしないで追加・削除ができるようになりました。
関数の戻り値をそのままウィンドウプロージャの戻り値にしたら?

つまり、メッセージハンドラ関数の戻り値をLRESULT型にしてそのままreturnしないのか、という意味です。
それでも良いのですが、実はわざわざ別にしているのには訳があります。

モードレスダイアログを扱ったことはありますか?
モードレスダイアログはダイアログプロージャが必要になります。
このダイアログプロージャはウィンドウプロージャと違い、以下のように定義されています。

BOOL CALLBACK DialogProc(
  HWND hwndDlg, // handle to dialog box
  UINT uMsg, // message
  WPARAM wParam, // first message parameter
  LPARAM lParam // second message parameter
);


引数は一緒ですが、戻り値が違います。
戻り値の意味は、処理を行った場合TRUEを返し、行わないFALSEを返さなくてはなりません。

んで、ウィンドウプロージャでもダイアログプロージャでも、少々の変更のみで移植できるものにしたかったのです。
0を返すか、TRUEを返すかで意味が逆転してしまいます。
また、そのままreturnすると、各ハンドラ関数の最後でDefWindowProcを呼ばなくてはいけなくなります。
これはちょっといただけませんね。

そういった理由でわざわざ戻り値格納用変数を引数として渡しています。
あと、ウィンドウの場合はresult変数を0に初期化することで、わざわざ各関数で0をセットする必要がなくなります。
ダイアログを利用する場合はTRUEで初期化してください。
イベントハンドラ関数の移植性

上記に書いた通り、ウィンドウ・ダイアログどちらでも互換性があります。
例えば、別なプログラムにイベントハンドラごと移植することは、よくあるはずです。
そういう場合は、関数を丸々コピーすればそのまま使えるようになります。
switch文を使った場合だと、コピーするのにもインデントや変数の扱いなどで多少の変更が必要でした。
(何度も言いますが別にswitch文が悪いわけじゃない)


但し、switch文を使っているウィンドウプロージャの場合は当然そのままコピーできません。
これはまぁ全く違う設計をしているわけですから、しょうがないです。

とりあえずここまでで、ある程度構造化できました。
WM_COMMANDの処理

WM_COMMANDメッセージは子ウィンドウやメニューといったそのウィンドウに関連付いたコントロールなどから送られるメッセージです。
コントロールをたくさん作れば、WM_COMMAND内でのコードも大きくなってきます。

そこで、ここもウィンドウプロージャと同じく「コントロールハンドラ処理群」を作成しましょう。
テーブル定義や関数の型はちょっと変わります。
その辺りは、サンプルコードを見てください。
とりあえずここでは、WM_COMMANDに相当するOnCommand関数の中身を見てみましょう。

BOOL OnCommand(HWND hwnd, WPARAM wp, LPARAM lp, LRESULT* pResult)
{
    // コントロールID
    UINT ctrl = LOWORD(wp);
    // ハンドラテーブル配列の要素数
    static int table_max = sizeof(ctrl_handler) / sizeof(ctrl_handler[0]);

    // コントロールハンドラ
    for(int i=0; i < table_max; i++)
    {
        // コントロール処理関数へ
        if( ctrl_handler[i].ctrl_id == ctrl &&
            ctrl_handler[i].ctrl_func )
            return ctrl_handler[i].ctrl_func(hwnd, (HWND)lp, LOWORD(wp), HIWORD(wp), pResult);
    }
    return FALSE;
}


大体同じような処理を行っています。
メッセージではなくコントロールIDで識別しているのに注意してください。
詳しくはサンプルコードをご覧あれ。
テーブルを使わないでswitchでハンドラ関数へ分岐させたら?

ウィンドウプロージャでswitch文を使って、各ラベルでメッセージハンドラ関数を呼び出す。という意味です。
それでも良いと思います。
ただ、ある程度構造化、更に抽象化させたかったのでテーブル用の変数を設けています。

処理の追加・削除の手間は同じくらいかもしれません。
でも、どちらにせよウィンドウプロージャが徐々に誇大化するのは免れませんね。
パフォーマンス低下が気になるかも・・・

switchよりもforでいちいち比較するんじゃパフォーマンスが落ちないか・・・?

なぜかプログラムを組んでいるとその辺が気になってしまう場合があります。
それはそれで大事なことなんですが、これは大丈夫です。
実はswitch文も上から下へとラベルを比較しているんですね。つまりif文の羅列です。
比較回数はほぼ同じでしょう。
コストがかかる部分といえば変数へのアクセスと関数のオーバーヘッドくらいでしょうか。
ウィンドウプロージャ如きで、気にするものではありません。
入門〜初心者へ

関数ポインタとか構造体とか配列とか使っているので、それらを学ぶ良いサンプルになるかもしれません。
あとウィンドウプログラムではswitch文を使うのが一般的です。

SDKで構造化を図るくらいなら、最初からMFC使った方が良いと思います。
とりあえず、MFCをなんらかの事情で使わない場合で、ウィンドウの処理が膨大な時はswitchを使わない、こういった構造化(まとめ方)もあるということです。

こういった事を自分で考えてみるのも良いですね。
終わり

今回はウィンドウプロージャの構造化を図ってみました。
如何でしたでしょう。
構造化はオブジェクト指向への第一歩とも言えますね。

ウィンドウプロージャでのswitch分岐は常套手段であり、また初期の頃から頭に刷り込まれるコードです。MSDNにも書いてあるし。
簡単手軽に書けるのですが、大きくなってくるとまとめきれなくなるんですよね。
冒頭にも書いた通り、ローカル変数をゴテゴテ追加したりするし。これは俺が悪いのか。

あと、これは「手段の1つ」であって、これに統一する必要はありません。
プログラムも適材適所。
オブジェクト指向プログラムが全ての場面で必ずしも適材とは限りませんよね。
作る物や組織、自分の力量に合わせてやってきましょう。

今回のサンプルプロジェクトです。
コメント量がいつもの1.5倍です(当社比)
c08.lzh



[先頭ページへ] [次のページへ]

リンクフリー  Copyright (C) K_Yaguchi