Files
cmshoppe/docs/tasks/T-582.md
T
chengmaandClaude Opus 4.8 5ac18ccc1c docs(tasks): add T-582/T-583 fix dialogs rendering half (hidpi+helper, import worker)
T-582:弹窗偶发半截修复——启动设 HiDPI 舍入策略 + 统一弹窗 helper
(通知类延迟到重绘冲刷后再 exec 治绘制竞态、几何钳制到可用屏幕治出屏),
先覆盖采集导入链路。T-583(治本):①Excel 导入改 worker 线程,消除
GUI 阻塞与重绘积压,复用 CollectWorker 范式。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:48:40 +08:00

58 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: T-582
title: 修复弹窗偶发只显示一半:HiDPI 舍入策略 + 统一弹窗 helper(延迟冲刷 + 屏幕钳制)
phase: 7
deps: []
status: TODO
created: 2026-07-10
---
## 问题 / 背景
用户反馈:部分弹窗偶发只显示一半(例:①采集导入 Excel 后的弹窗)。「偶尔」这一特征本身排除了纯 HiDPI 缩放(那会稳定半截),指向**绘制/几何时序竞态**。
已核代码事实:
- ①导入是在 GUI 线程**同步**跑(`collect.py:242` `excel.import_tasks(...)`,非 worker),解析+入库期间事件循环阻塞;返回后紧接 `refresh_tasks()`(整表重绘)+ `_set_status()`,**再**弹框——模态框在积压重绘未冲刷时被 `exec()`,首帧可能几何/内容没算稳(偶发半截)。
- 应用启动**未设任何 HiDPI 缩放策略**(`app/gui/__init__.py:125` 裸 `QApplication(sys.argv)`)——分数缩放(125/150%)下放大上述问题。
- 弹窗**无屏幕边界钳制**:只有主窗口在 `main_window.py:66-71` 用 `move()` 钳到 `availableGeometry`,`QMessageBox` 靠近屏幕边缘/多显示器时下半截会被切。
根因排序:① 绘制竞态(最可能,吻合「偶尔」);② 弹窗跑出屏幕被切(次可能);③ 缺 HiDPI 舍入策略(放大器,非主因)。本任务修 ①②③ 中低风险的 HiDPI 策略 + 统一 helper(延迟冲刷治①、屏幕钳制治②、舍入策略治③);导入 worker 化(治本的另一半)见 T-583。
## 方案(改哪个文件、改成什么)
### 1. 启动加 HiDPI 舍入策略(`app/gui/__init__.py`)
- 在 `QApplication` **创建之前**设 `QGuiApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)`。
- 放在 `main()` 里 `app = QApplication.instance() or QApplication(sys.argv)` 之前;`QApplication.instance()` 已存在时(测试/复用)跳过设置避免告警。
### 2. 统一弹窗 helper:延迟一拍 + 屏幕钳制
- 新增共享 helper(放 `app/gui/widgets.py` 或合适模块),如 `show_message(parent, icon, title, text, buttons=...) -> QMessageBox.StandardButton`:
- 构造 `QMessageBox` 后,`box.show()` → 用 `QApplication.processEvents()` 或 `box.adjustSize()` 让尺寸算稳,再把窗口 `move()` **钳制到 `parent` 所在屏幕的 `availableGeometry`**(复用 `main_window.py:66-71` 同款:居中后夹到 max_x/max_y,防下半/右半出屏),最后 `exec()`。
- 对"弹框紧跟重活"的场景,用 `QTimer.singleShot(0, ...)` 把 `exec()` 推迟到当前事件循环把积压重绘冲刷完之后(治绘制竞态)。实现时确保 helper 在需要返回用户选择的场景仍能同步拿到结果(question/确认类),或为「仅通知类」(information/warning) 与「需返回值类」(question) 分别提供入口。
- 把 ①②③ 里高频、且出现在"重活之后"的通知类弹窗改走 helper:至少 `collect.py` 导入相关(`_show_error`、账号未就绪 warning、导入结果框)先接入;其余 `QMessageBox.information/warning` 逐步收敛(本任务先覆盖采集导入链路 + 提供 helper,不强制一次性全量替换)。
### 边界内的取舍
- question/确认类弹窗需要同步返回值,`QTimer.singleShot(0)` 延迟不适用——这类只做「屏幕钳制 + adjustSize」,不做延迟 exec;延迟冲刷只用于不需返回值的通知类。实现时区分清楚。
## 验收要点
- 启动设置了 `HighDpiScaleFactorRoundingPolicy.PassThrough`,且在 QApplication 创建前(单测可断言调用顺序,或至少不回归启动)。
- helper 存在并被采集导入链路的通知类弹窗使用;helper 会把弹窗几何钳制进父窗口所在屏幕 `availableGeometry`(单测:给一个会越界的父窗口位置,断言 helper 计算出的目标坐标被夹在可用区域内)。
- 通知类弹窗经 helper 延迟到重绘冲刷后再 exec(单测可 mock `QTimer.singleShot`/`processEvents` 断言被调用;不真正弹窗)。
- question/确认类仍同步返回用户选择、行为不回归(如③开始更新确认、重置确认)。
- 现有走 `QMessageBox` 的测试不回归。
- 验证命令(unittest,不引入 pytest;GUI 测试用 offscreen):
- `py -3.10 -m unittest tests.test_gui`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `py -3.10 -m unittest discover -s tests`
- `git diff --check`
## 边界(不改什么)
- 不把①导入改成 worker 线程(治本另一半在 T-583)。
- 不改弹窗文案/业务语义、不改主窗口尺寸逻辑。
- 不强制一次性替换全部 `QMessageBox` 调用点(先提供 helper + 覆盖采集导入链路)。
- 不改 CDP/DB/AI/cmhub/Excel 解析逻辑。
## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)