Windows API/イベントドリブン方式

イベントドリブン方式

編集

Win32APIのプログラムの実行順序は、ユーザーの操作によって変わります。

例えば、キーボード操作をされたときには、 case WM_KEYDOWN: のブロックが実行されます。(なお、初期設定のテンプレートには case WM_KEYDOWN: のブロックは無いので、自分で書く必要がある。)

キーボード操作など、なんらかのイベントがされた時にだけプログラムの特定ブロックが実行されるので、イベントドリブン方式といいます。

Win32APIのプログラムの実行方式はイベントドリブン方式です。

なお、(グローバル変数の宣言などの例外を除くと、)プログラム中のなんらかの処理は、ほとんどが、なんらかのイベントと関連づけられます。


中枢の関数は無い

編集

もし、「操作に関わらず常に何度も実行させたい処理を書きたい」と思っても、そういう処理は書けません。仮にアナタが「C言語のコンソールアプリのmain関数のように、特定の関数に依存しない処理を書こう」と思っても、Win32APIで書くプログラムには、そういう中枢の関数は無いのです。

wWinMain 関数のブロックは、単に、Win32APIアプリの開始時点をあらわすブロックにすぎず、アプリ起動時に1回だけ実行されるプログラムにすぎません。

また、一般に、けっして、なんらかの( wWinMain 以外の)他の関数の実行が終わったからといって、wWinMain 関数から呼び出した関数の終了直後でないかぎり、wWinMain 関数には戻りません。

Win32APIでは、キーボード操作とは無関係にアプリ起動時に1回だけ実行する処理というのは書けますが、それだって、「アプリ起動」というイベントによって駆動されたプログラムです。

なので、もし、wWinMain 関数のブロック内で変数を宣言しても、その変数は通常、他のイベントからは呼び出せないです。たとえば描画イベントやキーボード入力受付イベントなどからは、一切、wWinMain 関数のブロック内で宣言された変数は呼び出せないです。

このように、wWinMain 関数のブロックは、単に、アプリケーション開始というイベントを表すブロックに過ぎないのです。(なお、グローバル変数の宣言の場所は、別の場所にある。くわしくは後述。)

とにかく、Win32APIにおいて、原則的にプログラム内の個別の処理は、かならず、イベントと関連づけられます。(Win32APIでも、グローバル変数や、includeなど、冒頭文だけ、イベントと無関係に宣言できる。しかし、プログラムの具体的に動作は、必ず、発生条件のイベントと関連づけられる。)

グローバル変数の宣言の場所

編集

テンプレートを自動作成すると、

冒頭の数行目あたりに

// グローバル変数:
HINSTANCE hInst;                                // 現在のインターフェイス
WCHAR szTitle[MAX_LOADSTRING];                  // タイトル バーのテキスト
WCHAR szWindowClass[MAX_LOADSTRING];            // メイン ウィンドウ クラス名

とありますので、グローバル変数を宣言して使いたい場合、この下に入力します。


たとえば、整数型の変数 kazu を宣言したい場合、次のようになります。

// グローバル変数:
HINSTANCE hInst;                                // 現在のインターフェイス
WCHAR szTitle[MAX_LOADSTRING];                  // タイトル バーのテキスト
WCHAR szWindowClass[MAX_LOADSTRING];            // メイン ウィンドウ クラス名

int kazu = 5;


なお、wWinMain 関数で変数を宣言しても、その変数は他のイベントからは呼び出せません。

wWinMain 関数には、

// TODO: ここにコードを挿入してください。

とテンプレート文で書かれている場所がありますので、

てっきりここに入力しがちですが、

しかしグローバル変数の入力場所はwWinMain 関数とは違います。


イベント定義と、イベント発生後の処理は、通常、一緒に書かれる

編集

キーボード操作イベントにかぎらず、基本的にwin32APIプログラムでは、たいてい、あるイベントの詳細の定義と、そのイベント発生後に行う処理を、一緒に書く場合になることが、ほとんどです。

たとえば、キーボード操作イベントの処理は、どのキーが押されたかというイベントと、キーが押された後に行う処理とを、一緒のブロックに書きます。


C言語のコンソールアプリの感覚だと、ついつい、イベントの詳細の定義と、イベント発生後に行う処理とを、べつべつのブロックに書こうと考えがちですが、しかしWin32APIでは、そういう書き方は通常は不可能です。


もし中枢的なブロックが必要な場合、何かのイベントのブロックを流用する

編集

もし、どうしても、プログラム全体を管理したいブロックが欲しい場合、アプリ起動中に何度も発生するイベントの処理を書くブロックを流用します。

キーボード操作イベント(WM_KETDOWN)や、時間経過イベント(WM_TIMER)など、何度でも発生できるイベントのブロックを流用して、そのブロック内に、目的の、プログラム管理的な処理を書くことになります。


このため、一見すると、そのイベントとは関係なさそうな、管理的な処理も、何らかのイベント発生後処理のブロックに書かれることも、あります。


このため、他人の書いたプログラムを読む際に、一見するとキーボード操作とは関係なさそうな処理でも、キーボード操作のブロックに処理が書かれている場合があります。

また、上記のような都合のため、書かれたプログラムではキーボード操作のブロックは長くなりがちなので、プログラムの全体像が分かりづらくなるので、もし複数人で共同開発する際には、あらかじめ仕様書などを作っておき、その仕様書でシステムの概要を説明しておき、全体像を把握しやすくすべきでしょう。


無限ループは書かない

編集

なお、Win32APIプログラミングでは、いわゆる「無限ループ」をプログラマーが新規に書くことは、通常、ありません。もし、無理やり書いても、動作が遅くなったり、バグの原因になります。

