4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-03

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

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

  • ユーザー操作(マウス・キー)を監視
  • 指定時間操作が無ければ自動的にログアウト
  • IMessageFilter でグローバルに操作を監視
  • ApplicationContext によるアプリ管理

業務アプリにも安心して使えるレベルを目指しました✨

0.イメージ図

image.png

1.前提条件

  • Visual studio 2022 Version 17.13.6
  • .Net 9

なお、すべてのソースコードを公開しています。

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

2.なぜ IMessageFilter を選んだのか? 🎯

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

(1) Win32 API を使った DLL ベースのグローバルフックは危険

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

  • 管理者権限が必要な場合がある(UAC との相性が悪い)
  • アンチウイルスソフトに引っかかる可能性がある
  • アプリがクラッシュした際に OS 全体に影響するリスクがある
  • コードが煩雑で保守性が下がる

つまり、業務アプリのような 安定性と信頼性が求められるシステム では不適です。

(2) すべてのコントロールにイベントハンドラを設定するのは重い

当初、以下のような実装をしていました:

foreach (Control ctrl in parent.Controls)
{
    ctrl.MouseDown += OnMouseDown;
    ctrl.KeyDown += OnKeyDown;
    ...
}

しかしこの方式では以下の課題があります:

  • フォーム内のコントロールが多いと、イベントフック数が指数的に増える
  • 一部の動的に追加されるコントロールが漏れる可能性がある
  • ループが深くなり、起動時や画面切り替え時のパフォーマンスが落ちる

これは UI が複雑になるほど問題になっていきます。

(3) IMessageFilter のメリット

代わりに採用した IMessageFilter は以下の点で優れています:

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

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

3.実装ポイント

A) IMessageFilter の導入

// ユーザーの操作を監視するフィルタ
// 監視対象は
//  キーボードのキー押下、マウスの左ボタン、右ボタン、ホイールのクリック
// (負荷軽減のためあえてマウスの移動は検知しない)
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) アクティビティ監視の共通クラス

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で行う

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) 監視をシングルトンで起動する

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

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

public static class InactivityMonitorManager
{
    private static InactivityMonitor? _instance;

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

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

E) 自動ログアウト処理(ApplicationContextの役割)

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

4.最終構成 📦

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

5.最後に 📝

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

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

4
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?