diff --git a/README_new_arch.md b/README_new_arch.md index ff48383..cea05fd 100644 --- a/README_new_arch.md +++ b/README_new_arch.md @@ -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 ``` 启动后: diff --git a/_smoke_new_arch.py b/_smoke_new_arch.py index 8db1ae5..4b5cffa 100644 --- a/_smoke_new_arch.py +++ b/_smoke_new_arch.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()}") diff --git a/src/new/__init__.py b/src/new/__init__.py index 4edbedf..15c1ee2 100644 --- a/src/new/__init__.py +++ b/src/new/__init__.py @@ -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 结果 → 日志区 """ \ No newline at end of file diff --git a/src/new/main_router.py b/src/new/main_view.py similarity index 81% rename from src/new/main_router.py rename to src/new/main_view.py index 65c7744..d35d243 100644 --- a/src/new/main_router.py +++ b/src/new/main_view.py @@ -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__", "") + 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,10 +350,10 @@ class MainRouter(QMainWindow): def main(): from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) - win = MainRouter() + win = MainView() win.show() sys.exit(app.exec_()) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/new/services/placeholder_service.py b/src/new/services/placeholder_service.py index 7c54872..3902728 100644 --- a/src/new/services/placeholder_service.py +++ b/src/new/services/placeholder_service.py @@ -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" diff --git a/src/new/services/step1_service.py b/src/new/services/step1_service.py index f2a683f..ec1495b 100644 --- a/src/new/services/step1_service.py +++ b/src/new/services/step1_service.py @@ -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 { diff --git a/src/new/views/placeholder_view.py b/src/new/views/placeholder_view.py index 73bfcc9..2731b56 100644 --- a/src/new/views/placeholder_view.py +++ b/src/new/views/placeholder_view.py @@ -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) diff --git a/src/new/views/step1_view.py b/src/new/views/step1_view.py index cc60e93..562bb87 100644 --- a/src/new/views/step1_view.py +++ b/src/new/views/step1_view.py @@ -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()) \ No newline at end of file