QMAの自習プログラムを作る その1

もくじに戻る

はじめに

クイズゲームの自習用プログラムの作成を通して、Windows GUI アプリケーションの作り方を勉強をします。 作成するのは Windows 用のごく普通のアプリケーション、使用する言語は C++ です。 想定する読者はある程度 C++ (STL含む) が理解できる人です。 ついでに QMA プレイヤーならばもう少し楽しめるかもしれません。

内容に間違いがあるかもしれません。その場合は Twitter などで教えてもらえれば幸いです。

必要なもののインストール

まずここの解説ではプログラムの開発に Microsoft Visual C++ 2010 Express を使用します。 Microsoft Visual Studio Expressのページで無償で配布されているので、 ダウンロードしてインストールしてください。

OSはXP以降のバージョンが必要なようです。 最新のサービスパックが必要になるかもしれないので、Windows Update などで導入しておくと良いでしょう。 32bit, 64bit版どちらでも動きます。 ちなみに私はWindows 7 Ultimate 64bit を使用しています。

プロジェクトの作成

まずはVisual C++を起動し、プロジェクトを作成しましょう。 通常プログラムを作成するにはいくつものファイルを作成する必要があります。 これらのファイルをまとめて管理するために「プロジェクト」が必要です。

なおプロジェクトの他にソリューションというものもありますが、これは 複数のプロジェクトを更にまとめて管理するために用います。 プロジェクトを1つ作成するとそれを持つソリューションも自動的に作成されます。 今回はプロジェクトを1つしか作成しないのでプロジェクト≒ソリューションと思ってもらって構いません。

まずメニューから「ファイル」→「新規作成」→「プロジェクト」とたどります。

「Win32プロジェクト」を選んで、下のボックスに適当なプロジェクト名を入力します。 ここでは例として、プロジェクト名「myqma」とします。 そして「OK」をクリック。

「Win32 アプリケーションウィザード」画面が開きます。左側の「アプリケーションの設定」をクリックし、 「追加のオプション」の「空のプロジェクト」にチェックを入れます。 そして「完了」をクリック。

これでプロジェクトの作成を終了です。 左側のソリューションエクスプローラーのプロジェクト名の部分を右クリックし、 プロパティを選びます。

プロジェクトのプロパティページが開きます。ここでプロジェクトの様々な設定を行えます。 「構成プロパティ」「全般」の「文字セット」を見てください。 ここが「Unicode文字セットを使用する」になっていることを確認。 今回作成するプログラムは内部でUnicodeを用いるので、このようにします。

次にソースファイルを作成します。 ソリューションエクスプローラーの「ソースファイル」フォルダを右クリックし、「追加」「新しい項目」とたどってください。

「新しい項目の追加」画面が開きます。 下のボックスにファイル名を入力します。拡張子は .cpp にしてください。 分かりやすく、プロジェクトと同じ名前を付けると良いでしょう。 「追加」をクリックして完了です。

これで一通り終了です。 下の図の様に、作成したソースファイルが開かれていると思います。 ここにプログラムのソースを書いていきます。

何もしないプログラムを書く

プログラムを作るにあたって、目標を立てましょう。 作っていく内にあの機能を加えたい、これも加えたい、と一度に多くのことをやろうとすると、 効率が下がるばかりかバグの要因になります。 目標以外のことはなるべく考えずに、目標を達成したら 次の段階へ進む、という風にしたいと思います。 最初の目標は「ウィンドウを作り、文字を表示する」です。

さて、空のソースファイルではプログラムを作ることはできません。 とりあえず最低限必要なコードを書いてみましょう。

#include <Windows.h>

int WINAPI wWinMain( HINSTANCE instance, HINSTANCE prev_instance, LPWSTR commandline, int cmdshow ) {
    return 0;
}

そしてメニューから「デバッグ」→「ソリューションのビルド」を実行します。 ソースファイルをコンパイルし、ライブラリとリンクして、プログラムを生成する作業のことを「ビルド」といいます。

画面の下の「出力」の部分にビルドの結果が出力されます。 エラーの場合はコードが間違っているので確認します。 「1 正常終了」となれば成功です。

このプログラムは何もしないプログラムです。 実行しても目には何も見えません。 起動して一瞬で終了するからです。 メニューから「デバッグ」→「デバッグ開始」を選ぶと生成されたプログラムを実行できます。 デバッグ開始のショートカットキーは F5 なので覚えておくと楽です。 プログラム終了後、出力欄には以下のメッセージが出てきます。

「Cannot find or open the PDB file」とありますがこれはエラーではありません。無視してください。 「プログラム~はコード0で終了しました」と出ていることを確認してください。 ここの数字 0 は先程示したコードの「return 0」の後ろの数字に当たります。 プログラムは終了する時に、OSに整数値を1つ渡します。 何を返すかはプログラムの自由ですが、他のプログラムとの連携ではこの数字が重要な意味を持ちます。 プログラムが正常終了した場合は0を渡します。 エラーが起こって終了する場合は、どういう理由で終了するのかを表すエラーコードを渡します。

main と WinMain

Windows の GUI プログラムではスタートアップ関数として main 関数ではなく WinMain 関数を使います。 そして今回は Unicode アプリケーションを作ることになっているので、その派生である wWinMain 関数を使います。 main と WinMain の違いを見てみましょう:

int main( int argc, char **argv )
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow )

まず大きな違いは WinMain では HINSTANCE 型のインスタンスハンドルと呼ばれるものが渡されることです。 これはプログラムを識別するためのモジュールハンドルの一種で、 その正体はプログラムがメモリにロードされているアドレスです。 この解説ページの後ろの方でウィンドウを作るのですが、その時に必要になります。 よく見るとインスタンスハンドルは 2 つ渡されていますが、 第 2 引数の hPrevInstance は古い 16bit の Windows の名残りで現在は使用しません。

