4章 Windowsアプリケーションの骨格(その2)

<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=shift_jis"></meta>
<title>4章 Windowsアプリケーションの骨格(その2)</title>
</head>

<body bgcolor="WHITE">
<p>
<font size="5">4章 Windowsアプリケーションの骨格(その2)</font>
<hr>
<p>
<center>
</center>
<p>
 3章では、最小のWindowsアプリケーション全体に渡り、どのように制御が流れていくのかを中心に説明しました。この章では、アプリケーションを定義する箇所と言っても過言でないウインドウプロシージャ内部の処理について説明します。ウインドウプロシージャとはメッセージを処理するWindowsから呼ばれる関数です。そして、メッセージとはWindowsがウインドウプロシージャに制御を渡す時に引数として与える整数に過ぎません。これは、3章で説明しましたね。
<p>
 では、簡単なお絵描きソフトを例にメッセージの処理の仕方を説明します。Windows環境の方は実際に動かしながらの方が理解しやすいので、<a href="chap4.lzh">ここ</a>を押してダウンロードしてください。実行時の画面を見るには<a href="chap4.gif">ここ</a>押してください。
<p>
 今回のサンプルは、2章・3章のプログラムと異なり、ファイルが分割されています。つまり、WINMAIN.C、WNDPROC.C、そしてWNDPROC.Hです。筆者の分割の方針は、WinMain関数で1ファイル、各ウインドウプロシージャ毎に1ファイル、そして、その他、機能的に独立していると思われる単位毎に1ファイルです。勿論、必要ならそれぞれのファイル毎にヘッダファイルを作ります。
<p>
 ときたまプログラムのステップ数(行数)が少ない場合は分割しない方がいますが、2章・3章のプログラムのように全体像を見せたいなど特別な目的が無い限り、機能単位でファイルを分割するのはよい考え方と思います。なぜなら、C言語はPASCALの様に関数のネストによりファイル内で関数のスコープの制御が出来ないからです。だから、C言語のファイルスコープを利用して、PASCALのスコープ制御に近い効果を得ようとするのです。
<p>
 さて、この章での関心はウインドウプロシージャだけですので、ウインドウプロシージャ部分だけ、つまりWNDPROC.Cだけをリスト4-1として載せることにします。
<p>
<table border="1" bgcolor="#F0F0F0">
<caption align="BOTTOM">リスト4-1
<tr><td>
<pre>
/* C言語で始めるWindowsプログラミング */
/* 4章のサンプルプログラム */
/* Programmed by Y.Kondo */
/* 注:TABサイズは4で見てください */
/* このファイルでは、メインウインドウのウインド*/
/*ウプロシージャが定義されている */

/* 訂正版 */

#define STRICT
#include &lt;windows.h&gt;
#include &lt;stdio.h&gt;
#include "wndproc.h"

/*===============================================================================*/

/* このファイル内でのみ用いられる関数のプロトタイプ宣言 */
static LRESULT Wm_DestroyProc(void);
static LRESULT Wm_LButtonDownProc(HWND,WPARAM,WORD,WORD);
static LRESULT Wm_LButtonUpProc(HWND,WPARAM,WORD,WORD);
static LRESULT Wm_MouseMoveProc(HWND,WPARAM,WORD,WORD);

/* メインウインドウのウインドウプロシージャ */
LRESULT CALLBACK WindowProc(HWND hwnd,UINT message,WPARAM wparam,LPARAM lparam)
{
  switch(message)
   {
       case    WM_DESTROY:         /*  ウインドウの破壊後処理          */
           return  Wm_DestroyProc();
       case    WM_LBUTTONDOWN:     /*  マウスの左ボタンが押された時    */
           return  Wm_LButtonDownProc(hwnd,wparam,LOWORD(lparam),HIWORD(lparam));
       case    WM_LBUTTONUP:       /*  マウスの左ボタンが放された時    */
           return  Wm_LButtonUpProc(hwnd,wparam,LOWORD(lparam),HIWORD(lparam));
       case    WM_MOUSEMOVE:       /*  マウスが動かされた時            */
           return  Wm_MouseMoveProc(hwnd,wparam,LOWORD(lparam),HIWORD(lparam));
   }
   return  DefWindowProc(hwnd,message,wparam,lparam);
}

