【C++】WindowsAPIでマウス操作を検出する

本稿ではC++(WindowsAPI)でマウスボタンをクリックしたときなどの操作を検出する方法を確認していきたいと思います。



ウィンドウメッセージを利用したマウス操作

まずはウィンドウメッセージを利用した方法をやってみます。

ウィンドウメッセージに関してはこちらの記事で確認しました。

マウスボタンの操作を検出

ウィンドウメッセージにはマウスのボタンが押されたり離されたりしたときに発生するものが存在しており、これを利用することで簡単にマウスのクリックを検出できます。

マウスメッセージにはWM_LBUTTONDBLCLK(左ダブルクリック)やWM_LBUTTONDOWN(左押し下げ)など多数存在します。この辺はこちらのサイト様にて一覧で確認できます。

例えば基本的な押すメッセージと離すメッセージをswitch文で処理させる場合下記のようになるでしょうか。

LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
	WPARAM wParam, LPARAM lParam) {
	switch (message) {
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
		case WM_LBUTTONDOWN:
			//マウス左ボタン押下
			return 0;
		case WM_MBUTTONDOWN:
			//マウスホイール押下
			return 0;
		case WM_RBUTTONDOWN:
			//マウス右ボタン押下
			return 0;
		case WM_LBUTTONUP:
			//マウス左ボタン離した
			return 0;
		case WM_MBUTTONUP:
			//マウスホイール離した
			return 0;
		case WM_RBUTTONUP:
			//マウス右ボタン離した
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}

まあこれくらいしか使わないかなあ・・・あとはダブルクリックも使うかもしれません。

マウス座標の取得

さて、マウスのボタン操作はこれで検出できますが、マウスカーソルの座標も取得したいですよね。

実は座標もウィンドウメッセージと一緒に送られてきます。

WndProcの引数LPARAMの上位ビットと下位ビットがそれぞれy座標、x座標を示す値となっています。

面倒ですが、このLPARAMをビット演算するか、もしくはLOWORD、HIWORDマクロ関数を利用してx,y座標に分割してあげましょう。

例えばマウス左ボタン押し下げ時のx,y座標を取得する場合はこんな感じになりそうです。

LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
	WPARAM wParam, LPARAM lParam) {
	switch (message) {
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
		case WM_LBUTTONDOWN:
			//マウス左ボタン押下
			int x = LOWORD(lParam);
			int y = HIWORD(lParam);
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}

これでマウス座標取得もバッチリです。

ウィンドウメッセージを利用したマウス操作サンプルプログラム

それでは、ここで学んだことを利用して簡単なサンプルコードを作成してみます。

内容はマウスの左ボタンを押下したところに青枠の矩形、右ボタンを押下したところには赤、マウスホイールを押下したところには緑をそれぞれ描画するという単純なものです。

#include <windows.h>
#include <vector>

//前方宣言
struct PenPoint;

//グローバル変数にクリックポイントの記録
std::vector<PenPoint> points;
HPEN current;
HPEN HLPen;
HPEN HMPen;
HPEN HRPen;

//座標と描画色を保持する構造体(クラス)
struct PenPoint : POINT {
	HPEN pen;
	PenPoint(int x, int y, HPEN pen) : POINT({ x,y }) {
		this->pen = pen;
	}
};

//メッセージ処理(マウス処理)
LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
	WPARAM wParam, LPARAM lParam) {
	HDC hdc;
	PAINTSTRUCT ps;
	switch (message) {
		case WM_DESTROY:
			DeleteObject(HLPen);
			DeleteObject(HMPen);
			DeleteObject(HRPen);
			PostQuitMessage(0);
			return 0;
		case WM_CREATE:
			//マウスの左、ホイール、右をクリックしたときの色を定義
			//赤、緑、青
			LOGPEN LPen;
			LOGPEN MPen;
			LOGPEN RPen;
			LPen.lopnStyle = PS_SOLID;
			LPen.lopnWidth.x = 3;
			LPen.lopnColor = 0XFF0000;
			MPen.lopnStyle = PS_SOLID;
			MPen.lopnWidth.x = 3;
			MPen.lopnColor = 0X00FF00;
			RPen.lopnStyle = PS_SOLID;
			RPen.lopnWidth.x = 3;
			RPen.lopnColor = 0X0000FF;
			HLPen = CreatePenIndirect(&LPen);
			HMPen = CreatePenIndirect(&MPen);
			HRPen = CreatePenIndirect(&RPen);
			current = HLPen;
			return 0;
		case WM_LBUTTONDOWN:
			//マウス左ボタン押下
		{
			int x = LOWORD(lParam);
			int y = HIWORD(lParam);
			PenPoint p(x ,y, HLPen );
			points.push_back(p);
			current = HLPen;
			InvalidateRect(hwnd, NULL, TRUE);
			return 0;
		}
		case WM_MBUTTONDOWN:
			//マウスホイール押下
		{
			int x = LOWORD(lParam);
			int y = HIWORD(lParam);
			PenPoint p(x, y, HMPen);
			points.push_back(p);
			current = HMPen;
			InvalidateRect(hwnd, NULL, TRUE);
			return 0;
		}
		case WM_RBUTTONDOWN:
			//マウス右ボタン押下
		{
			int x = LOWORD(lParam);
			int y = HIWORD(lParam);
			PenPoint p(x, y, HRPen);
			points.push_back(p);
			current = HRPen;
			InvalidateRect(hwnd, NULL, TRUE);
			return 0;
		}
		case WM_LBUTTONUP:
			//マウス左ボタン離した
			return 0;
		case WM_MBUTTONUP:
			//マウスホイール離した
			return 0;
		case WM_RBUTTONUP:
			//マウス右ボタン離した
			return 0;
		case WM_PAINT:
			hdc = BeginPaint(hwnd, &ps);
			//ポイントを元に矩形を描画
			for (PenPoint p : points) {
				SelectObject(hdc, p.pen);
				Rectangle(hdc, p.x - 5, p.y - 5, p.x + 5, p.y + 5);
			}
			EndPaint(hwnd, &ps);
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}
int WINAPI WinMain(
	HINSTANCE hInstance,      // 現在のインスタンスのハンドル
	HINSTANCE hPrevInstance,  // 以前のインスタンスのハンドル
	LPSTR lpCmdLine,          // コマンドライン
	int nCmdShow              // 表示状態
) {
	HWND hWnd;
	LPCTSTR szclassName = "WinAPITest";
	WNDCLASSEX wcex;

	ZeroMemory((LPVOID)&wcex, sizeof(WNDCLASSEX));

	//ウィンドウクラスを登録
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = 0;
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = NULL;
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szclassName;
	wcex.hIconSm = NULL;
	RegisterClassEx(&wcex);

	//ウィンドウ作成
	hWnd = CreateWindowEx(0, szclassName, "Title", WS_OVERLAPPEDWINDOW,
		0, 0,
		400, 300,
		NULL, NULL, hInstance, NULL);


	//ウィンドウ表示
	ShowWindow(hWnd, SW_SHOW);
	
	//メッセージループ
	MSG msg = {};
	while (GetMessage(&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
}
実行結果

うん、コード長いね。
マウスボタンを押すと青や赤の矩形が配置されていくはずです。

その場でマウス操作

ウィンドウメッセージを利用せずその場で座標取得や押下状態を取得する方法を見ていきます。

多分ゲーム作るうえでは、こちらのほうが便利な方法になるかと思われます。

マウスボタンの操作を検出

マウスのボタン操作をウィンドウメッセージを経由せずに取得する関数はいくつかありますのでそれぞれ見ていきましょう。

GetKeyState

GetKeyState関数は引数に調べたいボタンの定数を引き渡すことで押されているかどうかを判定できます。

押されている場合は上位ビットに1が立つので下記のようなコードを記述することで判定できます。

	if (GetKeyState(VK_LBUTTON) & 0x80) {
		//マウスの左ボタンが押された"
	}
	if (GetKeyState(VK_RBUTTON) & 0x80) {
		//マウスの右ボタンが押された"
	}
	if (GetKeyState(VK_MBUTTON) & 0x80) {
		//マウスホイールが押された"
	}

せっかくなので実際にテストできるコードも作成してみましょう。

#include <windows.h>
#include <string>
#include <chrono>
#include <thread>

//メッセージ処理
LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
	WPARAM wParam, LPARAM lParam) {
	switch (message) {
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}
int WINAPI WinMain(
	HINSTANCE hInstance,      // 現在のインスタンスのハンドル
	HINSTANCE hPrevInstance,  // 以前のインスタンスのハンドル
	LPSTR lpCmdLine,          // コマンドライン
	int nCmdShow              // 表示状態
) {
	HWND hWnd;
	LPCTSTR szclassName = "WinAPITest";
	WNDCLASSEX wcex;

	ZeroMemory((LPVOID)&wcex, sizeof(WNDCLASSEX));

	//ウィンドウクラスを登録
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = 0;
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = NULL;
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szclassName;
	wcex.hIconSm = NULL;
	RegisterClassEx(&wcex);

	//ウィンドウ作成
	hWnd = CreateWindowEx(0, szclassName, "Title", WS_OVERLAPPEDWINDOW,
		0, 0,
		400, 300,
		NULL, NULL, hInstance, NULL);


	//ウィンドウ表示
	ShowWindow(hWnd, SW_SHOW);

	HDC hdc = GetDC(hWnd);
	MSG msg = {};

	while (true) {
		//メッセージを取得したら1(true)を返し取得しなかった場合は0(false)を返す
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
			if (msg.message == WM_QUIT) {
				//終了メッセージが来たらゲームループから抜ける
				break;
			}
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		} else {

			BYTE keys[256];
			GetKeyboardState(keys);
			if (keys[VK_LBUTTON] & 0x80) {
				std::string str = "マウスの左ボタンが押されています。";
				TextOut(hdc, 10, 10, str.c_str(), str.size());
			}
			if (keys[VK_RBUTTON] & 0x80) {
				std::string str = "マウスの右ボタンが押されています。";
				TextOut(hdc, 10, 30, str.c_str(), str.size());
			}
			if (keys[VK_MBUTTON] & 0x80) {
				std::string str = "マウスホイールが押されています。";
				TextOut(hdc, 10, 50, str.c_str(), str.size());
			}
			InvalidateRect(hWnd, NULL, TRUE);
			

			std::this_thread::sleep_for(std::chrono::milliseconds(100));
		}
	}

	//解放
	ReleaseDC(hWnd, hdc);
}
実行結果

マウスのボタン押してみると画面に押されているという文言が表示されるはずです。

GetAsyncKeyState

GetAsyncKeyState関数はGetKeyStateとほぼ同じなのですが、こちらの関数はより物理的な状態をチェックします。

GetKeyStateはメッセージを経由した結果で、GetAsyncKeyStateは直接結果をとってくるみたいなイメージかな?

	if (GetAsyncKeyState(VK_LBUTTON) & 0x80) {
		//マウスの左ボタンが押された"
	}
	if (GetAsyncKeyState(VK_RBUTTON) & 0x80) {
		//マウスの右ボタンが押された"
	}
	if (GetAsyncKeyState(VK_MBUTTON) & 0x80) {
		//マウスホイールが押された"
	}

。。。この二つを使い分けることも普通はなさそうだし、覚えなくても問題ないでしょう。

GetKeyboardState

GetKeyboardState関数はすべてのキーの状態、マウスの状態を取得する関数です。

256の大きさの配列を引き渡すことで一度に状態を格納します。判定方法はGetKeyStateと変わりません。

	BYTE keys[256];
	GetKeyboardState(keys);
	if (keys[VK_LBUTTON] & 0x80) {
		//マウスの左ボタンが押された"
	}
	if (keys[VK_RBUTTON] & 0x80) {
		//マウスの右ボタンが押された"
	}
	if (keys[VK_MBUTTON] & 0x80) {
		//マウスホイールが押された"
	}

マウス座標の取得

マウス座標の取得はGetCursorPos関数を使用します。引数はx,y座標を示すPOINT構造体です。

	POINT po;
	GetCursorPos(&po);
	//po.x
	//po.y

これは非常に簡単です。

マウスを使った簡易お絵かきサンプルプログラム

最後にその場で取得する関数を使って線を描けるだけのお絵かきプログラムを作ってみることにします。色も変えられないし、消せもしませんがw

#include <windows.h>
#include <chrono>
#include <thread>

//メッセージ処理
LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
	WPARAM wParam, LPARAM lParam) {
	switch (message) {
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}
int WINAPI WinMain(
	HINSTANCE hInstance,      // 現在のインスタンスのハンドル
	HINSTANCE hPrevInstance,  // 以前のインスタンスのハンドル
	LPSTR lpCmdLine,          // コマンドライン
	int nCmdShow              // 表示状態
) {
	HWND hWnd;
	LPCTSTR szclassName = "WinAPITest";
	WNDCLASSEX wcex;

	ZeroMemory((LPVOID)&wcex, sizeof(WNDCLASSEX));

	//ウィンドウクラスを登録
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = 0;
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = NULL;
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szclassName;
	wcex.hIconSm = NULL;
	RegisterClassEx(&wcex);

	//ウィンドウ作成
	hWnd = CreateWindowEx(0, szclassName, "Title", WS_OVERLAPPEDWINDOW,
		0, 0,
		400, 300,
		NULL, NULL, hInstance, NULL);


	//ウィンドウ表示
	ShowWindow(hWnd, SW_SHOW);

	//マウスの左押下時の色を定義
	LOGPEN RPen;
	RPen.lopnStyle = PS_SOLID;
	RPen.lopnWidth.x = 3;
	RPen.lopnColor = 0X0000FF;
	HPEN pen = CreatePenIndirect(&RPen);

	HDC hdc = GetDC(hWnd);
	MSG msg = {};
	bool isInitPos=false;

	while (true) {
		//メッセージを取得したら1(true)を返し取得しなかった場合は0(false)を返す
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
			if (msg.message == WM_QUIT) {
				//終了メッセージが来たらゲームループから抜ける
				break;
			}
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		} else {


			//マウス左が押されているか
			if (GetKeyState(VK_LBUTTON) & 0x80) {
				PAINTSTRUCT ps;
				POINT po;
				//マウス座標取得
				GetCursorPos(&po);
				po.x -= 7;
				po.y -= 30;
				SelectObject(hdc, pen);
				if (!isInitPos) {
					//描画開始位置
					MoveToEx(hdc, po.x, po.y, NULL);
					isInitPos = true;
				}
				//線を描画
				LineTo(hdc, po.x, po.y);
			} else {
				isInitPos = false;
			}

			std::this_thread::sleep_for(std::chrono::milliseconds(25));
		}
	}

	//解放
	DeleteObject(pen);
	ReleaseDC(hWnd, hdc);
}
実行結果

これでマウス操作の取得はOKじゃないかな。

C++

Posted by nompor