WinFormsアプリケーション向けに「指定時間操作されなければログアウトする機能」を自作しましたので、実装方法をまとめます。
2025年5月8日 木曜
色々と作りに粗があったので、かなり修正しました。
Formに依存せず、アプリケーション全体でタイマー監視するように修正してます。
- ユーザー操作(マウス・キー)を監視
- 指定時間操作が無ければ自動的にログアウト
- IMessageFilter でグローバルに操作を監視
- ApplicationContext によるアプリ管理
業務アプリにも安心して使えるレベルを目指しました✨
0.イメージ図
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);
}
InactivityMonitorManager は InactivityMonitor をシングルトンで起動するためのファクトリーの役割です。
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の採用が一番有効でした🌸
