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

4.8 KiB
Raw Blame History

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: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 解析逻辑。

执行记录

(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)