2025 WinForms 無操作→自動ログアウトを自作する

WinFormsアプリケーション向けに「指定時間操作されなければログアウトする機能」を自作しましたので、実装方法をまとめます。

:::note alert
2025年5月8日 木曜
色々と作りに粗があったので、かなり修正しました。
Formに依存せず、アプリケーション全体でタイマー監視するように修正してます。
:::

:::note 
* ユーザー操作(マウス・キー)を監視
* 指定時間操作が無ければ自動的にログアウト
* IMessageFilter でグローバルに操作を監視
* ApplicationContext によるアプリ管理
:::
業務アプリにも安心して使えるレベルを目指しました✨

## ０．イメージ図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/3e303da6-9848-4705-a706-482512bd7fed.png)

## １．前提条件
* Visual studio 2022 Version 17.13.6
* .Net 9

:::note 
なお、すべてのソースコードを公開しています。
:::
https://github.com/masayahak/AutoLogout2

なお旧ソースは以下です
https://github.com/masayahak/AutoLogout

## ２．なぜ IMessageFilter を選んだのか？ 🎯

Windowsアプリで「ユーザー操作の監視」を行う手段は複数ありますが、本記事では IMessageFilter を採用しました。その理由を説明します。

### (1) Win32 API を使った DLL ベースのグローバルフックは危険
グローバルにマウスやキーボードの操作をフックするには、SetWindowsHookEx を使って DLL を別プロセスに挿入する方式があります。
ですが、これは以下の理由から本プロジェクトでは採用しませんでした：

:::note alert
* 管理者権限が必要な場合がある（UAC との相性が悪い）
* アンチウイルスソフトに引っかかる可能性がある
* アプリがクラッシュした際に OS 全体に影響するリスクがある
* コードが煩雑で保守性が下がる
:::
つまり、業務アプリのような 安定性と信頼性が求められるシステム では不適です。

### (2) すべてのコントロールにイベントハンドラを設定するのは重い
当初、以下のような実装をしていました：

```C#
foreach (Control ctrl in parent.Controls)
{
    ctrl.MouseDown += OnMouseDown;
    ctrl.KeyDown += OnKeyDown;
    ...
}
```
しかしこの方式では以下の課題があります：

:::note alert
* フォーム内のコントロールが多いと、イベントフック数が指数的に増える
* 一部の動的に追加されるコントロールが漏れる可能性がある
* ループが深くなり、起動時や画面切り替え時のパフォーマンスが落ちる
:::
これは UI が複雑になるほど問題になっていきます。

### (3) IMessageFilter のメリット
代わりに採用した IMessageFilter は以下の点で優れています：

:::note
* すべての WinForms メッセージを低コストで横取りできる
* キーボードやマウスのクリック操作を簡単に検知できる
* フォームやコントロールの構造に依存せず、一括で操作を監視できる
* 軽量で安定性も高く、商用利用にも安心
:::

この方法で、「コントロール数が多いと重くなる」「マウス・キーボードの操作を正確に拾えない」といった課題を すべて解消 できます。


## ３．実装ポイント 

### A) IMessageFilter の導入

```csharp
// ユーザーの操作を監視するフィルタ
// 監視対象は
//  キーボードのキー押下、マウスの左ボタン、右ボタン、ホイールのクリック
// （負荷軽減のためあえてマウスの移動は検知しない）
public class UserActivityMessageFilter : IMessageFilter
{
    public bool PreFilterMessage(ref Message m)
    {
        const int WM_KEYDOWN = 0x0100;
        const int WM_LBUTTONDOWN = 0x0201;
        const int WM_RBUTTONDOWN = 0x0204;
        const int WM_MBUTTONDOWN = 0x0207;

        switch (m.Msg)
        {
            case WM_KEYDOWN:
            case WM_LBUTTONDOWN:
            case WM_RBUTTONDOWN:
            case WM_MBUTTONDOWN:
                UserActivityTracker.Update();
                break;
        }

        return false; // 他の処理も続けて行う
    }
}
```


### B) アクティビティ監視の共通クラス