/*===============================================================================*/

/* このファイル内でのみ用いられる構造体の定義 */
typedef struct {
  WORD    x;
   WORD    y;
} TPoint;

/* このファイル内でのみ用いられるグローバル変数 */
static TPoint PrevPoint; /* 以前の点を覚える変数 */

/* このファイル内でのみ用いられる変数定義 */
static LRESULT Wm_DestroyProc(void)
{
  PostQuitMessage(0);
   return  0;
}

static LRESULT Wm_LButtonDownProc(HWND hwnd,WPARAM fwKeys,WORD xPos,WORD yPos)
{
  SetCapture(hwnd);   /*  マウスをキャプチャする  */
   PrevPoint.x=xPos;   /*  マウスの左ボタンを押したときの位置を覚える  */
   PrevPoint.y=yPos;
   return  0;
}

static LRESULT Wm_LButtonUpProc(HWND hwnd,WPARAM fwKeys,WORD xPos,WORD yPos)
{
  HDC DC;
   if(GetCapture()==hwnd)  /*  マウスをキャプチャしているのが、このウインドウである事を確認して*/
   {
       DC=GetDC(hwnd);
       MoveToEx(DC,(short)PrevPoint.x,(short)PrevPoint.y,NULL);    /*  DEBUG   */
       LineTo(DC,(short)xPos,(short)yPos);                         /*  DEBUG   */
       ReleaseDC(hwnd,DC);
       ReleaseCapture();   /*  マウスを開放する    */
   }
   return  0;
}
static LRESULT Wm_MouseMoveProc(HWND hwnd,WPARAM fwKeys,WORD xPos,WORD yPos)
{
  HDC DC;
   if(GetCapture()==hwnd)  /*  マウスをキャプチャしているのが、このウインドウである事を確認して*/  
   {
       DC=GetDC(hwnd);
       MoveToEx(DC,(short)PrevPoint.x,(short)PrevPoint.y,NULL);    /*  DEBUG   */
       LineTo(DC,(short)xPos,(short)yPos);                         /*  DEBUG   */
       ReleaseDC(hwnd,DC);
       PrevPoint.x=xPos;
       PrevPoint.y=yPos;
   }
   return  0;
}
</pre>
</table>
<p>
 3章で説明したように、ウインドウプロシージャはその関数名と引数名のみが自由に決められます。つまり、その<b><i><u>関数の型は変更できません</u></i></b>。また、ウインドウプロシージャは<b><i><u>DefWindowProc関数を継承する形で実装します</u></i></b>。つまり、メッセージを元に条件分岐を使って、関心のあるメッセージだけ自前で処理し、他のメッセージは全てDefWindowProc関数に渡します。だから、ウインドウプロシージャ内部の骨格は、巨大なif文かswicth文になります。一般に多分岐はswitch文が好まれますので、巨大なswitch文になるのが普通でしょう。
<p>
 ウインドウプロシージャは、そもそもメッセージに対する機能を定義する場所ですから、switch文のcaseラベルに続けてダラダラとプログラムを書いてもかまいません。しかし、この方法を採ると、1つのウインドウプロシージャが数千行になる事もあり、極めて可読性の悪いプログラムになります。したがって、例外はありますが、普通はリスト4-1の様に、1メッセージ1関数で分割します。
<p>
 RADツールでプログラミングをしたことがある方なら、「ハッ」と思うでしょうね。結局、下の4つの関数は、VBのイベントプロシージャとそっくりですね。従って、/*====*/のコメントで囲まれているエリアというのは単なるオーバーヘッドでRADツールやMFCでは隠蔽されている部分です。
<p>
 メッセージというのは、何度も申しますように、単なる整数です。しかし、1とか2とかの整数表現を用いるのではなく、WM_COMMANDなどのようなシンボリックな物です。メッセージは先頭にWM_が付きます。MSDNライブラリーなどのオンラインヘルプで探してみますと、100は超えるのではないでしょうか?数えたことはありませんが結構な数になります。しかし、実際に使うメッセージは意外と少ないことが多く、使用頻度の高いメッセージは20も無いと思います。徐々にマニュアルを見ながらうろ覚えをしていけばいいと思います。必要になった時にマニュアルでしっかり調べれば良い事ですからね。
