●Win32API(C言語)編 第6章 クリックやダブルクリックを検出する

'2007/4/7 VisualC++2005 の警告対策を追加。
'2007/2/6 GetMessage() が -1 を返す可能性に対応。
'2006/12/18 _Tマクロ、TCHAR型、_stprintf() を使うように修正。

○左クリックの検出

C言語編で紹介してきたコンソールアプリケーションとは異なり、Windows アプリケーションでは、マウスからの入力を受け付けることも考えられます。昔は、マウスを使うことが標準的 なことではなかったのですが、現在ではマウスを使うことは常識化しています。Windowsのアプリケーションにお いて、マウスが使えないものはほとんどありません。

さて、それでは実際にマウスからの入力を検出する方法を見ていきます。単純にマウスからの入力といっても それには様々な種類があることが分かるでしょうか? クリック、ダブルクリック、移動(マウスカーソルの操作)、 ドラッグ&ドロップ、ホイールの操作といった具合です。クリックのような操作は、マウスに存在しているボタン の個数によって、更に分けられる(左クリック、右クリックなど)し、ボタンが押された瞬間と、離された瞬間と で処理を変えることもあります。

まずは簡単なところで、単純な左クリックの検出方法を説明します。具体的なプログラムは次のようになります。

#include <windows.h>
#include <tchar.h>

// 定数
#define WINDOW_WIDTH  (400)		// ウィンドウの幅
#define WINDOW_HEIGHT (300)		// ウィンドウの高さ

// プロトタイプ宣言
HWND Create(HINSTANCE hInst);
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);


// 開始位置
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR pCmdLine, int showCmd)
{
	HWND hWnd;
	MSG msg;

	// ウィンドウを作成する
	hWnd = Create( hInst );
	if( hWnd == NULL )
	{
		MessageBox( NULL, _T("ウィンドウの作成に失敗しました"), _T("エラー"), MB_OK );
		return 1;
	}

	// ウィンドウを表示する
	ShowWindow( hWnd, SW_SHOW );
	UpdateWindow( hWnd );

	// メッセージループ
	while( 1 )
	{
		BOOL ret = GetMessage( &msg, NULL, 0, 0 );  // メッセージを取得する
		if( ret == 0 || ret == -1 )
		{
			// アプリケーションを終了させるメッセージが来ていたら、
			// あるいは GetMessage() が失敗したら( -1 が返されたら )、ループを抜ける
			break;
		}
		else
		{
			// メッセージを処理する
			TranslateMessage( &msg );
			DispatchMessage( &msg );
		}
	}

	return 0;
}

// ウィンドウを作成する
HWND Create(HINSTANCE hInst)
{
	WNDCLASSEX wc;
	int x, y;

	// ウィンドウクラスの情報を設定
	wc.cbSize = sizeof(wc);               // 構造体サイズ
	wc.style = CS_HREDRAW | CS_VREDRAW;   // スタイル
	wc.lpfnWndProc = WndProc;             // ウィンドウプロシージャ
	wc.cbClsExtra = 0;                    // 拡張情報1
	wc.cbWndExtra = 0;                    // 拡張情報2
	wc.hInstance = hInst;                 // インスタンスハンドル
	wc.hIcon = (HICON)LoadImage(          // アイコン
		NULL, MAKEINTRESOURCE(IDI_APPLICATION), IMAGE_ICON,
		0, 0, LR_DEFAULTSIZE | LR_SHARED
	);
	wc.hIconSm = wc.hIcon;                // 子アイコン
	wc.hCursor = (HCURSOR)LoadImage(      // マウスカーソル
		NULL, MAKEINTRESOURCE(IDC_ARROW), IMAGE_CURSOR,
		0, 0, LR_DEFAULTSIZE | LR_SHARED
	);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); // ウィンドウ背景
	wc.lpszMenuName = NULL;                     // メニュー名
	wc.lpszClassName = _T("Default Class Name");// ウィンドウクラス名
	
	// ウィンドウクラスを登録する
	if( RegisterClassEx( &wc ) == 0 ){ return NULL; }

	// ウィンドウの作成位置を計算する
	x = ( GetSystemMetrics( SM_CXSCREEN ) - WINDOW_WIDTH ) / 2;
	y = ( GetSystemMetrics( SM_CYSCREEN ) - WINDOW_HEIGHT ) / 2;

	// ウィンドウを作成する
	return CreateWindow(
		wc.lpszClassName,      // ウィンドウクラス名
		_T("Sample Program"),  // タイトルバーに表示する文字列
		WS_OVERLAPPEDWINDOW,   // ウィンドウの種類
		x,                     // ウィンドウを表示する位置(X座標)
		y,                     // ウィンドウを表示する位置(Y座標)
		WINDOW_WIDTH,          // ウィンドウの幅
		WINDOW_HEIGHT,         // ウィンドウの高さ
		NULL,                  // 親ウィンドウのウィンドウハンドル
		NULL,                  // メニューハンドル
		hInst,                 // インスタンスハンドル
		NULL                   // その他の作成データ
	);
}

// ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
	switch( msg )
	{
	case WM_LBUTTONDOWN:	// マウスの左ボタンが押されたときに送られてくる
		MessageBox( hWnd, _T("左ボタンが押されました"), _T("メッセージ"), MB_OK );
		return 0;

	case WM_DESTROY:  // ウィンドウが破棄されるときに送られてくる
		PostQuitMessage( 0 );
		return 0;
	}

	return DefWindowProc( hWnd, msg, wp, lp );
}

ほとんどいつものプログラムです。変更箇所は、ウィンドウプロシージャの中だけです。このプログラムを実行 して、表示されるウィンドウの中で、左クリックすると、メッセージボックスが表示されて、入力が検出されたこと が分かります。

ウィンドウプロシージャの中で処理しているWM_LBUTTONDOWNメッセージは、 マウスの左ボタンが押されたときに送られてくるメッセージです。このメッセージをDefWindowProc()に 渡さなければよいだけです。実に簡単ですね。

アプリケーションを使うだけの一般ユーザにとっては、「クリック」という一言で表現されてしまうマウス入力 も、実際にプログラミングするときには、「ボタンが押された」「ボタンが離された」という2つの段階があること を考慮しなければなりません。例えば、Windowsのウィンドウの右上にある×ボタンをクリックするとウィンドウが 閉じられますが、これは「ボタンが離された」ときに反応しています。実際に試してみると分かります。

マウスの左ボタンが離されたときには、WM_LBUTTONUPメッセージが送られて きます。これをウィンドウプロシージャで処理すれば、好きなように処理できます。

○左ボタン以外のクリック検出

当然ですが、左ボタン以外の入力も検出できます。左ボタンのときと同様に、「押されたとき」と「離されたとき」 とで別々のメッセージが送られてきます。用意されているメッセージは、「左ボタン」「右ボタン」「中央ボタン」 の3つのボタンに関するメッセージです。

メッセージ対応するボタンタイミング
WM_LBUTTONDOWN左ボタン押されたとき
WM_LBUTTONUP左ボタン離されたとき
WM_RBUTTONDOWN右ボタン押されたとき
WM_RBUTTONUP右ボタン離されたとき
WM_MBUTTONDOWN中央ボタン押されたとき
WM_MBUTTONUP中央ボタン離されたとき

メッセージの名前から用途は明らかだとは思います。「WM_M〜」の「M」は「Middle(中央)」のことです。この 中央ボタンですが、マウスの種類によっては存在しないことがあるということを忘れないで下さい。最近のマウスなら、 ほぼ間違いなく中央ボタン(ホイールになっているものが多いが、ホイールを押せば中央ボタンになる)を持っていま すが、数年前のものは2つしかボタンがないのが普通です。古いマウスにも対応させたいアプリケーションでは、 中央ボタンを使うことを強制してはいけません(ホイールでの操作も同様です)。

○ダブルクリックの検出

次にダブルクリックの検出方法を説明します。初めてダブルクリックの処理を書くときは、必ず失敗すると 思います。

ダブルクリックを検出するには、やはり送られてくるメッセージを受け取ればいいのですが、そのメッセージは、 ウィンドウを作るときに、ウィンドウクラスの登録を一部、書き換えないと送られてきません。Create()の中の ウィンドウクラスを登録する部分を抜粋します。

// ウィンドウクラスの情報を設定
wc.cbSize = sizeof(wc);               // 構造体サイズ
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;   // スタイル
wc.lpfnWndProc = WndProc;             // ウィンドウプロシージャ
wc.cbClsExtra = 0;                    // 拡張情報1
wc.cbWndExtra = 0;                    // 拡張情報2
wc.hInstance = hInst;                 // インスタンスハンドル
wc.hIcon = (HICON)LoadImage(          // アイコン
	NULL, MAKEINTRESOURCE(IDI_APPLICATION), IMAGE_ICON,
	0, 0, LR_DEFAULTSIZE | LR_SHARED
);
wc.hIconSm = wc.hIcon;                // 子アイコン
wc.hCursor = (HCURSOR)LoadImage(      // マウスカーソル
	NULL, MAKEINTRESOURCE(IDC_ARROW), IMAGE_CURSOR,
	0, 0, LR_DEFAULTSIZE | LR_SHARED
);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); // ウィンドウ背景
wc.lpszMenuName = NULL;                     // メニュー名
wc.lpszClassName = _T("Default Class Name");// ウィンドウクラス名

// ウィンドウクラスを登録する
if( RegisterClassEx( &wc ) == 0 ){ return NULL; }