WinMain の第 3 引数にはコマンドライン引数が渡されます。GUIアプリケーションもCUIアプリケーションと同じく 起動時にコマンドライン引数を渡すことができます。 cmd.exe (コマンドプロンプト)でプログラム名の後ろに指定することもできますし、 ショートカットのプロパティで、「リンク先」の exe ファイル名の後ろに指定することもできます。 main 関数は引数が空白やタブなどの区切り文字に基づいてトークンに分解されますが、 Windows の場合はそのようなことは行われず、文字列 1 つがまるごと渡されます。 Unicode アプリケーションの場合は CommandLineToArgvW 関数を使って argc, argv 形式に変換することができます。

第 4 引数はウィンドウの表示形態が渡されます。 ショートカットのプロパティを開いた時、「実行時の大きさ」というのを指定できるのを知っていますか? プログラムを最初から最大化や最小化させたりしたい時に使うのですが、 ここで指定した値が WinMain の第 4 引数に渡されます。 ちなみにこの値は ShowWindow 関数にそのまま渡すことができます。

WinMain の戻り値型 int の後ろには WINAPI というものが付いています。 これは呼出規約の指定です。C/C++言語の呼出規約は、省略した場合は通常 cdecl が使われます。 しかし Windows API では基本的に stdcall が用いられます。 WINAPI は stdcall の別名です。 ただし stdcall では可変長引数が扱えないという欠点があるので、 wsprintf 関数など可変長引数をとる API では cdecl が使われています。 (※呼出規約についての説明はここでは割愛します。 C/C++言語より下の機械語レベルでの関数呼出しの実装方法を定義するものなので、 純粋にC/C++言語だけを扱う場合にはほとんど意識する必要はありません。 ただ呼出規約が Windows の要求とそぐわないとエラーになるので、 WinMain の後ろの WINAPI や、後述のウィンドウプロシージャで使う CALLBACK は省略しないでください。)

さっき作成したプロジェクトは GUI アプリケーション用なので、 スタートアップ関数として WinMain 関数が指定されます。 なので main 関数を書いてしまい WinMain 関数を用意しないとエラーになります。 逆に「Win32 コンソール アプリケーション」でプロジェクトを作成すると main 関数を記述する必要があるので、これを書かないとエラーになります。

コンソールアプリケーションの場合は exe ファイルにそのような情報が埋め込まれ、 プログラムを起動すると Windows によってコンソールウィンドウが立ち上げられます。 標準入力や標準出力の概念をもち、printf などのライブラリ関数を使うことができます。 一方、GUI アプリケーションではマウスを主としたグラフィカルな操作を前提としているので、 コンソールウィンドウは用意されません。 CUI アプリケーションで使えていた printf 関数などを呼び出しても何も出力されません。 これらを使いたい場合は API を用いて自分でコンソールウィンドウを作成し、 標準出力、標準入力をそのコンソールと結びつける必要があります。

Windows プログラミングでの型

Windows プログラミングではたくさんの型が登場します。 似たような型であっても、用途によって色々な名前で使い分けています。 Windows SDK 側で定義される型名は全て大文字です。 以下のような基本型や型名の命名規則があるので、一通り覚えておいてください。

ハンドルは今後色々なところで出てきます。 Windows API を使う時に様々なオブジェクト(上で挙げたウィンドウ、ブラシ、ペンなど…)を識別するためにハンドルを使います。 おそらく中身は単なるポインタなのですが、ブラックボックスとして扱った方がいいでしょう。 「ID」と「ハンドル」は異なるので注意してください。(例: プロセスIDとプロセスハンドルは別物)

Windows API を使う

先程の「目に見えないプログラム」は目標のウィンドウを作るというものからはかなり遠いものです。 これではつまらないので、簡単なメッセージウィンドウを表示させてみましょう。 これには MessageBox 関数を使います。 なにはともあれ、次のように MessageBox の一行を加えてください。

int WINAPI wWinMain( HINSTANCE instance, HINSTANCE prev_instance, LPWSTR commandline, int cmdshow ) {
    MessageBox( NULL, L"Hello, world!!", L"myqma", MB_OK );
    return 0;
}

F5 を押して動作を確認してみましょう。下のようなウィンドウが表示されるはずです。

Windowsのプログラムを作るにあたって、Windows API の理解は重要です。 C/C++ 標準のライブラリだけではウィンドウを作ることすらできません。 ウィンドウを作るという指示をOSに与えるには、OSが提供する API(Application Programming Interface)を使わなければなりません。

先程提示したソースコードの先頭のヘッダーファイル Windows.h は様々な Windows API の定義が記述されており、 これ1つをインクルードするだけで大抵のことは事足ります。

MessageBox 関数はそんな Windows API の1つです。引数は次の通りです。

引数意味
HWNDhWnd親ウィンドウのハンドル。親ウィンドウがない場合はNULL。
LPCTSTRlpTextメッセージの内容。
LPCTSTRlpCaptionウィンドウのタイトル。
UINTuTypeメッセージボックスのスタイル。

Windows API の使い方はマイクロソフトの MSDN ライブラリに詳しく載っています。 GoogleなどでAPI名を検索するとすぐ出てくるので試してみてください。 MessageBox 関数のページはhttp://msdn.microsoft.com/ja-jp/library/cc410914.aspxにあります。

第4引数にはメッセージボックスのスタイルを指定するとあります。 上のコードでは MB_OK だけを指定しており、これは「OKボタンだけ用意せよ」ということを意味します。 「はい」「いいえ」のボタンを用意するには MB_YESNO を指定します。 他の属性を組み合わせて指定することもできます。 例えば、ボタンを「はい・いいえ」かつ、アイコンを「疑問符」としたい場合、 MB_YESNO | MB_ICONQUESTION のように論理和演算子で結びます。 なお、どのボタンが押されたかを知りたい場合は MessageBox 関数の戻り値を調べます。 例えば、「はい」ボタンが押されたら IDYES・「いいえ」ボタンが押されたら IDNO が戻り値として渡されます。