<p>
 さて、今回のサンプルサンプルプログラムで用いられてるメッセージを見てみると、以下の4つです。
<p align="CENTER">
<table border="1" bgcolor="#F0F0F0">
<tr><td>
WM_DESTROY<br>
WM_LBUTTONDOWN<br>
WM_MOUSEMOVE<br>
WM_LBUTTONUP
</table>
<p>
 まず、それぞれのメッセージに対応する関数について説明する前に、筆者が採っているVBのイベントプロシージャなる物に相当する自作の関数の命名及び型定義の方針を紹介します。
<p align="CENTER">
<table border="1" bgcolor="#F0F0F0">
<tr><td>
・WM_何々メッセージの場合、Wm_何々Procと命名します。<br>
・戻り値は、ウインドウプロシージャと同じ型、つまり、LRESULTです。<br>
・引数の型は、WPARAMとLPARAMの引数をキャストした後の型を適用する。<br>
・static宣言をし、他のファイルに公開しない。
</table>
<p>
 最初の2つは説明の必要は無いでしょう。最後の1つもC言語の文法を知っていて、ファイル分割の目的を知っていれば当然のことですね。3番目は説明の必要があると思いますので、説明します。
<p>
 ウインドウプロシージャの第3引数と第4引数はメッセージによって意味が変わります。これは、以前に説明しましたね。どちらも32ビットつまり4バイト長の変数ですが、そう単純ではありません。
 たとえば、今回登場するWM_MOUSEMOVEの場合、マニュアルには以下のように書いてあります。
<p align="CENTER">
<table border="1" bgcolor="#F0F0F0" w>
<tr><td>
<pre>
WM_MOUSEMOVE
<big><b>
fwKeys = wParam; // key flags
xPos = LOWORD(lParam); // horizontal position of cursor
yPos = HIWORD(lParam); // vertical position of cursor
</b></big>

