Windowsプログラミングの基礎
サンプルコードをここにおいておきます。まずは全体を見てください。ほぼ最小の古典的なWindowsプログラムのHelloです。それでは、順に説明していきたいと思います。
インクルードファイルとプロトタイプ宣言
まず、インクルードファイルから。
#include <string.h>
#include <windows.h>
2つのヘッダがインクルードされています。最初の一行はリストの後半に出てくる標準のC関数strlenを使うためです。文字列の長さを返します。二行目はWindowsアプリに特有のヘッダです。通常Windowsプログラムを作成するときは必ずインクルードします。この中には色々な型宣言やプロトタイプ宣言、マクロ、定数宣言などWindowsプログラミングに必要な、または便利なものが記述されています。
次にその下のプロトタイプ宣言を見てみましょう。
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL InitApplication(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
unixやdos(あるいはコンソールモード)でプログラミングしてきた方には見慣れない型やマクロがあると思います。しかし、これら3行のどれもが関数のプロトタイプ宣言です。これも、一行ずつ見ていきましょう。
最初の行は4つの引数をとるWndProc関数の宣言です。この関数はLRESULT型の値を返します。LRESULT型は前出のwindows.hの中で定義されていて、
typedef LONG LRESULT;
となっています。また、LONGは
typedef long LONG;
と定義されていますので、LRESULTは実はlong型のことだということが分かりました。このように謎の型やマクロはたいていヘッダファイルを検索すると明らかになります。次のマクロCALLBACKはコンパイラにWndProc関数がコールバック関数であることを知らせるために使います。コールバック関数とはOSから呼び出される(実行される)関数のことです。次に4つの引数の型も謎ですがこれは後で説明します。ところで、WndProcは何をしているかというとアプリケーション固有の動作(注1)をします。このプログラムでは"Hello!"と表示する仕事をします。もし3Dのポリゴンを表示したいと思えばこの関数にそれなりの手を加えることになります。
残りの2つの関数のプロトタイプ宣言をみます。BOOL型はint型のことです。またHINSTANCE型はヘッダをたどっていくとvoid*つまりvoid型のポインタであることが分かります。これらがどのように使われているのかは後で詳しく説明します。しかし、Windowsプログラミングでは”HISTANCE型が本当は何型なのか”のような疑問ははほとんどの場合謎のままで不都合なくプログラミングできるようになっています。それよりも私たちプログラマはHINSTANCEがどのように使われるのかを知ることが重要になります。
InitApplication関数はプログラムのウインドウの初期化、InitInstance関数は初期化した内容をもとにウインドウを実際に作ります。InitInstance=インスタンスはC++(オブジェクト指向)で使われるインスタンスと意味が似ています。このように良くオブジェクト指向の用語や考え方が使われています。
メインループWinMain
ここまできてやっと WinMain関数まで到達しました。
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int
nCmdShow)
{
MSG msg;
if(!InitApplication(hInstance))
{
return FALSE;
}
if(!InitInstance(hInstance, nCmdShow))
{
return FALSE;
}
while(GetMessage(&msg, NULL, 0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
この関数はDOS上のプログラミングなどではおなじみのmain関数に相当します。プログラムが起動するとまず最初にこの関数が実行されます。この関数の中に記述されている仕事は大きく3つに別れています。
まず最初に先ほどプロトタイプ宣言が出てきたIniApplication関数を呼んでいます。ここでウィンドウの初期設定しています。もし初期化が失敗したらそれはもう非常事態なのでreturn FALSEで終了します。FALSEは0と同じです。
次に同じく前出のIniInstance関数を呼び出します。この関数ではIniApplication関数で初期設定した内容を基にアプリケーションのウインドウ(のインスタンス)を作りそれを画面に表示させています。これで、アプリケーションが何か仕事をする準備が整いました。
最後の仕事はwhile文でループしている部分です。Windowsアプリケーションの動作の多くは何かの”きっかけ”を必要とします。例えばマウスでボタンを押されたら何かをする、5分立ったら何かをする、キーボードが押されたら何かをする、などです。そのきっかけはOSがメッセージとして送ってきて知らせてくれます。そのようなしくみなのでここではOSのメッセージをループしながら待って、何かきたらそのメッセージにふさわしい動作をします。ここはもう少し詳しく説明します。
新しく出てきた関数が3つあります。
GetMessage
TranslateMessage
DispatchMessage
の3つです。これらはWindows API関数(以下API)と呼ばれwindows.hに宣言されています。
これらの関数はWindowsのOSの機能を直接操作するために使います。
まず、GetMessage関数でOSからのメッセージに聞き耳を立てます。そのメッセージは第一引数のmsg構造体(=クラス)に入ってきます。関数を呼び出すとmsgに何か書き込まれなくてはならないので&を付けてアドレスを渡しています。
もしユーザーがプログラムを閉じたりするとGetMessageは0を返すのでwhileのループから抜けプログラムは終了します。それまでは、ループが続きます。
TranslateMessageでmsgの内容をプログラムが前出のWndProcで処理できるように変換します。
最後に変換したメッセージをDispatchMessageでもう一度OSに返します。ここでWndProcがCALLBACK関数であったことを思い出してください。つまり、OSから呼ばれる関数でした。DispathcMessageを呼ぶことで間接的にOSが受け取ったメッセージを適当なタイミングでWndProcに渡します。
さて、これでWinMainの内容の説明は終わりました。つぎにその関数の返り値と引数の説明をします。もう一度関数の先頭の方を見てみましょう。
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int
nCmdShow)
{
MSG msg;
返り値はint型ですね。この関数の一番最後に
return msg.wParam;
とあるので、アプリケーションが終了するとき送られたOSからの最後のメッセージの何かを返していることになります。ところで、MSG構造体は上で説明したようにメッセージを扱う為のものであることは流れからわかるかと思います。これもwindows.hの中を辿っていくと
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
のような構造をしています。ここでは、深入りせずに次に進みましょう。
ところでWinMainの帰値はいったいどこに渡されるのでしょう。WinMainはOSから呼ばれるので帰値もOSに渡されます。ただし帰値は今のところ使われていないようです。
次にPASCALというマクロがあります。これはWinMain関数をPASCAL型関数としてコンパイルするようにコンパイラに知らせるためのものです。前出のCALLBACKと似ていますね。ここも深入りしないでそういうものとして進んで不都合はないでしょう。
実のところ、PASCALもCALLBACKも__stdcallに変換されます。結局同じやり方で関数呼び出しされています。
最後に引数です。HINSTANCE型の第一引数はOSがこのプログラの実行時につけたハンドル(識別番号)です。第二引数も同じくHINSTANCE型ですが、これは16bit時代の名残で常にNULLが入ってきます。三番目はLPSTR型の引き数ですね。実行時に与えられたコマンドラインの文字列が入ってきます。これはDOSのmain関数でも同じようなのがありましたね。ちなみにLPSTR型は
typdef CHAR *LPSTR;
と宣言されています。最後のint型の引数はウインドウをどのような状態で開くかという要求を表した数字が入ってきます。ここで、ウインドウは最大化、最小(アイコン)化、など色々な状態がある事を思い出してください。この引数はその状態を指定しています。
ウインドウの初期化
さて前回の続きです。InitApplication,InitInstance,WndProc関数それぞれの内容を見ていきましょう。まずはInitApplicationです。
BOOL InitApplication(HINSTANCE hInstance)
{
WNDCLASS wc;
wc.style = CS_HREDRAW |
CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL,
IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL,
IDC_ARROW);
wc.hbrBackground= GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName= "test";
return RegisterClass(&wc);
}
初期化するということでほとんどが代入文ですね。まず、ここで何をしたいかということを説明します。Windowsアプリケーションは一部の例外を除いて外見を持ちます。その外見とはアプリケーションを実行したときに開く窓=ウインドウのことです。ここでは、その外見がどのようなスタイルなのかとか、そのアプリケーション固有の動作はどこで行うのか(ここではWndProc関数がHello!と表示するんでしたね)などの設定を行います。
まず、返り値はBOOL型です。これより初期化が成功したらTRUE失敗したらFALSEを返すということが分かります。引数はHINSTANCE型です。この引数はアプリケーション起動時にWinMainに渡されるOSが定めた識別子でした。さて関数の内側を見てみましょう。WNDCLASSというのは構造体です。これは
typedef struct tagWNDCLASSA {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
} WNDCLASSA, *PWNDCLASSA, NEAR *NPWNDCLASSA, FAR *LPWNDCLASSA;
と定義されさらに
typedef WINDCLASSA WNDCLASS;
となっています。
この構造体のメンバに値をセットしてそれを引数にしてAPIであるRegisterClass関数を呼ぶわけです。こうすることでOSに”これこれしかじかのウインドウを使います”と申し込んでOSが”はい分かりました”となればRegisterClass関数はTRUEを返してきます。この返り値をそのままInitApplicationの返り値として使っているわけですね。この作業はC++でclassを構築することに似ています。ですから実際のウインドウズの実体化(インスタンス化=Instatiation)は次の関数InitInstanceで行います。
さて、ここまで分かればあとはメンバそれぞれの意味を理解すれば良いわけですね。今度はそれを順に説明します。
styleにはウインドウがどのように動作するかなどを指定します。このメンバの値はビットごとに意味を持つのでそのビットを立てるためにOR演算子
|を使い複数の指定をします。ここでは
wc.style = CS_HREDRAW | CS_VREDRAW;
と設定しています。最初の定数CS_HREDRAWは横方向のウインドウズのサイズがマウスなどで変更されたらウインドウに書かれた内容を再び書き直すという動作をするんだということを指定しています。次のCS_VREDRAWは縦方向について同じ事です。つまりこの2つで”ウインドウのサイズが変更されたら書いてあった内容はサイズに合わせて書き直すことにする”という意味になります。これを指定することでプログラム実行時にサイズが変更されるたびにOSからプログラムに”書き直してね”という意味のメッセージが送られてくることになります。
lpfnWndProcはメッセージを処理する関数のアドレスが入ります。その関数とはここではWndProcのことです。ここに登録することでOSはWndProcをCALLBACKすることが出来るんですね。
wc.lpfnWndProc = (WNDPROC) WndProc;
関数の名前は関数のアドレスを意味することを思い出してください。代入際に関数のアドレスをWNDPROCで型変換していますね。何故かといえはlpfnWndProcがWNDPROC型だからというのが簡潔な答えですが、ピンと来ないのでWNDPROCの定義を見ると・・・
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
結局(HWND, UINT, WPARAM, LPARAM)の4つの引数をとる返り値がLRESULT型のCALLBACK関数型・・・・、つまり関数のポインタであることが分かりますね。
cbClsExtra;
cbWndExtra;
この2つは今のところプログラムでは使わないのでとりあえず0をセットしておきましょう。
メンバhInstance にはそのまま、InitApplicationの引数のhInstanceをセットします。
hIconはHICON型のメンバで最小化されたときなどに使うアイコンを指定します。独自アイコンを指定できますがここでは簡単のためにデフォルトのものを使っています。アイコンをAPIであるLoadIcon関数で読み込みその返り値(アイコンのハンドル)を代入しています。
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
LoadIconの第一引数はHINSTANCE型です。独自のアイコンを使うときはそのアプリケーション自体にアイコンをリソースファイルとして埋め込みますので自分自身のhInstanceを渡します。他のアプリのものを渡せば他のアプリのアイコンを使うことも出来ます。NULLを渡した場合はOSの標準のアイコンを使うということになります。第二引数はアイコンの識別子(名前)です。IDI_APPLICATIONとは標準のアイコンの識別子です。
hCursorはアプリケーションにマウスカーソルを当てたときにどんなカーソルを使うかということを指定します。ここもアイコンと同じくデフォルトのもの(普通の矢印カーソル)を使っています。
hbrBackgroundはウインドウの背景色を指定します。ウインドウのクリアはこれを使って塗りつぶすわけです。このメンバはHBRUSH型です。つまり、色のみならずブラシ(筆、刷毛)としてカスタマイズして指定できるわけです。自分で独自のブラシを作って指定することも出来ますが、ここでは標準の白いブラシを指定しています。
wc.hbrBackground= GetStockObject(WHITE_BRUSH);
標準のブラシはGetStockObjectで取得します。
lpszMenuNameはメニューを指定します。メニューとはおなじみのウインドウの上の方にある”ファイル”、”編集”などのことです。ここではメニューは使いませんのでNULLを指定します。
最後のlpszClassNameはその名の通りクラス名です。上で述べたようにOSに”これこれしかじかのウインドウを使います”と申し込むわけですが、その時これを含めて10個のパラメータをまとめたWNDCLASSをわたします。そのパラメータ一まとまりの名前をここで付けておくわけです。名前はこれを識別するためにつけるわけですから後でまた使う場面が出てきます。
wc.lpszClassName= "test";
何でも良いのですが、ここではtestと名前を付けました。
これで、InitApplicationの説明が終わりました。
ウインドウの実体化(表示)
次にInitInstanceの説明です。ここではInitApplicatinで登録したしたパラメータをもとに実際にウインドウを作り表示させています。
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hWnd =
CreateWindow("test","TestApp",WS_OVERLAPPEDWINDOW,
160,120,320,240,
NULL,NULL, hInstance, NULL);
if(!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
この関数の返り値引数の意味はもう説明の必要はありませんね。二番目の引数はウインドウをどのような状態で表示するかを指定するものでした。
関数の中を見ていきましょうHWND型はウインドウを識別するものです。一つのアプリケーションはウインドウを複数もつ事が出来ます。それで、ウインドウ一つ一つを区別するために識別子を付けるために使います。
API関数CreateWindowはその名の通りウインドウを作る関数です。作成に成功すれば返り値として上で述べたウインドウの識別子が返ってきます。失敗したらNULLが返ってきます。それでは引数の意味についてみていきましょう。
最初の引数はInitAoolicationの中で付けた名前"test"ですね。ここに登録したパラメータWNDCLASSの名前を指定します。
二番目の引数はタイトルバーに現われる文字です。ここでは適当に”TestApp”としました。
3番目はウインドウの外見について指定します。例えばウインドウの枠の種類や、左上にある×ボタンを付けるか付けないかなどを指定します。ここではWS_OVERLAPPEDWINDWSというごく普通のウインドウを指定しています。ここもOR演算子|で複数の指定が可能です。
4番目はウインドウの左上のx座標です。
5番目は同じくy座標です。
6番目はウインドウの幅です。
7番目はウインドウの高さです。
8番目は、そのウインドウに対する親ウインドウのハンドルです。上でウインドウは複数持てると述べましたがその時に使います。ここでは一枚目のウインドウで親はないのでNULLを指定します。
9番目はメニューの指定です。InitApplicationでもメニューは指定できましたが、こちらでも出来ます。違いはこちらではハンドル(HMENU型)で指定するということです。もちろんメニューは使わないのでNULLをセットしています。
10番目はhInsatanceをそのまま渡します。
11番目はWNDCLASSのcbClsExtraメンバに関わる引数ですが使わないのでNULLです。
さて、これでCreateWindowsによりメモリ上にウインドウを作ることが出来ました。今度は表示させます。それを行うのがShowWindowAPI関数です。
ShowWindow(hWnd, nCmdShow);
引数に今作ったばかりのウインドウハンドルhWndとどのように表示するかのパラメータnCmdShowを渡します。これでウインドウの外見があらわれます。ただこれだとウインドウの枠の中まだ描かれていません。それで最後に
UpdateWindow(hWnd);
とします。このAPI関数はOSがアプリケーション自身に”書き直してね”をいうメッセージを送ってもらうために使います。今作っているアプリはHello!と表示するという動作をします。それは関数WndProcで行います。”書き直してね”というメッセージもWindProcに送られます。
そしてWndProc
さて、やっと最後の関数WndProcにたどり着きました。ここにはアプリケーション独自の動作(正しくはメッセージ処理)を記述します。
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
char szText[] ="Hello!";
switch(uMessage)
{
case WM_PAINT:
BeginPaint(hWnd, &ps);
hDC = ps.hdc;
TextOut(hDC, 0,0,szText,strlen(szText));
EndPaint(hWnd,&ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, uMessage, wParam,
lParam);
}
return 0;
}
ここが働きとしては事実上のメインループとなりますが、実際のメインループはWinMain関数中のwhile文でしたね。while文の中でOSからのメッセージを受け処理できるように加工してから再びOSに戻しその後OSからWndProcを呼んでもらうのでした。だからWndProcはCALLBACK関数なんでしたね。
WndProc関数の中で独自の処理をしているのはWM_PAINTメッセージとWM_DESTROYメッセージです。これらは単なる定数です。
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam)
先頭から見ていきましょう返り値はLRESULT型ですね。この返り値はOSに渡されます。次に引数ですがこれらは全て前出のMSG構造体のメンバであることに注意してください。つまりWinMain関数の中のループ中GetMessageで受け取ったOSからのメッセージの一部がWndProcに引数として渡されているわけです。一つずつ見ていきましょう。
最初の引数はメッセージを受け取ったウインドウのハンドルが入っています。ここではウインドウは一枚なので必ず、そのハンドルが入ってきますね。
2番目はメッセージの番号が入ります。例えばここで扱うWM_PAINTは
#define WM_PAINT 0x000F
と定義されていますのでこのメッセージがきたときにはuMessageには0x000F
= 15が入ってきます。
3番目と、4番目はどんなメッセージが送られてきたかによって意味が変わります。つまりメッセージの付加情報です。例えばマウスのボタン押された時のメッセージWM_MBUTTONDOWNではwParameにはどのボタン(右か左かなど)が押されたか、lParamにはウインドウのどの位置で押されたかの情報が入ってきます。
さて、WndProcの中を見ていきましょう。最初に変数宣言がありますね。
HDC hDC;
PAINTSTRUCT ps;
char szText[] ="Hello!";
これらは全て表示関係に使います。HDCはデバイスコンテキストと呼ばれるものでウインドウに何か図形や文字を描くときこれを使います。PAINTSTRUCT構造体はWM_PAINTメッセージがきたときにどの部分を描いたら良いのかという情報を得るために使います。最後の文字列はいいですよね?(笑)私たちはまさにこの文字列をウインドウに表示したいが為にここまできました。さあ次に進みましょう。
switch(uMessage)
{
switch文できたメッセージに応じた処理をします。
case WM_PAINT:
BeginPaint(hWnd, &ps);
hDC = ps.hdc;
TextOut(hDC, 0,0,szText,strlen(szText));
EndPaint(hWnd,&ps);
break;
この分がこのアプリケーション(プログラム)独自の処理です。画面に"Hello!"と表示しています。API関数BeginPaintでPAINTSTRUCT構造体psに描画する範囲取得します。PAINTSTRUCTは・・・
typedef struct tagPAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;
のように定義されています。最初のメンバhdcが欲しいデバイコンテキストなので
hDC = ps.hdc;
として取り出します。
次にAPI関数TextOutで文字を表示します。第一引数はデバイスコンテキスト、第2、第3引数は表示位置でそれぞれx、yです。次が文字列のポインタで、最後に文字列の長さです。
表示が終わったらAPI関数EndPaintで後始末をします。名前からも想像できるようにBeginPaintとEndPaintは必ず対で使います。そうしないとメモリクリーク(開放し忘れ)が起こります。
case WM_DESTROY:
PostQuitMessage(0);
break;
次のメッセージは終了のメッセージです。ウインドウが閉じられようとすると送られます。API関数PostQuitMessageは終了するときに呼び出す関数です。ここは決まりきった手順でなのでそういうものとして進みましょう。
default:
return DefWindowProc(hWnd, uMessage, wParam,
lParam);
最後にデフォルトの処理です。ここでは2つのメッセージしか扱っていませんが実際に送られてくるメッセージは膨大なものです。それで、その他のメッセージはDefWindowProc関数にそのまま渡してデフォルトの処理をしてもらうわけです。これはアイコンやカーソルをデフォルトのものを使うのと同じ感覚ですね。
さあ、これでHelloの説明が全て終わりました。ただ、そのほとんどがどのアプリでも共通の動きです。独自の部分はWndProcの一部分です。ですから、一度helloを作ってしまえばあとはそれに色々コードを追加していくのが楽なやり方です。この発想を発展させたのMFCやOWLのようなクラスライブラリなわけです。
注釈1.本当はウインドウ固有の動作(メッセージ処理)をここに記述しますが、このサンプルではウインドウは一つなのでアプリ固有の動作ということで話を進めています。