MessageBox に表示する文字列で L"Hello, world!!" のように、文字データの前に L が付いていることに注意してください。 この L はその文字列がワイド文字(正確には UTF-16)としてプログラムに埋め込まれることを意味します。 今回のプログラムは Unicode アプリケーションとして作成しているのでこのようにします。 L を外すとマルチバイト文字としてコンパイルされ、エラーを引き起こします。 他にもマルチバイト文字、ワイド文字、両方の場合でコンパイルできるように TEXT というマクロも用意されていますが、 この解説では使用しないこととします。

これでウィンドウはできた?

MessageBox 関数を使ってウィンドウを作れましたし、文字を表示することもできました。 これで最初の目標は達成されたということになります。

それでは次の目標を設定しましょう: 「○×クイズを作る」

次のようなものを作りたいとすると、どうすればいいでしょう。

MessageBox 関数を使えばいい? しかしこの関数でできることは限界があります。

そもそも MessageBox 関数は単純にユーザーに対しメッセージを表示させる目的しか想定していません。 「ちゃんとした」ウィンドウを作るには少し手間がかかるのですが、たかが文字列を表示するのに そんなことやってられない、というプログラマーの負担を軽減するために用意されている API です。 なので目標を達成するには、自分であれこれと細かい指示を出せる「ちゃんとした」ウィンドウを作らなければなりません。 そのためには CreateWindow 関数を使います。

ちゃんとしたウィンドウは、さっき少し述べた通り、かなり面倒です。 しかし面倒であるということは柔軟性があることも意味します。 MSDN ライブラリで調べると、CreateWindow 関数の引数は 11 もあります。

引数意味
LPCTSTRlpClassNameクラス名
LPCTSTRlpWindowNameウィンドウのタイトル (タイトルバーに表示される)
DWORDdwStyleウィンドウのスタイル (論理和で組み合わせる)
intxウィンドウ左上のx座標 (ピクセル単位)
CW_USEDEFAULT を指定すると Windows により適切な座標が決められる
intyウィンドウ左上のy座標 (ピクセル単位)
CW_USEDEFAULT を指定すると Windows により適切な座標が決められる
intnWidthウィンドウの幅 (ピクセル単位)
CW_USEDEFAULT を指定すると Windows により適切なサイズが決められる
intnHeightウィンドウの高さ (ピクセル単位)
CW_USEDEFAULT を指定すると Windows により適切なサイズが決められる
HWNDhWndParent親ウィンドウのハンドル
親ウィンドウがない場合は NULL
HMENUhMenuメニューハンドル(ウィンドウにメニューを付ける場合)、子ウィンドウの場合はそのID(WM_COMMANDで使用)、未使用なら NULL
HINSTANCEhInstanceアプリケーションのインスタンスハンドル
LPVOIDlpParam初期化に用いるデータ (自由に設定可能。使用しないなら普通は NULL)
WM_CREATE メッセージの lParam に渡される構造体の中に格納される。

第1引数にクラス名とあります。これはウィンドウクラスのこと。 どのウィンドウも必ず1つのウィンドウクラスに属します。 ウィンドウクラスはウィンドウの動作を定義します。 なのでウィンドウを作成する前にウィンドウクラスも作らなければいけません。 ウィンドウクラスを作るには RegisterClassEx 関数を使います。

int WINAPI wWinMain( HINSTANCE instance, HINSTANCE prev_instance, LPWSTR commandline, int cmdshow ) {
    WNDCLASSEX windowclass;
    LPCWSTR classname = L"wc_myqma";

    ZeroMemory( &windowclass, sizeof(windowclass) );
    windowclass.cbSize = sizeof(windowclass);
    windowclass.hInstance = instance;
    windowclass.lpszClassName = classname;
    windowclass.lpfnWndProc = DefWindowProc;
    if( 0 == RegisterClassEx( &windowclass ) ) {
        MessageBox( NULL, L"ウィンドウクラスの登録に失敗", L"myqma", MB_OK );
    } else {
        MessageBox( NULL, L"Hello, world!!", L"myqma", MB_OK );
    }
    return 0;
}

ウィンドウクラスの登録のために WNDCLASSEX 構造体を使います。 この構造体にウィンドウクラスを作るのに必要な情報を格納していきます。

ウィンドウクラスを構成する最も重要な要素はウィンドウプロシージャです。 ユーザーがウィンドウに対して何らかの操作(クリック、キー入力、移動、etc...)を行うと ウィンドウに対してメッセージが送られます。 ウィンドウプロシージャはメッセージを受けとって、そのメッセージの種類を判別して対応する処理を行います。 ウィンドウプロシージャの正体は単なる関数であり、 lpfnWndProc メンバにその関数ポインタを指定してあげます。 ここでは DefWindowProc 関数という、Windows 備えつけのウィンドウプロシージャを指定していますが、 これではウィンドウのオリジナルな動作を定義できないので、最終的には自分で作成する必要があります。

WNDCLASSEX.hInstance にはアプリケーションのインスタンスハンドルを渡します。 これは WinMain で受け取った最初の引数 instance をそのまま渡すだけでOKです。

クラス名は自分で適切に定めて WNDCLASSEX.lpszClassName に設定します。日本語は使用しない方が良いでしょう。

さて、上のコードを実行するとまた Hello, World!! というメッセージボックスが開くはずです。 RegisterClassEx 関数は失敗すると 0 を返します。 その時は「ウィンドウクラスの登録に失敗」というようなメッセージボックスが開くようになっています。 (パラメータの設定を間違っていなければ失敗することはまずありません。)

