路由壳升级:TaskWorker 三信号 + main_router→main_view 迁移(54/54 smoke 通过)
This commit is contained in:
@ -16,7 +16,7 @@ src/new/
|
||||
│ ├── __init__.py
|
||||
│ ├── step1_view.py # Step 1 真实视图(继承 BaseView)
|
||||
│ └── placeholder_view.py # step2-step13 占位视图
|
||||
└── main_router.py # 路由与调度壳(QMainWindow + QThread)
|
||||
└── main_view.py # 路由与调度壳(QMainWindow + QThread)
|
||||
```
|
||||
|
||||
## 端到端调用链
|
||||
@ -28,13 +28,13 @@ Step1View._on_run_clicked (绿色按钮)
|
||||
BaseView.dispatch_execute (沿父链上溯)
|
||||
│ ancestor.run_single_step(step_id, config)
|
||||
▼
|
||||
MainRouter.run_single_step (查 ROUTES 表 → 注入 work_dir)
|
||||
MainView.run_single_step (查 ROUTES 表 → 注入 work_dir)
|
||||
│ TaskWorker(service_func, config).start()
|
||||
▼
|
||||
services.step1_service.execute_step1(config)
|
||||
│ 调 WaterMaskStep.run(...) → 包装成 dict 返回
|
||||
▼
|
||||
MainRouter._on_step_done (按 status 写日志)
|
||||
MainView._on_step_done (按 status 写日志)
|
||||
```
|
||||
|
||||
## 运行验证
|
||||
@ -52,13 +52,13 @@ python _smoke_new_arch.py
|
||||
|
||||
```cmd
|
||||
cd D:\111\office\ZHLduijie\1.WQ\WQ_GUI
|
||||
python -m src.new.main_router
|
||||
python -m src.new.main_view
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```cmd
|
||||
python src\new\main_router.py
|
||||
python src\new\main_view.py
|
||||
```
|
||||
|
||||
启动后:
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
三层冒烟:端到端模块化新架构(src/new/)
|
||||
|
||||
Layer 1 service 单元:execute_step1 在各种错误路径返回正确 dict
|
||||
Layer 2 view offscreen:Step1View UI 行为符合契约(不依赖 main_router)
|
||||
Layer 3 end-to-end:MainRouter 创建 → 切到 step1 → 点击 → 日志验证
|
||||
Layer 2 view offscreen:Step1View UI 行为符合契约(不依赖 main_view)
|
||||
Layer 3 end-to-end:MainView 创建 → 切到 step1 → 点击 → 日志验证
|
||||
|
||||
运行方式(项目根)::
|
||||
|
||||
@ -247,17 +247,17 @@ def smoke_view():
|
||||
# =========================================================================
|
||||
def smoke_e2e():
|
||||
print("\n" + "=" * 70)
|
||||
print("Layer 3 —— end-to-end(MainRouter 路由 + TaskWorker 调度)")
|
||||
print("Layer 3 —— end-to-end(MainView 路由 + TaskWorker 调度)")
|
||||
print("=" * 70)
|
||||
|
||||
from PyQt5.QtCore import QEventLoop, QTimer
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
|
||||
from src.new.main_router import MainRouter, ROUTES
|
||||
from src.new.main_view import MainView, ROUTES
|
||||
|
||||
win = MainRouter()
|
||||
report("L3", "MainRouter 实例化成功", win is not None)
|
||||
win = MainView()
|
||||
report("L3", "MainView 实例化成功", win is not None)
|
||||
report("L3", "ROUTES 共 13 条", len(ROUTES) == 13, f"实际={len(ROUTES)}")
|
||||
report("L3", "nav_list 项数 == 13", win.nav_list.count() == 13,
|
||||
f"count={win.nav_list.count()}")
|
||||
|
||||
@ -8,12 +8,12 @@ WQ_GUI 端到端模块化新架构
|
||||
* ``core/`` 基础通讯接口(BaseView 等契约)
|
||||
* ``views/`` 前端皮囊 —— 仅 PyQt UI,零业务
|
||||
* ``services/`` 后端大脑 —— 纯函数计算,零 PyQt
|
||||
* ``main_router.py`` 路由与调度壳,连接前端按钮与后端服务
|
||||
* ``main_view.py`` 路由与调度壳,连接前端按钮与后端服务
|
||||
|
||||
调用链:
|
||||
Step1View._on_run_clicked
|
||||
→ BaseView.dispatch_execute(step_id, config)
|
||||
→ MainRouter.run_single_step(step_id, config)
|
||||
→ MainView.run_single_step(step_id, config)
|
||||
→ TaskWorker 线程调用 services.step1_service.execute_step1(config)
|
||||
→ 返回 dict 结果 → 日志区
|
||||
"""
|
||||
@ -1,35 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MainRouter —— 端到端模块化新架构的路由与调度壳
|
||||
=================================================
|
||||
MainView —— 端到端模块化新架构的路由与调度壳(主窗口)
|
||||
=========================================================
|
||||
|
||||
职责
|
||||
----
|
||||
1. **路由**:根据 ``ROUTES`` 表动态加载每个 step 的 view(前端皮囊)
|
||||
和 service(后端大脑),左侧 ``QListWidget`` 导航,右侧
|
||||
``QStackedWidget`` 承载 view。
|
||||
2. **解耦通讯**:view 不直接持有 main_router 引用;按钮点击通过
|
||||
2. **解耦通讯**:view 不直接持有 main_view 引用;按钮点击通过
|
||||
``BaseView.dispatch_execute`` 沿父链回到本类 ``run_single_step``。
|
||||
3. **后台执行**:内置 ``TaskWorker(QThread)``,把 service 调用丢进
|
||||
子线程,避免阻塞 UI。
|
||||
3. **后台执行**:内置 ``TaskWorker(QThread)``(三信号协议:
|
||||
``log_msg / finished / error``),把 service 调用丢进子线程,
|
||||
避免阻塞 UI。
|
||||
4. **日志** :底部 ``QTextEdit`` 集中输出路由与 service 结果。
|
||||
|
||||
本文件由原 ``src/new/main_router.py`` 迁移而来,类名 ``MainRouter`` 改名为
|
||||
``MainView`` 以贴合"视图壳"语义,同时按用户原意把 ``TaskWorker`` 从单信号
|
||||
(``finished_with_result``)升级为三信号协议。
|
||||
|
||||
运行方式
|
||||
--------
|
||||
|
||||
::
|
||||
|
||||
# 方式 A:作为模块运行(推荐)
|
||||
cd D:/111/office/ZHLduijie/1.WQ/WQ_GUI
|
||||
python -m src.new.main_router
|
||||
python -m src.new.main_view
|
||||
|
||||
# 方式 B:直接脚本运行
|
||||
python src/new/main_router.py
|
||||
python src/new/main_view.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
@ -41,7 +46,7 @@ from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
# 让 ``python src/new/main_router.py`` 直接运行时也能 import ``src.xxx``
|
||||
# 让 ``python src/new/main_view.py`` 直接运行时也能 import ``src.xxx``
|
||||
_HERE = Path(__file__).resolve().parent # .../src/new
|
||||
_SRC_ROOT = _HERE.parent # .../src
|
||||
if str(_SRC_ROOT) not in sys.path:
|
||||
@ -80,15 +85,27 @@ ROUTES = [
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# TaskWorker:QThread 后台执行器
|
||||
# TaskWorker:QThread 后台执行器(三信号协议)
|
||||
# =========================================================================
|
||||
class TaskWorker(QThread):
|
||||
"""后台执行 ``service_func(config)`` 并把结果 emit 回主线程
|
||||
"""后台执行 ``service_func(config)`` 并通过三信号协议回报主线程
|
||||
|
||||
信号协议
|
||||
--------
|
||||
* ``log_msg(str)`` —— 启动 / 完成 / 异常等关键节点的人类可读日志
|
||||
* ``finished(dict)`` —— service 正常返回(含 service 内部 try/except
|
||||
转成的 error 状态 dict)
|
||||
* ``error(str)`` —— worker 自身捕获的未预期异常(含 traceback),
|
||||
与 finished 中的 status='error' 语义不同:error 表示 worker
|
||||
完全崩溃,无法产出任何结果 dict。
|
||||
|
||||
为什么用 QThread 而不是 Thread:service_func 可能涉及 rasterio /
|
||||
matplotlib 等原生扩展,主线程与子线程的 Qt 事件循环隔离更稳。
|
||||
"""
|
||||
finished_with_result = pyqtSignal(dict)
|
||||
|
||||
log_msg = pyqtSignal(str)
|
||||
finished = pyqtSignal(dict)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, service_func, config: dict):
|
||||
super().__init__()
|
||||
@ -96,22 +113,25 @@ class TaskWorker(QThread):
|
||||
self.config = config
|
||||
|
||||
def run(self):
|
||||
func_name = getattr(self.service_func, "__name__", "<anonymous>")
|
||||
self.log_msg.emit(f"[Worker] 启动 {func_name}")
|
||||
try:
|
||||
result = self.service_func(self.config)
|
||||
except Exception as e: # noqa: BLE001
|
||||
result = {
|
||||
"status": "error",
|
||||
"output_path": None,
|
||||
"message": f"{type(e).__name__}: {e}",
|
||||
"traceback": traceback.format_exc(),
|
||||
}
|
||||
self.finished_with_result.emit(result)
|
||||
except Exception as e: # noqa: BLE001 —— worker 兜底捕获所有未预期异常
|
||||
tb = traceback.format_exc()
|
||||
self.log_msg.emit(f"[Worker] 异常 {type(e).__name__}: {e}")
|
||||
self.error.emit(f"{type(e).__name__}: {e}\n{tb}")
|
||||
return
|
||||
|
||||
status = result.get("status", "unknown") if isinstance(result, dict) else "unknown"
|
||||
self.log_msg.emit(f"[Worker] 完成,status={status}")
|
||||
self.finished.emit(result)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# MainRouter:QMainWindow 路由壳
|
||||
# MainView:QMainWindow 路由壳
|
||||
# =========================================================================
|
||||
class MainRouter(QMainWindow):
|
||||
class MainView(QMainWindow):
|
||||
"""新架构的轻量路由主窗口"""
|
||||
|
||||
def __init__(self):
|
||||
@ -128,7 +148,7 @@ class MainRouter(QMainWindow):
|
||||
self._build_ui()
|
||||
self._build_routes()
|
||||
|
||||
self._log("[Boot] MainRouter 初始化完成")
|
||||
self._log("[Boot] MainView 初始化完成")
|
||||
self._log(f"[Boot] 已注册 {len(ROUTES)} 条路由(step1 真实 / step2-step13 占位)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@ -249,14 +269,15 @@ class MainRouter(QMainWindow):
|
||||
1. 查路由表;
|
||||
2. 注入 ``work_dir``(避免 view 重复感知);
|
||||
3. 启动 ``TaskWorker`` 在子线程运行 service;
|
||||
4. 完成后 emit 回主线程并写日志。
|
||||
4. 三信号路由:log_msg → 日志框;finished → 状态分支日志;
|
||||
error → 未预期异常写日志 + traceback。
|
||||
"""
|
||||
route = self._route_map.get(step_id)
|
||||
if route is None:
|
||||
self._log(f"[Router] 未知 step_id: {step_id}")
|
||||
return
|
||||
|
||||
# 注入 work_dir(仅当 view 未带且 main_router 已配置时)
|
||||
# 注入 work_dir(仅当 view 未带且 main_view 已配置时)
|
||||
if self.work_dir and "work_dir" not in config_dict:
|
||||
config_dict = {**config_dict, "work_dir": self.work_dir}
|
||||
|
||||
@ -272,7 +293,9 @@ class MainRouter(QMainWindow):
|
||||
return
|
||||
|
||||
self.worker = TaskWorker(service_func, config_dict)
|
||||
self.worker.finished_with_result.connect(self._on_step_done)
|
||||
self.worker.log_msg.connect(self._log)
|
||||
self.worker.finished.connect(self._on_step_done)
|
||||
self.worker.error.connect(self._on_step_error)
|
||||
self.worker.start()
|
||||
|
||||
def _load_callable(self, module_name: str, attr_name: str):
|
||||
@ -286,7 +309,11 @@ class MainRouter(QMainWindow):
|
||||
return None
|
||||
|
||||
def _on_step_done(self, result: dict):
|
||||
"""TaskWorker 完成回调——按 status 分支写日志"""
|
||||
"""TaskWorker finished 回调——按 status 分支写日志
|
||||
|
||||
注:service 内部 try/except 转换的 status='error' 也走这里;
|
||||
这与 error 信号(worker 自身崩溃)语义不同。
|
||||
"""
|
||||
status = result.get("status", "unknown")
|
||||
message = result.get("message", "")
|
||||
output_path = result.get("output_path")
|
||||
@ -298,12 +325,16 @@ class MainRouter(QMainWindow):
|
||||
self._log(f"[Service↻] {message}(已复用历史结果)")
|
||||
elif status == "not_implemented":
|
||||
self._log(f"[Service…] {message}")
|
||||
else: # error
|
||||
else: # error(service 内部已转 dict)
|
||||
self._log(f"[Service✗] {message}(mode={mode or 'n/a'})")
|
||||
tb = result.get("traceback")
|
||||
if tb:
|
||||
self._log(tb)
|
||||
|
||||
def _on_step_error(self, msg: str):
|
||||
"""TaskWorker error 信号回调——worker 自身捕获的未预期异常"""
|
||||
self._log(f"[Worker✗] 未预期异常:\n{msg}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 日志:所有日志统一走这里
|
||||
# ------------------------------------------------------------------
|
||||
@ -319,7 +350,7 @@ class MainRouter(QMainWindow):
|
||||
def main():
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
win = MainRouter()
|
||||
win = MainView()
|
||||
win.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"""
|
||||
PlaceholderService —— 通用占位后端(用于尚未迁移的步骤)
|
||||
|
||||
返回 ``{"status": "not_implemented", ...}``,main_router 据此在日志区
|
||||
返回 ``{"status": "not_implemented", ...}``,main_view 据此在日志区
|
||||
输出友好提示;不抛异常、不阻塞 UI 线程。
|
||||
"""
|
||||
|
||||
@ -15,7 +15,7 @@ def execute_placeholder(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""通用占位执行函数
|
||||
|
||||
Args:
|
||||
config: 由 view.get_config() 序列化、再经 main_router 注入 work_dir 的字典
|
||||
config: 由 view.get_config() 序列化、再经 main_view 注入 work_dir 的字典
|
||||
|
||||
Returns:
|
||||
标准结果字典 ``{status, output_path, message}``,其中 status="not_implemented"
|
||||
|
||||
@ -3,14 +3,14 @@
|
||||
Step1 后端计算服务(水域掩膜生成)
|
||||
=====================================
|
||||
|
||||
这是一个**纯计算函数**——绝对不引用 PyQt、绝对不引用 main_router、
|
||||
这是一个**纯计算函数**——绝对不引用 PyQt、绝对不引用 main_view、
|
||||
绝对不读写全局变量。它只:
|
||||
|
||||
1. 从 ``config`` 字典读取参数;
|
||||
2. 调用旧版 ``WaterMaskStep.run`` 执行水域掩膜生成;
|
||||
3. 返回结果字典 ``{status, output_path, message}``。
|
||||
|
||||
调用入口(由 main_router 在后台 QThread 中调用):
|
||||
调用入口(由 main_view 在后台 QThread 中调用):
|
||||
|
||||
execute_step1({
|
||||
"mask_path": "D:/xx.shp", # 现有掩膜路径(NDWI 模式下可为 None)
|
||||
@ -18,7 +18,7 @@ Step1 后端计算服务(水域掩膜生成)
|
||||
"ndwi_threshold": 0.4, # NDWI 阈值
|
||||
"img_path": "D:/ref.dat", # 参考影像
|
||||
"output_path": "D:/mask.dat", # NDWI 输出路径(可选)
|
||||
"work_dir": "D:/workspace", # 工作目录(main_router 注入)
|
||||
"work_dir": "D:/workspace", # 工作目录(main_view 注入)
|
||||
})
|
||||
|
||||
返回字典字段:
|
||||
@ -56,7 +56,7 @@ def execute_step1(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Step 1 后端计算入口——纯函数
|
||||
|
||||
Args:
|
||||
config: 由前端 view.get_config() 序列化、再经 main_router 注入 work_dir 的字典
|
||||
config: 由前端 view.get_config() 序列化、再经 main_view 注入 work_dir 的字典
|
||||
|
||||
Returns:
|
||||
标准结果字典 ``{status, output_path, message, mode}``
|
||||
@ -87,7 +87,7 @@ def execute_step1(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
generate_png=True,
|
||||
output_path=output_path,
|
||||
water_mask_dir=water_mask_dir,
|
||||
callback=None, # 日志由 main_router 统一接管
|
||||
callback=None, # 日志由 main_view 统一接管
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
return {
|
||||
|
||||
@ -5,7 +5,7 @@ PlaceholderView —— 通用占位视图(用于尚未迁移的步骤)
|
||||
仅显示 step 名称与"待实现"提示,仍遵循 BaseView 契约:
|
||||
|
||||
* ``get_config()`` 返回空 dict(service 端据此走占位分支)
|
||||
* 点击"执行"按钮仍会触发 ``dispatch_execute``,便于在 main_router
|
||||
* 点击"执行"按钮仍会触发 ``dispatch_execute``,便于在 main_view
|
||||
中验证路由链路完整通畅
|
||||
"""
|
||||
|
||||
@ -42,7 +42,7 @@ class PlaceholderView(BaseView):
|
||||
|
||||
hint = QLabel(
|
||||
"此步骤的 View 与 Service 尚未迁移到端到端模块化新架构。\n"
|
||||
"按钮仍可点击以验证 main_router 路由链路完整性。"
|
||||
"按钮仍可点击以验证 main_view 路由链路完整性。"
|
||||
)
|
||||
hint.setAlignment(Qt.AlignCenter)
|
||||
hint.setWordWrap(True)
|
||||
|
||||
@ -17,9 +17,9 @@ Step1View —— Step 1(水域掩膜生成)的前端视图(端到端模块
|
||||
|
||||
Step1View._on_run_clicked
|
||||
→ BaseView.dispatch_execute("step1", config)
|
||||
→ MainRouter.run_single_step(step_id, config)
|
||||
→ MainView.run_single_step(step_id, config)
|
||||
→ TaskWorker → services.step1_service.execute_step1(config)
|
||||
→ 返回结果 dict → MainRouter 日志区
|
||||
→ 返回结果 dict → MainView 日志区
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -279,6 +279,6 @@ class Step1View(BaseView):
|
||||
|
||||
所有跨层通讯一律走 ``dispatch_execute``:
|
||||
|
||||
Step1View → 沿父链上溯 → MainRouter.run_single_step(step_id, config)
|
||||
Step1View → 沿父链上溯 → MainView.run_single_step(step_id, config)
|
||||
"""
|
||||
self.dispatch_execute("step1", self.get_config())
|
||||
Reference in New Issue
Block a user