楽しいキーパンチ～KeySender～

今回は、某ゲームのために作った、Windows向けのキーセンダーです。
キー入力を覚えて送り続けます。

#何をしているか。
Windowsのイベントドリブンを利用して、
自分のWindowが非アクティブになったときにカーソルの下にあるWindowのハンドルを取得してロックオンします。
そのWindowにKeyDownインベントをポストします。

#使い方
起動したら、自分のWindowを左ダブルクリックして、キー入力を送りたいWindowをクリックします。
Windowのフォーカスを失った時に、カーソル下のWindowのHWNDを取得するので自分のWindowに戻ってキーを入力してください。
最後に打ったキーのパラメータを保存するので、右ダブルクリックでそれを使ったタイマーを起動できます。
タイマーは自分のWindowを右クリックをすると止まります。

UPとDownのアローキーでタイマーの時間をいじれます。

#免責
デバッグはしていますが、もしかしたらバグがあるかもしれません。
自分はWindows開発の初心者なので、作法を間違っている可能性があります。

今回も、以前懺悔した、ウィンドウライブラリを使っています。便利でよいです。

#顛末
このコードはウルティマオンラインというMMORPGのスキル上げのために書いたのですけど、あのゲームは非公認ツールを使うとアカウント停止の危険があるのですよ。
しかし、スキル上げも慣れればボタンポチポチ押してるだけで上がるので、PCを監視しながら適当に自動化したかったのですよ。
一応、通信をいじらなければある程度見逃してくれるという噂は有ります。

使うべきか使わざるべきか。

