WinForms 中攔截關閉行為的標準切入點是 FormClosing 事件,透過 FormClosingEventArgs.Cancel 決定是否取消關閉。
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
// 僅使用者主動關閉 (點擊 ×、Alt+F4、系統選單) 才詢問
// 避免 Application.Exit()、Task Manager 關閉、Windows 關機時誤觸發
if (e.CloseReason == CloseReason.UserClosing)
{
var result = MessageBox.Show(
"此操作會捨棄您的當前設置,確定要繼續?",
"確認退出",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2); // 預設 No,安全優先
if (result != DialogResult.Yes)
{
e.Cancel = true; // 取消關閉
}
}
}
| 屬性/參數 | 作用 | 注意事項 |
|---|---|---|
e.CloseReason |
關閉來源:UserClosing/ApplicationExitCall/WindowsShutDown/TaskManagerClosing/FormOwnerClosing |
僅攔截 UserClosing,其他情況讓系統正常關閉 |
e.Cancel |
true = 取消關閉,表單保持開啟;false = 允許關閉 |
預設 false,僅在需阻止時設 true |
MessageBoxDefaultButton.Button2 |
預設焦點在「否」按鈕 | 防止用戶慣性按 Enter 誤關閉,符合「破壞性操作需確認」UX 原則 |
MessageBoxIcon.Question |
顯示問號圖示 | 視覺提示此為詢問而非警告/錯誤 |
避免用戶未修改任何資料時也彈窗,體驗更佳。
public partial class SettingsForm : Form
{
private bool _isDirty = false;
public SettingsForm()
{
InitializeComponent();
// 綁定所有輸入控制項的變更事件
txtName.TextChanged += (s, e) => _isDirty = true;
chkEnable.CheckedChanged += (s, e) => _isDirty = true;
cmbTheme.SelectedIndexChanged += (s, e) => _isDirty = true;
// ... 其他控制項
}
private void SettingsForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing && _isDirty)
{
var result = MessageBox.Show(
"您有未儲存的變更,確定要捨棄並關閉?",
"未儲存的變更",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (result != DialogResult.Yes)
e.Cancel = true;
}
}
private void btnSave_Click(object sender, EventArgs e)
{
SaveSettings();
_isDirty = false; // 儲存後重置
}
}
參考 VS/Word 關閉未儲存文件的體驗。
private void EditorForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason != CloseReason.UserClosing || !_isDirty) return;
// 自訂按鈕文字需用 TaskDialog (Win32) 或自訂 Form
// 這裡用 MessageBox 模擬:Yes=儲存並關閉, No=不儲存關閉, Cancel=取消關閉
var result = MessageBox.Show(
"要儲存變更嗎?",
"未儲存的變更",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button1); // 預設儲存
switch (result)
{
case DialogResult.Yes:
if (!SaveSettings()) // 儲存失敗 (如驗證不通過)
e.Cancel = true; // 取消關閉,讓用戶修正
break;
case DialogResult.No:
// 直接關閉,_isDirty 會在 Dispose 時自動清理
break;
case DialogResult.Cancel:
e.Cancel = true; // 取消關閉
break;
}
}
private bool SaveSettings()
{
try
{
// 驗證邏輯...
// 持久化邏輯...
_isDirty = false;
return true;
}
catch (Exception ex)
{
MessageBox.Show($"儲存失敗:{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
若儲存涉及雲端同步、大檔案寫入,避免 UI 凍結。
private async void EditorForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason != CloseReason.UserClosing || !_isDirty) return;
e.Cancel = true; // 先取消,等非同步完成再決定
var result = await Task.Run(() => MessageBox.Show(
"要儲存變更嗎?\n(儲存可能需要幾秒鐘)",
"未儲存的變更",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Warning));
if (result == DialogResult.Yes)
{
bool saved = await SaveSettingsAsync(); // 非同步儲存
if (saved) this.Close(); // 程式化關閉,不再觸發確認
}
else if (result == DialogResult.No)
{
_isDirty = false;
this.Close();
}
// Cancel: 什麼都不做,表單保持開啟
}
private async Task SaveSettingsAsync()
{
try
{
await Task.Run(() =>
{
// 耗時 I/O 操作...
});
_isDirty = false;
return true;
}
catch
{
// UI 執行緒顯示錯誤
this.Invoke(() => MessageBox.Show("儲存失敗", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error));
return false;
}
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (_isDirty)
{
var result = MessageBox.Show("儲存變更?", "確認", MessageBoxButton.YesNoCancel, MessageBoxImage.Warning);
if (result == MessageBoxResult.Cancel) e.Cancel = true;
else if (result == MessageBoxResult.Yes) { if (!Save()) e.Cancel = true; }
}
}
// MAUI: Page.Disappearing 或 Shell.Navigating
protected override bool OnBackButtonPressed()
{
if (_isDirty)
{
var result = await DisplayAlert("未儲存", "捨棄變更?", "是", "否");
return !result; // true = 阻止返回
}
return base.OnBackButtonPressed();
}
[Test]
public void FormClosing_UserClosing_Dirty_ShowsDialog_And_Cancels_When_No()
{
// Arrange
var form = new SettingsForm();
form.SetDirty(true); // 透過反射或公共方法設定
// Act - 模擬 UserClosing
var args = new FormClosingEventArgs(CloseReason.UserClosing, false);
form.InvokeFormClosing(args); // 需暴露 protected 方法或用測試專用繼承類
// Assert
Assert.IsTrue(args.Cancel); // 取消關閉
}
// 測試非 UserClosing 不攔截
[Test]
public void FormClosing_ApplicationExitCall_DoesNotCancel()
{
var form = new SettingsForm();
form.SetDirty(true);
var args = new FormClosingEventArgs(CloseReason.ApplicationExitCall, false);
form.InvokeFormClosing(args);
Assert.IsFalse(args.Cancel);
}
關閉確認看似微不足道,實則是資料保護的最後一道防線。從基本的 FormClosing 攔截,到髒旗標優化、三選項交互、非同步儲存支援,每一層演進都在平衡防止誤操作損失與不干擾正常流程。記住核心原則:僅在使用者主動關閉且有未持久化資料時才彈窗,預設按鈕指向安全選項,提供清晰的「儲存/捨棄/取消」三路徑。