Win32 で「かざぐるマウス」の類似ソフトを作ろうとした話

# はじめに
この記事は [SLP KBIT AdventCalendar2022](https://adventar.org/calendars/7569) 1日目の記事です。URL先では当サークルメンバーの記事を閲覧することが出来ます。IT関連に関わる様々な面白い記事を投稿しておりますので、興味がありましたら是非ご覧ください。それでは本編スタートっ！

# 目次
- 今回の記事にあたって
- 制作物
- なにがダメだったのか
- おわりに
- おまけ

# 今回の記事にあたって
ということで始まりました、AdventCalendar！！なんと今年は当サークルで記念すべき10年周年の企画です。当サークルメンバーはこの大きな節目のイベントにやる気を見せています。かく言う私も今年は頑張るぞと気合十分であり、その中で私はトップバッターである1日目を任させてもらいました。そうしてAdventCalendarの企画は12月01日からスタートしましたが、果たして私の記事はいつになったら投稿されるのでしょうか。いつまで経っても投稿されません。「トップバッターは任せてください！」と意気込んでいた青年はどこへ消えてしまったのか。サークルメンバーの皆様方には多大な心配と迷惑をかけてしまいました。この場をお借りして謝罪を申し上げます。

さあ今年は何をしようかいろいろ考えてみたところ、「Windows API」を使って自作ソフトに挑戦してみようかなと至りました。結論を先に述べますと、まだ完成しておりません。思った以上にWin32のプログラム構築が複雑で、心が折れてしまいました。しかし、未完にしてもそのゴールまでに至ろうとする過程や様々な試行錯誤の失敗は価値のあるものと捉えています。よって、失敗したコードも含めて記事にするので、皆さんは是非この記事を反面教師として活用してください。

# 制作物
## 導入
昔パソコンを使ってる時、どうしても許せないことがありました。

「ブラウザバックの矢印ボタン小さっ」

です。これが非常に押しづらい。マウスポインタを小さな左矢印に合わせてクリックって。というかブラウザバックするたびにその位置まで移動すること自体が面倒。

という事で当時インストールしたのが「かざぐるマウス」です。

このソフトはマウスジェスチャを登録できるソフトであり、このソフトを使うことによってブラウザバックを「右クリック長押しでマウスを左にスライド後、右クリックボタンを離す」だけで行うことが可能になりました。これが超便利で、サポート終了後もブラウザバックする際、無意識にその動作を行ってしまいます。

あの頃の便利な機能をもう一度蘇らせたい！！
そんな感じで「なければ作ればいいじゃないか」のノリで制作を試みます。

## とりあえずやってみるか
最初に頭に思い浮かんだのが「Windows API」です。今まで直接触ったことはありませんでしたが、DxLibを使ってゲーム制作を行ったことがあり、かつシステム系のプログラムを構築するならこれでいいだろという考えがあったので、採用しました。早速他サイトの解説ページで学んだことを活用して VisualStudio2019 で「Hello World」表示してみます。
```C++
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{
    MessageBox(NULL, TEXT("Hello World"), TEXT("Test"), MB_OK);
    return 0;
}
```
![01_hello.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384147/d25f0f8a-bec5-3580-ee7f-b9ed2e4dc501.png)

無事「Hello World」と書かれたダイアログが表示されました。楽勝ですね。次にメモ帳などの終了時に表示されるダイアログを作ってみます。
```C++
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{
    MessageBox(NULL, TEXT("変更を保存しますか？"), TEXT("偽メモ帳"), MB_YESNOCANCEL | MB_ICONEXCLAMATION);
    return 0;
}
```
![02_note.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384147/65dab220-d504-d173-6694-88783d254c96.png)

いつも見るダイアログが表示されます。しかし現段階では形式的な文章を追加しただけでボタンを選択してもなにも起きません。なので今度は「はい」「いいえ」の押すボタンで異なった結果が表示されるものを作ります。
```C++
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{
    int id;
    id = MessageBox(NULL, TEXT("結婚してください！"), TEXT("運命の決断"), MB_YESNO | MB_ICONQUESTION);

    switch (id)
    {
    case IDYES:
        MessageBox(NULL, TEXT("(＊^_^＊)"), TEXT("はい"), MB_ICONINFORMATION);
        break;

    case IDNO:
        MessageBox(NULL, TEXT("(´・ω・`)"), TEXT("いいえ"), MB_ICONINFORMATION);
        break;
    }

    return 0;
}
```
![03_1_kekkon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384147/77c284e2-bbf9-1006-e9e0-a796daaa3d20.png)
![03_2_Yes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384147/09f87456-6e20-56b8-3fc1-cc6588c87f6f.png)
![03_3_No.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384147/61eaec96-c0f4-bc95-b0fb-54a54d623056.png)
可愛いですね。
## 次の段階
マウスの座標をウィンドウに表示させてみます。
```C++
#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{
    TCHAR szAppName[] = TEXT("TestApp");
    WNDCLASS wc;
    HWND hwnd;
    MSG msg;

    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = szAppName;

    if (!RegisterClass(&wc)) return 0;

    hwnd = CreateWindow(
        szAppName, TEXT(""),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL,
        hInstance, NULL);

    if (!hwnd) return 0;

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    int x, y;
    TCHAR szTemp[1024];

    switch (uMsg) {
    case WM_MOUSEMOVE:
        y = HIWORD(lParam);
        x = LOWORD(lParam);
        wsprintf(szTemp, TEXT("WM_MOUSEMOVE (%d, %d)"), x, y);

        SetWindowText(hwnd, szTemp);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
```
![04_mouse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384147/b5276cbe-39ce-1812-61e5-4e3e12e78141.png)
画像では分かりづらいかもしれませんが、左がx座標、右がy座標となっています。一応解説を行いますと、WndProc関数の第二引数であるUNIT型のuMsg変数にマウス操作のイベント判定が格納されます。この場合、switch文で uMsg == WM_MOUSEMOVE が真になるとマウスカーソルのx、y座標が表示される仕組みになっています。

更にswitch文に判定式を増やしてみましょう。
```C++
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    int x, y;
    TCHAR szTemp[1024];

    switch (uMsg) {
    case WM_MOUSEMOVE:
        y = HIWORD(lParam);
        x = LOWORD(lParam);
        wsprintf(szTemp, TEXT("WM_MOUSEMOVE (%d, %d)"), x, y);

        SetWindowText(hwnd, szTemp);
        return 0;

    case WM_LBUTTONDOWN:
        y = HIWORD(lParam);
        x = LOWORD(lParam);
        wsprintf(szTemp, TEXT("L_DOWN (%d, %d)"), x, y);
        SetWindowText(hwnd, szTemp);
        return 0;

    case WM_LBUTTONUP:
        y = HIWORD(lParam);
        x = LOWORD(lParam);
        wsprintf(szTemp, TEXT("L_UP (%d, %d)"), x, y);
        SetWindowText(hwnd, szTemp);
        return 0;

    case WM_RBUTTONDOWN:
        y = HIWORD(lParam);
        x = LOWORD(lParam);
        wsprintf(szTemp, TEXT("R_DOWN (%d, %d)"), x, y);
        SetWindowText(hwnd, szTemp);
        return 0;

    case WM_RBUTTONUP:
        y = HIWORD(lParam);
        x = LOWORD(lParam);
        wsprintf(szTemp, TEXT("R_UP (%d, %d)"), x, y);
        SetWindowText(hwnd, szTemp);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
```
上手くスクリーンショットが撮れなかったので画像は割愛しますが、コードから見てわかる通り左クリックを押しっぱなしで「L_DOWN」離すと「L_UP」右クリックを押しっぱなしで「R_DOWN」離すと「R_UP」が表示されるようにしました。だんだん完成系に近づいてきましたね。
# なにがダメだったのか
事は順調に運んでいると思いきや、ここで壁にあたります。

マウスが動いている状態 かつ 右クリックが押されている状況をどう判定すればいいのか

このコード内でマウスの動きを判定している変数はuMsgの一つのみです。よって
「uMsg == WM_RBUTTONDOWN && uMsg == WM_MOUSEMOVE」
なんてことを記述すれば必ず上記は偽になります（そりゃ当然）。

それじゃフラグを使えば良いのでは

となるのはプログラマとして当たり前の思考回路であり、当然自分も同じことを考えました。そしてWndProc関数を変更したのが以下のプログラムです。
```C++
int flg = 0;

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    TCHAR szTemp[1024];

    if (uMsg == WM_MOUSEMOVE)
    {
        flg = 1;
    }
    if (flg == 1 && uMsg == WM_RBUTTONDOWN)
    {
        wsprintf(szTemp, TEXT("success"));
        SetWindowText(hwnd, szTemp);
    }
    if (flg == 1 && uMsg == WM_RBUTTONUP)
    {
        flg = 0;
    }
    if (uMsg == WM_DESTROY)
    {
        PostQuitMessage(0);
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
```
![05_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384147/89cd29c5-35c7-e0f2-b4b0-965ad55de494.png)
あれぇぇぇぇ～～～～？？？？(´Д｀)

なぜこのコードがダメなのでしょうか。というのも、このコードは先のコードと比べて色々そぎ落としたものがあります。おそらくそれが原因なのでしょうか。

今度は先のコードにギリギリ似せたものに改良してみます。
```C++
int flg = 0;

LRESULT CALLBACK WndProc(
    HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    TCHAR szTemp[1024];

    if (uMsg == WM_MOUSEMOVE && flg == 0)
    {
        flg = 1;
        wsprintf(szTemp, TEXT("faild 01"));
        SetWindowText(hwnd, szTemp);
        return 0;
    }
    if (flg == 1 && uMsg == WM_RBUTTONDOWN)
    {
        wsprintf(szTemp, TEXT("success"));
        SetWindowText(hwnd, szTemp);
        return 0;
    }
    if (flg == 1 && uMsg == WM_RBUTTONUP)
    {
        flg = 0;
        wsprintf(szTemp, TEXT("faild 02"));
        SetWindowText(hwnd, szTemp);
        return 0;
    }
    if (uMsg == WM_DESTROY)
    {
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
```
実行してみると、なんか思ってたのと違う...。

変数を新しく作ったり、プログラムを変更してみたりといろいろ検証してみたところ、自分の想像していたプログラムの処理の順番が実際と異なっていたことが判明しました。そもそもの話、一体どこでループ処理を行っているのすら分かっていません。WinMain関数の中でループ処理をしているわけでもなく、というかWndProc関数がどこで呼び出されているかも分かっていません。

またこの後起こりうる問題を考えたところ、「ウィンドウ外でもマウス座標が表示される必要がある」「ウィンドウにフォーカスが置かれていなくても動作する必要がある」といったのが挙げられます。

これはちょっと今の段階では...。

# おわりに
今回Windows API（Win32）を使って自作ソフトに挑戦しました。しかし完成にまで至ることはできず、自分の勉強不足を痛感しました ~~（いけると思ったんだけどなぁ）~~。

Windows APIで出来ることや作ってみたいソフトは沢山ありますので、きちんと勉強してから、いつかリベンジしたいと思います。

長々と書きましたが最後までご覧いただきありがとうございます。以上です。

# おまけ
いろいろ調べたところ、「かざぐるマウス」の類似ソフトが既にWeb上で公開されていたため、それの紹介URLを貼っておきます。
[かざぐるマウスの後継ソフトはopenmausujiで決まり](https://giraffeong.com/computer/10285)