Code4Fun - 程式操作未完成前阻擋關機/登出
0 |
前天說到提醒上班打卡的小程式,有讀者提到:下班關機時也很需要打卡提醒! (不過該個案為按完關機鈕,立刻關上螢幕瀟灑轉身離開... 灑脫至此,所有防呆機制望塵莫及。) 關機或登出時提示尚有未儲存修改,允許使用者取消關機或登出回桌面存檔的做法很常見,像是 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