首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 捕獲 Windows 關機/登出事件:SystemEvents.SessionEnding 實戰與記憶體優化

     編輯:關於C語言

需求背景

下班打卡常忘?寫一個常駐小工具,**在 Windows 關機、重啟、登出前彈窗提醒「您簽退了嗎?」**,若使用者選「否」則取消關機流程。核心技術點:Microsoft.Win32.SystemEvents.SessionEnding 事件。

核心限制與前置條件

  • ✅ **僅限 WinForms / WPF** 等有訊息迴圈的 UI 應用程式有效。
  • ❌ **主控台應用程式無法觸發**(缺乏訊息泵)。
  • ⚠️ **必須在程式結束時移除事件訂閱**(`-=`),否則會造成記憶體洩漏(靜態事件持有表單實例參考,阻礙 GC)。
  • ❌ **無法捕獲「休眠/喚醒」事件**(需用 `PowerModeChanged`)。

完整實作程式碼(WinForms .NET Framework / .NET 6+)

Program.cs — 入口點與事件訂閱

using System;
using System.Windows.Forms;
using Microsoft.Win32;

namespace ShutdownReminder
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var mainForm = new MainForm();

            // 訂閱 SessionEnding 事件(靜態事件,必須手動移除)
            SystemEvents.SessionEnding += mainForm.OnSessionEnding;

            Application.Run(mainForm);

            // 程式正常結束時移除訂閱(防記憶體洩漏)
            SystemEvents.SessionEnding -= mainForm.OnSessionEnding;
        }
    }
}

MainForm.cs — 表單邏輯與事件處理

using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.Win32;

namespace ShutdownReminder
{
    public partial class MainForm : Form
    {
        private const string MsgText = "您簽退了嗎?";
        private const string MsgTitle = "下班提醒";

        public MainForm()
        {
            InitializeComponent();
            // 可選:啟動時隱藏至系統匣
            this.Load += (s, e) => 
            {
                this.WindowState = FormWindowState.Minimized;
                this.ShowInTaskbar = false;
                notifyIcon.Visible = true;
            };
        }

        /// 
        /// 系統關機/登出前觸發
        /// 
        internal void OnSessionEnding(object sender, SessionEndingEventArgs e)
        {
            // 僅在關機/重啟/登出時提醒(排除其他原因)
            if (e.Reason == SessionEndReasons.Logoff 
                || e.Reason == SessionEndReasons.SystemShutdown)
            {
                DialogResult result = MessageBox.Show(
                    MsgText, MsgTitle, 
                    MessageBoxButtons.YesNo, 
                    MessageBoxIcon.Question,
                    MessageBoxDefaultButton.Button1,
                    // 關鍵:強制置頂,防止被其他視窗遮擋
                    (MessageBoxOptions)0x40000 /* MB_TOPMOST */);

                // 使用者選「否」→ 取消關機
                e.Cancel = (result == DialogResult.No);
            }
        }

        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            // 若使用者嘗試關閉視窗,改為最小化到匣(常駐特性)
            if (e.CloseReason == CloseReason.UserClosing)
            {
                e.Cancel = true;
                this.WindowState = FormWindowState.Minimized;
                this.ShowInTaskbar = false;
            }
            base.OnFormClosing(e);
        }

        protected override void OnClosed(EventArgs e)
        {
            // 表單真正關閉時(如系統關機強制結束)移除事件
            SystemEvents.SessionEnding -= OnSessionEnding;
            base.OnClosed(e);
        }
    }
}

關鍵技術細節深度解析

1. 為什麼要 `MB_TOPMOST` (0x40000)?

關機流程中,系統可能已停止建立新視窗或調整 Z-Order。若不加 `MB_TOPMOST`,`MessageBox` 可能被其他視窗遮蓋,使用者看不到提醒,導致關機被意外取消或錯過提醒。透過 P/Invoke `MessageBox` 並傳入 `MB_TOPMOST | MB_SETFOREGROUND` 最可靠,但 WinForms `MessageBox.Show` 多載不支援直接傳選項,故用 `(MessageBoxOptions)0x40000` 強制轉型(僅限 Windows)。

2. 記憶體佔用 12MB+ 是正常的嗎?

**是**。.NET Framework / .NET Core 執行階段本身載入 CLR、BCL、JIT 編譯器、GC 堆等基礎設施,空白 WinForms 程式啟動即佔用 10-20 MB 私有工作集。這非記憶體洩漏,而是**執行階段基礎開銷**。若極度在意體積,可考慮:

  • **Native AOT 編譯**(.NET 7+)產出無執行階段依賴的原生執行檔,體積可壓至 1-2 MB。
  • 改用 **C++ / Rust / Go** 撰寫原生工具,但開發效率大幅下降。
  • 接受現狀:現代電腦 8GB+ 記憶體下,12 MB 微不足道。

3. 無法捕獲「休眠」怎麼辦?

改用 `SystemEvents.PowerModeChanged` 事件:

SystemEvents.PowerModeChanged += (s, e) => 
{
    if (e.Mode == PowerModes.Suspend) { /* 休眠前 */ }
    else if (e.Mode == PowerModes.Resume) { /* 喚醒後 */ }
};

同樣需在結束時 `-=` 移除。

4. 系統強制關機(`shutdown /f` / 長按電源鍵)會怎樣?

強制關機會**直接終止進程**,不發送 `SessionEnding`,也無法取消。本工具僅能攔截「正常關機/登出」流程。

部署建議

  1. 設定專案輸出類型為 **Windows Application**(無主控台視窗)。
  2. 建立捷徑放入 **`shell:startup`**(使用者啟動資料夾)實現開機自啟。
  3. 簽名憑證(代碼簽章)避免 SmartScreen 攔截。
  4. 若需系統級服務(使用者未登入也運行),改寫為 **Windows Service** + `SessionChangeDescription` 監聽 `SessionLogoff`/`SessionLogon`,但無法彈 UI(Session 0 隔離),需改用通知區域圖示或 IPC 通訊前台 UI。

總結

Microsoft.Win32.SystemEvents.SessionEnding 是 WinForms 攔截關機/登出的標準方式。**關鍵三要素**:僅限 UI 應用、必須手動移除訂閱防洩漏、MessageBox 加 `MB_TOPMOST` 確保可見。記憶體佔用屬執行階段基礎開銷,非洩漏;若極度敏感可用 Native AOT。休眠事件請改用 `PowerModeChanged`。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved