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

<html>

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

<body bgcolor="WHITE">
<p>
<font size="5">5章 Windowsアプリケーションの骨格(その3)</font>
<hr>
<p>
<center>
</center>
<p>
 4章では、どのようにしてウインドウプロシージャに送られたメッセージを処理するのかを説明しました。この章では、4章のサンプルが抱えていた問題を解決する方法を説明します。
<p>
 この章のサンプルを用意しましたので、<a href="chap5.lzh">ここ</a>を押してダウンロードしてください。
<p>
 4章のサンプルであるお絵描きソフトでウインドウ内にに描いた絵は、最小化した後に元に戻したり、上に被さったウインドウを移動さすと消えてしまうものでした。つまり、一部の例外はありますが、一般に、Windowsはアプリケーションがウインドウに描画したものを覚えていて勝手に修復してくれないのです。それゆえ、消された(これを<b><i><u>無効化</u></i></b>されたという)領域の修復作業はすべてアプリケーションで行う必要があります。しかし、再描画こそ自動で行ってくれませんが、<b><i><u>Windowsはアプリケーションに再描画をするタイミングを教えてくれます</u></i></b>。つまり、Windowsは再描画を促すメッセージをメッセージキューに置くのです。したがって、<b><i><u>アプリケーションは再描画のときの為に何らかの方法で再描画用のデータを蓄えておく必要があります</u></i></b>。この章では、再描画を促すメッセージと再描画のためのデータの蓄えかたの1つを紹介します。
<hr>
<p>
<b>・WM_PAINT</b>
<p>
 まず、今回のサンプルのウインドウプロシージャをリスト5-1に示します。WM_PAINTというメッセージに対して自前のウインドウプロシージャが応答している事が分かりますね。<b><i><u>このWM_PAINTというメッセージこそが、アプリケーションに再描画の必要性を知らせるメッセージなのです</u></i></b>。
<p>
<table border="1" bgcolor="#F0F0F0">
<caption align="BOTTOM">リスト5-1
<tr><td>
<pre>
/* C言語で始めるWindowsプログラミング */
/* 5章のサンプルプログラム */
/* Programmed by Y.Kondo */
/* 注:TABサイズは4で見てください */
/* このファイルでは、メインウインドウのウインド*/
/*ウプロシージャが定義されている */

#define STRICT
#include &lt;windows.h&gt;
#include &lt;stdio.h&gt;
#include "wndproc.h"
<b><i><u>#include "doc.h"</u></i></b>

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

/* このファイル内でのみ用いられる関数のプロトタイプ宣言 */
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);
static LRESULT Wm_PaintProc(HWND);

/* メインウインドウのウインドウプロシージャ */
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));
       case    WM_PAINT:           /*  再描画の必要な時に生じるメッセージ  */
           return  Wm_PaintProc(hwnd);
   }
   return  DefWindowProc(hwnd,message,wparam,lparam);
}

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

/* このファイル内でのみ用いられる定数の定義 */
#define TYPE_MOVETO 1
#define TYPE_LINETO 2

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

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

static LRESULT Wm_DestroyProc(void)
{
  <b><i><u>DestroyDoc()</u></i></b>;           /*  保存したデータを廃棄する    */
   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)  /*  マウスをキャプチャしているのが、このウインドウである事を確認して*/
   {
       TPoint  forSave;    /*  保存用  */
       DC=GetDC(hwnd);
       MoveToEx(DC,(short)PrevPoint.x,(short)PrevPoint.y,NULL);    /*  DEBUG   */  
           <b><i><u>AddLast(TYPE_MOVETO,sizeof(TPoint),&PrevPoint;)</u></i></b>;
       LineTo(DC,(short)xPos,(short)yPos);                         /*  DEBUG   */
           forSave.x=xPos;
           forSave.y=yPos;
           <b><i><u>AddLast(TYPE_LINETO,sizeof(TPoint),&forSave;)</u></i></b>;
       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   */
           <b><i><u>AddLast(TYPE_MOVETO,sizeof(TPoint),&PrevPoint;)</u></i></b>; /*  保存    */
       LineTo(DC,(short)xPos,(short)yPos);                         /*  DEBUG   */
       ReleaseDC(hwnd,DC);
       PrevPoint.x=xPos;
       PrevPoint.y=yPos;
           <b><i><u>AddLast(TYPE_LINETO,sizeof(TPoint),&PrevPoint;)</u></i></b>; /*  保存    */
   }
   return  0;
}

static LRESULT Wm_PaintProc(HWND hwnd)
{
  PAINTSTRUCT ps;
   int         type;
   TPoint      *Data;
   HDC         PaintDC;
   if(GetUpdateRect(hwnd,NULL,TRUE))
   {
       PaintDC=BeginPaint(hwnd,&ps;);
       if(<b><i><u>First()</u></i></b>) /*  最初の位置にカーソルを持ってくる    */
       {
           do
           {
               <b><i><u>type=GetDataType()</u></i></b>; /*  データタイプを取得  */
               switch(type)        /*  データタイプによって使用するGDI関数を選択    */
               {
                   case    TYPE_MOVETO:
                       <b><i><u>Data=(TPoint*)GetData()</u></i></b>;
                       <b><i><u>MoveToEx(PaintDC,(short)Data-&gt;x,(short)Data-&gt;y,NULL)</u></i></b>;       /*  DEBUG   */
                       break;
                   case    TYPE_LINETO:
                       <b><i><u>Data=(TPoint*)GetData()</u></i></b>;
                       <b><i><u>LineTo(PaintDC,(short)Data-&gt;x,(short)Data-&gt;y)</u></i></b>;              /*  DEBUG   */
                       break;
               }
           }
           while(<b><i><u>Next()</u></i></b>);      /*  カーソルを次に進める    */
       }
       EndPaint(hwnd,&ps;);
   }
   return  0;
}
/PRE&gt;
</table>
<p>
 WM_PAINTを実際に処理している関数は、Wm_PaintProc関数です。この内部で、ウインドウの再描画処理を行っています。実際に行っている事を概念的に書いてみますと以下のようになります。
<p align="CENTER">
<table border="1" bgcolor="#F0F0F0" w>
<tr><td>
・再描画専用のデバイスコンテキストの取得<br>
・マウスでウインドウに描画した時に覚えておいた情報を元に線を描く<br>
・再描画専用のデバイスコンテキストの開放<br>
</table>
<p>
 最初の再描画処理専用のデバイスコンテキストの取得は、たった1つの関数で行っています。つまり、<b><i><u>BeginPaint関数</u></i></b>です。同様に、それの開放もたった1つの関数で行っています。つまり、<b><i><u>EndPaint関数</u></i></b>です。
<p>
 この章では、アプリケーションの骨格についての説明に主眼を置いています。したがって、それら2つの関数について詳しい説明はしません。ただ、BeginPaint関数を呼べば、再描画処理専用のデバイスコンテキストを取得し、そのハンドルを得ることが出来る。後は、これを用いて再描画し、EndPaint関数を呼べば、そのデバイスコンテキストは開放される。これで十分と思います。BeginPaint関数とEndPaint関数に共通の引数の型であるPAINTSTRUCT構造体は、この2つの関数を使うときには必ず必要だが、あまり使う機会はありません。
<p>
 さて、再描画のデータの保存・再生の方法ですが、大きく二つの方法が考えられます。一つは、メモリ上にウインドウと同じイメージを持ち必要になればウインドウにベタっとコピーする。もう一つは、マウスで描画中に取得した点の座標情報を覚えておいて、これを元に再度、線を描き直す。今回のサンプルは、後者を採っています。
<p>
 さて、アプリケーションの骨格という意味で、大変重要なのは、DOC.Hで宣言された関数群を用いて個々の点の座標情報を記憶し再生している事です。つまり、<b><i><u>Windowsからの情報の取得や画面への書き込みとデータの管理を別のモジュールで行っている</u></i></b>事です。実は、これこそがMFCの最初の難所である<b><i><u>ドキュメントビューアーキテクチャ</u></i></b>の原始的な形なのです。
<p>
 さて、DOC.H(リスト5-2)を見てみましょう。
<p>
<table border="1" bgcolor="#F0F0F0">
<caption align="BOTTOM">リスト5-2
<tr><td>
<pre>
/* C言語で始めるWindowsプログラミング */
/* 5章のサンプルプログラム */
/* Programmed by Y.Kondo */
/* 注:TABサイズは4で見てください */
/* このファイルでは、ドキュメントにアクセスする*/
/*関数を宣言している */

/* ドキュメントの最後にデータを付け加える関数 */
/* 正常終了時は、1を返し、異常終了時0を返す */
/* 第一引数は型、第二引数はサイズ */
/* 第三引数はデータへのアドレス */
int AddLast(int,int,void*);

/* カーソルに最初のデータを示さす関数 */
/* 最初のデータをカーソルが示せた場合は1を返し*/
/* 示せなかった場合は、0を返す */
int First(void);

