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

知道能做但沒親自演練過,感覺很不踏實,於是我趁機寫了模擬打卡程式當練習 - 若使用者尚未打下班卡就登出或關機,程式將顯示提醒訊息並阻擋關機;程式並支援關機時倒數 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,需要參考的同學自取。

A .NET example program to stop shutdown process to simulate punch application with auto punch out while logout or shutdown.


Comments

Be the first to post a comment

Post a comment