From 4149d203b07b399ff442a78527d652f4f852ff59 Mon Sep 17 00:00:00 2001 From: hulk Date: Mon, 28 Sep 2026 15:20:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20v0.4.1=20=E4=BF=AE=E5=A4=8D=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E5=88=87=E6=8D=A2=E4=B8=A2=20swatch=20=E5=BA=95?= =?UTF-8?q?=E8=89=B2=E4=B8=8E=E8=BF=9E=E6=8E=A5=E5=A4=B1=E8=B4=A5=E9=95=BF?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=B8=B2=E6=92=91=E7=88=86=E5=B8=83=E5=B1=80?= =?UTF-8?q?=EF=BC=9B=E6=96=B0=E5=A2=9E=20selftest=5Ftheme=EF=BC=9B?= =?UTF-8?q?=E9=87=8D=E6=89=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DLPilot/AGENTS.md | 47 +++++++++++++++++--- DLPilot/src/app_fluent.py | 24 ++++++++-- DLPilot/src/dlpc_rs485.py | 18 +++++--- DLPilot/src/selftest_theme.py | 82 +++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 DLPilot/src/selftest_theme.py diff --git a/DLPilot/AGENTS.md b/DLPilot/AGENTS.md index 2dbd819..cb0463d 100644 --- a/DLPilot/AGENTS.md +++ b/DLPilot/AGENTS.md @@ -92,6 +92,13 @@ DLPilot/ 五卡(显示器/模式/EDID 信息/测试图案/hex),右列光机五卡(串口/数据/电源/命令/滑条), 支持的模式和 EDID 信息默认折叠;顶栏 Dark/Light/System 切换。Light/Dark 同时更新 Fluent 主题、Qt 调色板、容器样式与 Windows DWM 标题栏,勿只调用 setTheme。 + **fluent 控件的自定义样式禁止直接 setStyleSheet**:setTheme 会用库自带 qss 重刷全部 + 已注册控件,直写样式随之丢失(v0.4.0 swatch 丢底色的根因)。须走 + `setCustomStyleSheet(控件, lightQss, darkQss)`(qss 需覆盖 hover/pressed/disabled 态, + 否则悬停瞬间被自带态样式盖回);仅 PushButton 等无 themeChanged 钩子的控件可幸存。 + FluentLabelBase 系(StrongBodyLabel 等)例外:其 setTextColor 在每次切主题时整体重写 + 自定义槽,任何 setCustomStyleSheet 都会被冲掉——字号等改用 QFont(自带 label.qss 无 + font-size 规则,QFont 不会被冲)。RGB swatch 底色回归测试见 selftest_theme.py。 PD 协议成功显示绿色,FPDO 仍独立判定。打包必须收集 `qfluentwidgets` 资源,并加入 Windows 系统 `icuuc.dll`,否则 QtCore 可能被其它 ICU DLL 解析失败。 ## 4. 领域语义(测试值都有含义,勿"顺手改") @@ -162,12 +169,14 @@ python publish_gitea.py ../release/DLPilot-v<版本>-win64.zip --replace --insec 1. `python -m compileall -q src`(在 DLPilot/ 下) —— 全模块可编译; 2. `python selftest_rs485.py` —— 光机协议回归(改 dlpc_rs485.py 后必跑,向量来自上游 protocols.md); -3. `python hdmi_gui.py --seconds 5` —— GUI 冒烟(自动开关); -4. `python hdmi_gui.py --test-pattern Checker --area 800x600 --seconds 3` —— 投图链路; -5. `python hdmi_gui.py --dump` —— EDID 解算不抛异常; -6. `python hdmi_gui.py --dlpc-cmd M999`(不带 --port)—— 应明确报错而非崩溃(无硬件时的优雅降级); -7. `python build_exe.py` → 双击 release/DLPilot.exe 确认图标/双栏布局/投图/光机区渲染; -8. `python package_release.py <版本>` —— 不报"引用图片缺失"即栏位完整性 OK。 +3. `python selftest_theme.py` —— 主题切换回归(改 app_fluent.py 涉及样式/主题后必跑; + 离屏渲染,Dark/Light 往返后校验 swatch 底色样式与渲染像素、标题字号); +4. `python hdmi_gui.py --seconds 5` —— GUI 冒烟(自动开关); +5. `python hdmi_gui.py --test-pattern Checker --area 800x600 --seconds 3` —— 投图链路; +6. `python hdmi_gui.py --dump` —— EDID 解算不抛异常; +7. `python hdmi_gui.py --dlpc-cmd M999`(不带 --port)—— 应明确报错而非崩溃(无硬件时的优雅降级); +8. `python build_exe.py` → 双击 release/DLPilot.exe 确认图标/双栏布局/投图/光机区渲染; +9. `python package_release.py <版本>` —— 不报"引用图片缺失"即栏位完整性 OK。 ## 8. v0.4.0 进度记录(2026-09-24) @@ -180,3 +189,29 @@ python publish_gitea.py ../release/DLPilot-v<版本>-win64.zip --replace --insec 发布使用仓库预置配置,临时打包后恢复本机文件。 - 关联动态:EDID-Viewer 已于 2026-09-25 从本工具的"源端 EDID 查看"需求独立成工具 入库并发布 v0.1.0,与本目录无代码共享;I2C-Inject 已标记暂停开发(详见根 `agent.md`)。 + +## 9. v0.4.0 后修复记录 + +- 2026-09-28 修复"切换 Light 后 R/G/B hex 按钮底色丢失、切回 Dark 也不恢复":根因是 + fluent 控件直接 `setStyleSheet` 的自定义样式在 `setTheme` 时被库重刷的自带 qss 整体 + 覆盖(任何方向的切换都会丢,非 Light 特有)。swatch 改走 `setCustomStyleSheet`(qss + 覆盖 hover/pressed/disabled 态),同病的顶栏标题 24px 字号改用 QFont(FluentLabelBase + 的自定义槽被库的 setTextColor 每次切主题重写,见 §3.12)。新增 `src/selftest_theme.py` + 离屏回归(样式表存活 + 渲染像素比对),本次验证:compileall / selftest_rs485 / + selftest_theme / GUI 冒烟全部通过。 +- 2026-09-28 以 v0.4.1 重新打包:APP_VERSION → 0.4.1,`build_exe.py` 生成新 + release/DLPilot.exe(67.1 MB,exe `--seconds 5` 定时退出冒烟 exit 0),按 §8 约定 + 临时还原预置 pattern_slots.json 后 `package_release.py 0.4.1` 生成 + `DLPilot-v0.4.1-win64.zip`(66.8 MB,zip 内为预置 COM15 配置,本机 COM5 运行态 + 已还原未入库)。**Gitea Release 尚未发布**,上传命令: + `python publish_gitea.py ../release/DLPilot-v0.4.1-win64.zip --replace --insecure`。 +- 2026-09-28 第二修:"串口未选/选错点连接后窗口被拉长、刷新按钮够不到"。根因: + pyserial 原始异常串(`could not open port '': FileNotFoundError...`)整段塞进 + 状态 BodyLabel,QLabel 不换行时最小宽度=整行文本宽,把右列参数行最小宽度撑爆。 + 三层修复:①`_dlpc_toggle` 空串口直接短提示返回(不再先写 SERIAL_CFG/save_slots + 再 open——旧代码这顺序还会把本机串口记忆覆写成空串,本机 COM5 记忆即这样丢失过, + 已手工修回);②dlpc_rs485 open 失败消息分类为短提示(被占用/不存在/其它), + 完整异常改走 on_event 进收发日志;③`lbl_dlpc_st.setWordWrap(True)` 兜底。 + 验证:compileall / selftest_rs485 ALL PASS / 离屏断言(空串口提示、长文本下标签 + 最小宽度<300 可换行)/ GUI 冒烟 / 新 exe `--seconds 5` exit 0;v0.4.1 exe(15:05) + 与 zip 均已用该修复重打。 diff --git a/DLPilot/src/app_fluent.py b/DLPilot/src/app_fluent.py index afe675a..ec1ab62 100644 --- a/DLPilot/src/app_fluent.py +++ b/DLPilot/src/app_fluent.py @@ -52,7 +52,8 @@ from PySide6.QtWidgets import ( ) from qfluentwidgets import ( BodyLabel, CardWidget, ComboBox, LineEdit, PlainTextEdit, - PrimaryPushButton, PushButton, Slider, StrongBodyLabel, Theme, setTheme, + PrimaryPushButton, PushButton, Slider, StrongBodyLabel, Theme, + setCustomStyleSheet, setTheme, ) import config @@ -62,7 +63,7 @@ import edid_parse import patterns APP_NAME = "DLPilot" -APP_VERSION = "0.4.0" +APP_VERSION = "0.4.1" REPO_URL = "https://gitea.hulk.wang/hulk/HW-Debug-Tools" MONO = QFont("Consolas", 10) CHIP_IDLE = "#666666" @@ -266,7 +267,12 @@ class App(QMainWindow): outer.setSpacing(12) header = QHBoxLayout() title = StrongBodyLabel("DLPilot", root) - title.setStyleSheet("font-size: 24px; font-weight: 700;") + # 标题字号走 QFont:FluentLabelBase 在每次主题切换时会用自带的颜色规则 + # 重写自定义 qss 槽,setStyleSheet/setCustomStyleSheet 写的字号都会被冲掉 + title_font = title.font() + title_font.setPixelSize(24) + title_font.setBold(True) + title.setFont(title_font) header.addWidget(title) header.addWidget(BodyLabel("EDID · 测试图案 · RS485 光机控制", root)) header.addStretch(1) @@ -355,7 +361,13 @@ class App(QMainWindow): row.addWidget(BodyLabel(f"{channel}: 0x", card)) for label, color in items: button = PushButton(label, card) - button.setStyleSheet(f"PushButton {{ background: {color}; color: {'black' if color == '#00FF00' else 'white'}; }}") + # fluent 控件的样式须走 setCustomStyleSheet:直接 setStyleSheet 的内容 + # 会在 setTheme 时被库重刷的自带 qss 冲掉;且须覆盖 hover/pressed 态, + # 否则悬停瞬间底色仍会被自带态样式盖回 + fg = 'black' if color == '#00FF00' else 'white' + qss = ("PushButton, PushButton:hover, PushButton:pressed, " + f"PushButton:disabled {{ background-color: {color}; color: {fg}; border: none; }}") + setCustomStyleSheet(button, qss, qss) button.clicked.connect(lambda checked=False, c=color: self._show_solid(c)) row.addWidget(button) row.addStretch(1) @@ -414,6 +426,9 @@ class App(QMainWindow): setattr(self, attr, combo) params.addWidget(combo) self.lbl_dlpc_st = BodyLabel("未连接", card) + # 状态标签允许换行兜底: 任何长错误串都不得撑大参数行最小宽度(否则窗口拉长, + # 刷新/连接按钮被挤出可视区) + self.lbl_dlpc_st.setWordWrap(True) params.addWidget(self.lbl_dlpc_st) params.addStretch(1) body.addLayout(params) @@ -770,6 +785,7 @@ class App(QMainWindow): def _dlpc_toggle(self): if self.dlpc.is_connected(): self.dlpc.close(); self._serial_reset_state(); return port = self._dlpc_selected_port() + if not port: self.lbl_dlpc_st.setText('未选择串口,请先选择或刷新'); return try: baud, bytesize, stopbits = int(self.cbo_baud.currentText()), int(self.cbo_databits.currentText()), int(self.cbo_stopbits.currentText()) except ValueError: self.lbl_dlpc_st.setText("串口参数非法"); return parity = self.cbo_parity.currentText()[:1] or 'N'; config.SERIAL_CFG.update(port=port, baud=baud, bytesize=bytesize, parity=parity, stopbits=stopbits); config.save_slots(self.slots, self.cbo_area.currentText()); self.dlpc.open(port, baud, bytesize=bytesize, parity=parity, stopbits=stopbits) diff --git a/DLPilot/src/dlpc_rs485.py b/DLPilot/src/dlpc_rs485.py index 7668204..70c0516 100644 --- a/DLPilot/src/dlpc_rs485.py +++ b/DLPilot/src/dlpc_rs485.py @@ -326,11 +326,19 @@ class DLPC485: self.ser = None with self._lock: self._target = None - et = str(e) - if 'PermissionError' in repr(e) or '拒绝访问' in et \ - or 'Access is denied' in et: - et += '(串口被占用: 检查串口助手/另一个本工具实例)' - self.on_status(f'连接失败: {et}') + blob = str(e) + repr(e) + if 'PermissionError' in blob or '拒绝访问' in blob \ + or 'Access is denied' in blob: + hint = '串口被占用(串口助手/另一实例?)' + elif 'FileNotFoundError' in blob or '找不到指定的' in blob \ + or 'No such file' in blob: + hint = '串口不存在,请刷新后重选' + else: + hint = '无法打开,详情见日志' + # 状态标签只放短提示(长异常串会撑爆参数行把窗口拉长), + # 完整异常走 on_event 进收发日志 + self.on_status(f'连接失败: {hint}') + self.on_event(f'连接失败: {e!r}') if self.ser is not None: try: item = None diff --git a/DLPilot/src/selftest_theme.py b/DLPilot/src/selftest_theme.py new file mode 100644 index 0000000..9681079 --- /dev/null +++ b/DLPilot/src/selftest_theme.py @@ -0,0 +1,82 @@ +# coding: utf-8 +"""主题切换回归自检(无窗口, 离屏渲染)。 + +背景: fluent 控件直接 setStyleSheet 的样式会在 setTheme 时被库重刷的自带 qss 冲掉 +(v0.4.0 RGB swatch 按钮切主题丢底色的根因)。swatch 改走 setCustomStyleSheet、 +标题字号走 QFont 后,本测试保证: + 1. Dark -> Light -> Dark -> Light 往返后 9 个 swatch 按钮样式表仍带底色; + 2. Light 下实际渲染出的按钮中心像素 = swatch 色值(不只样式表字符串存在); + 3. 标题 24px 加粗(QFont)在往返后不变。 + +运行: python selftest_theme.py (依赖 PySide6-Fluent-Widgets) +""" +import os +import sys + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from PySide6.QtGui import QColor +from PySide6.QtWidgets import QApplication +from qfluentwidgets import PushButton, StrongBodyLabel, Theme, setTheme + +import app_fluent +import patterns + + +def check_style_sheet(win, app): + swatches = [b for b in win.findChildren(PushButton) if b.text() in {"55", "AA", "FF"}] + assert len(swatches) == 9, f"expect 9 swatch buttons, got {len(swatches)}" + title = [l for l in win.findChildren(StrongBodyLabel) if l.text() == "DLPilot"] + assert len(title) == 1, "title label not found" + colored = sum(1 for b in swatches if "background-color: #" in b.styleSheet()) + f = title[0].font() + print(f" swatch 样式表带底色: {colored}/9, 标题 24px 粗体: " + f"{f.pixelSize() == 24 and f.bold()}") + return colored == 9 and f.pixelSize() == 24 and f.bold() + + +def check_rendered_pixel(win, app): + """Light 主题下逐个渲染 swatch,与目标色一致的像素须占明显多数(背景为主, + 中心处是文字字形,不能只采单点)。""" + ok = True + for channel, items in patterns.SOLID_ROWS: + for label, color in items: + button = next(b for b in win.findChildren(PushButton) + if b.text() == label and f"background-color: {color}" in b.styleSheet()) + image = button.grab().toImage() + target = QColor(color) + total, matched = image.width() * image.height(), 0 + for y in range(image.height()): + for x in range(image.width()): + if image.pixelColor(x, y) == target: + matched += 1 + ratio = matched / total + same = ratio >= 0.2 + ok &= same + if not same: + print(f" 渲染不符: {channel}/{label} 期望 {color} 仅占 {ratio:.0%}") + print(f" Light 下渲染像素比对: {'一致' if ok else '存在偏差'}") + return ok + + +def main(): + app = QApplication.instance() or QApplication(sys.argv) + setTheme(Theme.AUTO) + win = app_fluent.App(0) + passed = True + for step, theme in (("初始 Dark", Theme.DARK), ("切 Light", Theme.LIGHT), + ("切回 Dark", Theme.DARK), ("再切 Light", Theme.LIGHT)): + setTheme(theme) + app.processEvents() + print(f"[{step}]") + passed &= check_style_sheet(win, app) + if theme == Theme.LIGHT: + passed &= check_rendered_pixel(win, app) + win.close() + print("ALL PASS" if passed else "FAILED") + return 0 if passed else 1 + + +if __name__ == "__main__": + sys.exit(main())