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