タイマー


そして時は動き出す

前回、デッドタイムを使った処理方法を説明しました
これで、無限ループのような処理も可能になります

しかし、これだけではアニメーションなどを作成することはできません
アニメーションを実現するには、CPUの速度に依存することのないよう
ある程度の時間を待機させるような処理が必要なのです

実際に、500 Mhz 以上の速度を持つインテルのCPUならば
前回の方法で、塗りつぶされた矩形が黒から赤色にフェードするプログラムを実行すると
一瞬で作業が終わってしまい、何が起こったのかわからないでしょう

そこで、PeekMessage() とは違った方法でこれを実現させなければなりません
このような動作はタイマーによる、時間差処理が好ましいと思います

タイマーとは、ある一定の指定した時間が経過すると
メッセージキューにメッセージをポストするという便利な機能です
これらの作業は一見非同期のように思われますが、タイマは入出力装置です

つまり、処理方法はキーボードやマウスと同じ方法でできるのです
指定時間がたつとタイマーはキューにメッセージをポストしますが、それは割込みではありません
その前に他のメッセージが存在していれば、当然そのメッセージが先行して処理されます

一般に、タイマーを使うには SetTimer() ファンクションを使います
UINT SetTimer(
	HWND hWnd , UINT nIDEvent ,
	UINT uElapse ,
	TIMERPROC lpTimerFunc
);
hWnd には関連付けるウィンドウのハンドルを指定します
この値を NULL にするとウィンドウには関連付けられないタイマーが作成されます

nIDEvent は 0 以外の値を指定し、タイマーのIDを指定します
このIDは複数のタイマーを同時に扱う時に重要になります
hWnd が NULL の場合、このパラメータは無視されます

uElapse は、時間をミリ秒単位(1/1000秒)で指定します
この時間が経過するたびに、メッセージを生成します

lpTimerFunc はコールバック関数のポインタを指定します
これは、メッセージキューにメッセージをポストするのではなく
指定時間が経過するごとに、指定関数を実行させるという処理に使います
メッセージキューにポストさせたい場合は、この値を NULL に指定します

戻り値は関数が成功すると、新しいタイマIDが返され、失敗すると 0 が返ります

タイマーは、指定した時間が経過すると WM_TIMER メッセージを発行します
lpTimerFunc が NULL の場合は、メッセージキューにポストされます
//タイマーのコールバック関数の利用法は後記

WM_TIMER は WPARAM にタイマIDを
LPARAM には、コールバック関数へのアドレスが格納されています
lpTimerFunc に NULL を指定している場合は 0 になります
このメッセージを処理した場合は 0 を返します
#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;	
	RECT rctDimension;

	static BOOL blRight = TRUE;
	static int x = 0;

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		SetTimer(hwnd , 1 , 1 , NULL);
		return 0;
	case WM_TIMER:
		GetClientRect(hwnd , &rctDimension);
		if (x + 100 >= rctDimension.right) blRight = FALSE;
		else if (x <= 0) blRight = TRUE;

		if (blRight) x++;
		else x--;

		InvalidateRect(hwnd , NULL , TRUE);
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hwnd , &ps);
		SelectObject(hdc , GetStockObject(BLACK_BRUSH));
		Ellipse(hdc , x , 50 , x + 100 , 150);
		EndPaint(hwnd , &ps);
		return 0;
	}
	return DefWindowProc(hwnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow ) {
	HWND hwnd;
	MSG msg;
	WNDCLASS winc;

	winc.style		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= WndProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return -1;

	hwnd = CreateWindow(
			TEXT("KITTY") , TEXT("Kitty on your lap") ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hwnd == NULL) return -1;

	while(GetMessage(&msg , NULL , 0 , 0)) DispatchMessage(&msg);
	return msg.wParam;
}
このプログラムを実行すると、黒く塗りつぶされた楕円が左右に移動しつづけます
クライアントエリアの右端にたどり着くと、左に移動し
左端につくと、再び右にインクリメントしはじめ、この動作を繰り返します

重要なことがいくつかあります
まず、SetTimer() 関数で1ミリ秒単位でメッセージを発行するようにしていますが
実際に1秒間に1000回 WM_TIMER が実行されるわけではありません

