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

255 lines
6.2 KiB
Markdown
Raw Permalink Normal View History

# 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. ✅ 不留任何后台进程