自分のウィンドウを作る

いよいよウィンドウの作成にとりかかります。

int WINAPI wWinMain( HINSTANCE instance, HINSTANCE prev_instance, LPWSTR commandline, int cmdshow ) {
    WNDCLASSEX windowclass;
    LPCWSTR classname = L"wc_myqma";
    HWND mywindow;

    ZeroMemory( &windowclass, sizeof(windowclass) );
    windowclass.cbSize = sizeof(windowclass);
    windowclass.hInstance = instance;
    windowclass.lpszClassName = classname;
    windowclass.lpfnWndProc = DefWindowProc;
    if( 0 == RegisterClassEx( &windowclass ) ) {
        MessageBox( NULL, L"ウィンドウクラスの登録に失敗", L"myqma", MB_OK );
        return 1;
    }
    mywindow = CreateWindow( classname,
        L"myqma",
        WS_VISIBLE|WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        600,
        450,
        NULL,
        NULL,
        instance,
        NULL );
    DestroyWindow( mywindow );
    return 0;
}

さて、上のコードを実行してみると… ウィンドウが一瞬出ますが、すぐ消えてしまいます。 実は CreateWindow 関数はウィンドウを作るだけの関数です。 さっきウィンドウプロシージャの説明をした時に、「ユーザーがウィンドウに対して何らかの操作を行うとウィンドウに対して メッセージが送られる」と述べました。 MessageBox 関数はこのメッセージの処理までやってくれる親切な関数です。 一方で CreateWindow 関数はウィンドウを作り終わったら即座に処理をプログラムに返します。 なので次の行にある DestroyWindow 関数(ウィンドウを破棄する関数)が呼ばれて、すぐに消えてしまいます。 じゃあ DestroyWindow を呼び出さなきゃいいんじゃないの?と思うかもしれませんが、 return 文で WinMain 関数を抜けるとプログラムが終了します。 プログラムが終了すると、それに関連付けられたウィンドウも Windows によって自動的に破棄されます。 なので結果的には同じことです。 (DestroyWindow を呼び出しているのは単純に行儀良く(使い終わった物の後始末をちゃんとして)プログラムを終わらせる為です)

ということで作ったウィンドウを表示し続けるには、ウィンドウが破棄される前に 一時停止しなければいけません。DestroyWindow の前に MessageBox 関数を入れて、動作を止めてみましょう。

ウィンドウを表示させ続けることは出来ました。 しかし、メッセージボックスを閉じるとウィンドウも一緒に消えてしまいます。

メッセージボックスに頼らずに何とか制御できないでしょうか。 …となるとまず無限ループが考えられます。

    //MessageBox( NULL, L"", L"", MB_OK );
    while( true ) {
        Sleep( 10 );
    }

MessageBox の行を無限ループで書換えてみました。Sleep 関数はプログラムを小休止させる API です。 ここでは 10 ミリ秒間だけ休むようにしています。 この Sleep 関数を外すとプログラムが CPU を独占して高い負荷がかかり、 論理プロセッサ数が 1 しかないシステムでは OS が不安定になるので注意してください。

これを実行するとウィンドウは表示されますが、 ユーザーはウィンドウを何も操作することができません。フリーズしてしまっています。 これはウィンドウに対するメッセージを処理できていないからです。 フリーズしたプログラムを強制終了するには Visual C++ 側のメニューから 「デバッグ」→「デバッグの停止」を選ぶか、Shift + F5 キーを押します。

繰り返しになりますが、ウィンドウが受け取ったメッセージはウィンドウプロシージャに処理させないといけません。 メッセージは勝手にプロシージャに送られるわけではなく、 プログラム側でウィンドウのメッセージキュー(イメージとしては郵便受け)を調べ、メッセージが届いていれば そのメッセージをプロシージャに渡して処理させます。 この一連の処理を行うメッセージループをプログラムに追加します。

    MSG message;
    while( GetMessage( &message, NULL, 0, 0 ) ) {
        DispatchMessage( &message );
    }

GetMessage 関数はメッセージが届いているかどうかを調べます。 メッセージキューが空の場合は新しいメッセージが届くまで待機します (中で Sleep 関数のようなことをしているので CPU に負荷はかかりません)。 届いたメッセージは第 1 引数の MSG 構造体に格納されます。 そしてこれを DispatchMessage 関数でウィンドウプロシージャへと渡すと、 メッセージの処理が行われます。 ここでウィンドウプロシージャを指定する必要はありません。 ウィンドウは必ず 1 つのウィンドウクラスに属し、ウィンドウクラスは 1 つのウィンドウプロシージャを定義しています。 つまり DispatchMessage 関数は、メッセージの受取人(=ウィンドウ)を調べ、 そのウィンドウが属するウィンドウクラスのウィンドウプロシージャを調べて、 メッセージを渡してくれます。 現在のプログラムでは自分で作ったウィンドウクラスのウィンドウプロシージャに DefWindowProc 関数が 指定されているので、届いたメッセージは DefWindowProc 関数へと渡されて処理されるわけです。 これによりウィンドウクラスが異なる、複数のウィンドウを作成した場合でも問題なくメッセージを処理できます。

無限ループの部分をそのまま上のコードで置き換えてください。 これを実行してみると、メッセージボックスに頼ることなく、またウィンドウが固まることもなく、操作することができます。

図にしてまとめると、下のようになります。

