# P2-03 证据:WebView2 原生窗口遮挡层(重命名遮罩) **任务**:修复 RenameOverlay 被 WebView2 原生子窗口压住的确定性缺陷,审计同类遮罩,只修可确认的原生窗口遮挡。 **结论**:`RenameOverlay` 已重写为独立顶层透明 Tool 窗(与 `AttachmentPreviewOverlay` 同一验证过的模式);审计确认它是**唯一**受影响的遮罩,其余全部本就是顶层窗口,未改动。自动化目标测试全绿。 ## 根因(源码级) - `core/webview2.py`(L10-11 注释 + 子窗口发现实现):WebView2 的 `Chrome_WidgetWin_*` 是**主窗口 HWND 的原生子 HWND**(EnumChildWindows 轮询发现,SetBoundsAndZoomFactor 定位)。 - Windows 上原生子 HWND 永远绘制在其父 HWND 内所有 Qt 渲染内容**之上**(Qt 绘入主窗口 backing store,原生子窗在 z 序更高)。 - 旧 `RenameOverlay` 是 `bg_widget` 的**子控件**(`QWidget(parent=bg_widget)` + `setGeometry(parent.rect())`):遮罩与卡片都是主窗口内的 Qt 绘制 → 在聊天区(WebView2 所在区域)内被原生 webview 窗盖住:遮罩不暗、卡片被压。 - 唯一可盖住它的结构:独立顶层窗口(独立 HWND)+ 逐像素 alpha(`WA_TranslucentBackground`)。`AttachmentPreviewOverlay` 早已按此模式修复(其 docstring 明文记录同一缺陷),`RenameOverlay` 是遗留的旧模式。 ## 修改(ui/views/main_window.py,仅 RenameOverlay 类,L1363 起整类替换) | 项 | 旧 | 新 | |---|---|---| | 窗口类型 | `bg_widget` 子控件 | 顶层 `FramelessWindowHint \| Tool`(独立 HWND,拥有者=主窗口,Windows 上默认浮于拥有者之上,不入任务栏) | | 透明 | 无(fillRect 半透明灰) | `WA_TranslucentBackground` + paintEvent 半透明灰(逐像素 alpha) | | 覆盖范围 | `parent.rect()`(客户区内嵌) | **主窗口客户区**:`mapToGlobal(main.rect().topLeft())` + `main.rect().size()` —— 不含系统标题栏/边框,标题栏与窗口控制保持可操作 | | 跟随 | `resizeEvent`(子控件自动跟随) | eventFilter 挂主窗口:`Move` → move;`Resize` / `WindowStateChange`(最大化/还原;DPI 变化时 Qt 对主窗合成 move+resize,同路跟随)→ 客户区几何重同步 + 卡片重居中;`Close`/`Hide` → 关闭遮罩 | | 入场动画 | QGraphicsOpacityEffect(顶层窗不可靠) | windowOpacity 属性动画 150ms(同附件预览层) | | 行为 | 点空白/✕/取消 关闭、输入全选、Enter 提交 | 全部保留,**新增 Esc 关闭**(keyPressEvent);`confirm()` 空标题不 emit | | 释放 | `deleteLater` | `close_overlay()`:`removeEventFilter(main)` + `main.activateWindow()/raise_()`(焦点回主窗)+ `deleteLater`;`WA_DeleteOnClose`;`_closed` 幂等防重入 | | 卡片拖拽 | 有(限父窗内) | 保留(限客户区内) | `_rename_session` 调用点未改(仍 `RenameOverlay(current_title, self.bg_widget)`;`parent.window()` 取主窗口)。无新增辅助函数(几何同步逻辑与附件预览层各 10 行,不构成"真实重复",未抽公共函数)。 ## 同类遮罩审计(只修可复现者) | 遮罩/弹窗 | 位置 | 窗口类型 | 结论 | |---|---|---|---| | `AttachmentPreviewOverlay` | L53 | 顶层 Tool + `WA_TranslucentBackground` + eventFilter 跟随(frameGeometry) | **不受影响**(已是正确模式,本次修复的参照) | | `SettingsWindow` | L424 | 顶层 `FramelessWindowHint`(独立窗,内部虚化遮罩是其子控件) | **不受影响**(独立 HWND,天然在 webview 之上) | | `SessionContextPopup` | L1267 | `Popup \| FramelessWindowHint` | **不受影响**(Popup 为独立顶层原生窗) | | `ModelSelectPopup` | L1624 | `Popup \| FramelessWindowHint`(L1649) | **不受影响** | | `SessionModePopup` | L2245 | `Popup \| FramelessWindowHint`(L2260) | **不受影响** | | `PdfModePopup` | L2368 | `Popup \| FramelessWindowHint`(L2392) | **不受影响** | | `RenameOverlay` | L1363 | ~~bg_widget 子控件~~ → 顶层 Tool + 透明 | **受影响,已修复**(唯一) | 未把普通 popup/dialog 重写成统一框架(硬约束)。 ## 自动化验证(本机 Windows 11 10.0.26200 x64,`.venv` CPython 3.10.21) 新建 `tests/diag_rename_overlay.py`(隔离临时 DB,offscreen,34 项断言,EXIT=0): ``` RESULT: 34/34 -> ALL PASS R0 遮罩创建且为顶层窗口 R1 结构:isWindow / window() is self(非内嵌子控件)/ Tool / Frameless / WA_TranslucentBackground / WA_DeleteOnClose / 主窗口未设透明 R2 几何:覆盖客户区左上角与尺寸(±2px)/ 卡片居中 / 输入框初始全选 / 预填旧标题 R3 跟随:主窗 move(+150,+80)→精确跟随 / resize(1200x700)→尺寸同步+卡片重居中 / WindowStateChange 分支(最大化/还原同路)不崩溃且几何仍正确 R4 行为:Enter 提交→DB+侧栏标题更新 / 空标题 confirm 不改标题 / Esc 关闭 / 点空白关闭 / ✕ 关闭 / 取消关闭 / 非确认关闭不改标题 R5 释放:顶层窗口消失、无残留 rename_form、主窗口存活可用 R6 焦点回主窗(offscreen 软检查,INFO 记录) ``` 目标测试 + 回归(均 EXIT=0): | 套件 | 结果 | |---|---| | `tests/diag_rename_overlay.py` | 34/34 ALL PASS | | `tests/smoke_offscreen.py`(QtWebEngine 路径) | 8/8 ALL PASS | | `tests/smoke_copy_session.py`(侧栏/Popup 链路) | ALL PASS | | `tests/smoke_bash_panel.py` | 140 项 ALL PASS | | `tests/run_tests.py` | 41 passed, 0 failed | ## 真机 WebView2 验收(用户走查步骤) 真机 WebView2 自动化被有意放弃:`core/webview2.py` 的 `get_environment()` 含 `taskkill /F /IM msedgewebview2.exe`(会杀掉用户其他 WebView2 应用进程)且 SDK 怪癖要求默认共享 profile(无法重定向到临时目录)——无人值守自动化运行该路径风险不可接受。 **【2026-09-17 更新】本机 WebView2 加载阻塞已解除**(`core/webview2.py` 加 byte[] 回落,见 evidence/P1-03.md):主程序现已能在本机以真实 WebView2 启动(Runtime 153.0.4234.32、controller ready、index.html NavigationCompleted、stderr 无错)。下表人工清单现可在本机真实 WebView2 模式下执行。 机制保证(与生产已验证的 AttachmentPreviewOverlay 完全同构):新遮罩是**独立顶层 HWND**(WS_EX_TOOLWINDOW + 拥有者=主窗口);Windows z 序规则下,拥有者窗口之上的顶层窗永远绘制在拥有者的原生子 HWND(WebView2 `Chrome_WidgetWin_*`)之上,与 offscreen 平台无关。 人工验收清单(Windows 桌面,真实 WebView2 模式运行主程序后): 1. 会话右键 → 重命名:遮罩盖住**整个客户区**(含聊天区 webview,webview 变暗),标题栏(含最小化/最大化/关闭)仍可点。 2. 拖主窗口 / 拉边框缩放 / 最大化 / 还原:遮罩与卡片全程贴合客户区、卡片保持居中。 3. 多显示器间拖主窗 / 改缩放比后重开遮罩:几何正确(mapToGlobal 路径)。 4. Esc / 点空白 / ✕ / 取消 → 遮罩消失、焦点回主窗、侧栏可继续操作;Enter 或确定 → 标题更新。 5. 连续开/关 5 次:无残留遮罩、无卡顿、任务栏无新增图标。 ## 教训(跨压缩持久) - 【P2-03 发现】**offscreen/无真实事件循环时 `processEvents()` 不处理 `DeferredDelete`**:`deleteLater()` 的控件必须显式 `QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)` 才会真正删除(探针证实:仅 processEvents 循环后对象仍 alive)。生产事件循环常驻不受影响,但所有 offscreen 测试的"已删除"断言前必须冲刷 DeferredDelete。 - 【P2-03 教训】PyQt6 `setGeometry` 无 `(QPoint, QSize)` 重载 → 构造 `QRect(tl, size)`;`QTest.mouseClick(widget, button, modifier, pos)` 第 3 参是 **modifier**(易误当 pos)。 - 【P2-03 发现】`core/webview2.py` `get_environment()` 含 `taskkill /F /IM msedgewebview2.exe` + 共享默认 profile 不可重定向 → 无人值守自动化不得走真实 WebView2 启动路径;真机验收走人工清单。 - 【P2-03 结构判据】"顶层窗可带 owner parent":`setWindowFlags(Tool|Frameless)` 后 `parentWidget()` 仍可非 None(owner 关系),判据是 `isWindow()` / `window() is self`,不是 `parentWidget() is None`。 ## 修改文件 - `ui/views/main_window.py`(仅 `RenameOverlay` 类整类替换,~170 行;调用点未改) - `tests/diag_rename_overlay.py`(新,34 项断言)