#コード
```cpp:WndProc.h
#pragma once
#include <sstream>
#include <string>
#include "WindowClass.h"

class KeySenderDProc :public Window::EventHandler {

	LRESULT WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {

		RECT crt = { 0, };
		GetClientRect(hWnd, &crt);

		switch (msg) {
		case WM_RBUTTONDOWN:
			KillTimer(hWnd, TimerID);
			IsTimerMove = false;
			//Target = nullptr;
			InvalidateRect(hWnd, &crt, TRUE);

			break;
		case WM_LBUTTONDOWN:
			if (IshWndGetting) { IshWndGetting = false; }
			InvalidateRect(hWnd, &crt, TRUE);
			break;
		case WM_LBUTTONDBLCLK:
			//Target = nullptr;
			if (SetForegroundWindow(hWnd) == TRUE) {
				//ShowWindow(hWnd, SW_HIDE);
				IshWndGetting = true;
			}
			InvalidateRect(hWnd, &crt, TRUE);
			break;
		case WM_RBUTTONDBLCLK:
			if (Target != nullptr) {
				KillTimer(hWnd, TimerID);
				TimerID = SetTimer(hWnd, TimerID, TimeLimit, nullptr);
			}
			UpdateWindow(hWnd);
			InvalidateRect(hWnd, &crt, TRUE);
			break;

		case WM_TIMER: {
			LRESULT r(0);
			if (Target != nullptr) {
				if (IsWindowEnabled(Target) == TRUE) {
					r = PostMessage(Target, WM_KEYDOWN, KPW, KPL);
					IsTimerMove = true;
				}
			}
			else {
				if (IsTimerMove) {
					KillTimer(hWnd, TimerID);
				}
			}
			InvalidateRect(hWnd, &crt, TRUE);
			break;
		}
		case WM_KEYDOWN: {
			LRESULT r(0);
			if (wp == VK_UP) {
				TimeLimit += 100;
			}
			if (wp == VK_DOWN) {
				TimeLimit -= 100;
			}
			TimeLimit -= TimeLimit % 100;
			if (TimeLimit == 0) { TimeLimit = 16 * 3; }
			if (Target != nullptr) {
				if (wp == VK_ESCAPE) {
					Target = nullptr;
					IshWndGetting = false;
					TimeLimit = 1000;
					KPL = 0;
					KPW = 0x41;//a
					IsTimerMove = false;

					KillTimer(hWnd, TimerID);
				}
				if (IsWindowEnabled(Target) == TRUE) {
					r = PostMessage(Target, WM_KEYDOWN, wp, lp);
					KPL = lp;
					KPW = wp;
				}
			}
			InvalidateRect(hWnd, &crt, TRUE);
			break;
		}

		case WM_KILLFOCUS:
			if (IshWndGetting) {
				Target = nullptr;
				POINT P = { 0, };
				if (GetCursorPos(&P) == TRUE) {
					Target = WindowFromPoint(P);
				}
				IshWndGetting = false;
			}
			InvalidateRect(hWnd, &crt, TRUE);
			break;
		case WM_PAINT:
		{
			RECT rt = { 0, };
			PAINTSTRUCT ps;
			HDC hDC = BeginPaint(hWnd, &ps);
			if (IshWndGetting) {
				TCHAR T[] = _T("Select Forcus Another Window.");
				DrawText(hDC, T, std::size(T) - 1, &rt, DT_CALCRECT);
				DrawText(hDC, T, std::size(T) - 1, &rt, DT_NOCLIP);
			}
			else {
				TCHAR T[] = _T("DoubleClick To Capture.");
				DrawText(hDC, T, std::size(T) - 1, &rt, DT_CALCRECT);
				DrawText(hDC, T, std::size(T) - 1, &rt, DT_NOCLIP);
			}
			{
				std::stringstream ss;
				ss << "KPL:" << KPL << ' ' << "KPW:" << KPW;

				DrawTextA(hDC, ss.str().data(), ss.str().size(), &rt, DT_CALCRECT);
				rt.top += 16;
				rt.bottom += 16;
				DrawTextA(hDC, ss.str().data(), ss.str().size(), &rt, DT_NOCLIP);
			}

			{
				std::stringstream ss;
				ss << Target;

				DrawTextA(hDC, ss.str().data(), ss.str().size(), &rt, DT_CALCRECT);	
				rt.top += 16;
				rt.bottom += 16;
				DrawTextA(hDC, ss.str().data(), ss.str().size(), &rt, DT_NOCLIP);
			}
			{
				std::stringstream ss;
				ss << TimeLimit<<"msec.";

				DrawTextA(hDC, ss.str().data(), ss.str().size(), &rt, DT_CALCRECT);	
				rt.top += 16;
				rt.bottom += 16;
				DrawTextA(hDC, ss.str().data(), ss.str().size(), &rt, DT_NOCLIP);
			}
			{
				std::stringstream ss;
				ss << (IsTimerMove ? "TimerIsMoving" : "");
				DrawTextA(hDC, ss.str().data(), ss.str().size(), &rt, DT_CALCRECT);
				rt.top += 16;
				rt.bottom += 16;
				DrawTextA(hDC, ss.str().data(), ss.str().size(), &rt, DT_NOCLIP);
			}
			{
				DrawTextA(hDC, Menu.data(), Menu.size(), &rt, DT_CALCRECT);	
				rt.top += 20;
				rt.bottom += 20;	
				DrawTextA(hDC, Menu.data(), Menu.size(), &rt, DT_NOCLIP);
			}
			EndPaint(hWnd, &ps);
			break;
		}

		case WM_DESTROY:
			KillTimer(hWnd, TimerID);
			PostQuitMessage(0);
			break;

		default:
			return DefWindowProc(hWnd, msg, wp, lp);
		}
		return 0;
	}
protected:
	HWND Target = nullptr;
	bool IshWndGetting = false;
	unsigned short TimeLimit = 1000;
	UINT_PTR TimerID = 0xdeadbeef;
	LPARAM KPL = 0;
	WPARAM KPW = 0x41;//a
	bool IsTimerMove = false;
	std::string Menu = "LButtonDown:CancelCapture.\nRButtonDown:KillTimer.\nLButtonDoubleClick:Capture.\nRButtonDoubleClick:Timer.\nKeyDown:SendKey.\n";
};



```

