Windows API - CUI プログラミング編 第1回 〜ウィンドウとウィンドウプロシージャ〜

WinMain() 関数とウィンドウ

WinMain() 関数

C 言語では、

int main(int argc, char *argv[]);

からスタートするわけですが、Windows API においては、

#include <windows.h>
int WINAPI WinMain(
 HINSTANCE hInstance, HINSTANCE hPrevInstance,
 PSTR lpCmdLine, int nCmdShow
);

という、WinMain() 関数からスタートします。lpCmdLine っていうのが、argv に対応する以外は、 ほとんど main() 関数と全く別物です。

御託はいいから、早くウィンドウを表示しろという人のために、ソースを書きます。

int WINAPI WinMain(
 HINSTANCE hInstance, HINSTANCE hPrevInstance,
 PSTR lpCmdLine, int nCmdShow){
 InitApp(hInstance);
 return InitWnd(hInstance, nCmdShow)?Run():FALSE;
}

結局、WinMain()関数でするべきことは、 要約すると、ここで書いた、InitApp() という作業と、InitWnd() という作業、 そして、Run() という作業を行うことに過ぎないのです。

ウィンドウクラスの登録とウィンドウの作成

InitApp() と InitWnd() なる作業が必要なわけですが、それぞれが、 ウィンドウクラスの登録という作業と、ウィンドウの作成という作業に相当します。

まず、ウィンドウクラスとはなんぞという話ですが、要約すると、次のような作業をします。

void InitApp(HINSTANCE hInstance){
 WNDCLASSEX wc={
  sizeof(WNDCLASSEX),
  CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS,
  WndProc, 0, 0, hInstance,
  LoadIcon(NULL,IDI_APPLICATION),
  LoadCursor(NULL,IDC_ARROW),
  (HBRUSH)(COLOR_BTNFACE+1),
  NULL,
  TEXT("TestWindowClass"),
  LoadIcon(NULL,IDI_APPLICATION)
 };
 RegisterClassEx(&wc);
}

WNDCLASSEX wc; なるものが、ウィンドウクラスなる構造体で、ごちゃごちゃやってますが、 最終的に、RegisterClassEx() API 関数を使うことでウィンドウクラス "TestWindowClass" を登録しています。

・・・で、続いて、ここで登録したウィンドウクラスに基づいてウィンドウの作成という作業へと移ります。

BOOL InitWnd(HINSTANCE hInstance, int nCmdShow){
 HWND hWnd=CreateWindow(
  TEXT("TestWindowClass"),TEXT("Title"),
  WS_OVERLAPPEDWINDOW|WS_VISIBLE,
  CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
  NULL,NULL,hInstance,NULL
 );
 if(!hWnd) return FALSE;
 ShowWindow(hWnd,nCmdShow);
 UpdateWindow(hWnd);	
 return TRUE;
}

毎回こんなの書いてると、プログラミングする前に疲れるばっかりなんで、 いちいち内容なんて考えたことありませんが、とにかく、こうすることでウィンドウを 作ることが出来ます。無責任な・・・。

メッセージループ

結局のところ、Run() というのがメッセージループという作業になります。

これもまた、要約すると、次のような作業となります。

int Run(void){
 MSG msg;
 while(GetMessage(&msg,NULL,0,0)){
  TranslateMessage(&msg);
  DispatchMessage(&msg);
 }
 return (int)msg.wParam;
}

MSG msg; なる構造体があって、GetMessage() したあと、TranslateMessage() して、 DispatchMessage() しているわけですね。

Windows API プログラミングっていうのが、メッセージ駆動っていう考え方を使うわけですが、 それが、ここに要約されているわけですね。メッセージなるものを、 Get して、Translate して、Dispatch する作業を、while で無限ループさせているわけです。

ウィンドウプロシージャ

で、そのメッセージというものはどこに行く?という話になったときに、 最初のウィンドウクラスの話へと戻ります。実は、ここで、

WndProc

なるものを指定しているのですが、これが、ウィンドウプロシージャなるコールバック関数です。

