Seaside Laboratory

Posts

怪盗ロワイヤル用ウィンクツール作成記 (Win32 API 編)

ウィンクツール作成にあたり色々と調べたことを備忘録として書いておく。久しぶりの Windows プログラミングだった (DirectX だと Win32 API に触れる機会が少ない) ので、なかなか苦労した。

マウス操作

マウスカーソルの移動は SetCursorPos で簡単に移動できるのだが、何故か単純なクリック API は用意されていない。調べてみたところ、SendInput という API を使えばクリックができるらしく、MSDN によると、この API はキーボードからマウスまで扱える汎用的な作りで、どういう入力を行うかは引数の INPUT 構造体で指定する仕組みだった。

UINT SendInput
(
    UINT nInputs,    // 入力イベントの数
    LPINPUT pInputs, // 挿入する入力イベントの配列
    int cbSize       // 構造体のサイズ
);

引数にある INPUT 構造体については日本語マニュアルに解説がないので英語版を見ることになる。

typedef struct tagINPUT
{
    DWORD type;
    union
    {
        MOUSEINPUT mi;
        KEYBDINPUT ki;
        HARDWAREINPUT hi;
    };
} INPUT, *PINPUT;

そしてここで使われている MOUSEINPUT 構造体も日本語版の解説がない。

typedef struct tagMOUSEINPUT
{
    LONG dx;               // 水平位置または移動量
    LONG dy;               // 垂直位置または移動量
    DWORD mouseData;       // ホイールの移動
    DWORD dwFlags;         // 移動とクリックのオプション
    DWORD time;
    ULONG_PTR dwExtraInfo; // アプリケーション定義の情報
} MOUSEINPUT, *PMOUSEINPUT;

ただ、mouse_event という API の引数がこの構造体と一部同じっぽいので、日本語版の mouse_event を参照するとある程度意味が掴める。

マウス操作の度に SendInput を呼び出すのは辛いので、ネット上のサンプルプログラムを参考にしながらラッパー関数を作成した。

// マウスをクリック
void ClickMouse()
{
    INPUT inputs[] =
    {
        { INPUT_MOUSE, { 0, 0, 0, MOUSEEVENTF_LEFTDOWN, 0, 0 } },
        { INPUT_MOUSE, { 0, 0, 0, MOUSEEVENTF_LEFTUP  , 0, 0 } }
    };

    ::SendInput( 2, inputs, sizeof( INPUT ) );
}

クリックは「押す」だけと思いがちだが「押す」と「離す」がセットになって初めて成立する。

次はマウスの移動。

// マウスを移動
void MoveMouse( int nX, int nY )
{
    int dx = nX * 65535 / GetSystemMetrics( SM_CXSCREEN );
    int dy = nY * 65535 / GetSystemMetrics( SM_CYSCREEN );

    INPUT inputs[] =
    {
        { INPUT_MOUSE, { dx, dy, 0, MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE, 0, 0 } }
    };
    ::SendInput( 1, inputs, sizeof( INPUT ) );
}

SetCursorPos を使わずに自作関数を用意したのは、SendInput と混在させたときにうまく動かなかったため。おそらく、異なる API を混在させると移動とクリックの前後関係が保証されなくなってしまうのが原因と思われる。

あと特殊なのは、関数の頭の方で dx と dy を計算しているところ。

MOUSEEVENTF_ABSOLUTE フラグを指定した場合、dx と dy の各パラメータは 0 ~ 65,535 の範囲で、正規化された絶対座標を保持しています。イベントプロシージャは、これらの座標を表示サーフェスの座標へマップ(変換)します。座標 (0,0) は表示サーフェスの左上隅、(65535,65535) は表示サーフェスの右下隅へマップされます。

という仕様なので、現在の座標を画面の大きさで割って割合を算出し、それを 65535 でスケーリングしてやると API が求めている値になる。

キーボード操作

暫くしてからマウス操作版のウィンクツールを起動してみたら使い物にならなくなっていた。原因は運営による不正対策とかではなく、単純に画面レイアウトのリニューアル。ウィンクツールは決められた場所を一定間隔でクリックするだけなので、デザイン変更にはめっぽう弱いという欠点がある。

そこで「キーボードを使ってブラウジングすれば位置とか関係ないやん!」と思い立ち、キーボード版のウィンクツールを作成することに。前回のマウスと同様に SendInput を使ってキーメッセージを送信するのだが、MOUSEINPUT 構造体のときと同様に KEYBDINPUT 構造体も解説が乏しいので keybd_event API の引数を参考にする。

typedef struct tagKEYBDINPUT
{
    WORD wVk;              // 仮想キーコード
    WORD wScan;            // ハードウェアスキャンコード
    DWORD dwFlags;         // オプション
    DWORD time;
    ULONG_PTR dwExtraInfo; // 追加のキーストロークデータ
} KEYBDINPUT, *PKEYBDINPUT;

構造体の各メンバは癖が強く、何を設定すれば正解なのかよくわからない。ネット上のサンプルを色々と見比べてとりあえず以下のコードを作成。

void PushKey( WORD wVk )
{
    INPUT input;

    input.type           = INPUT_KEYBOARD;
    input.ki.wVk         = wVk;
    input.ki.wScan       = ::MapVirtualKey( wVk, 0 );
    input.ki.dwFlags     = 0;
    input.ki.time        = 0;
    input.ki.dwExtraInfo = ::GetMessageExtraInfo();

    ::SendInput( 1, &input, sizeof( INPUT ) );
}

最初は構造体宣言時に各メンバを丸ごと初期化する方法をとっていたが、何故か動かなかったので各メンバをひとつずつ初期化する方式に変更した。もしかすると内部的なメンバの並びがヘルプとは違っていたのかもしれない。

出来上がったキーボード関数は以下のような感じで使う。

void CClicker::PushUpdateButton()
{
    PushKey( VK_F5 );
}

F5 キーはページ更新のショートカットなので、PushUpdateButton を呼べばページが更新される。こんな調子でショートカットキーと機能を対応付けていけばキーボード版のウィンクツールが完成する。

早速動かしてみたところ、まったく関係ないページを激しく更新したりと挙動がおかしい。よく見てみると、目的のリンクの手前にあるリンクの数が一定ではなく、関係ないページに飛んでいた。

囲ってある部分のリンクの数が変化する。

結論。キーボードもダメだった。