これでめでたしめでたし… かと言うと、そうでもありません。 ウィンドウを閉じても、出力欄に「プログラム ~ はコード 0 (0x0) で終了しました。」と表示されません。 これはウィンドウは消えてもプログラムは裏で動き続けていることを意味します。 今はデバッグ中なので Shift + F5 でプログラムを終了させることができますが、 これを完成品としてデバッガ(Visual C++)の外で動かすと、タスクマネージャで強制終了させるしか方法がなくなります。 使わないプログラムが、目には見えないけれども裏で動き続けているという状態は好ましいことではありません。 ウィンドウを閉じればプログラムも終了する、という方が良いですね。

なぜプログラムが終了しないのかというと、メッセージループを抜けられていないからです。 while のループ条件として GetMessage 関数の戻り値が指定されています。 MSDN ライブラリなどで関数の戻り値を見ると分かりますが、 この関数は WM_QUIT メッセージを受けとった時だけ 0(false) を返します。 これは「メッセージループを抜けてプログラムを終了せよ」という通知メッセージなのですが、 Windows は、例えば1つのプログラムで複数のウィンドウを使用している時、 どのウィンドウを閉じた時がプログラムを終了する時なのかは知らないので(それを知るのは開発者当人だけ)、 自動的に WM_QUIT を通知してくれるということはしてくれません。 最初の方でウィンドウプロシージャとして指定した DefWindowProc 関数はほとんどの処理を自動でやってくれます。 しかし、ウィンドウが閉じたら WM_QUIT メッセージを出してメッセージループを抜けるように工作しなくてはいけません。 この為にウィンドウプロシージャを自分で作る必要性が生じます。

では、ウィンドウプロシージャを自作しましょう。WinMain の前に次のような関数を記述します。

LRESULT CALLBACK WndProc( HWND window, UINT message, WPARAM wp, LPARAM lp ) {
    switch( message ) {
    case WM_DESTROY:
        PostQuitMessage( 0 );
        break;
    default:
        return DefWindowProc( window, message, wp, lp );
    }
    return 0;
}

int WINAPI wWinMain( HINSTANCE instance, HINSTANCE prev_instance, LPWSTR commandline, int cmdshow ) {
    WNDCLASSEX windowclass;
    LPCWSTR classname = L"wc_myqma";
    ZeroMemory( &windowclass, sizeof(windowclass) );
    windowclass.cbSize = sizeof(windowclass);
    windowclass.hInstance = instance;
    windowclass.lpszClassName = classname;
    windowclass.lpfnWndProc = WndProc;
    if( 0 == RegisterClassEx( &windowclass ) ) {
        MessageBox( NULL, L"ウィンドウクラスの登録に失敗", L"myqma", MB_OK );

lpfnWndProc の部分を自分で作った WndProc に書換えるのを忘れないようにしてください。 ここで指定しているのは関数ポインタなので、型さえ合っていれば関数名は自分で自由に決めることができます。 ウィンドウプロシージャの呼出規約には CALLBACK(stdcallの別名) を指定します。

さっき作ったウィンドウに対して通知されるメッセージは全てこの関数にやってきます。 自分で処理しないメッセージはそのまま DefWindowProc 関数に渡して自動的に処理させます。 まずウィンドウが閉じられた時は WM_CLOSE 通知がやってくるのですが、 これはそのまま DefWindowProc 関数に渡してしまいます。するとこの関数は内部で DestroyWindow 関数を 呼びだしてウィンドウを破棄しようとします。 この時に WM_DESTROY メッセージが送られます。 ここで上の関数では PostQuitMessage 関数を呼び出しています。 これは WM_QUIT メッセージをプログラムの(正確にはスレッドの)メッセージキューに送る関数です。 引数には終了コードを渡します(ここでは正常終了なので 0 を渡す)。 この終了コードは WM_QUIT メッセージのパラメータとして渡るのですが、 メッセージループを終了して WinMain 関数から return する時の値にこのパラメータをそのまま渡すことが推奨されています。

さて、上の記述をしたら実行してみましょう。 ウィンドウを閉じると、ちゃんとメッセージループを抜けてプログラムが終了するはずです。

ウィンドウクラスを少し改善

できたウィンドウのサイズを少し広げると下の図のように黒いところが表われます。 (環境によっては挙動が異なるかもしれません。以下の図は私の環境で起こったことです。)

本当はちゃんと塗り潰しされるべきですが、 ウィンドウクラスに塗り潰しを行うブラシが設定されていないためにこのようなことが起きます。 また気付いた人もいるかもしれませんが、マウスカーソルの形状が変化しない時があります。 これらを防ぐために、ウィンドウクラスの設定に少し手を加えます。 WNDCLASSEX構造体の設定部分に以下の2行を追加してください。

    windowclass.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH ) ;
    windowclass.hCursor = LoadCursor( NULL, IDC_ARROW );

hbrBackground メンバにはウィンドウの背景を塗り潰すブラシを指定します。ここでは GetStockObject 関数を使用して システムで既に定義されている白色のブラシを取得して指定しています。 またウィンドウクライアント内のカーソルを LoadCursor 関数で、これもまたシステムで定義されている 標準の形状(普通の矢印)のものを取得して hCursor メンバに指定します。

なお GetStockObject 関数の戻り値は HGDIOBJ 型なので、HBRUSH 型にキャストしてください。

問題文を表示する

クイズをするにはまず問題文を表示させなければいけません。 文字を描画するための API として TextOut 関数が用意されています。 引数は次の通りです。

引数意味
HDChdc描画先のデバイスコンテキストハンドル
intnXStart描画を始める x 座標
intnYStart描画を始める y 座標
LPCTSTRlpString描画する文字列
intcbString描画する文字数

最初のデバイスコンテキストハンドルとは何でしょうか? Windows に対して何らかの描画(線や多角形、今回のような文字列など…)をさせたいとき、 GDI(Graphical Device Interface) と呼ばれる Windows の機能を用います。 パソコンの描画を司るグラフィックボードにアクセスすることで画面に様々な図形や文字や画像を出力することができますが、 機種やメーカーによって当然仕様が異なります。GDIはグラフィックドライバとプログラムの中間に位置し、描画機能の抽象化を行うことで、 プログラムから見れば GDI に対し同じ命令を出せば、どのグラフィックボードに対しても同じように処理させることができます。 GDI に対して描画を指示する時に必要になるのがデバイスコンテキストです。 デバイスコンテキストは「どのオブジェクトに対する描画なのか」と 「どの色やフォントを使って描画するか」といった情報をまとめて、抽象化したオブジェクトです。

まずは作ったウィンドウに対するデバイスコンテキストを取得しましょう。これには GetDC 関数を使用します。 ここで得たデバイスコンテキスト(のハンドル)を使えば、ウィンドウに対する描画ができるようになります。 この関数の戻り値は HDC 型なので、TextOut 関数の第 1 引数に渡すことができます。 GetDC 関数の引数にウィンドウのハンドルを指定します。 なお、ここで得たデバイスコンテキストハンドルは必ず開放する必要があります。 これには ReleaseDC 関数を使用します。

ウィンドウ上でマウスの左ボタンが押されるとウィンドウプロシージャに WM_LBUTTONDOWN メッセージが 届くのを利用して、左ボタンをクリックすると文字を描画するようにプログラムしましょう。

LRESULT CALLBACK WndProc( HWND window, UINT message, WPARAM wp, LPARAM lp ) {
    HDC hdc;
    switch( message ) {
    case WM_DESTROY:
        PostQuitMessage( 0 );
        break;
    case WM_LBUTTONDOWN:
        hdc = GetDC( window );
        TextOut( hdc, 10, 10, L"問題文", 3 );
        ReleaseDC( window, hdc );
        break;
    default:
        return DefWindowProc( window, message, wp, lp );
    }
    return 0;
}

マウスを左クリックすると上の図のように文字列が描画されるはずです。

これだけでは問題があります。 マウスを左クリックした後で、ウィンドウを画面の外に出して、また戻してみてください。 せっかく描画された文字が消えてしまっています。 このように GDI は描画したものはいちいち保存してくれません。 ウィンドウ上に描いたものを全部保存すると、メモリの使用量がとんでもないことになってしまうからです。 このため Windows では必要な時に再描画を行うという形式を採用しています。 ここでいう「必要な時」を通知してくれる便利なメッセージがあります。WM_PAINT がそれです。

LRESULT CALLBACK WndProc( HWND window, UINT message, WPARAM wp, LPARAM lp ) {
    PAINTSTRUCT ps;
    HDC hdc;
    switch( message ) {
    case WM_DESTROY:
        PostQuitMessage( 0 );
        break;
    case WM_PAINT:
        hdc = BeginPaint( window, &ps );
        TextOut( hdc, 10, 10, L"問題文", 3 );
        EndPaint( window, &ps );
        break;
    default:
        return DefWindowProc( window, message, wp, lp );
    }
    return 0;
}

WM_PAINT の処理の際は GetDC の変わりに BeginPaint 関数を使用します(理由は後述)。 この関数の戻り値も HDC 型なので、TextOut 関数などの各種 GDI 描画関数に使うことができます。 BeginPaint の対になる関数は EndPaint です。これも必ず呼び出してください。

ウィンドウが画面の外に出る、リサイズされる、他のウィンドウの下に隠れるなどすると、 ウィンドウ上に描画したものは消えてしまいます。そしてまたウィンドウが表に出てきた時、 その消えてしまった部分を再描画しなくてはいけません。この「消えてしまって再描画しなければいけない部分」を 更新リージョンといいます。 ウィンドウに更新リージョンが生じると、Windows はウィンドウに WM_PAINT メッセージを送って、 その部分を再描画するように通知します。

さてここで GetDC を使わずに BeginPaint 関数を使う理由は、 更新リージョンを自動的に削除してくれるということ、それと描画範囲が更新リージョンにクリッピングされることです。

まず前者ですが、ウィンドウに更新リージョンが残っていると Windows はまだ再描画が完了していないと判断し、 さらに WM_PAINT メッセージを送出します。更新リージョンが全て削除されない限り WM_PAINT が送出されるので、 何度も何度も再描画することになり CPU に高い負荷がかかってしまいます。 しかし BeginPaint 関数を使うことで自動的に更新リージョンが削除されます。これにより次に実際に再描画が 必要になる時まで WM_PAINT が送られることはなくなります。 ちなみに更新リージョンの削除は ValidateRect 関数を用いて明示的に行なえます。

描画範囲のクリッピングとは、本当に再描画が必要な範囲だけ再描画されることを意味します。下の図の例では、描画した文字「Hello, world!!」のうち、 Hello, の部分だけ隠れてしまって、ここが更新リージョンになったとします。

この時「本当に再描画が必要」なのはHello, の部分だけであり、world!! の部分は消えていないのでその必要はありません。 消えていない部分を再描画するのは CPU 時間の無駄なので省きたいものです。 BeginPaint 関数を使うとデバイスコンテキストにクリッピングが設定され、 更新リージョン以外の部分は(API を用いて描画しても)クリッピングによって更新されません。

○×ボタンを作る

さて、WM_PAINT を使って文字を消えないように表示することが出来ました。 問題文は TextOut 関数の引数を変えればいいです。文字数の指定が必要なので、 wcsnlen 関数(string.h のインクルードが必要) などを使うと良いでしょう。

次に解答用の○×ボタンを作りましょう。 ○や×を描画してマウスメッセージを処理する、という方法もありますが、 ここでは Windows のボタンコントロールを使用して簡単にやってみます。

コントロールを作成する専用の API があるのかな? と思われるかもしれませんが、 実はこれらのコントロールはウィンドウとして作成されます。 正確には親ウィンドウに属する子ウィンドウですが、 作る為に使うのはやはり CreateWindow 関数です。 とりあえず下のソースコードを見てください。

#include <Windows.h>

#define ID_OBUTTON 1001
#define ID_XBUTTON 1002

LRESULT CALLBACK WndProc( HWND window, UINT message, WPARAM wp, LPARAM lp ) {
    switch( message ) {
    case WM_DESTROY:
        PostQuitMessage( 0 );
        break;
    case WM_COMMAND:
        switch( LOWORD(wp) ) {
        case ID_OBUTTON:
            MessageBox( window, L"正解!", L"", MB_OK );
            break;
        case ID_XBUTTON:
            MessageBox( window, L"不正解...", L"", MB_OK );
            break;
        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc;
            wchar_t *question_text = L"日本の首都は東京である";
            hdc = BeginPaint( window, &ps );
            TextOut( hdc, 10, 10, question_text, 11 );
            EndPaint( window, &ps );
        }
        break;
    default:
        return DefWindowProc( window, message, wp, lp );
    }
    return 0;
}

int WINAPI wWinMain( HINSTANCE instance, HINSTANCE prev_instance, LPWSTR commandline, int cmdshow ) {
    WNDCLASSEX windowclass;
    LPCWSTR classname = L"wc_myqma";
    ZeroMemory( &windowclass, sizeof(windowclass) );
    windowclass.cbSize = sizeof(windowclass);
    windowclass.hInstance = instance;
    windowclass.lpszClassName = classname;
    windowclass.lpfnWndProc = WndProc;
    windowclass.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH );
    windowclass.hCursor = LoadCursor( NULL, IDC_ARROW );
    if( 0 == RegisterClassEx( &windowclass ) ) {
        MessageBox( NULL, L"ウィンドウクラスの登録に失敗", L"myqma", MB_OK );
        return 1;
    }
    HWND mywindow;
    mywindow = CreateWindow( classname,
        L"myqma",
        WS_VISIBLE|WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        600, 450,
        NULL, NULL, instance, NULL );
    CreateWindow( L"BUTTON", L"○",
        WS_CHILD|WS_VISIBLE,
        10, 100, 100, 100,
        mywindow, (HMENU)ID_OBUTTON, instance, NULL );
    CreateWindow( L"BUTTON", L"×",
        WS_CHILD|WS_VISIBLE,
        150, 100, 100, 100,
        mywindow, (HMENU)ID_XBUTTON, instance, NULL );
    MSG message;
    while( GetMessage( &message, NULL, 0, 0 ) ) {
        DispatchMessage( &message );
    }
    DestroyWindow( mywindow );
    return message.wParam;
}

