GUI 應用中,若用戶點擊關閉按鈕或按 Esc 時,存在**耗時阻塞任務**(網路連線、資料庫事務、執行緒等待)正在運行,直接退出會導致資源洩漏、數據不一致甚至崩潰。必須攔截關閉事件,實現**優雅關閉**邏輯。
QMainWindow(繼承自 QWidget)提供兩個關鍵虛函數:
void closeEvent(QCloseEvent *event):視窗關閉請求(點擊 ×、close()、系統關機)void keyPressEvent(QKeyEvent *event):鍵盤按鍵(含 Esc)// MainWindow.h
#include <QMainWindow>
#include <QCloseEvent>
#include <QKeyEvent>
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
protected:
void closeEvent(QCloseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private:
void gracefulShutdown(); // 統一關閉邏輯
// 假設成員:WorkerThread *worker; QTimer *pollTimer; ...
};
// MainWindow.cpp
void MainWindow::closeEvent(QCloseEvent *event) {
if (isShuttingDown) { // 防重入
event->accept();
return;
}
// 1. 忽略默認關閉,啟動優雅關閉流程
event->ignore();
gracefulShutdown();
// 2. 實際關閉在 gracefulShutdown 完成後調用 close()
// 或由 worker 完成信號觸發 this->close()
}
void MainWindow::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_Escape) {
// Esc 鍵觸發關閉,復用 closeEvent 邏輯
close(); // 會調用 closeEvent,而非直接銷毀
} else {
QMainWindow::keyPressEvent(event); // 傳遞給父類處理其他鍵
}
}
void MainWindow::gracefulShutdown() {
isShuttingDown = true;
// 1. 停止定時器/週期性任務
if (pollTimer) pollTimer->stop();
// 2. 通知工作執行緒退出
if (worker) {
worker->requestInterruption(); // Qt 5.15+ 標準方式
// 或 worker->setStopFlag(true);
// 不要在這裡直接 wait(),會凍結 GUI!
}
// 3. 關閉資料庫連線、網路 Socket、檔案句柄等
// db.close(); socket.disconnectFromHost(); ...
// 4. 若有異步操作,連接完成信號到真正關閉
// connect(worker, &WorkerThread::finished, this, [this]{
// this->close(); // 重新發起關閉,此時 closeEvent 會 accept
// });
// 5. 若無異步或強制退出(超時),直接 accept
// 這裡演示:設置 3 秒強制退出定時器
QTimer::singleShot(3000, this, [this]{
qWarning() << "Graceful shutdown timeout, forcing exit";
QApplication::quit(); // 強制退出事件循環
});
}
event->ignore() vs event->accept():
ignore():告訴 Qt「我暫不處理,視窗先別關」。稍後調用 close() 會再次觸發 closeEvent,此時可 accept()。accept():同意關閉,視窗進入銷毀流程。closeEvent 中阻塞等待(如 thread.wait()、sleep()),會凍結事件循環,導致界面無響應、甚至觸發系統「程序無響應」對話框。close() 是**非阻塞**請求,會投遞 QCloseEvent 到事件循環;QApplication::quit() 是**強制退出**事件循環。QDialog 按 Esc 會 reject();QMainWindow **無預設處理**,需自行實現(如本文)。WM_QUERYENDSESSION、Linux SIGTERM 也會觸發 closeEvent。同樣需快速返回,異步清理。enum class ShutdownState { Running, Closing, Forced };
ShutdownState shutdownState = ShutdownState::Running;
void closeEvent(QCloseEvent *e) {
if (shutdownState == ShutdownState::Running) {
e->ignore();
shutdownState = ShutdownState::Closing;
startAsyncShutdown(); // 異步啟動
} else if (shutdownState == ShutdownState::Closing) {
e->ignore(); // 正在關閉中,忽略重複請求
} else { // Forced
e->accept(); // 強制接受
}
}
// 異步完成時
void onAsyncShutdownFinished() {
shutdownState = ShutdownState::Forced;
close(); // 再次觸發 closeEvent,此時 accept
}
Qt 視窗關閉的正確姿勢:**攔截 closeEvent → ignore() → 啟動異步清理 → 完成後 close() 觸發 accept()**。Esc 鍵調用 close() 復用同一流程。切記:**GUI 執行緒永不阻塞**,所有耗時操作移至工作執行緒並以信號槽通知完成。