Files
dtm-py-all/UI关闭退出说明.md

6.2 KiB
Raw Blame History

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)

🎯 工作流程

启动流程

  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: 使用测试脚本

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 主窗口时,整个程序会:

  1. 捕获关闭事件
  2. 触发退出回调
  3. 依次关闭所有后台服务
  4. 完全退出程序
  5. 不留任何后台进程