6.2 KiB
6.2 KiB
UI 关闭时退出程序功能说明
📋 需求
关闭 UI 界面的主框架时,后台的所有程序自动退出,整个程序结束。
🔧 实现方案
1. 架构设计
UI 主窗口关闭
↓
触发 closeEvent
↓
发出 window_closed 信号
↓
调用退出回调函数
↓
执行 exit_program()
↓
依次关闭所有后台服务
↓
程序完全退出
2. 关键修改
2.1 MainWindow 添加关闭事件处理
文件: UI/views/main_window.py
class MainWindow(QMainWindow):
# 定义窗口关闭信号
window_closed = pyqtSignal()
def closeEvent(self, event: QCloseEvent):
"""重写窗口关闭事件"""
print("主窗口关闭事件触发")
# 发出窗口关闭信号
self.window_closed.emit()
# 接受关闭事件
event.accept()
print("主窗口关闭完成")
2.2 dtmgtUI 添加退出回调机制
文件: UI/dtmgtUI.py
# 全局退出回调函数
_on_exit_callback = None
def set_on_exit_callback(callback):
"""设置退出时的回调函数"""
global _on_exit_callback
_on_exit_callback = callback
def main():
# ... 创建窗口 ...
# 连接窗口关闭信号
def on_window_closed():
print("接收到主窗口关闭信号")
if _on_exit_callback:
print("调用退出回调函数...")
_on_exit_callback()
window.window_closed.connect(on_window_closed)
2.3 dtmgtApp 主线程运行 UI 并处理退出
文件: dtmgtApp.py
# 在主线程中启动PyQt5 UI
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer
# 创建 QApplication
app = QApplication(sys.argv)
# 导入 UI 模块
from dtmgtUI import main as ui_main, set_on_exit_callback
# 设置退出回调函数
def on_ui_exit():
print_with_timestamp("UI 窗口关闭,开始退出程序...")
# 使用 QTimer 延迟调用 exit_program,确保 UI 关闭完毕
QTimer.singleShot(100, lambda: exit_program())
set_on_exit_callback(on_ui_exit)
# 启动 UI(这会阻塞直到窗口关闭)
exit_code = ui_main()
# UI 关闭后,退出程序
exit_program()
2.4 优化 exit_program 函数
文件: dtmgtApp.py
def exit_program(sig=None, frame=None):
"""优雅地关闭所有服务并退出程序"""
print("="*60)
print("开始退出程序...")
print("="*60)
# 依次关闭:
# 1. Flask 服务器
# 2. WebSocket 服务器
# 3. Modbus 服务器
# 4. 数据插入线程
# 5. dtMachineService
# 强制退出
os._exit(0)
🎯 工作流程
启动流程
- 主程序启动 Flask、WebSocket、Modbus 等后台服务(daemon 线程)
- 在主线程中启动 PyQt5 UI(阻塞主线程)
- UI 显示,用户可以操作界面
退出流程
- 用户点击关闭按钮或窗口 X 按钮
- MainWindow.closeEvent() 被触发
- 发出 window_closed 信号
- 信号触发回调函数 on_window_closed()
- 调用全局退出回调 _on_exit_callback()
- 通过 QTimer 延迟 100ms 调用 exit_program()
- exit_program() 依次关闭所有后台服务:
- Flask HTTP 服务器
- WebSocket 服务器
- Modbus 服务器
- 数据插入线程
- dtMachineService
- 使用 os._exit(0) 强制退出程序
✅ 特性
1. 优雅关闭
- 按顺序关闭各个服务
- 每个服务有超时保护(2-3秒)
- 输出详细的关闭日志
2. 异常处理
- 每个服务关闭都有 try-except 保护
- 即使某个服务关闭失败,也会继续关闭其他服务
- 最后使用 os._exit(0) 确保程序退出
3. 主线程 UI
- UI 在主线程运行(PyQt5 要求)
- 后台服务在 daemon 线程运行
- UI 关闭时,daemon 线程自动终止
4. 延迟退出
- 使用 QTimer.singleShot(100ms) 延迟调用退出函数
- 确保 UI 完全关闭后再清理资源
- 避免退出过程中的竞态条件
🧪 测试方法
方法 1: 使用测试脚本
conda activate dtmgt
python test_ui_exit.py
手动关闭窗口,观察控制台输出。
方法 2: 运行完整程序
conda activate dtmgt
python dtmgtApp.py
登录后关闭主窗口,观察所有服务是否正确关闭。
方法 3: 使用打包后的程序
cd dist
.\dtmgtApp.exe
关闭窗口,检查任务管理器中进程是否完全退出。
📌 注意事项
1. daemon 线程
所有后台服务线程都设置为 daemon=True:
flask_server_thread.daemon = True
socketio_thread.daemon = True
insert_result_thread.daemon = True
这样主线程退出时,daemon 线程会自动终止。
2. QTimer 延迟
使用 100ms 延迟确保 UI 事件循环完成:
QTimer.singleShot(100, lambda: exit_program())
3. os._exit vs sys.exit
sys.exit(): 抛出 SystemExit 异常,可能被捕获os._exit(0): 立即终止进程,不执行清理
最终使用 os._exit(0) 确保程序完全退出。
4. 多进程处理
Modbus 服务器是独立进程,使用 terminate() 和 join() 关闭:
modbus_server_obj.terminate()
modbus_server_obj.join(timeout=3)
🔍 调试输出
退出时会看到类似输出:
============================================================
Received SIGINT or window close, exiting...
============================================================
1. 停止 Flask 服务器...
Flask 服务器已停止
2. 停止 WebSocket 服务器...
WebSocket 服务器已停止
3. 停止 Modbus 服务器...
Modbus 服务器已停止
4. 停止数据插入线程...
数据插入线程已停止
5. 停止 dtMachineService...
dtMachineService 服务已停止
============================================================
所有服务已停止,程序退出
============================================================
📚 相关文件
UI/views/main_window.py- 主窗口关闭事件处理UI/dtmgtUI.py- UI 入口和退出回调dtmgtApp.py- 主程序和退出逻辑test_ui_exit.py- 测试脚本
🎉 总结
现在关闭 UI 主窗口时,整个程序会:
- ✅ 捕获关闭事件
- ✅ 触发退出回调
- ✅ 依次关闭所有后台服务
- ✅ 完全退出程序
- ✅ 不留任何后台进程