これを実行すると下の図のように表示されます。

ボタンを作る際、CreateWindow のウィンドウクラス名に BUTTON という文字列を指定しています。(クラス名に大文字小文字の区別はありません) これは RegisterClassEx 関数を呼び出すことなく Windows 側で既に定義されているウィンドウクラスで、自由に利用することができます。 他にもテキストボックスにしたい場合は EDIT, リストボックスにしたい場合は LISTBOX などを指定します。

ウィンドウスタイルには必ず WS_CHILD (子ウィンドウの意味)を指定します。これがないと通常のウィンドウとして作成されてしまいます。 また親ウィンドウとして自分で作成した親ウィンドウのハンドル(上の例ではmywindow)を第 8 引数に指定します。

第 9 引数には子ウィンドウの ID を指定します。これはウィンドウハンドルとは異なるものです。 番号のつけかたは自由ですが重複しないようにしてください。 ここで指定したものは WM_COMMAND メッセージを処理する際に使われます。 HMENU 型にキャストする必要がある点も注意。

Windows側で用意されているウィンドウクラスの場合、当然ウィンドウプロシージャは Windows が持っているので、 子ウィンドウに対するメッセージは Windows が処理することになります。 なのでコントロールの描画などは Windows が勝手にやってくれることで、こちらが意識する必要はありません。

こちらがやることは、ボタンが押された時の動作を定義することです。 ボタンコントロールが押されると、その親ウィンドウに WM_COMMAND メッセージが送信されます。 どのボタンが押されたかを判断するには WM_COMMAND メッセージの WPARAM パラメータの下位ワードを調べます。 この中に CreateWindow 関数の第 9 引数で指定した ID が入っています。

ボタンが押されたら正解か不正解を表示しましょう。 ここでは MessageBox 関数を使って簡単に済ませています。 MessageBox 関数の第 1 引数に親ウィンドウのハンドルを指定すると、 メッセージボックスが表示されている間は親ウィンドウの前面に表示されて 親ウィンドウの操作をブロックするようにできます。

さて、○×ゲームはできました。2番目の目標達成です。 でもこれでは問題が 1 つしか出せませんし、問題を変えるのにソースコードを書換えてコンパイルし直さなければならない、というのは アプリケーションとして成立していません。

ファイルから問題文を読み込む

次の目標を設定します: 「ファイルから問題文を読み込んで表示する」

ファイルから読み込めば、ソースコードを書き換える必要はなくなります。

さて、文字を扱う上で注意しなくてはいけないのは文字コードです。 特に日本語を扱う場合は様々な文字コードがあることに注意しなければいけません。 最近は多言語対応のため Unicode が採用されることが多くなっています。 今回のプログラムで Unicode を採用したのもその流行に沿って… というような感じです。 Unicode といってもその符号化形式は UTF-8, UTF-16, UTF-32 ... など色々あります。 Unix 系の OS ではかつては EUC がよく用いられていましたが、最近は UTF-8 化が進んでいます。 Windows は伝統的に日本語のコードとして ShiftJIS を拡張したコードページ 932 というものが用いられていましたが、 Windows 2000 から Unicode の対応が始まり、現在はどちらも使用できるようになっています。 Windows では Unicode の符号化には UTF-16 が使われています。

