255 lines
6.2 KiB
Markdown
255 lines
6.2 KiB
Markdown
# 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. ✅ 不留任何后台进程
|