ゼロから作るRPA　その１の３　マウス動作記録編（記録用EXE）

#はじめに
[その１の２](https://qiita.com/yasunari_matsuo/items/a1d294f09ac21e9508b6)では、マウスイベントをフックするDLLを作成しました。今回は、そのフックを使用してマウスイベントを記録するEXEを作成していきます。

#環境
私の使用している環境を以下に挙げます。環境が異なる場合は適宜読み替えてください。

- Windows 10 pro
- Visual Studio 2019 Community Edition

#今回やること

1. Visual Studioで画面を作成する
2. フックを開始する処理を呼び出す
3. フックからのイベントを処理する
4. マウスイベントをファイルに保存する

#画面作成
マウスイベントの記録を開始するための画面を作成します。

####Visual Studio 2019 でプロジェクトを作成
前回はDLL用のプロジェクトを作成しましたが、今回はEXE用のプロジェクトを作成します。
DLLの時に作成したソリューションのソリューションエクスプローラーで、DaphRPAソリューションを右クリックし、「追加」-「新しいプロジェクト」を選択します。
すると以下の画面が表示されるので、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/3b06bd7b-fb22-d0f6-266b-3ca00c650e79.png)
Windowsデスクトップアプリケーションを選択し、「次へ」をクリックします。

プロジェクト名はいい感じの名前を付けてください。
自分はDLLのほうにDaphRPAという名前を付けてしまったので、DaphRPAppという名前にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/1802d19e-c41c-935a-ea88-328449bf7a46.png)

「作成」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/15f66dc8-f8c4-7edb-e0a2-f2004db46afa.png)

デフォルトのプロジェクトが出来上がりました。

####ダイアログ作成
リソースビューのDialogで右クリックして「Dialogを挿入」を選択すると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/19483fda-7707-bd2f-f4b0-597aa315e787.png)
ダイアログが作成されます。このダイアログをいい感じに編集して記録ボタンを追加していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/56760c10-bb95-c459-10cf-979f892d03ea.png)
「OK」やら「キャンセル」やらは削除して、小さくして、記録ボタンを配置しましょう。
※記録再生の実験用画面なので、最低限のボタンだけの画面を作ってます。将来的には、RPAっぽい画面に変更していきますのでお楽しみに。

ちょちょいと編集して、以下のような画面にしてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/0bd1c6e7-6a1a-cf22-2b20-683f27ea1184.png)

プロパティは以下のように変えています

| 対象       |       プロパティ |    値    |
|:-----------------|------------------:|:------------------:|
| ダイアログ        |             ID | IDD_DAPH_DIALOG |
| ダイアログ        |  Caption |       ゼロRPA      |
| 記録ボタン        |       ID |        IDC_REC_BTN       |
| 記録ボタン        | Caption |       記録         |

#フック開始処理実装
DaphRPApp.cppにデフォルトで追加されたウィンドウは今回は使用しないので、いったん、`wWinMain`のコードを全部消してしまいます。

```C++:DaphRPApp.cpp
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);


    return (int) 1;
}
```
ほかの不要ファイルも適当に消してください。

先ほど作ったダイアログを表示するには、`DialogBox`マクロを使用します。

```C++:DialogBoxマクロ
void DialogBoxA(
   hInstance,
   lpTemplate,
   hWndParent,
   lpDialogFunc
);
```
wWinMainに渡されたインスタンスと、先ほど作成したダイアログリソース、親ウィンドウのハンドルとダイアログプロシージャを指定してダイアログを表示します。

######コールバック関数を定義する
第４引数に指定するコールバック関数のダイアログプロシージャを定義します。

```C++:DaphRPApp.cpp
LRESULT CALLBACK DialogProc(HWND hDlgWnd, UINT msg, WPARAM wp, LPARAM lp);
```
下記が、最低限の処理を追加したコールバック関数を実装したソースファイルです。

```C++:DaphRPApp.cpp
#include "framework.h"
#include "DaphRPApp.h"
#include "resource.h"

LRESULT CALLBACK DialogProc(HWND hDlgWnd, UINT msg, WPARAM wp, LPARAM lp);


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

	DialogBox(hInstance, MAKEINTRESOURCE(IDD_DAPH_DIALOG), NULL, (DLGPROC)DialogProc);
	return (int) 1;
}

LRESULT CALLBACK DialogProc(HWND hDlgWnd, UINT msg, WPARAM wp, LPARAM lp)
{
	switch (msg) {
	case WM_INITDIALOG:
		return FALSE;
	case WM_COMMAND:
		switch (LOWORD(wp)) {
		case IDC_REC_BTN:
			return FALSE;
		default:
			return FALSE;
		}

	default:
		return FALSE;
	}
	return TRUE;
}
```
起動するとダイアログが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/68a50e9b-59b3-32c0-1df6-2afa65d189f4.png)