Visual Studio での Win32APIのテンプレート作成時に、アプリを勝手に終了させないための無限ループのようなものが、すでに書かれているので、通常は、そのテンプレートを流用します。


タイマー機能

編集

なお、タイマー機能のある  case WM_TIMER: のブロックを追加したり SetTimer というコマンドを使い、「一定の秒数が経過したら○○を実行する」というタイマーのイベントを宣言することにより、キーボード操作とは無関係に自動的な処理を書くこともできます。しかし、これだって「○○秒の経過」というイベントによって駆動されるプログラムにすぎません。

そして、このタイマーのブロックにも、けっして「何秒、経過したか」という時間経過のプログラムだけでなく、さらに時間経過イベント後に必要になる処理も一緒に書くことになります。


イベントドリブン方式をやめたい場合

編集

伝聞では、PeekMessage() 関数を使うと、イベントドリブン方式はやめられると言われており、(かつてのMS-DOSやBASICのような)手続き型のような処理になると言われています。

PeekMessage() 関数とは、マイクロソフト社の仕様用上では、イベントからのメッセージを待たずに、制御を実行するという関数です。なので理屈上は、PeekMessage() 関数でイベントドリブン方式をやめることができるだろう、と考えられています。

ですが、実際の実装があまりよくなく、PeekMessage() 関数を用いたアプリケーションがしばしば動作停止をするので、あまりイベントドリブン方式をやめる事を試みるのは、すすめられません。

どうも、Visual C++ でイベント・ドリブン方式をやめる事は、マイクロソフト的にはサポートされていないようです。

どうしてもPeekMessage() 関数を使う場合、使い方は下記の通りです。

手法

編集

WindowsAPIの起動時にVisual Studio の自動作成するコードをよく見ると、

	MSG msg;

	// メイン メッセージ ループ:
	while (GetMessage(&msg, nullptr, 0, 0))
	{
		if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;

というような感じのコードが、最初から作成されています(Visual Studioのバージョンによって多少の差異の可能性あり)。

コード中のGetMessage() 関数を除去し、おおむね次のコードのようにPeekMessage() 関数に置き換えれば

	MSG msg;

	// メイン メッセージ ループ:
	while (TRUE) // whileの論理式が変わっている
	{
		if ( PeekMessage(&msg,NULL,0,0,PM_REMOVE) ) // if文の条件式が変わっている
		{
            // このif文↓が無いとウィンドウを閉じてもアプリが続いてしまう。
        	if (msg.message == WM_QUIT) {
			PostQuitMessage(0);
			break;
            }
        
            // このブロックはそのままでいい
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
        else // elseが追加されている
        {
          // 描画処理など
          // InvalidateRect(hWnd , NULL , FALSE); // 描画の実行
        }        
	}
    
	return (int)msg.wParam;


( たとえばPeekMessage(&msg,NULL,0,0,PM_REMOVE)など )、イベントドリブン方式をやめて、メッセージの到着を待たずに処理をしていく、従来的な手続き型になります。

一方、GetMessage() 関数のままの場合なら、メッセージの到着を待って処理をしていく方式になるので、「イベントドリブン方式でループをしていく」と宣言していることになります。


なお、実際には描画処理などのため、さらに CreateWindowW() 関数などによる hWnd の初期化が必要です、

    // これ↓が加わっている。
	HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME,
		CW_USEDEFAULT, 0, 640, 480, nullptr, nullptr, hInstance, nullptr);

	MSG msg;

	// メイン メッセージ ループ:
	while (TRUE) // whileの論理式が変わっている
	{
		if ( PeekMessage(&msg,NULL,0,0,PM_REMOVE) ) 
		{
            
        	if (msg.message == WM_QUIT) {
			PostQuitMessage(0);
			break;
            }
        
            // このブロックはそのままでいい
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
        else 
        {
          // 描画処理など
          InvalidateRect(hWnd , NULL , FALSE); // 描画の実行
        }        
	}
        
	return (int)msg.wParam;


エラー例

なお、ネットには下記のように while側で WM_QUIT の有無を判断する類のコードがありますが、しかし2020年現代のVisual Studio2019では、これはコンパイルが通らないです。(「C4700 初期化されていないローカル変数 msg が使用されます」というエラーが出ます。)

// エラーになります。(×)
	HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME,
		CW_USEDEFAULT, 0, 640, 480, nullptr, nullptr, hInstance, nullptr);

	MSG msg;

	// メイン メッセージ ループ:
	while (msg.message != WM_QUIT) // WM_QUIT の有無をwhileで判断
	{
		if ( PeekMessage(&msg,NULL,0,0,PM_REMOVE) ) 
		{
        
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
        else
        {
          // 描画処理など
          // InvalidateRect(hWnd , NULL , FALSE); // 描画の実行
        }        
	}
    
	return (int)msg.wParam;


対処法

先に PeekMessage を実行してからでないと、msgの値が取得されないからか、上記のコードはエラーになるようです。

なので、while文ではなくdo~while文によって、先に PeekMessage を実行すれば、エラーは無くなり、コンパイルできるようになります。

// do~while文 なので、コンパイル可能(〇)
	HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME,
		CW_USEDEFAULT, 0, 640, 480, nullptr, nullptr, hInstance, nullptr);

	MSG msg;

	// メイン メッセージ ループ:
	do {
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{

			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
		{
			// 描画処理など
			// InvalidateRect(hWnd , NULL , FALSE);
		}
	} while (msg.message != WM_QUIT); // 末尾のセミコロンを忘れないように。
    
    return (int)msg.wParam;


注意

上述のこれらのコードは、実際にelseブロック内に図形の描画を描くなどの処理をすると、コンパイルは可能ですが、フリーズを多発します。

また、動作の開始直後は動きますが、しばらく秒数が経つと、動作停止をすることがあります。