6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

私はAutoHotKeyというキーカスタマイズできるOSSを好んで使っていたのですが、
AutoHotKeyを悪用した攻撃も過去にあったようで、会社から使用禁止の通達がありました。

正直AutoHotKeyが無いと堪えられないレベルなので、生成AIを頼りながら自分が設定しているキーを打てるソフトを自作してみることにしました。

設定したいキー操作

以下が前提です。

以下が実際にAutoHotKeyで設定していたコードです。

; F13 + ホイール回転で高速スクロール
F13 & WheelUp::Send "{WheelUp 10}"
F13 & WheelDown::Send "{WheelDown 10}"

; 便利キー操作
F13 & e::Send "{End}"
F13 & a::Send "{Home}"
F13 & n::Send "{F7}"
F13 & d::Send "{Delete}"
F13 & c::Send "{BS}"
F13 & o::Send "{Enter}"

; JIKL を矢印キーとして利用
F13 & j::Send "{Left}"
F13 & i::Send "{Up}"
F13 & k::Send "{Down}"
F13 & l::Send "{Right}"

実装内容詳細

構成

ChatGPTとかに聞きながら検討してC++を使うことにしました。

.
├── README.md
├── main.cpp // C++のソースコード
├── keycustom.exe // C++でビルドした実行ファイル
├── icon.ico // タスクトレイに表示するアイコン
└── build.bat // C++のビルド用バッチファイル(keycustom.exeを削除して再ビルドする)

pythonで最初検討しましたが、F13 + a で Homeとかは実現できても、aが入力されてしまったり、aがそもそも入力できなくなったり等してうまくいきませんでした。
AutoHotKey公式を見てもC++で書かれているのでC++が良さそうです。

用語とか

g++

  • C++ プログラムのコンパイラ

WinLibs

  • Windows 用のフリーでオープンソースな C/C++ コンパイラツールチェーンを提供するプロジェクト
    MinGW-w64 がベース

MSYS2 → g++インストールのためのサーバーが動いていない?ため利用せず

  • Windows 上で Unix ライクな環境を提供するためのソフトウェアパッケージ

実施した手順