ボタンクリックの処理部分にブレークポイントを置いて「記録」ボタンをクリックするとそこで止まります。きちんとボタンが機能していることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/434abc3b-b324-7313-42dd-7c10a176d707.png)
ここに、記録開始の処理を追加します。
まずは、DLLの関数を使用できるよう、ヘッダファイルをインクルードします。
インクルードするために、プロパティにDLLのヘッダファイルの在りかを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/fe36e34d-f422-ba96-02e6-64096fff5512.png)

ソースファイルにも、インクルード指定を追加します。

```C++:DaphRPApp.cpp
#include "framework.h"
#include "DaphRPApp.h"
#include "resource.h"
#include "DaphRPAHook.h"

LRESULT CALLBACK DialogProc(HWND hDlgWnd, UINT msg, WPARAM wp, LPARAM lp);
```
`#include "resource.h"`の部分が追加した部分です。
インクルードを追加したら、「録画」ボタンの処理部分に、フックの開始コードを追加します。

```C++:DaphRPApp.cpp
LRESULT CALLBACK DialogProc(HWND hDlgWnd, UINT msg, WPARAM wp, LPARAM lp)
{
	switch (msg) {
	case WM_INITDIALOG:
		return FALSE;
	case WM_COMMAND:
		switch (LOWORD(wp)) {
		case IDC_REC_BTN:
			StartMouseHook(hDlgWnd);
			return FALSE;
		default:
			return FALSE;
		}

	default:
		return FALSE;
	}
	return TRUE;
}
```
これで、ボタンクリックするとフックが開始されます。ちょっと実験してみましょう。
DLLのコールバック関数にブレークポイントを置いてEXEを実行し、「記録」ボタンを押してみましょう。
ブレークポイントを置いて、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/8208d22f-a178-da75-8320-dcb640013088.png)
EXEを起動して、「記録」ボタンをクリックします。
そして、マウスを少し動かすと・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/70ba4b5f-a14f-7400-9fe3-07f6b852dcab.png)
マウスフックのコールバック関数処理が呼び出されるようになります。

####マウスからのイベント処理
マウスフックのコールバック関数が呼び出されるようになると、マウスを動かすたびに、PostMessageにより`DAPHRPA_WM_MOUSEHOOK`イベントがダイアログに通知されるようになります。
その処理をダイアログのコールバック関数に記述して、マウスからのイベントを処理します。

ダイアログのコールバック関数に、以下のようにイベントの処理を追加します。
追加したのは、`case DAPHRPA_WM_MOUSEHOOK:`の部分です。

```C++:DaphRPApp.cpp
LRESULT CALLBACK DialogProc(HWND hDlgWnd, UINT msg, WPARAM wp, LPARAM lp)
{
	switch (msg) {
	case WM_INITDIALOG:
		return FALSE;
	case WM_COMMAND:
		switch (LOWORD(wp)) {
		case IDC_REC_BTN:
			StartMouseHook(hDlgWnd);
			return FALSE;
		default:
			return FALSE;
		}
	case DAPHRPA_WM_MOUSEHOOK:
		return FALSE;
	default:
		return FALSE;
	}
	return TRUE;
}
```

ここにもブレークポイントを置いてみて、止まるかどうかを確認しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/337cb176-67ff-4fc1-2dd5-6fe39e46c2c2.png)

記録を開始してマウスを動かすと、まず、DpahRPA.dll内のマウスフック用コールバック関数でマウスのイベントメッセージがフックされ、その中のPostMessageでダイアログコールバック関数にメッセージがポストされ、最終的に上記ソースコードに到達します。

あとは、ここに、取得したメッセージを再生用のファイルに出力するだけです。

####イベントの保存
イベントを保存します。ここでは一旦、固定ファイル`rec.log`に保存するコードで試してみます。本格作成の時にいろいろと修正します。