The WM_MOUSEMOVE message is posted to a window when the cursor moves. If the mouse
is not captured, the message is posted to the window that contains the cursor.
Otherwise, the message is posted to the window that has captured the mouse.
                《以下省略》
                     (『Win32Programmer's Reference』から引用)
</pre>
</table>
<p>
 WM_MOUSEMOVEの場合、第三引数のwParamはそのままの型で用いていますね。しかし、第四引数は下位16ビットをX座標、上位16ビットをY座標として、マウスの位置情報を表しています。このように、実際に使う場合は、32ビット変数をマクロなどで分解したりキャストしたりして使う事がよくあります。この場合、結局、分解・キャスト後の型を持つ変数に代入して使うので、ウインドウプロシージャ内にダラダラと書くと結構冗長な代入式が出来ます。結局、3番目の方針は、この冗長な代入を関数呼び出しの過程で行い、事実上、冗長な代入式を無くす工夫だったのです。
<p>
 では、メッセージ毎にVBのイベントプロシージャに相当する4つの関数を見てみましょう。<hr>
<p>
<b>・WM_DESTROY</b>
<p>
 これは、3章にも登場したメッセージですね。ウインドウが破壊された後にウインドウプロシージャに送られるメッセージです。かかるウインドウプロシージャのウインドウはメインウインドウですので、このウインドウが破壊された時、即ち、アプリケーションが終了する時です。だから、アプリケーションのメッセージキューにWM_QUITを置く処理をすればいいのですね。したがって、以下のような処理になるのです。
<p align="CENTER">
<table border="1" bgcolor="#F0F0F0">
<tr><td>
<pre>
static LRESULT Wm_DestroyProc(void)
{
  PostQuitMessage(0);
   return  0;
}
</pre>
</table>
<p>
<b>・WM_LBUTTONDOWN、WM_MOUSEMOVE、WM_LBUTTONUP</b>
<p>
 これら3つのメッセージは、線の描画に直接関係するメッセージです。順に、マウスの左ボタンが押された時、マウスが動かされた時、マウスの左ボタンが放された時に発生します。<b><i><u>これらのメッセージが発生する順番は、いかなる仮定も間違いです</u></i></b>。普通に考えたら、「マウスのボタンが押されもしていないのに放されるという事は無い」と仮定できると思いますが、この仮定すら間違いです。なぜならば、<b><i><u>マウスに関するメッセージは、マウスカーソルの直下のウインドウにのみ送られてくる</u></i></b>物だからです。たとえば、自分のウインドウ以外の場所でマウスの左ボタンが押されたままマウスが動かされ、自分のウインドウに入ってからマウスの左ボタンが放された場合、WM_LBUTTONDOWN無しに、WM_LBUTTONUPが発生するのです。また、逆に、マウスの左ボタンが押されたままマウスが動かされ、自分のウインドウの外でマウスの左ボタンが放された場合、L_BUTTONUPが発生しません。マウスに関するメッセージに限らず、<b><i><u>一般に、メッセージが発生する順序を仮定したコーディングはしてはいけない</u></i></b>。勿論、最初に1回しか呼ばれないとか、最後に1回しか呼ばれないなど特殊なメッセージはあります。たとえば、WM_DESTROYがそうです。この様なメッセージは、マニュアルを調べれば書いてありますので、これらのメッセージのみ発生する順序を仮定してかまいません。

<p align="CENTER">
<table border="1" bgcolor="#F0F0F0">
<tr><td>
<pre>
/* このファイル内でのみ用いられる構造体の定義 */
typedef struct {
  WORD    x;
   WORD    y;
} TPoint;

/* このファイル内でのみ用いられるグローバル変数 */
static TPoint PrevPoint; /* 以前の点を覚える変数 */
</pre>
<tr><td>
<pre>
/* このファイル内でのみ用いられる変数定義 */
static LRESULT Wm_DestroyProc(void)
{
  PostQuitMessage(0);
   return  0;
}
</pre>
<tr><td>
<pre>
static LRESULT Wm_LButtonDownProc(HWND hwnd,WPARAM fwKeys,WORD xPos,WORD yPos)
{
  SetCapture(hwnd);   /*  マウスをキャプチャする  */
   PrevPoint.x=xPos;   /*  マウスの左ボタンを押したときの位置を覚える  */
   PrevPoint.y=yPos;
   return  0;
}
</pre>
<tr><td>
<pre>
static LRESULT Wm_LButtonUpProc(HWND hwnd,WPARAM fwKeys,WORD xPos,WORD yPos)
{
  HDC DC;
   if(GetCapture()==hwnd)  /*  マウスをキャプチャしているのが、このウインドウである事を確認して*/
   {
       DC=GetDC(hwnd);
       MoveToEx(DC,(short)PrevPoint.x,(short)PrevPoint.y,NULL);    /*  DEBUG   */
       LineTo(DC,(short)xPos,(short)yPos);                         /*  DEBUG   */
       ReleaseDC(hwnd,DC);
       ReleaseCapture();   /*  マウスを開放する    */
   }
   return  0;
}
</pre>
<tr><td>
<pre>
static LRESULT Wm_MouseMoveProc(HWND hwnd,WPARAM fwKeys,WORD xPos,WORD yPos)
{
  HDC DC;
   if(GetCapture()==hwnd)  /*  マウスをキャプチャしているのが、このウインドウである事を確認して*/  
   {
       DC=GetDC(hwnd);
       MoveToEx(DC,(short)PrevPoint.x,(short)PrevPoint.y,NULL);    /*  DEBUG   */
       LineTo(DC,(short)xPos,(short)yPos);                         /*  DEBUG   */
       ReleaseDC(hwnd,DC);
       PrevPoint.x=xPos;
       PrevPoint.y=yPos;
   }
   return  0;
}
</pre>
</table>
<p>
 では、Wm_LButtonDownProc関数、Wm_MouseMoveProc関数、Wm_LButtonUpProc関数と順に見ていきましょう。
<p>
 まず、<b><i><u>Wm_LButtonDownProc関数</u></i></b>。ここでの処理は、マウスのキャプチャ(<b><i><u>SetCapture関数</u></i></b>)とマウスカーソルの位置の保存です。マウスのキャプチャというのは、マウスがこのウインドウの外から出ても、マウスに関するイベントをメッセージとして通知し続けるようにする事です。
<p>
 次、<b><i><u>Wm_MouseMoveProc関数</u></i></b>。ここでは、マウスをキャプチャしているウインドウが自分自身である場合のみ(<b><i><u>GetCapture関数</u></i></b>)、以前のマウスイベントが起きたマウスの位置から現在のマウスの位置まで直線を引き、現在のマウスの位置を次のマウスイベントに備えて保存しています。
<p>
 Windowsアプリケーションでは、ウインドウ内に線を引いたり文字を書いたりする場合は、デバイスコンテキストなるWindowsが管理する有限の資源を取得し(<b><i><u>GetDC関数</u></i></b>)、そのハンドルをもってウインドウに描画します。これは極めて少ない資源ですので、必要が無くなればすぐに開放する必要があります(<b><i><u>ReleaseDC関数</u></i></b>)。
<p>
 最後に、<b><i><u>Wm_LButtonUpProc関数</u></i></b>。ここでは、Wm_MouseMoveProc関数同様、マウスをキャプチャしているウインドウが自分自身である場合のみ、以前のマウスイベントが起きたマウスの位置から現在のマウスの位置まで直線を引きます。そして、キャプチャしたマウスを開放(<b><i><u>ReleaseCapture関数</u></i></b>)しています。
<p>
 さて、メッセージを処理した場合の戻り値についてですが、これは、各メッセージで異なりますので、マニュアルを調べてください。一般に、自前で処理した場合は0である事が多いようです。
<hr>
<p>
 今回は、ウインドウプロシージャ内部での処理について説明しました。メッセージとは、単なる整数でウインドウプロシージャの引数に過ぎない。自前で処理をしないメッセージはDefWindowProc関数に全て渡す。<b><i><u>ウインドウプロシージャに流れるメッセージの順序を仮定してはいけない</u></i></b>。これだけ分かれば、十分でしょう。今回は、デバイスコンテキストがいきなり登場するなど、理解に苦しむところがあったと思います。しかし、これはWindowsプログラミングの複雑さゆえ、やむをえない事だと思います。
<p>
 最後に、実は今回のサンプルプログラムは、ウインドウを最小化してから元に戻すとか別のウインドウを上に被せてからそのウインドウを移動さすと描画した線が消えてしまいます。つまり、<b><i><u>今回のサンプルはWindowsアプリケーションの最低条件を満たしていないのです</u></i></b>。次章ではこれを解決する方法を示します。
<p align="RIGHT">
1999年1月17日<br>
加筆修正:1999年2月6日<br>
修正:2000年8月20日
<hr>
<p>
<b>・1999年2月6日の加筆修正について</b>
<p>
 一人の読者からメールで教えていただいたのですが、1999年1月17日に発表したサンプルプログラムとその説明に重大な誤まり(バグ)がありました。
<p>
 誤まりの根源は、私がマウスキャプチャの挙動を理解していなかった事です。私は次のように仮定(理解)していました。つまり、「マウスのボタンを押すことでマウスをキャプチャすれば、マウスのボタンを離すまで、そのウインドウは最前面にあり続ける。また、マウスをこちらから開放しなければキャプチャし続ける」。しかし、実際は、キーボードの操作によって最前面になるウインドウを変えることが出来、これによってキャプチャしたマウスがWindowsにより強制的に開放させられます。つまり、元のサンプルでは、この様な状況で誤作動を起こしていました。
<p>
 教えていただいた読者の方に感謝し、また、間違った事を伝えてしまった事に謝罪します。
<p align="RIGHT">
1999年2月6日
<hr>
<p align="RIGHT">
<a href="index.html">目次</a><br>
<a href="chap5.html">次へ</a>
<hr>
<p align="RIGHT">
著作権者:近藤妥

</body>
</html>

  • 最終更新:2018-03-11 04:10:43

このWIKIを編集するにはパスワード入力が必要です

認証パスワード