# UI 关闭时退出程序功能说明 ## 📋 需求 关闭 UI 界面的主框架时,后台的所有程序自动退出,整个程序结束。 ## 🔧 实现方案 ### 1. 架构设计 ``` UI 主窗口关闭 ↓ 触发 closeEvent ↓ 发出 window_closed 信号 ↓ 调用退出回调函数 ↓ 执行 exit_program() ↓ 依次关闭所有后台服务 ↓ 程序完全退出 ``` ### 2. 关键修改 #### 2.1 MainWindow 添加关闭事件处理 文件: `UI/views/main_window.py` ```python 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` ```python # 全局退出回调函数 _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` ```python # 在主线程中启动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` ```python def exit_program(sig=None, frame=None): """优雅地关闭所有服务并退出程序""" print("="*60) print("开始退出程序...") print("="*60) # 依次关闭: # 1. Flask 服务器 # 2. WebSocket 服务器 # 3. Modbus 服务器 # 4. 数据插入线程 # 5. dtMachineService # 强制退出 os._exit(0) ``` ## 🎯 工作流程 ### 启动流程 1. 主程序启动 Flask、WebSocket、Modbus 等后台服务(daemon 线程) 2. 在主线程中启动 PyQt5 UI(阻塞主线程) 3. UI 显示,用户可以操作界面 ### 退出流程 1. 用户点击关闭按钮或窗口 X 按钮 2. MainWindow.closeEvent() 被触发 3. 发出 window_closed 信号 4. 信号触发回调函数 on_window_closed() 5. 调用全局退出回调 _on_exit_callback() 6. 通过 QTimer 延迟 100ms 调用 exit_program() 7. exit_program() 依次关闭所有后台服务: - Flask HTTP 服务器 - WebSocket 服务器 - Modbus 服务器 - 数据插入线程 - dtMachineService 8. 使用 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: 使用测试脚本 ```bash conda activate dtmgt python test_ui_exit.py ``` 手动关闭窗口,观察控制台输出。 ### 方法 2: 运行完整程序 ```bash conda activate dtmgt python dtmgtApp.py ``` 登录后关闭主窗口,观察所有服务是否正确关闭。 ### 方法 3: 使用打包后的程序 ```bash cd dist .\dtmgtApp.exe ``` 关闭窗口,检查任务管理器中进程是否完全退出。 ## 📌 注意事项 ### 1. daemon 线程 所有后台服务线程都设置为 daemon=True: ```python flask_server_thread.daemon = True socketio_thread.daemon = True insert_result_thread.daemon = True ``` 这样主线程退出时,daemon 线程会自动终止。 ### 2. QTimer 延迟 使用 100ms 延迟确保 UI 事件循环完成: ```python 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()` 关闭: ```python 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 主窗口时,整个程序会: 1. ✅ 捕获关闭事件 2. ✅ 触发退出回调 3. ✅ 依次关闭所有后台服务 4. ✅ 完全退出程序 5. ✅ 不留任何后台进程