WheelBallをAutoHotKeyで再現する

トラックボーラーにとって必須ソフトウェアである[WheelBall](http://www.kamigaki.net/wheelball.htm)ですが、
だいぶ昔のソフトウェアということもあり、Windows10では動作が不安定です。
(作者のkamigamiさんも更新予定はないと表明しています。 http://www.kamigaki.net/blog/?p=12)

そこで、後発の[W10Wheel.NET](https://github.com/ykon/w10wheel.net)が開発されています。
このソフトウェアを利用しようとしたのですが、社内のセキュリティソフトに引っかかってしまいました :sob:

ということで、なければ作れば良いじゃないの精神で`AutoHotKey`を使って再現してみました :sunglasses:

※ 自分が使える分の機能をそれらしく再現しただけで完全再現ではないです
※ コードだけみたい方は[こちら](https://github.com/totto357/AHKWheelBall)


## 要件

`WheelBall`のようなスクロールをエミュレートするソフトウェアに求めている要件は以下のものです。

- マウスの右ボタンを押している間にカーソル(ボール)を動かすとスクロール扱いになる
    - 公開するなら設定できるようにしたほうが良いが、まずは自分用に右クリック固定
- スクロール中はカーソルが動かない
- カーソルの移動量とスクロールの移動量が連動する
- 横方向のスクロールに対応
    - エクセルでできなくても良い(そこまで使わないので)
- 縦スクロールと横スクロールは移動量が多い方を使う
- 右クリック中にボールが動かされない場合は、通常の右クリックとする


## 既存の実装

ゼロベースから作り上げるのは大変なので、`AutoHotKey`で似たようなものがないか探してみました。
すると以下のページがヒットしました。

https://autohotkey.com/board/topic/46203-mouse-wheel-emulator/

使ってみたところやりたいことはほとんど同じで、これを改良すれば望むものが得られそうです。
ここから改良すべき点は以下の3点。

- カーソルの移動量とスクロールの移動量が連動する
- 縦スクロールと横スクロールは移動量が多い方を使う
- 右クリック中にボールが動かされない場合は、通常の右クリックとする


## カーソルの移動量とスクロールの移動量が連動する

まずは使用感に大きく作用する移動量に手を入れていきます。

既存の`MouseWheelEmulator`は移動量に応じてスクロール量を変えているのではなく、
マウスの移動イベントを一定時間毎に検知し、その間の移動量が閾値を超えたら1回スクロールイベントを発火するという実装になっています。

具体的には96行目からの`InputMsg(wParam, lParam)`メソッドで行なわれています。

```
InputMsg(wParam, lParam) {
    local x, y
    Critical
    
    x := HID_GetInputInfo(lParam, II_MSE_LASTX)
    y := HID_GetInputInfo(lParam, II_MSE_LASTY)
    If ((Abs(x) > 0.0) or (Abs(y) > 0.0))
       mouse_Moved = y
    if y > %mouse_Threshold%
    {
        ScrollDown()
    }
    else if y < -%mouse_Threshold%
        ScrollUp()
    if x > %mouse_Threshold%
    {
        ScrollRight()
    }
    else if x < -%mouse_Threshold%
        ScrollLeft()
    ;ToolTip, % "dX = " . x . "  " . "dY = " . y . a_tab . winID . a_tab . control . a_tab . procName . a_tab . hw_m_target . a_tab . scrollMode
    ;; Uncomment the above line for handy debug info shown while scrolling
}
```

※ `InputMsg`が呼び出されるタイミング、頻度は外部依存です。

このため、移動量に関わらず閾値を超えていれば一定のスクロールをするという動きになっています。
これを移動量を加味した計算に変更します。

具体的にはカーソルの移動量をある定数で割った値でスクロール量を決めます。

以下が修正後のコードです。(コードフォーマットが少し変わっています)

```
InputMsg(wParam, lParam) {
  ; 省略

  if y > %mouse_Threshold%
    ;; 移動量に重み付けしてスクロールイベントを発火
    loop, % Abs(1 + y//mouse_delta)
      ScrollDown()
  else if y < -%mouse_Threshold%
    loop, % Abs(-1 + y//mouse_delta)
      ScrollUp()

  if x > %mouse_Threshold%
    loop, % Abs(1 + x//mouse_delta)
      ScrollRight()
  else if x < -%mouse_Threshold%
    loop, % Abs(-1 + x//mouse_delta)
      ScrollLeft()

  ; ToolTip, % "dX = " . x . " " . "dY = " . dy . a_tab . winID . a_tab . control . a_tab . procName . a_tab . hw_m_target . a_tab . scrollMode
  ;; Uncomment the above line for handy debug info shown while scrolling
}
```

`mouse_delta`が重み付けの定数です。
これで移動量が加味されるようになりました!


## 縦スクロールと横スクロールは移動量が多い方を使う

既存の実装では斜めに動かすと縦横両方のスクロールイベントが発火します。
ブラウザによっては横スクロールでブラウザバックを発火させてしまうので、移動量が多い方のみを選択するようにします。
これも`InputMsg`を修正します。
スクロールイベントを発火する前に、移動量の大きい方のみを選択します。

```
InputMsg(wParam, lParam) {
  ; 省略

  ;; ここで大きい方を判断する
  if Abs(x) > Abs(y)
  {
    if x > %mouse_Threshold%
      loop, % Abs(1 + x//mouse_delta)
        ScrollRight()
    else if x < -%mouse_Threshold%
      loop, % Abs(-1 + x//mouse_delta)
        ScrollLeft()
  }
  else
  {
    if y > %mouse_Threshold%
      loop, % Abs(1 + y//mouse_delta)
        ScrollDown()
    else if y < -%mouse_Threshold%
      loop, % Abs(-1 + y//mouse_delta)
        ScrollUp()
  }

  ; ToolTip, % "dX = " . x . " " . "dY = " . dy . a_tab . winID . a_tab . control . a_tab . procName . a_tab . hw_m_target . a_tab . scrollMode
  ;; Uncomment the above line for handy debug info shown while scrolling
}
```

## 右クリック中にカーソルが動かされない場合は、通常の右クリックとする

右クリックの長押しをトリガーにしていますが、通常の右クリックが使えなくなっては困ります。
そこで、右クリック押している間にカーソルが動かなかったら、右クリックのイベントを発火させます。

既存の実装では動きがなかった場合に`Middle`ボタンのクリックイベントを発火しています。
(88行目からのコード)

```
scrollChord_Up:
ToolTip
BlockInput, MouseMoveOff
HID_Register(1,2,0,RIDEV_REMOVE)
if mouse_Moved = n
    MouseClick, Middle
return
```

これを右クリックに変更します。

```
scrollChord_Up:
  ; 省略
  if mouse_Moved = n
    ;; MiddleからRIGHTに変更
    MouseClick, RIGHT
return
```


## 細かい調整

これまでの修正でおおむね自分のやりたいことは再現できたのですが、
ボールの移動量に対してスクロールが早すぎるので調整していきます。

調整するのは`InputMsg`メソッドの間引きと、移動量のスクロールイベント数に対する重み付けです。
`InputMsg`メソッドの呼び出しを間引くために、メソッドの最後に`Sleep`を挿入します。

```
InputMsg(wParam, lParam) {
  ; 省略

  ; ToolTip, % "dX = " . x . " " . "dY = " . dy . a_tab . winID . a_tab . control . a_tab . procName . a_tab . hw_m_target . a_tab . scrollMode
  ;; Uncomment the above line for handy debug info shown while scrolling

  ; 一定時間スリープさせてスクロールイベントの発火数を押さえる
  Sleep, % sleep_interval
}
```

あとは`mouse_delta`と`sleep_interval`を調整していきます。
地味な作業ですが、数値を動かしながら試していく他ありません。

最終的には`mouse_delta`を`3`、`sleep_interval`を`30`にすることで落ち着きました。
また、スクロールイベント発火の閾値(`mouse_Threshold`)も`3`から`1`に変更しています。

## おわり

今回は`AutoHotKey`を使って`WheelBall`を再現してみました。
スクロールイベントのエミュレートまでできるなんで、本当に`AutoHotKey`は奥が深いですね。

最終的なコードは[AHKWheelBall](https://github.com/totto357/AHKWheelBall)においてありますので、よかったら使ってみてください。 (プルリク大歓迎です。)
使う際はご使用の環境に合わせて調整してみてください。


ちなみに自分は[CST2545-5W](http://www.diatec.co.jp/shop/det.php?prod_c=3590)というトラックボールでビリヤードの5番を使っています。
(オレンジが好き)