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

255 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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