3

Code4Fun - 程式操作未完成前阻擋關機/登出

 1 year ago
source link: https://blog.darkthread.net/blog/prevent-shutdown-in-dotnet/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Code4Fun - 程式操作未完成前阻擋關機/登出-黑暗執行緒

前天說到提醒上班打卡的小程式,有讀者提到:下班關機時也很需要打卡提醒! (不過該個案為按完關機鈕,立刻關上螢幕瀟灑轉身離開... 灑脫至此,所有防呆機制望塵莫及。) 關機或登出時提示尚有未儲存修改,允許使用者取消關機或登出回桌面存檔的做法很常見,像是 Word、Notepad,連小畫家都有,儼然已成標配,沒做到反而掉潻。那麼,用 .NET 也能實現嗎?Sure, Of Course, Why Not?

Fig1_638194126369027013.png

知道能做但沒親自演練過,感覺很不踏實,於是我趁機寫了模擬打卡程式當練習 - 若使用者尚未打下班卡就登出或關機,程式將顯示提醒訊息並阻擋關機;程式並支援關機時倒數 15 秒自動打卡,若使用者不想打卡,倒數過程亦可取消。

直接看結果:

範例展示

程式原理很簡單,在登出作業階段或關機時,Windows 會送出 WM_QUERYENDSESSION 或 WM_ENDSESSION 給所有桌面程式,若應用程式有修改內容未儲存,可透過 ShutdownBlockReasonCreate 系統 API 傳回拒絕結束理由,使用者可選擇強制登出/關機或取消動作回到桌面操作。MVP GÉRALD BARRÉ 有篇 Prevent Windows shutdown or session ending in .NET 對原理有詳細說明並提供 Windows Form 的完整範例,我的程式便是以其為基礎修改,但功能上再複雜一些,包含可選擇是否自動打卡結束,設定倒數並可取消。原程式用 QueueUserWorkItem 等待五秒結束,我改成 .NET 4.5+ 的新時代寫法 - 用 Task.Run()、async/await、CancellationToken 實現。
(註:WPF 的話,可參考這篇 WPF — How to Veto a Windows Shutdown by Luke Puplett)

程式碼如下:

using System.Runtime.InteropServices;

namespace prevent_shutdown;
public partial class Form1 : Form
{
    public const int WM_QUERYENDSESSION = 0x0011;
    public const int WM_ENDSESSION = 0x0016;
    public const uint SHUTDOWN_NORETRY = 0x00000001;

    [DllImport("user32.dll", SetLastError = true)]
    static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string reason);
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);
    [DllImport("kernel32.dll")]
    static extern bool SetProcessShutdownParameters(uint dwLevel, uint dwFlags);
    public Form1()
    {
        InitializeComponent();
        // Define the priority of the application (0x3FF = The higher priority)
        SetProcessShutdownParameters(0x3FF, SHUTDOWN_NORETRY);
        timer1_Tick(null!, null!);
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_QUERYENDSESSION || m.Msg == WM_ENDSESSION)
        {
            if (!ReadyForShutdown()) return;
        }
        base.WndProc(ref m);
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        lblTime.Text = DateTime.Now.ToString("HH:mm:ss");
    }

    CancellationTokenSource _cts = null;
    private int _countDown = -1;
    private bool InCountDown => _countDown > -1;
    private bool _done = false;

    private bool ReadyForShutdown()
    {
        if (_done) return true;
        if (InCountDown) return false;
        // Prevent windows shutdown
        ShutdownBlockReasonCreate(this.Handle, "休蛋幾壘,不打下班卡逆?" +
                             (cbxAuto.Checked ? "自動打卡中..." : string.Empty));
        if (!cbxAuto.Checked) return false;
        StartCountDown();
        Task.Run(async () =>
        {
            while (_countDown-- > 0 && !_cts.Token.IsCancellationRequested)
            {
                this.BeginInvoke(UpdatePunchOutBtn);
                await Task.Delay(1000);
            }
            if (_cts.Token.IsCancellationRequested) return;
            this.BeginInvoke(() =>
            {
                PunchOut();
                ShutdownBlockReasonCreate(this.Handle, "自動打卡完成。");
                ShutdownBlockReasonDestroy(this.Handle);
                this.Close();
            });
        }, _cts.Token);
        return false;
    }

    void StartCountDown()
    {
        _cts = new CancellationTokenSource();
        lblStatus.Text = @"自動打卡中...";
        _countDown = 15;
        UpdatePunchOutBtn();
    }
    void StopCountDown()
    {
        _cts.Cancel();
        lblStatus.Text = @"自動打卡已取消";
        _countDown = -1;
        UpdatePunchOutBtn();
    }
    void UpdatePunchOutBtn()
    {
        btnPunchOut.Text = _countDown == -1 ? "打卡下班" : $"取消({_countDown})";
    }

    void PunchOut()
    {
        lblStatus.ForeColor = Color.Green;
        lblStatus.Text = $@"於{DateTime.Now:HH:mm:ss}打卡下班";
        _done = true;
    }

    private void btnPunchOut_Click(object sender, EventArgs e)
    {
        if (InCountDown) StopCountDown(); else PunchOut();
    }

    // 模疑觸發關機事件,方便測試
    private void lblTime_DoubleClick(object sender, EventArgs e)
        => ReadyForShutdown();
}

範例程式碼已上傳 Github,需要參考的同學自取。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK