Teamsを退席中にしないツール作った

TeamsをはじめとするいわゆるグループウェアってしばらくPCを操作しないと「退席中」と割とすぐに表示されてしまいます。実際は作業してるのに…みたいな状態にならないように退席中になるのを防止するツールを作ってみました。MouseKeeperってツールです。

ただ、ジョークアプリの類なので使用は自己責任でお願いします。

作成環境

以下環境で作成・動作確認を行っています。

  • Windows10
  • VisualStudio2019
  • .NET Frmaework 4.7.2 & C#8.0

概要

Github のリリースページにコードと実行形式のファイルを配置しています。使い方などは以下を参照ください。

github.com

このツールを起動すると以下の画像のようにタスクトレイ上で常駐アプリが起動します。1分間PCを操作しないと30秒ごとにマウスカーソルの移動を行ってPCを操作しているように見せかけます。

f:id:Takachan:20211217234450p:plain

技術的詳細

対象が Windows のみで C# なので .NET Framework 4.7.2 を保守的な意味で選択しています。このアプリを作成する際に使用したテクニックをいくつか紹介したいと思います。デスクトップアプリ技術は WPF ではなく Windows Form で作成しました。

タスクトレイにWinFormを常駐させる

これはテクニック的には有名だと思いますが、WinForm の NotifyIfon を使用します。フォームのデザイナーを表示しツールボックスから NotifyIfon をフォーム上にドラッグすると設定できます。

各プロパティの設定は以下の通りです。アイコンを設定しないとタスクバーにアプリが表示されないのでアイコンをアイコン形式で自作して設定しています。

コンテキストメニューを設定するとタスクトレイのアイコンを右クリックしたときにメニューが表示されるようになります。アプリの終了だけは必要なので終了ボタンだけついてるコンテキストメニューを合わせて設定しています。

f:id:Takachan:20211217235339p:plain

起動時にウインドウが表示されないようにする

ウインドウがアプリ起動時に表示されないようにするためは以下のように Main 関数を変更します。

Application.Run にフォームのインスタンスを渡すのではなく、フォームのインスタンスは生成するが、Application.Run にはインスタンスを渡さないようにするとウインドウが表示されないけどタスクバーにはアプリが常駐するようになります。

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    // 既存の処理はコメントアウト
    // Application.Run(new FormTaskTray());

    // 以下のように宣言する
    var form = new FormTaskTray();
    Application.Run();
}

この時、メインフォームの「ShowInTaskbar」のプロパティは「false」に設定する必要があります(これ設定しないと、ウインドウは表示されないのにタスクバーにはアイコンが表示されてめちゃくちゃ気持ち悪い感じになります)

マウス移動をOSに通知する

「一定時間操作が無ければマウスを移動する」という一番大事な処理ですが、WindowsForm の Cursor クラスに値を設定するとマウスポインターを移動できますがこれだとOSはマウスを動かしたと認識してくれません。したがって .NET の Cursor クラスの API でマウスを移動しても一定時間たつとスクリーンセーバーが起動したりディスプレイの電源が落ちてしまいます。

なので、 OS にマウス操作したことを通知するためには Win32 API を以下のように宣言して使用します。

// 宣言
[DllImport("USER32.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);

// 使用
mouse_event(1, 0, 0, 0, 0); // 現在位置から0の距離に移動

mouse_event の第一引数は MOUSEEVENTF_MOVE (0x01) で、2,3番目のパラメータが移動ピクセルですが、0を指定したら現在位置に移動という指定になります。これでOSに移動を指示したけど現在位置からポインターは移動しないという都合の良い処理ができます。

1分間操作しないと30秒ごとにマウスを動かす

一定時間操作しなかったらN秒間隔でマウス移動を行う処理ですが、フォームのタイマーを 500ms 周期で起動してタイマーのイベントハンドラ内に以下の通り処理を記述することで実現できます。

DateTime _lastMoved; // 最後に人間がマウスを移動した時間
DateTime _movedPos; // 最後にOSにマウス移動を通知した時間
Point _pos; // 前回のマウスポインターの位置

private void timer_Tick(object sender, EventArgs e)
{
    try
    {
        // 1分以上位置が変わらなかった場合30秒に一度ポインタを刺激する
        var now = DateTime.Now;

        var pos = Cursor.Position;
        if (_pos != pos)
        {
            _lastMoved = now;
        }
        _pos = pos;

        if (now - _lastMoved > TimeSpan.FromSeconds(60)) // 60秒操作しなかったら
        {
            if (now - _movedPos > TimeSpan.FromSeconds(30)) // 30秒ごとにマウス移動する
            {
                _movedPos = now;
                mouse_event(1, 0, 0, 0, 0); // OSにマウス位置の移動を通知
            }
        }
    }
    catch (Exception ex)
    {
        Trace.WriteLine(ex.ToString());
    }
}

以上です。