【C#】Teamsを退席中にしないツールを作ってみた

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

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

作成環境

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

  • Windows10、Windows11
  • VisualStudio2019、VisualStudio2022
  • .NET Frmaework 4.7.2 & C# 7.2
  • .NET Framework 4.8.1 & C# 7.3

1.1.0 をリリースしたので VS2022 と .NET 4.8 に対応バージョンを変更しました。

1.2.0 をリリースしたので VS2022 と .NET 4.8.1 に対応バージョンを変更しました。

ダウンロード

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

github.com

このツールを起動すると以下の画像のようにタスクトレイにアプリが常駐し始めます。アプリが起動した状態で 1分間 PC を操作しないとアプリが 30秒ごとにマウスカーソルの移動を行ってPCをユーザーが操作しているように偽装します。

技術的詳細

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

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

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

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

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

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

ウインドウがアプリ起動時に表示されないようにするためは以下のように 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に移動を指示したけど現在位置からポインターは移動しないという都合の良い処理ができます。

終了した後にアイコンが残らないようにする

アプリを終了した後にタスクトレイにアイコンが残らないようにタスクトレイ用のフォームのインスタンスは自分で Dispose しましょう。そうでないと終了後までアイコンがタスクトレイに残ってマウスオーバーした時に消える不思議な挙動になります。

using (FormTaskTray formTaskTray = new FormTaskTray()) // ★Disposeすること
{
    Application.Run();
}

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());
    }
}

以上です。