1. WinLibsのDL(https://winlibs.com/)

以下をDL

- Release versions
- UCRT runtime
- GCC 14.2.0 (with POSIX threads) + LLVM/Clang/LLD/LLDB 19.1.7 + MinGW-w64 12.0.0 UCRT - release 3   (LATEST)
- without LLVM/Clang/LLD/LLDB: 7-Zip archive*

2. 解凍して以下にg++.exeを配置

$ C:\winlibs\mingw64\bin\g++.exe

3. main.cppを作成

以下ソースコードです
#include <windows.h>
#include <shellapi.h>

HHOOK kbdHook;
HHOOK mouseHook;
bool f13_pressed = false;
bool sending_scroll = false;

#define WM_TRAYICON (WM_USER + 1)
#define ID_TRAY_EXIT 1001
#define TRAY_ICON_UID 1

NOTIFYICONDATA nid;
HWND hwnd;
HMENU hMenu;
HANDLE hMutex; // 多重起動防止用

//========================= キーボードフック =========================
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode == HC_ACTION) {
        KBDLLHOOKSTRUCT* p = (KBDLLHOOKSTRUCT*)lParam;

        if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
            switch (p->vkCode) {
                case VK_F13:
                    f13_pressed = true;
                    return 1; // F13キーイベントをキャプチャしてシステムに渡さない
                // Home
                case 'A':
                    if (f13_pressed) {
                        keybd_event(VK_HOME, 0, 0, 0);
                        keybd_event(VK_HOME, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
                // End
                case 'E':
                    if (f13_pressed) {
                        keybd_event(VK_END, 0, 0, 0);
                        keybd_event(VK_END, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
                // Delete
                case 'D':
                    if (f13_pressed) {
                        keybd_event(VK_DELETE, 0, 0, 0);
                        keybd_event(VK_DELETE, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
                // Backspace
                case 'C':
                    if (f13_pressed) {
                        keybd_event(VK_BACK, 0, 0, 0);
                        keybd_event(VK_BACK, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
                // Left Arrow
                case 'J':
                    if (f13_pressed) {
                        keybd_event(VK_LEFT, 0, 0, 0);
                        keybd_event(VK_LEFT, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
                // Up Arrow
                case 'I':
                    if (f13_pressed) {
                        keybd_event(VK_UP, 0, 0, 0);
                        keybd_event(VK_UP, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
                // Down Arrow
                case 'K':
                    if (f13_pressed) {
                        keybd_event(VK_DOWN, 0, 0, 0);
                        keybd_event(VK_DOWN, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
                // Right Arrow
                case 'L':
                    if (f13_pressed) {
                        keybd_event(VK_RIGHT, 0, 0, 0);
                        keybd_event(VK_RIGHT, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
                // F7=カタカナ変換
                case 'N':
                    if (f13_pressed) {
                        keybd_event(VK_F7, 0, 0, 0);
                        keybd_event(VK_F7, 0, KEYEVENTF_KEYUP, 0);
                        return 1;
                    }
                    break;
            }
        }

        if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
            if (p->vkCode == VK_F13) {
                f13_pressed = false;
            }
        }
    }

    return CallNextHookEx(kbdHook, nCode, wParam, lParam);
}

//========================= マウスフック =========================
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode == HC_ACTION && f13_pressed && !sending_scroll && wParam == WM_MOUSEWHEEL) {
        MSLLHOOKSTRUCT* p = (MSLLHOOKSTRUCT*)lParam;
        int delta = GET_WHEEL_DELTA_WPARAM(p->mouseData);
        sending_scroll = true;

        // PageUp/PageDown を複数回送信(調整可能)
        for (int i = 0; i < 1; ++i) { // 1回だけ送信。個々の数値を変更すれば大きくスクロールできます。
            if (delta > 0) {
                keybd_event(VK_PRIOR, 0, 0, 0);         // PageUp
                keybd_event(VK_PRIOR, 0, KEYEVENTF_KEYUP, 0);
            } else {
                keybd_event(VK_NEXT, 0, 0, 0);           // PageDown
                keybd_event(VK_NEXT, 0, KEYEVENTF_KEYUP, 0);
            }
            Sleep(10); // 少し間隔を空けて安定させる
        }

        sending_scroll = false;
        return 1;  // 元のスクロールイベントをブロック
    }

    return CallNextHookEx(mouseHook, nCode, wParam, lParam);
}

//========================= ウィンドウプロシージャ =========================
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    if (msg == WM_TRAYICON && lParam == WM_RBUTTONUP) {
        POINT p;
        GetCursorPos(&p);
        SetForegroundWindow(hWnd);  // これがないとメニューがすぐ消えることがある
        TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, p.x, p.y, 0, hWnd, NULL);
    } else if (msg == WM_COMMAND && LOWORD(wParam) == ID_TRAY_EXIT) {
        Shell_NotifyIcon(NIM_DELETE, &nid);
        PostQuitMessage(0);
    } else if (msg == WM_DESTROY) {
        Shell_NotifyIcon(NIM_DELETE, &nid);
        PostQuitMessage(0);
    }

    return DefWindowProc(hWnd, msg, wParam, lParam);
}

//========================= エントリーポイント =========================
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int) {
    // 二重起動チェック:名前付きミューテックスを作る
    hMutex = CreateMutex(NULL, TRUE, L"F13KeyCustomMutex");
    if (GetLastError() == ERROR_ALREADY_EXISTS) {
        MessageBox(NULL, L"すでに起動しています。", L"F13 Key Custom", MB_OK | MB_ICONEXCLAMATION);
        return 0;
    }

    // ウィンドウクラス登録
    WNDCLASS wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = L"TrayWindowClass";
    RegisterClass(&wc);

    // メッセージウィンドウ作成(非表示)
    hwnd = CreateWindowEx(
        0,                     // 拡張スタイルなし
        L"TrayWindowClass",   // ウィンドウクラス名(さっき RegisterClass() で登録したやつ)
        L"",                   // ウィンドウタイトル(なし)
        0,                     // スタイル(表示しない)
        0, 0, 0, 0,            // x, y, width, height(全部ゼロ)
        NULL, NULL,            // 親ウィンドウなし、メニューなし
        hInstance,             // アプリケーションインスタンス
        NULL                   // 追加データなし
    );

    // HICON型の変数を用意(hCustomIcon はグローバルじゃなくてもOK)
    HICON hCustomIcon = (HICON)LoadImage(
        NULL,                // モジュールハンドル(NULLで外部ファイル)
        L"icon.ico",         // ファイル名(実行ファイルと同じ場所に置く)
        IMAGE_ICON,          // アイコンを読み込む
        0, 0,                // サイズ(0でデフォルトサイズ)
        LR_LOADFROMFILE | LR_DEFAULTSIZE  // ファイルから読み込み+自動サイズ
    );
    // 読み込めなかったらフォールバック(Windows標準アイコン)
    if (!hCustomIcon) {
        hCustomIcon = LoadIcon(NULL, IDI_APPLICATION);
    }

    // タスクトレイにアイコン追加
    nid.cbSize = sizeof(nid);
    nid.hWnd = hwnd;
    nid.uID = TRAY_ICON_UID;
    nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
    nid.uCallbackMessage = WM_TRAYICON;
    nid.hIcon = hCustomIcon; // カスタムアイコン
    lstrcpy(nid.szTip, L"keycustom.exe");
    Shell_NotifyIcon(NIM_ADD, &nid);

    // メニュー作成(右クリック用)
    hMenu = CreatePopupMenu();
    AppendMenu(hMenu, MF_STRING, ID_TRAY_EXIT, L"終了");

    // フック設定
    kbdHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, hInstance, 0);
    mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, hInstance, 0);

    // メッセージループ
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // クリーンアップ
    UnhookWindowsHookEx(kbdHook);
    UnhookWindowsHookEx(mouseHook);
    DestroyMenu(hMenu);
    Shell_NotifyIcon(NIM_DELETE, &nid); // タスクトレイからアイコン削除(必須)
    if (hCustomIcon) DestroyIcon(hCustomIcon); // 最後に解放(忘れるとメモリリークする)
    if (hMutex) CloseHandle(hMutex); // ミューテックス解放(重複起動対策)

    return 0;
}

4. g++でビルド(bashの場合のコマンド)

$ /c/winlibs/mingw64/bin/g++.exe main.cpp -o keycustom.exe -mwindows

5. build.bat作成

exe作成のためにbatを作成しました。
既存のexeがあれば削除してビルドしてくれます。

@echo off
taskkill /IM keycustom.exe /F >nul 2>nul
del keycustom.exe >nul 2>nul
"C:\winlibs\mingw64\bin\g++.exe" main.cpp -municode -o keycustom.exe -mwindows
pause

6. keycustom.exeを実行

これでC++に書いた通りのキー操作ができます。

  • keycustom.exeを停止したい場合は、タスクマネージャーから探して停止
  • keycustom.exeは再ビルドする前に削除しておくこと
  • exeが実行されているか確認するコマンド
    tasklist | findstr keycustom.exe
  • taskkillコマンド
    taskkill /PID {PID} /F

おわりに

C++を書いたこと無い自分でも数時間で実現できて、生成AIさまさまでした。
車輪の再発名という言葉はありますが、生成AIのおかげで短時間でツールを作ってみるのは良い経験だと思いました。
また自分のお気に入りのツール等が会社から禁止されたら自作してみようと思います。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?