LRESULT CALLBACK WndProc(
 HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
 return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

DefWindowProc() API 関数は、基本的なウィンドウの挙動、振る舞いを表現する コールバック関数で、こいつを呼ぶことにより、大部分の処理を任せることが出来ます。

このあたりから、ぼちぼちと型やら定数やらの説明をはじめたいと思います。 さて、見慣れない大文字の型がやたらと続きますが、この、WndProc() コールバック関数には、 4つの引数があり、それぞれ、HWND 型、UINT 型、WPARAM 型、LPARAM 型の変数です。

HWND 型変数 hWnd は、ウィンドウハンドルと呼ばれるもので、CreateWindow() API 関数により 発行される識別子みたいなもんです。ウィンドウに関する API 関数は第1引数に HWND 型引数をとります。第2引数の UINT ってのは、 ここでは、ディスパッチされたメッセージの識別子を表します。 このウィンドウプロシージャの中では、まず最初に、uMsg を switch ステートメントで 振り分けることから始めます。そして、一緒にディスパッチされるデータが、 wParam, lParam という2つのデータになり、これらは、uMsg によりその意味が変わります。

以上をまとめると、Windows は、メッセージというものをキューで持っていて、 Run() の中の GetMessage() がここからメッセージをとり、TranslateMessage() したあと、 これを、DispatchMessage() します。そして、ウィンドウクラス構造体で登録された コールバック関数を Windows が呼び出し、ディスパッチされたメッセージを処理するという 作業を無限ループで繰り返すわけです。

WM_CREATE メッセージと WM_DESTROY メッセージ

DefWindowProc() API 関数にすべて任せてもいいのですが、実はこのままだと メッセージループが無限ループに陥ります。

LRESULT CALLBACK WndProc(
 HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
 switch(uMsg){
 case WM_CREATE:
  
  break;
 case WM_DESTROY:
  PostQuitMessage(0);
  break;
 default:
  return DefWindowProc(hWnd, uMsg, wParam, lParam);
 }
 return 0;
}

そこで、switch(uMsg) とすることで、メッセージを振り分けます。 たとえば、WM_CREATE メッセージは、WindowCreate() API 関数を呼び出すと発行されるメッセージです。 普通、このウィンドウの中にいろんなものを配置していったり、記憶領域を確保したりと いろいろな作業をここでしますが、何も思いつかなかったので、何もしていません。

WM_DESTROY メッセージは、ウィンドウを閉じると発行されるメッセージです。 ここでは、PostQuitMessage(0) という API 関数を呼んで、プログラムを終了させるような メッセージをポストしています。ところで、なぜ、これで終了できるかというと、 Run() の中の無限ループにそのヒントがあって、実は、while() のループの条件が、 GetMessage() の値が真であることなわけですが、この値が 0 となると、 このループから抜けることになり、これでプログラムが終了するわけです。 つまり、この値が 0 となるようなメッセージを PostQuitMessage() がポストしたわけですね。

CUI プログラミングとの相違点

TEXT マクロと Unicode 文字列

さて、話が変わり、Unicode の話をここでしておきます。先程のプログラム中でも、

TEXT("Title")

のような記述がありましたが、この TEXT マクロというもので文字列リテラルを囲うと、 これが、Unicode 文字列リテラルとなります。

Windows API では、余程の事情がない限り、文字列といえば、Unicode 文字列を指します。 文字列リテラルを受け取る型は、LPCWSTR というこれまた大文字だけの型です。

というわけで、これまで、char* の C 文字列や、std::string なんで文字列オブジェクトを 使ってきていた場合、なんとかして、これを、Unicode に変換する 必要が出てきます。

C 文字列を Unicode 文字列に変換、あるいはその逆をするためには、

int MultiByteToWideChar(
 UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr,
 int cchMultiByte, LPWSTR lpWideCharStr, int cchWideChar
);
int WideCharToMultiByte(
 UINT CodePage, DWORD dwFlags, LPCWSTR lpWideCharStr,
 int cchWideChar, LPSTR lpMultiByteStr, int cchMultiByte,
 LPCSTR lpDefaultChar, LPBOOL lpUsedDefaultChar
);

なる API 関数を使う必要があります。頭が痛くなりそうな引数ですが、 まず、第1引数には定数 CP_ACP なるものを指定します。第2引数は 0 で問題なしです。 で、第3引数が変換しようとする文字列リテラルで、第4引数は文字列の長さですが、 -1 としておけば勝手に計算されます。

問題が、第5引数と第6引数なわけですが、もちろん、第6引数はバッファの長さ、 第5引数がバッファなわけですが、実は、第6引数を 0 としておくと、 この API 関数は必要となるバッファの長さを返してくれます。 WideCharToMultiByte() API 関数の第7引数と第8引数ですが、NULL で問題ありません。

基本型

Windows API においては、多くの C 言語基本型がこれまで出てきたようなやたらと 大文字ばかりの型に変更されています。

SHORT     short 型整数
INT       int 型整数
UINT      unsigned int 型整数
LONG      long 型整数
FLOAT     float 型浮動小数点数
BYTE      unsigned char 型 8 ビット列
WORD      unsigned short 型 16 ビット列
DWORD     unsigned long 型 32 ビット列
CHAR      char 型文字
LPSTR     char* 型 C 文字列
LPCSTR    const char* 型 C 文字列リテラル
WCHAR     wchar_t 型 Unicode 文字
LPWSTR    wchar_t* 型 Unicode 文字列
LPCWSTR   const wchar_t* 型 Unicode 文字列リテラル
BOOL      int 型真偽値
VOID      void 型
LPVOID    void* 型ポインタ変数

標準出力と標準入力

実は、コンソールアプリケーションでは当たり前に使ってきた、標準出力 stdout/std::cout や、 標準入力 stdin/std::cin が使えません。

printf() CRT 関数や std::cout に直接流すようなプログラミングスタイルを 使っていた場合、これを、sprintf() CRT 関数や、std::stringstream に流すようなスタイルに 変更しなくてはなりません。 その上で、根本的に文字列を表示したり入力したりという方法が CUI と GUI では異なります。

方針としては3つあります。もちろん、使えないはずの標準出力や標準入力を使えるように する方針もあるのですが、1つの方法としてここでは、 エディットコントロールというものを使う方針で進めていきます。


文章作成 : 新宅 貴行 (-+-twilight serenade-+- [stage])