```csharp
public static class UserActivityTracker
{
    public static DateTime LastActionTime { get; private set; } = DateTime.Now;

    public static void Update()
    {
        LastActionTime = DateTime.Now;
    }

    public static bool IsInactive(TimeSpan timeout)
    {
        return DateTime.Now - LastActionTime > timeout;
    }
}
```

### C) タイムアウトの監視をInactivityMonitorで行う
```csharp
public class InactivityMonitor
{
    private readonly Timer _timer;
    private readonly int _interval;
    private readonly TimeSpan _timeout;

    private static bool _filterAdded = false;

    public InactivityMonitor(int interval, TimeSpan timeout)
    {
        _timeout = timeout;
        _interval = interval;

        _timer = new Timer();
        _timer.Interval = _interval * 1000;
        _timer.Tick += (s, e) => CheckTimeout();
        _timer.Start();

        // フィルターは一度だけ登録する
        if (!_filterAdded)
        {
            Application.AddMessageFilter(new UserActivityMessageFilter());
            _filterAdded = true;
        }
    }

    public void Stop ()
    {
        _timer.Stop();
    }

    private void CheckTimeout()
    {
        if (UserActivityTracker.IsInactive(_timeout))
        {
            _timer.Stop();
            Program.AppContextInstance?.ShowLoginAndCloseOthers();
        }
    }
}
```
🔸この中の **Application.AddMessageFilter(new UserActivityMessageFilter())** の行が、ユーザー操作の検知の要です。


### D) 監視をシングルトンで起動する
```csharp
[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    var loginForm = new FormLogin();
    AppContextInstance = new MyAppContext(loginForm);

    // 起動時に1回だけ監視を開始
    InactivityMonitorManager.Start(InactivityCheckIntervalSeconds, InactivityTimeout);

    Application.Run(AppContextInstance);
}
```

**InactivityMonitorManager** は **InactivityMonitor** をシングルトンで起動するためのファクトリーの役割です。

```csharp
public static class InactivityMonitorManager
{
    private static InactivityMonitor? _instance;

    public static void Start(int interval, TimeSpan timeout)
    {
        if (_instance != null)
        {
            _instance.Stop(); // 旧インスタンスが動いていたら止める
        }

        // _instanceをstaticにしアプリ全体で１つだけ持ちたい。複数起動させない。
        _instance = new InactivityMonitor(interval, timeout);
    }
}
```

### E) 自動ログアウト処理（ApplicationContextの役割）
```csharp
public class MyAppContext : ApplicationContext
{
    public MyAppContext(FormLogin loginForm)
    {
        loginForm.Show();
    }

    // 新しいログインフォームを表示し、他のフォームを全て閉じる
    public void ShowLoginAndCloseOthers()
    {
        // ログアウト
        CurrentUser.Logout();

        // ログインフォームは常にNEWする
        var loginForm = new FormLogin();
        loginForm.Show();

        foreach (Form f in Application.OpenForms.Cast<Form>().ToList())
        {
            // 過去のログインフォームが残っていても、NEWしたインスタンス以外は閉じる
            if (f != loginForm)
                f.Close();
        }

        // 最終操作時刻を現在時刻に更新（再スタート時の即タイムアウトを防ぐ）
        UserActivityTracker.Update();

        InactivityMonitorManager.Start(
            Program.InactivityCheckIntervalSeconds,
            Program.InactivityTimeout
        );
    }
}
```

## ４．最終構成 📦

* InactivityMonitor.cs（タイマー監視用）
* InactivityMonitorManager.cs（タイマー監視をシングルトンで起動するマネージャー）
* MyAppContext.cs（ログアウト処理）
* UserActivityMessageFilter.cs（ユーザーの操作を監視するフィルタ）
* UserActivityTracker.cs（ユーザー操作の間隔を取得）

## ５．最後に 📝

この構成は、
✅ シンプル
✅ 軽量
✅ 実務アプリでも安定
をすべて両立できることを目指しました。

色々試した結果、IMessageFilterの採用が一番有効でした🌸