見出し画像

25行で書けてちょっと便利なマウスジェスチャーソフト


はじめに

株式会社SHIFT DevOps推進部からシステムアイに出向中の水谷です。

マウスカーソルを特定のパターンで動かすことで何か動作を実行する仕組みのことを、マウスジェスチャー(マウスジェスチャー機能)とか言ったりしますが、よく行う操作が(キーボード操作などもなしに)ささっと簡単に実行できるとやっぱり便利ですよね。

マウスジェスチャーを実現するアプリもいろいろありますが、実は私もつい28年ほど前(四半世紀以上前w)にWindows用のマウスジェスチャーアプリを作って公開していたことがあります(使用した言語はPascalだったw)。そのアプリはマウスポインタが画面上で円を描いたことを検出し、そのタイミングで指定しておいたアプリを立ち上げたり、ブラウザの戻るボタンが押したりする、というものでした。

例えば、メモを取りたいときはさっとマウスを回して円(というか丸、以降「〇」と書きます)を描けば、テキストエディタが起動してくれてすぐにメモを取り始めることができたし、逆向きに回せば最前面のウィンドウを最小化してくれて、後ろにあったアプリをすぐ操作できるなど、うまく使えばそれなりに便利なツールだったかなと思います。

そんな昔作ったアプリを最近またちょっと使いたくなったので、アルゴリズムを思い出しながら簡単に再実装してみようかと思います。

〇の検出方法

まずは核となる「〇」の検出方法ですが、皆さんはどのようなアルゴリズムが思い浮かびますでしょうか?

例えば、マウスの軌跡を画像として記録していって、用意しておいた〇のパターンとマッチングさせる、とか、AIでいろんな形の〇の画像を学習させておいて推論させる……、とかが考えられますね。

しかし、当時はPCの性能も低くてパターンマッチングはとてもコストが高く、AIのフレームワークもなかったので、そんな実装はもちろんできませんでした。

で、当時思い付いたアルゴリズムが、マウスの移動方向を4方向に集約(単純化)し、その方向がどう変化したかを見る、というものでした。

下のように、マウスが止まっている状態を⓪、左上に向かって移動している状態を①、右上に移動している状態を②……、と記述することにします。なお、この際 x, y の値が増加したか減少したかだけに注目し、角度や距離は考慮しないことがポイントです。

前回のマウスの位置を原点として動いた方向を①②③④に分類。移動がなかった場合は⓪。

こうすると、マウスが時計の12時の位置から時計回りに〇を描くことは「状態が④③①②と順に変化すること」と表現することができます。

同様に、6時の位置から反時計回りに〇を描くことは、「状態が②①③④と変化すること」と表現できます。

かなり極端な単純化に見えますが、〇を描くときは必ず④③①②(あるいは②①③④など)のような状態変化が伴いますので、この単純化を行っても、〇を描いたという情報は残っていることになります。そもそも人間の手によるマウス操作はかなり不正確で、ペンで紙に〇を描くときは真ん丸に描けても、マウスで〇を描く場合はいびつな〇になることが多かったりしますし、そもそも簡単なマウスジェスチャーの実装には正確な軌跡情報は不要だ、と言うこともできるかと思います。

もちろん、例えば12時の位置から反時計回りに〇を描いても、数字の「6」を描いても、状態は③④②①と変化するため、これが「6」なのか〇なのかの判別はできません。このあたりも判断できるようにするにはもう少し工夫が必要ですが、今回は単純なマウスジェスチャーアプリなので、そこは気にせず行きますw

ところで、④③①②と状態が変化した時点で〇と判断する場合、12時の位置から動き始めて、9時を超えたところで〇と判断することになります。これでもいいのですが、少し誤検出率(誤動作率)が高くなるので、再度12時を超えたときに〇を描き終えたと判断したいところです。これは、12時を超えて、再度④の方向に動き始めた時点、つまり状態が④③①②④と変化した時点で〇と判断すればOK、ということになります。

最小限の実装

では、C言語でできるだけ簡単に実装してみましょう。

まずは、Visual Studioのコミュニティエディションなどを使って、Win32コンソールアプリのソリューションを作成します。そして、簡単のためコードはmain関数内だけに記述していきます。

マウスの位置取得は、Win32 APIの GetCursorPos を使用しました。他の言語で実装する場合も、同じ機能を持つ関数が簡単に見つけられると思います。

マウスの位置が取得できたら、前回の値(位置)と比較し、⓪(移動無し)なのか①~④方向への移動なのか判定します。⓪ではない場合に、①~④のどれなのかを求めるには、