/* カーソルを後方に1つ進める関数 */
/* カーソルを動かせた場合は1を返す */
/* 終端に達していた場合は、0を返す */
int Next(void);

/* カーソル位置のデータを削除する関数 */
/* 成功すれば1を返し、失敗すれば0を */
/* 返す。 */
/* 削除後、カーソルは削除したデータの */
/* 次を示す */
int Delete(void);

/* ドキュメントを破棄する関数 */
void DestroyDoc(void);

/* カーソル位置のデータの型を取得する関数 */
/* カーソルが正しくデータのあるノードを示していない */
/* 場合、0を返す。 */
int GetDataType(void);

/* カーソル位置のデータの型を取得する関数 */
/* カーソルが正しくデータのあるノードを示していない */
/* 場合、0を返す。 */
int GetDataSize(void);

/* カーソル位置のデータへのポインタを返す関数 */
/* カーソルが正しくデータのあるノードを示していない */
/* 場合、NULLを返す。 */
void const *GetData(void);
</pre>
</table>
<p>
 DOC.Hで宣言している関数群は、線画のデータを管理するモジュールとやり取りする為の関数の集合です。つまり、線画のデータを管理するためのモジュールが外部に公開している<b><i><u>インターフェース</u></i></b>といえます。<b><i><u>他のモジュールは、その実装ではなく仕様のみを意識してプログラミングをする事が出来ます</u></i></b>。インターフェースというとC++やJavaから広まった感がありますが、モジュールのインターフェースというのは、TurboPASCALなど分割コンパイルをサポートしたPASCALを使っていたプログラマにはおなじみの考え方ですね。
<p>
 ちょっと余談になりますが、モジュールのインターフェースの仕様を決める事がアプリケーション(プログラム)の<b><i><u>構造設計</u></i></b>そのものなのです、そして、プログラマにとって腕の見せ所なのです(筆者の場合は、馬鹿にされ所か?)。今回は、線画のデータを管理するモジュールに汎用性を持たしていませが、汎用性を持たそうとすると、結構、経験を必要とします。

<p>
 リスト5-2には、それぞれの関数の仕様がコメントとして書いてありますので、これとリスト5-1とを照らし合わせると、これだけで、WM_PAINTによる再描画の全貌が見えると思います。
<hr>
<p>
<b>・線画のデータ管理部の実装</b>
<p>
 さて、ここからは、Windowsプログラミングの話ではありません。<b><i><u>アルゴリズムとデータ構造</u></i></b>の話です。DOC.H(リスト5-2)で公開されているインターフェースを実装しているのは、DOC.Cです。これをリスト5-3に示します。
<p>
<table border="1" bgcolor="#F0F0F0">
<caption align="BOTTOM">リスト5-3
<tr><td>
<pre>
/* C言語で始めるWindowsプログラミング */
/* 5章のサンプルプログラム */
/* Programmed by Y.Kondo */
/* 注:TABサイズは4で見てください */
/* このファイルでは、ドキュメントにアクセスする*/
/*関数を定義している */
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include "doc.h"

/* データを双方向リストで繋ぐためのノード(節)型の定義*/
typedef struct tagT {
  int     DataType;
   int     DataSize;
   void*   Data;
   struct tagT *Previous;
   struct tagT *Next;
} TNode;

/* ファイルグローバルな変数定義 */
<b><i><u>static TNode Head={0,0,NULL,&Head;,&Head;}</u></i></b>;
<b><i><u>static TNode *Cursor=&Head;</u></i></b>;

/* ドキュメントの最後にデータを付け加える関数 */
/* 正常終了時は、1を返し、異常終了時0を返す */
int AddLast(int type,int size,void* data)
{
  TNode   *p;
   TNode   *newdata;
   p=Head.Previous;

  /*  ここでメモリ領域を取得  */
   if((newdata=malloc(sizeof(TNode)))==NULL)
       return  0;
   if((newdata-&gt;Data=malloc(size))==NULL)
   {
       free(newdata);
       return  0;
   }

  /*  ここで、新たなデータをセット    */
   newdata-&gt;DataType=type;
   newdata-&gt;DataSize=size;
   memcpy(newdata-&gt;Data,data,size);
   newdata-&gt;Previous    =Head.Previous; /*  リンク  */
   newdata-&gt;Next        =&Head;         /*  リンク  */

  /*  リンク  */
   p-&gt;Next              =newdata;
   Head.Previous       =newdata;
   
   return  1;                          /*  99/02/10 DEBUG  */
}