#コード
```cpp:WindowClass.h
#pragma once
#include <memory>
#define NOMINMAX
#include <windows.h>
#include <windowsx.h>
#include <tchar.h>

//https://qiita.com/seekerkrt/items/2478c05c590c308146fd
//base code auther can use this.
//who can drop this talk.

class Window {
public:
	class EventHandler;
	typedef std::shared_ptr<EventHandler> SharedEventHandler;
public:
	Window();
	virtual ~Window();
public:	
	static const int MAX_LOADSTRING = 100;
	bool Create(HINSTANCE hInstance, const TCHAR (class_name)[],const TCHAR (app_name)[], int width, int height);
	bool IsCreated();   //ウィンドウが生成（生存）されているか確認
	HWND GetHWND() { return hWnd; }
	bool UpdateWindow();
	bool ShowWindow(int nCmdShow);
	void SetTitle(const TCHAR title[]);
	WPARAM GetMessageLoop(UINT wMsgFilterMin, UINT wMsgFilterMax);  //汎用かつ定番のメッセージループ
	WPARAM PeekMessageLoop(UINT wMsgFilterMin, UINT wMsgFilterMax,/* UINT wRemoveMSG,*/ UINT NanoSleep);    //特定用途メッセージループ


	bool AddStyle(LONG In) {
		LONG lStyle;
		lStyle = GetWindowLong(hWnd, GWL_STYLE);
		lStyle |= In;
		lStyle = SetWindowLong(hWnd, GWL_STYLE, lStyle);
		return true;
	}
	bool SubStyle(LONG In) {
		LONG lStyle;
		lStyle = GetWindowLong(hWnd, GWL_STYLE);
		lStyle &= ~In;
		lStyle = SetWindowLong(hWnd, GWL_STYLE, lStyle);
		return true;
	}

	bool SetClientArea(int W, int H) {
		RECT wrt = { 0, };
		RECT crt = { 0, };
		GetWindowRect(hWnd, &wrt);
		GetClientRect(hWnd, &crt);

		int BW = (wrt.right - wrt.left) - (crt.right-crt.left);
		int BH = (wrt.bottom - wrt.top) - (crt.bottom-crt.top);

		return TRUE == MoveWindow(hWnd, wrt.left, wrt.top, BW + W, BH + H, TRUE);
		
		
	}

	class EventHandler {
	public:
		virtual void InnerPeekMessage();//PeekMessageLoopで呼ばれる
		virtual LRESULT WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp); //デフォルトではDefWndProcを呼ぶかWM_DESTORYを処理するぐらい
		virtual ~EventHandler() {}
	};
	
	bool SetEventHandler(SharedEventHandler In);
	

public: //カスタマイズするメソッド
	virtual void InnerPeekMessage();//PeekMessageLoopで呼ばれる
	//virtual LRESULT LocalWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp); //デフォルトではDefWndProcを呼ぶかWM_DESTORYを処理するぐらい

private:
	static LRESULT CALLBACK BaseWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp); //ラップするためのトリックな関数
	void SetPointer(HWND hWnd); //windowオブジェクト(thisポインタ)とウィンドウハンドルを関連付けるためのトリックな関数

protected:
	volatile HWND hWnd = nullptr;
	SharedEventHandler EH = std::make_shared<EventHandler>();
};

```