タイマーには 分解能 という限界周期が定められており
この分解能以上の周期でメッセージを発行することはできないのです
この周期は 9x と NT で異なり、 9x の場合は55ミリ秒、NT は10ミリ秒です
つまり、9x は1秒間に最大で18回、 NT は100回までしか WM_TIMER を発生させられないのです

さらに、タイマーは 1000 ミリ秒に設定したからといって
必ず1秒後に WM_TIMER が実行されるとは限りません
WM_TIMER が発生した時、他のメッセージが処理中であれば
その間 WM_TIMER はキューの中で実行を待つことになります

また、コールバック関数を使って指定関数を呼び出すことも可能です
この場合は、指定時間が経過するとWindowsが関数を呼び出します

SetTimer() 関数の第四引数には TIMERPROC 型の
コールバック関数のポインタを指定することで次の型になっています
VOID CALLBACK TimerProc(
	HWND hwnd , UINT uMsg ,
	UINT idEvent , DWORD dwTime
);
外部から呼び出される関数のため CALLBACK を宣言します
hwnd には関連付けられているウィンドウのハンドル
uMsg にはメッセージが入りますが、この関数が処理するメッセージは
常に WM_TIMER メッセージなので、このメッセージが入っています

idEvent には、タイマーの ID が
dwTime は Windows が起動してからの経過時間がミリ秒単位で入っています
#include <windows.h>

VOID CALLBACK TimerProc(HWND hwnd , UINT uMsg ,UINT idEvent , DWORD dwTime) {
	HDC hdc;
	static int iCount = 0;
	TCHAR strCount[64];

	iCount++;

	hdc = GetDC(hwnd);
	wsprintf(strCount , "%d" , iCount);
	TextOut(hdc , 10 , 10 , strCount , lstrlen(strCount));
	ReleaseDC(hwnd , hdc);
}

LRESULT CALLBACK WndProc(HWND hwnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		SetTimer(hwnd , 1 , 100 , TimerProc);
		return 0;
	}
	return DefWindowProc(hwnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow ) {
	HWND hwnd;
	MSG msg;
	WNDCLASS winc;

	winc.style		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= WndProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return -1;

	hwnd = CreateWindow(
			TEXT("KITTY") , TEXT("Kitty on your lap") ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hwnd == NULL) return -1;

	while(GetMessage(&msg , NULL , 0 , 0)) DispatchMessage(&msg);
	return msg.wParam;
}
タイマーがメッセージを発行するごとにカウントをインクリメントし
カウントの値を描画するプログラムです

SetTimer() 関数の第四引数に、コールバック関数のポインタを指定しています
WM_TIMER が発行されると、Windows はこのポインタを呼び出します


タイマーをKill

SetTimer() によってタイマーを発生させると
指定時間ごとに WM_TIMER が発生しファンクションを実行します

しかし、プログラムが起動してから終了するまで必要となるのは
時計などの一部のアプリケーションだけであり
通常は数サイクル実行されると、タイマーが不要になったりするでしょう

タイマーが不要になった場合は KillTimer() 関数を使います
この関数を使うことによって、タイマーは停止します

BOOL KillTimer(HWND hWnd , UINT uIDEvent);

hWnd には、タイマーに関連付けられているウィンドウのハンドルを指定します
これは SetTimer() の第一引数で設定した値と同じでなければなりません
uIDEvent には、タイマーの ID を指定します

uIDEvent は、SetTimer() の第二引数で設定した値と同じでなければいけませんが
ウィンドウのハンドルを NULL に設定していた場合はIDがありません
この時は SetTimer() の戻り値を指定 しなければいけません
#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;
	static int iCount = 0;
	static TCHAR strCount[64];

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		SetTimer(hwnd , 1 , 100 , NULL);
		return 0;
	case WM_TIMER:
		iCount++;
		if(iCount == 50) KillTimer(hwnd , 1);
		InvalidateRect(hwnd , NULL , TRUE);
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hwnd , &ps);
		wsprintf(strCount , "%d" , iCount);
		TextOut(hdc , 10 , 10 , strCount , lstrlen(strCount));
		EndPaint(hwnd , &ps);
	}
	return DefWindowProc(hwnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow ) {
	HWND hwnd;
	MSG msg;
	WNDCLASS winc;

	winc.style		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= WndProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return -1;

	hwnd = CreateWindow(
			TEXT("KITTY") , TEXT("Kitty on your lap") ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hwnd == NULL) return -1;

	while(GetMessage(&msg , NULL , 0 , 0)) DispatchMessage(&msg);
	return msg.wParam;
}
このプログラムは、タイマーが WM_TIMER を発行するごとにカウントをインクリメントし
それをウィンドウに描画する動作を繰り返すプログラムですが
カウントが50に達すると KillTimer() によってタイマーを破棄します