書き換える、というか追加する部分は、WNDCLASSEX構造体のstyleメンバです。ここに、 CS_DBLCLKSを追加して下さい。これを追加することにより、ダブルクリックの メッセージが送られてくるようになります。

このような仕様になっているのは、ダブルクリックと、2連続の単なるクリックとを区別するためです。CS_DBLCLKS がない場合、ダブルクリックすると、次のようにメッセージが送られてきます。

WM_LBUTTONDOWN → WM_LBUTTONUP → WM_LBUTTONDOWN → WM_LBUTTONUP

つまり、単なるクリック×2です。一方、CS_DBLCLKSがある場合には、

WM_LBUTTONDOWN → WM_LBUTTONUP → WM_LBUTTONDBLCLK → WM_LBUTTONUP

になります。3つ目のWM_LBUTTONDBLCLKが、ダブルクリックのメッセージです。 これを見て分かるように、ダブルクリックするときには、必ず4つのメッセージが送られてきます。ここが、ダブルクリック を正しく処理するときに障害となります。つまり、クリックとダブルクリックとを個別に処理したい場合には、 WM_LBUTTONDOWNやWM_LBUTTONUPの扱いが問題になるのです。ダブルクリックの過程では、通常のクリックのときの処理 を実行してはいけない訳です。

この辺りの処理を正しく行う方法は少し難しいです。具体的にはタイマーを使って処理すればいいのですが、 ここでは触れないことにします。すぐに知りたければ、例えば ここなどを見てください。タイマー の話をするときになったら、改めて触れることにしましょう。

ちなみに、ほとんどのアプリケーションでは、ダブルクリックといったら左ボタンを使うことになりますが、 メッセージ自体は、右ボタンや中央ボタンのものも用意されています。

// ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
	switch( msg )
	{
	case WM_LBUTTONDBLCLK:	// 左ダブルクリックをしたときに送られてくる
		MessageBox( hWnd, _T("左ダブルクリックを検出しました"), _T("メッセージ"), MB_OK );
		return 0;
	case WM_RBUTTONDBLCLK:	// 右ダブルクリックをしたときに送られてくる
		MessageBox( hWnd, _T("右ダブルクリックを検出しました"), _T("メッセージ"), MB_OK );
		return 0;
	case WM_MBUTTONDBLCLK:	// 中央ダブルクリックをしたときに送られてくる
		MessageBox( hWnd, _T("中央ダブルクリックを検出しました"), _T("メッセージ"), MB_OK );
		return 0;

	case WM_DESTROY:  // ウィンドウが破棄されるときに送られてくる
		PostQuitMessage( 0 );
		return 0;
	}

	return DefWindowProc( hWnd, msg, wp, lp );
}

○クリックされた位置を知る方法

クリックやダブルクリックという操作は、「どこをクリックしたか」という情報も必要になる場合がほとんどでしょう。 クリックされた位置を知るには、メッセージの付属情報(WPARAM、LPARAM)を調べます。ここではLPARAMの中に目的の 情報が含まれています。

WM_LBUTTONDOWNやWM_LBUTTONUPなどの「押された」「離された」「ダブルクリックされた」という各種メッセージの LPARAMは、どれも同一の内容が含まれています。LPARAMは32ビットの整数ということでしたが、この下位16ビットの部分 にX座標、上位16ビットの部分にY座標が含まれています。

ここでいう座標は、クライアント座標と呼ばれるものです。クライアント座標は、 ウィンドウ内の描画部分(現在までの例では、白い背景になっている部分。この部分をクライアント領域 と呼びます)の左上隅を(0,0)とする座標系です。

32ビット整数の上位と下位とに分かれて情報が含まれていることがよくあるので、便利なマクロが既に用意されています。 上位16ビットだけを取り出すにはHIWORDマクロ、下位16ビットだけを取り出すには LOWORDマクロを使います。いずれも引数に32ビット整数を1つ渡します。

では、実際にウィンドウプロシージャの中を書き換えて試してみます。

#define _CRT_NON_CONFORMING_SWPRINTFS 1 // VisualC++2005 での警告抑制
#define _CRT_SECURE_NO_DEPRECATE 1      // VisualC++2005 での警告抑制
#include <windows.h>
#include <tchar.h>

// 定数
#define WINDOW_WIDTH  (400)		// ウィンドウの幅
#define WINDOW_HEIGHT (300)		// ウィンドウの高さ

// プロトタイプ宣言
HWND Create(HINSTANCE hInst);
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);