/* カーソルに最初のデータを示さす関数 */
/* 最初のデータをカーソルが示せた場合は1を返し*/
/* 示せなかった場合は、0を返す */
int First(void)
{
  Cursor=Head.Next;
   if(Cursor==&Head;)
       return  0;
   return  1;
}

/* カーソルを後方に1つ進める関数 */
/* カーソルを動かせた場合は1を返す */
/* 終端に達していた場合は、0を返す */
int Next(void)
{
  if(Cursor==&Head;)
       return  0;
   Cursor=Cursor-&gt;Next;
   if(Cursor==&Head;)
       return  0;
   return  1;
}

/* カーソル位置のデータを削除する関数 */
/* 成功すれば1を返し、失敗すれば0を */
/* 返す。 */
/* 削除後、カーソルは削除したデータの */
/* 次を示す */
int Delete(void)
{
  TNode   *p;
   TNode   *n;
   if(Cursor==&Head;)
       return  0;
   p=Cursor-&gt;Previous;
   n=Cursor-&gt;Next;
   free(Cursor-&gt;Data);
   free(Cursor);
   p-&gt;Next      =n;
   n-&gt;Previous  =p;
   Cursor=n;           /*  削除したデータの次のデータを示すようにする  */
   return  1;
}

/* ドキュメントを破棄する関数 */
void DestroyDoc(void)
{
  while(First())
       Delete();
}

/* カーソル位置のデータの型を取得する関数 */
/* カーソルが正しくデータのあるノードを示していない */
/* 場合、0を返す。 */
int GetDataType(void)
{
  if(Cursor==&Head;)
       return  0;
   return  Cursor-&gt;DataType;
}

/* カーソル位置のデータの型を取得する関数 */
/* カーソルが正しくデータのあるノードを示していない */
/* 場合、0を返す。 */
int GetDataSize(void)
{
  if(Cursor==&Head;)
       return  0;
   return  Cursor-&gt;DataSize;
}

/* カーソル位置のデータへのポインタを返す関数 */
/* カーソルが正しくデータのあるノードを示していない */
/* 場合、NULLを返す。 */
void const *GetData(void)
{
  if(Cursor==&Head;)
       return  NULL;
   return  Cursor-&gt;Data;
}
</pre>
</table>
<p>
 DOC.Cの本体は、外部に公開されていない変数Headと変数Cursorです。そして、そのアルゴリズムは、<b><i><u>環状の双方向リスト</u></i></b>です。変数Headが環状のリンクドリストの核になり、変数Cursorが現在のデータを示すのです。結局、外部に公開している関数は、この二つの変数を操作する物にほかなりません。
<p>
 順序のあるデータというと、すぐに配列を思い浮かべる人が多いと思います。しかし、配列というのは、列という概念の一形態にすぎません。列というのは、データの並びです。そして、列という概念の中に配列とリンクドリストがあります。配列は、個々のデータを先頭からの変位でもって表し、ランダムアクセスが出来るのが特徴です。しかし、配列は、あらかじめデータの個数を仮定しておかねばならず、また、データの列の中に新たなデータを挿入したり途中のデータを削除するのが苦手です(実装が難しいのでなく実行時間がかかる)。それに対して、リンクドリストというのは、あるデータの前後といった具合に相対的位置関係をもってデータの列を表現するものです。リンクドリストには、一方的に次のデータを示す単方向とお互いにデータを示しあう双方向があります。さて、リンクドリストの特徴は、全く配列の逆で、ランダムアクセスは出来ません。しかし、あらかじめデータ量を仮定する必要が無く、データの挿入・削除が極めて高速に行えます。その中で、環状の双方向リストというのは実装がしやすいので、今回、採用したのです。
<hr>
<p>
 どうでしたか?今回は、次のことを学びました。ウインドウの再描画はアプリケーションが行わなければならない。再描画のタイミングを知らせるメッセージはWM_PAINTである。WM_PAINTに対する応答のときは、再描画専用のデバイスコンテキストを用いる。再描画の為のデータは、ウインドウプロシージャとは別モジュールにして、公開されたインターフェースを用いて管理する。
<p>
 次回は今後の為に、ユーザーが独自のコマンドをアプリケーションに伝える手段を説明します。つまり、メニューです。では、お楽しみに。
<p align="RIGHT">
1999年2月9日<br>
加筆修正:1999年2月10日<br>
修正:2000年8月20日<br>
<hr>
<p align="RIGHT">
<a href="index.html">目次</a><br>
<a href="chap6.html">次へ</a>
<hr>
<p align="RIGHT">
著作権者:近藤妥

</body>
</html>

  • 最終更新:2018-03-11 04:16:34

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

認証パスワード