複数のタイマー実行

これまでは、単一のタイマーを作成して実行していました
しかし、タイマーは複数作成することもできます

この場合、発行されたメッセージは WPARAM パラメータに
タイマーのIDが入力されているため、これで判断します
または、コールバック関数をそれぞれに用意して実行することもできます

複数のタイマーを使う場合、一般的には ID を定数にして
switch などを用いてメッセージと同様に処理すると見やすくなります
#include <windows.h>

#define TM_COUNT1 1
#define TM_COUNT2 2

LRESULT CALLBACK WndProc(HWND hwnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;
	RECT rctSize;
	static int iCount1 = 0 , iCount2 = 0;
	static TCHAR strCount[64];

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		SetTimer(hwnd , TM_COUNT1 , 100 , NULL);
		SetTimer(hwnd , TM_COUNT2 , 500 , NULL);
		return 0;
	case WM_TIMER:
		switch(wp) {
		case TM_COUNT1:
			iCount1++;
			break;
		case TM_COUNT2:
			iCount2++;
			break;
		}
		InvalidateRect(hwnd , NULL , TRUE);
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hwnd , &ps);

		wsprintf(strCount , "TM_COUNT1 = %d\nTM_COUNT2 = %d" ,
			 iCount1 , iCount2
		);
		GetClientRect(hwnd , &rctSize);
		DrawText(hdc , strCount , -1 , &rctSize , DT_LEFT);
		EndPaint(hwnd , &ps);
	}
	return DefWindowProc(hwnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow ) {
	HWND hwnd;
	MSG msg;
	WNDCLASS winc;

	winc.style		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= WndProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return -1;

	hwnd = CreateWindow(
			TEXT("KITTY") , TEXT("Kitty on your lap") ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hwnd == NULL) return -1;

	while(GetMessage(&msg , NULL , 0 , 0)) DispatchMessage(&msg);
	return msg.wParam;
}


異なるスピードでカウントをインクリメントしていくプログラムです
TM_COUNT1 は 100 ミリ秒ごとにインクリメントが実行され
TM_COUNT2 は 500 ミリ秒ごとにインクリメントされます


SetTimer()

UINT SetTimer(
	HWND hWnd , UINT nIDEvent ,
	UINT uElapse ,
	TIMERPROC lpTimerFunc
);
タイマーを指定したタイムアウト値で作成します

hWnd - 関連付けるウィンドウのハンドル。関連付けない場合は NULL を指定します
nIDEvent - 0 以外でタイマーの ID を指定します。hWnd が NULL ならば無視します
uElapse - タイムアウト値をミリ秒単位で指定します
lpTimerFunc - 時間が経過すると呼び出す TIMERPROC 型関数へのポインタ

戻り値 - 新しく設定した ID

KillTimer()

BOOL KillTimer(HWND hWnd , UINT uIDEvent);

指定したタイマーを破棄します

hWnd - タイマーに関連付けられているウィンドウへのハンドル
uIDEvent - タイマーのID

戻り値 - 成功すると 0 以外。失敗すると 0

TimerProc

VOID CALLBACK TimerProc(
	HWND hwnd , UINT uMsg ,
	UINT idEvent , DWORD dwTime
);
アプリケーション定義のコールバック関数です
WM_TIMER を処理します

hwnd - タイマーに関連付けられているウィンドウへのハンドルが入ります
uMsg - WM_TIMER が入ります
idEvent - タイマーのIDが入ります
dwTime - Windows が起動してからの経過時間がミリ秒単位で入ります




前のページへ戻る次のページへ