// 開始位置
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR pCmdLine, int showCmd)
{
	HWND hWnd;
	MSG msg;

	// ウィンドウを作成する
	hWnd = Create( hInst );
	if( hWnd == NULL )
	{
		MessageBox( NULL, _T("ウィンドウの作成に失敗しました"), _T("エラー"), MB_OK );
		return 1;
	}

	// ウィンドウを表示する
	ShowWindow( hWnd, SW_SHOW );
	UpdateWindow( hWnd );

	// メッセージループ
	while( 1 )
	{
		BOOL ret = GetMessage( &msg, NULL, 0, 0 );  // メッセージを取得する
		if( ret == 0 || ret == -1 )
		{
			// アプリケーションを終了させるメッセージが来ていたら、
			// あるいは GetMessage() が失敗したら( -1 が返されたら )、ループを抜ける
			break;
		}
		else
		{
			// メッセージを処理する
			TranslateMessage( &msg );
			DispatchMessage( &msg );
		}
	}

	return 0;
}

// ウィンドウを作成する
HWND Create(HINSTANCE hInst)
{
	WNDCLASSEX wc;
	int x, y;

	// ウィンドウクラスの情報を設定
	wc.cbSize = sizeof(wc);               // 構造体サイズ
	wc.style = CS_HREDRAW | CS_VREDRAW;   // スタイル
	wc.lpfnWndProc = WndProc;             // ウィンドウプロシージャ
	wc.cbClsExtra = 0;                    // 拡張情報1
	wc.cbWndExtra = 0;                    // 拡張情報2
	wc.hInstance = hInst;                 // インスタンスハンドル
	wc.hIcon = (HICON)LoadImage(          // アイコン
		NULL, MAKEINTRESOURCE(IDI_APPLICATION), IMAGE_ICON,
		0, 0, LR_DEFAULTSIZE | LR_SHARED
	);
	wc.hIconSm = wc.hIcon;                // 子アイコン
	wc.hCursor = (HCURSOR)LoadImage(      // マウスカーソル
		NULL, MAKEINTRESOURCE(IDC_ARROW), IMAGE_CURSOR,
		0, 0, LR_DEFAULTSIZE | LR_SHARED
	);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); // ウィンドウ背景
	wc.lpszMenuName = NULL;                     // メニュー名
	wc.lpszClassName = _T("Default Class Name");// ウィンドウクラス名
	
	// ウィンドウクラスを登録する
	if( RegisterClassEx( &wc ) == 0 ){ return NULL; }

	// ウィンドウの作成位置を計算する
	x = ( GetSystemMetrics( SM_CXSCREEN ) - WINDOW_WIDTH ) / 2;
	y = ( GetSystemMetrics( SM_CYSCREEN ) - WINDOW_HEIGHT ) / 2;

	// ウィンドウを作成する
	return CreateWindow(
		wc.lpszClassName,      // ウィンドウクラス名
		_T("Sample Program"),  // タイトルバーに表示する文字列
		WS_OVERLAPPEDWINDOW,   // ウィンドウの種類
		x,                     // ウィンドウを表示する位置(X座標)
		y,                     // ウィンドウを表示する位置(Y座標)
		WINDOW_WIDTH,          // ウィンドウの幅
		WINDOW_HEIGHT,         // ウィンドウの高さ
		NULL,                  // 親ウィンドウのウィンドウハンドル
		NULL,                  // メニューハンドル
		hInst,                 // インスタンスハンドル
		NULL                   // その他の作成データ
	);
}

// ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
	TCHAR str[20];

	switch( msg )
	{
	case WM_LBUTTONDOWN:	// マウスの左ボタンが押されたときに送られてくる
		_stprintf( str, _T("X:%d  Y:%d"), LOWORD(lp), HIWORD(lp) );
		MessageBox( hWnd, str, _T("座標情報"), MB_OK );
		return 0;

	case WM_DESTROY:  // ウィンドウが破棄されるときに送られてくる
		PostQuitMessage( 0 );
		return 0;
	}

	return DefWindowProc( hWnd, msg, wp, lp );
}

結果をメッセージボックスで表示するために、_stprintf() で文字列化しています。 _stprintf() は、_Tマクロと同じように、_UNICODEマクロの定義の有無で、置き換わる文字列が変化します。 _UNICODE が定義されていれば、swprint() に、定義されていなければ sprintf() になります。swprintf() は、sprintf() のワイド文字バージョンである というだけで、役割は sprintf() と同じです。sprintf()については、C言語編第44章 を参照して下さい。なお _stprintf() を使うと、VisualC++2005 では警告が出ます。この警告を抑制するためには プログラムの先頭にある2行が必要です。

また、TCHAR型の変数を宣言しています。これは _UNICODE が定義されていれば、 wchar_t型に、定義されていなければ char型になります。これを使う場合も、tchar.h の include が必要です。

なお、_stprintf() が sprintf() 等の関数に化けるので、stdio.h の include が必要です。




今回はクリックとダブルクリックについて説明しました。マウスに関する操作はまだ幾つか存在しているので、 次回以降で説明します。


Win32API(C言語)編のトップページに戻る

サイトのトップページに戻る