```C++:DaphRPApp.cpp
void SaveMouseEvent(DWORD message, LPMSLLHOOKSTRUCT mousell)
{
	std::ofstream fout;
	fout.open(L"rec.log", std::ios::app);
	fout << message << "," << mousell->pt.x << "," << mousell->pt.y << "," << mousell->time << std::endl;
	fout.close();
}
```
第１引数にマウスメッセージ、第２引数にマウスのローレベルフックで得られた構造体を受け取ります。
`std::ofstream fout`でSTLの書き込み用ファイルストリームを定義し、`fout.open`でファイルを追加モードで開きます。
ファイルにマウスイベントの情報を書き込んだのち、ファイルをクローズします。
※この処理では、毎回ファイルのオープンクローズが実行されるので遅いです。停止ボタンを追加した際に、この関数は修正します。

これで、CSV形式のファイルが出力されます。

マウスフックのコールバック関数からのメッセージを処理する部分に、この保存処理を追加します。

```C++:DaphRPApp.cpp
	case DAPHRPA_WM_MOUSEHOOK:
		mousell = (LPMSLLHOOKSTRUCT)lp;
		SaveMouseEvent(wp, mousell);
		delete mousell;
		return FALSE;

```
受け取ったLPARAMをLPMSLLHOOKSTRUCTにキャストし、ファイル保存関数に渡します。

ファイルの出力結果は以下のようになることでしょう。

```text:rc.log
512,46,74,93257000
512,46,74,93257000
512,46,76,93257015
512,46,77,93257015
512,48,81,93257031
512,53,86,93257046
512,68,101,93257046
512,75,107,93257062
512,85,118,93257078
512,105,141,93257093
512,148,193,93257109
512,157,204,93257125
512,188,238,93257125
512,200,249,93257140
512,219,265,93257156
512,231,276,93257171
512,239,280,93257187
512,242,283,93257203
512,248,287,93257218
512,253,289,93257250
512,256,291,93257265
512,263,294,93257296
```
一番左がマウスメッセージ、次の二つがマウスの座標、最後がタイムスタンプです。
記録の停止は、後ほど停止ボタンを追加しますが、とりあえずはデバッガの停止により行ってください。

#DLLを少し修正
[その１の２](https://qiita.com/yasunari_matsuo/items/a1d294f09ac21e9508b6)で作成したDLLにバグがありました。以下の通り修正しました。

```C++:DaphRPAHook.cpp修正後
LRESULT CALLBACK MouseHookProc(int code, WPARAM wParam, LPARAM lParam)
{
    if (!g_hparent || code < 0)
        return CallNextHookEx(g_hhook, code, wParam, lParam);
    // MSLLHOOKSTRUCTをそのままPostMessageすると受取先でメモリがクリアされた
    // 状態になるので、別のメモリにコピーして渡す。そして、呼び出し先で使用後解放する
    LPMSLLHOOKSTRUCT lpmshook = new MSLLHOOKSTRUCT;
    memcpy(lpmshook, (LPMSLLHOOKSTRUCT)lParam, sizeof(MSLLHOOKSTRUCT));
    PostMessage(g_hparent, DAPHRPA_WM_MOUSEHOOK, wParam, (LPARAM)lpmshook);

    return CallNextHookEx(g_hhook, code, wParam, lParam);
}
```

`MouseHookProc`のlParamで受け取った`MSLLHOOKSTRUCT`のポインタをそのまま`PostMessage`の引数としていましたが、スコープを抜けると構造体のメモリが解放されてしまうため、呼び出し先で不正なメモリとなってしまっていました。
構造体のメモリを別途確保し、そこにコピーしたうえで渡すようにしました。

#つづく
今回で、マウスイベントの記録ができました。次回は、記録したマウスイベントを再生します。
続きはこちらです。
[その１の４](https://qiita.com/yasunari_matsuo/items/8a528066cda871c345d4)
https://qiita.com/yasunari_matsuo/items/8a528066cda871c345d4

この記事のプロジェクトは以下にアップしています。記事が進むごとに下記プロジェクトも成長します。
https://github.com/yasunarim/DaphRPA

その１の１は以下のリンクから参照できます。
https://qiita.com/yasunari_matsuo/items/b1e56ad06c6a7843dfae
その１の２は以下のリンクから参照できます。
https://qiita.com/yasunari_matsuo/items/a1d294f09ac21e9508b6