プログラム内部では UTF-16 を使用しているので読み込むファイルの文字列も UTF-16 でエンコードしておくことにします。 そうすればコードを変換せずにそのままメモリに取り込むことができます。 Unicode のファイルを読み込むには次のように _wfopen_s 関数を使ってファイルを開き、 各種入出力関数を使ってデータを読み込んでいきます。

#include <Windows.h>
#include <cstdio>
#include <cstring>

#define ID_OBUTTON 1001
#define ID_XBUTTON 1002

wchar_t question_text[1000];
int question_len;

LRESULT CALLBACK WndProc( HWND window, UINT message, WPARAM wp, LPARAM lp ) {
    switch( message ) {
    /* ... 省略 ... */
    case WM_PAINT: // 問題文を描画する
        {
            PAINTSTRUCT ps;
            HDC hdc;
            hdc = BeginPaint( window, &ps );
            TextOut( hdc, 10, 10, question_text, question_len );
            EndPaint( window, &ps );
        }
        break;
    default:
        return DefWindowProc( window, message, wp, lp );
    }
    return 0;
}

int WINAPI wWinMain( HINSTANCE instance, HINSTANCE prev_instance, LPWSTR commandline, int cmdshow ) {
    // ファイルから問題文を読み込む
    FILE *fp;
    _wfopen_s( &fp, L"question.txt", L"rt, ccs=UTF-16LE" );
    if( fp == NULL ) {
        MessageBox( NULL, L"問題ファイルを読み込めませんでした", L"", MB_OK );
        return 1;
    }
    fgetws( question_text, sizeof(question_text)/sizeof(question_text[0]), fp );
    question_len = wcsnlen( question_text, sizeof(question_text)/sizeof(question_text[0]) );
    question_text[question_len--] = L'\0';
    fclose( fp );
    /* ... 省略 ... */

_wfopen_s は fopen のマイクロソフトによる拡張です。(wがワイド文字引数版、sがセキュリティ強化版を意味します) モード指定の引数に ccs=UTF-16LE と見慣れないものがついていますが、 Unicode ファイルを読み込む場合はこのように指定しなくてはなりません。 これは Visual C++ 2005 以降でサポートされています。

これを試す前に C++ の std::wifstream を試してみたのですが、 うまくいきませんでした。Unicode のファイルは読み込めないみたいです。 ShiftJIS なファイルを変換しながら読み込むことはできるようなのですが、 それだとアプリケーションに Unicode を採用する意味がありません。

さて次に問題を読み込ませるファイルを用意しましょう。名前は question.txt とでもしておきます。 Visual C++ のプロジェクトファイルと同じディレクトリに置いてください。 メモ帳を使って最初の行に問題文を書いておきます。

ファイルを保存する時に「Unicode」を指定するのを忘れないでください。 (実はここでUTF-8で保存してもプログラムを変更することなく正常に動作します。 fopen_s, _wfopen_s はファイルの先頭の BOM を見るため、どの Unicode エンコーディングなのかを判定することができます。 ccsパラメータとBOMとが食い違っている場合、BOM のエンコードが優先されます。) ANSIを指定すると文字化けを起こします。Unicodeであれば、下の図のように特殊な文字を扱うことができます。

問題文をファイルから読み込んで表示させることは成功しました。3 つ目の目標達成です。 しかし問題文と共にその答えも定義しなければいけません。 問題ファイルの 2 行目に 0 か 1 の数字を指定して、0 ならば「○」が正解、1 ならば「×」が正解という風にします。

LRESULT CALLBACK WndProc( HWND window, UINT message, WPARAM wp, LPARAM lp ) {
    switch( message ) {
    case WM_DESTROY:
        PostQuitMessage( 0 );
        break;
    case WM_COMMAND: // ボタンが押された時の動作
        if( LOWORD(wp)==ID_OBUTTON&&question_answer==0 || LOWORD(wp)==ID_XBUTTON&&question_answer==1 ) {
            MessageBox( window, L"正解!", L"", MB_OK );
        } else if( LOWORD(wp)==ID_OBUTTON&&question_answer==1 || LOWORD(wp)==ID_XBUTTON&&question_answer==0 ) {
            MessageBox( window, L"不正解...", L"", MB_OK );
        }
        break;
    /* ... 省略 ... */
    }
    return 0;
}

int WINAPI wWinMain( HINSTANCE instance, HINSTANCE prev_instance, LPWSTR commandline, int cmdshow ) {
    // ファイルから問題文を読み込む
    FILE *fp;
    wchar_t temporary[1000];
    _wfopen_s( &fp, L"question.txt", L"rt, ccs=UTF-16LE" );
    if( fp == NULL ) {
        MessageBox( NULL, L"問題ファイルを読み込めませんでした", L"", MB_OK );
        return 1;
    }
    fgetws( question_text, sizeof(question_text)/sizeof(question_text[0]), fp );
    question_len = wcsnlen( question_text, sizeof(question_text)/sizeof(question_text[0]) );
    question_text[question_len--] = L'\0';
    fgetws( temporary, sizeof(temporary)/sizeof(temporary[0]), fp );
    fclose( fp );
    if( temporary[0] == L'0' ) {
        question_answer = 0;
    } else if( temporary[0] == L'1' ) {
        question_answer = 1;
    } else {
        MessageBox( NULL, L"答えの定義が不正です", L"", MB_OK );
        return 1;
    }
    /* ... 省略 ... */

良い感じです。

ここまでのソースファイルをまとめておきます。

src001.zip (3,939 バイト)

作成日 2011/02/22 ... 最終更新日 2011/02/26
by zeroichi

QMAは株式会社コナミデジタルエンタテインメントの登録商標です