#コード
```cpp:WindowClass.cpp
#include <thread>
#include <chrono>
#include "WindowClass.h"

//コンストラクタ
Window::Window() {

}
//デストラクタ
Window::~Window() {

}

bool Window::Create(HINSTANCE hInstance,const TCHAR class_name[],const TCHAR app_name[], int width, int height) {
	WNDCLASSEX wcex;
	ZeroMemory(&wcex, sizeof(wcex));

	wcex.cbSize = sizeof(WNDCLASSEX);   //この構造体自体のサイズなので。
	wcex.cbClsExtra = 0;    //拡張用？とりあえず使わないので0
	wcex.cbWndExtra = 0;    //拡張用？とりあえず使わないので0
	wcex.lpfnWndProc = Window::BaseWndProc;//このウィンドウクラスのプロシージャを指定。このソースコードではこうする
	wcex.hInstance = hInstance;
	wcex.lpszClassName = class_name;
	wcex.lpszMenuName = nullptr;    //メニューの名前。使わないならとりあえずNULLでOK
	wcex.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS;
	wcex.hCursor = ::LoadCursor(hInstance, IDC_ARROW);      //カーソルアイコン
	wcex.hIcon = ::LoadIcon(hInstance, IDI_APPLICATION);    //プログラムアイコン
	wcex.hIconSm = ::LoadIcon(hInstance, IDI_APPLICATION);  //プログラムアイコン（小）::タイトルバーに使われるやつ？
	wcex.hbrBackground = GetStockBrush(WHITE_BRUSH);      //クライアント領域の塗りつぶし色（ブラシ）
//↑とかは特に、メソッドの引数を受け取ってカスタマイズできるようにするのが望ましいかな                                                 
			//ATOMへWNDCLASSEX登録
	if (!::RegisterClassEx(&wcex)) {
		return false;
	}
	//ウィンドウ生成
	/** /
	HWND ret = ::CreateWindowEx(
		WS_EX_COMPOSITED,
		class_name,
		app_name,
		WS_OVERLAPPEDWINDOW,    //ウィンドウスタイル。とりあえずデフォルトな感じで
		CW_USEDEFAULT, CW_USEDEFAULT,   //初期位置。適当にやってくれる。
		width, height,      //ウィンドウサイズ
		nullptr,    //親ウィンドウのハンドル。特にないんで今回はNULL
		nullptr,    //メニューハンドル。特にないので今回はNULL
		hInstance,
		this    //トリックの肝。CreateParameterに設定
	);
	/**/
	HWND ret = ::CreateWindow(
		class_name,
		app_name,
		WS_OVERLAPPEDWINDOW,    //ウィンドウスタイル。とりあえずデフォルトな感じで
		CW_USEDEFAULT, CW_USEDEFAULT,   //初期位置。適当にやってくれる。
		width, height,      //ウィンドウサイズ
		nullptr,    //親ウィンドウのハンドル。特にないんで今回はNULL
		nullptr,    //メニューハンドル。特にないので今回はNULL
		hInstance,
		this    //トリックの肝。CreateParameterに設定
	);
	/**/
	//ウィンドウ生成に失敗？
	if (ret == nullptr) {
		return false;
	}
	//成功したのでtrueを返して抜ける
	return true;
}

bool Window::IsCreated()
{
	if (this->hWnd == nullptr) {
		return false;
	}
	else {
		return true;
	}
}

bool Window::UpdateWindow() {
	if (this->hWnd == nullptr) {
		return false;
	}
	::UpdateWindow(this->hWnd);
	return true;
}

bool Window::ShowWindow(int nCmdShow) {
	if (this->hWnd == nullptr) {
		return false;
	}
	::ShowWindow(this->hWnd, nCmdShow);
	return true;
}

void Window::SetTitle(const TCHAR title[MAX_LOADSTRING]) {
	::SetWindowText(hWnd, title);
}

WPARAM Window::GetMessageLoop(UINT wMsgFilterMin, UINT wMsgFilterMax) {
	MSG msg;
	while (::GetMessage(&msg, nullptr, wMsgFilterMin, wMsgFilterMax) > 0) {  //「-1」が返ってくるかもしれないのでこうする
		::TranslateMessage(&msg);
		::DispatchMessage(&msg);
	}
	this->hWnd = nullptr; //ココに記述しておいてみる
	return msg.wParam;
}
WPARAM Window::PeekMessageLoop(UINT wMsgFilterMin, UINT wMsgFilterMax/*, UINT wRemoveMSG*/,UINT NanoSleep) {
	MSG msg;
	do {
		//既定の処理はプロシージャなどに任せて……
		if (::PeekMessage(&msg, nullptr, wMsgFilterMin, wMsgFilterMax, PM_REMOVE)) {//if 2nd param not null to cant get WM_QUIT.
			::TranslateMessage(&msg);
			::DispatchMessage(&msg);
		}
		//そうでない処理は自前でする
		else {
			//自前の処理であるInnerPeekMessage()を呼び出し
			this->InnerPeekMessage();
		}
		std::this_thread::sleep_for(std::chrono::nanoseconds(NanoSleep));
	} while (msg.message != WM_QUIT);//イベントドリブン処理ループを抜けるまで実行
	this->hWnd = nullptr; //ココに記述しておいてみる
	return msg.wParam;
}

void Window::InnerPeekMessage() {
	//TODO:InnerPeekMessage()
	EH->InnerPeekMessage();
}
/** /
LRESULT Window::LocalWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
	switch (msg) {
	case(WM_DESTROY):
		::PostQuitMessage(0);
		break;
	default:
		return ::DefWindowProc(hWnd, msg, wp, lp);
	}
	return 0;
}
/**/
LRESULT CALLBACK Window::BaseWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
	//Window::SetPointerでセットしたthisポインタを取得
	Window* window = (Window*)(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
	//取得に失敗してる場合
	if (window == nullptr) {
		//おそらくWM_CREATEの最中なので
		if (msg == WM_CREATE) {
			//CreateWindowのパラメータから取得する
			window = (Window*)(((LPCREATESTRUCT)lp)->lpCreateParams);
		}
		//↑の処理で取得できてるはずなんだけど、一応チェックしてから
		if (window != nullptr) {
			//windowオブジェクトとウィンドウハンドルを関連付ける
			window->SetPointer(hWnd);
		}
	}
	//無事取得できててる
	if (window != nullptr) {
		//そのオブジェクトによるウィンドウプロシージャ実装を使うというか振り分ける
		return window->EH->WndProc(hWnd, msg, wp, lp);
	}
	//よくわからんけど、それでも取得できたりしてない場合（以下のようにするよりも例外作って投げるのがよさそうだがｗ
	else {
		return ::DefWindowProc(hWnd, msg, wp, lp);
	}
}
void Window::SetPointer(HWND hWnd) {
	::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
	this->hWnd = hWnd;
}

bool Window::SetEventHandler(SharedEventHandler In) {
	this->EH = In;
	return true;
}
void Window::EventHandler::InnerPeekMessage() {
	//TODO:InnerPeekMessage()
	
}
LRESULT Window::EventHandler::WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
	switch (msg) {
	case(WM_DESTROY):
		::PostQuitMessage(0);
		break;
	default:
		return ::DefWindowProc(hWnd, msg, wp, lp);
	}
	return 0;
}
```

#コード
```cpp:Main.cpp
#include <windows.h>

#include "WindowClass.h"
#include "WndProc.h"

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPWSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
	UNREFERENCED_PARAMETER(hPrevInstance);
	UNREFERENCED_PARAMETER(lpCmdLine);

	Window W;
	Window::SharedEventHandler WE = std::make_shared<KeySenderDProc>();
	W.SetEventHandler(WE);

	const TCHAR Name[] = _T("KeySenderYaki");
	const TCHAR Title[] = _T("KeySenderYaki");

	W.Create(hInstance, Name, Title, 320, 232);

	W.SubStyle(WS_MAXIMIZEBOX);
	W.SubStyle(WS_THICKFRAME);

	W.ShowWindow(nCmdShow);
	W.UpdateWindow();

	//W.SetClientArea(256, 32+16 + 256);
	
	return W.GetMessageLoop(0,0);
}
```

#スクリーンショット

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509227/076298ca-5d04-190a-5f75-d16c3e0cc1ce.png)