T-582:弹窗偶发半截修复——启动设 HiDPI 舍入策略 + 统一弹窗 helper (通知类延迟到重绘冲刷后再 exec 治绘制竞态、几何钳制到可用屏幕治出屏), 先覆盖采集导入链路。T-583(治本):①Excel 导入改 worker 线程,消除 GUI 阻塞与重绘积压,复用 CollectWorker 范式。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.8 KiB
4.8 KiB
id, title, phase, deps, status, created
| id | title | phase | deps | status | created |
|---|---|---|---|---|---|
| T-582 | 修复弹窗偶发只显示一半:HiDPI 舍入策略 + 统一弹窗 helper(延迟冲刷 + 屏幕钳制) | 7 | TODO | 2026-07-10 |
问题 / 背景
用户反馈:部分弹窗偶发只显示一半(例:①采集导入 Excel 后的弹窗)。「偶尔」这一特征本身排除了纯 HiDPI 缩放(那会稳定半截),指向绘制/几何时序竞态。
已核代码事实:
- ①导入是在 GUI 线程同步跑(
collect.py:242excel.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_guipython -m ruff check app tests main.pypy -3.10 -m compileall app main.pypy -3.10 -m unittest discover -s testsgit diff --check
边界(不改什么)
- 不把①导入改成 worker 线程(治本另一半在 T-583)。
- 不改弹窗文案/业务语义、不改主窗口尺寸逻辑。
- 不强制一次性替换全部
QMessageBox调用点(先提供 helper + 覆盖采集导入链路)。 - 不改 CDP/DB/AI/cmhub/Excel 解析逻辑。
执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)