(UINT)(pos.y > oldpos.y) * 2 + (UINT)(pos.x > oldpos.x) + 1

この1行いけますね(posが今のマウスの位置、oldposが前回のマウスの位置)。もちろん1行で書かずに、if文を並べても良いのですが、ここは好みの問題ですね(?)。

次に、この得られた状態が前回の状態から変わっているかどうかを調べます。状態が変わっていない(引き続き右上方向に動き続けている場合、など)場合は、なにも行いません。逆に状態が変わった場合は、状態の履歴に最新の状態を追加します。

ここで、履歴の管理はC++のvectorにpush_backしていく方法などを使ってもよかったのですが、今回は(自分の好みのやり方で)、下のように1つのUINT値(32bit符号なし整数)内に履歴を4bitのデータとして並べていくことにします。

新しい状態が発生した場合は、この値を4bit左にシフトし、一番下に最新の状態を入れます。最新の状態と過去の状態4つを記録しておけば十分なので、上位12bitは不使用です(後の処理を簡単にするため、コードでは 0xfffffとANDをとって上位12bitは常に0にしています)。

そして、今の状態を含めた5つの状態が、④③①②④などの〇を描いた時に現れる特定のパターンかどうかをチェックします。上のように4bit単位で格納しているので、④③①②④のパターンかどうかを調べるには、上のUNITの変数の値が 0x43124 かどうかをif文で比較すればOKとなりますね。なお、「マウスで〇を描いてください」と言われても、上を起点に(12時を起点に)描く人もいれば、下を起点(6時を起点に)描く人もいるので、どちらにも対応できるようにしています。

あとは、そのif文の中でプログラムを起動するなどのアクションを記述します。ここでは簡単のため、時計回りの〇の場合にはメモ帳を起動し、反時計回りの〇の場合には電卓を起動することにしたため、コードは以下のようになりました。

#include "windows.h"

int main()
{
    UINT directionHistory = 0;
    POINT pos, oldpos = { 0, 0 };
    while (true) {
        GetCursorPos(&pos);
        UINT direction = 0;
        if ((pos.x != oldpos.x) || (pos.y != oldpos.y)) 
            direction = (UINT)(pos.y > oldpos.y) * 2 + (UINT)(pos.x > oldpos.x) + 1;
        if (direction != (directionHistory & 0xf)) 
            directionHistory = ((directionHistory << 4) + direction) & 0xfffff;
        if (directionHistory == 0x43124 || directionHistory == 0x12431) {
            WinExec("notepad.exe", SW_SHOW);
            directionHistory = 0;
        }
        if (directionHistory == 0x34213 || directionHistory == 0x21342) {
            WinExec("calc.exe", SW_SHOW);
            directionHistory = 0;
        }
        oldpos = pos;
        Sleep(50);
    }
}

少し良くするには

28年前に作ったアプリの場合は、アクションの選択やカスタマイズができたり、誤動作を減らすため、小さい〇や平べったい〇ではアクションを起こさないようにする機能や、画面のどこで(右上の方とか)〇を描くかでアクションを変えられたり、あとはSHIFTキーやCTRLキーが押されている状態で〇を描いた場合は別のアクションを起こすようにする、などの機能を持っていました。

ま、どれもそれほどすぐ欲しいと思うような機能ではないですねw このあたりは使いながら欲しくなったら実装していこうかなと思ってますが、1つどうしても欲しいアクションがあって、まずはそれを実装しようかと思っています。

最近各種オンラインミーティングアプリ(Zoomとか、WebEXとか、Teamsミーティング、それからGoogle Meetなど)のミュート機能がみんな違うキーボード操作に割り当てられていたり、ボタンの位置が違っていますよね。仕事柄いろんなミーティングソフトを使うので、キーボードショートカットも覚えられないし、ボタンを探してクリックするのも面倒なので、マウスを左回転するだけでどんなオンラインミーティングアプリでもミュートしたりミュート解除のトグル動作ができるようなアクションを作りたいな、と思っていたりします。実装はちょっと面倒くさそうだけど、価値はあるかなと。

――――――――――――――――――――――――――――――――――

執筆者プロフィール:水谷 裕一
大手外資系IT企業で15年間テストエンジニアとして、多数のプロジェクトでテストの自動化作業を経験。その後画像処理系ベンチャーを経てSHIFTに自動化エンジニアとして入社。
SHIFTでは、テストの自動化案件を2件こなした後、株式会社リアルグローブ・オートメーティッド(RGA)に出向、現在システム・アイに出向中。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/