
Serial Studio CSD 窗口原生阴影改造从自绘阴影到系统 DWM 阴影的完整落地清单【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio导读Serial Studio 在 Windows 10 与 Linux 上使用自绘的客户区装饰CSDClient-Side Decoration标题栏早期版本为了让窗口看起来有阴影会额外绘制一层自绘投影导致窗口几何尺寸大于可见内容引发对话框尺寸计算错误、窗口无法贴边、调整大小热区偏移、下拉弹窗被裁剪等一系列缺陷。本文基于仓库中 spec 0004 的任务清单tasks.md结合配套的 spec.md需求与验收标准与 plan.md技术设计完整梳理这次移除自绘阴影、改由操作系统原生绘制改造的 8 个任务、每一步的验证方式与依赖关系并对照 core/Ui/Platform/CSD.cpp 等源码说明其最终实现形态。读完本文你可以掌握 CSD 窗口真尺寸几何模型的重构方法以及如何在 Qt 无边框窗口上通过 Win32 恢复 DWM 原生阴影与贴靠动画。背景为什么要移除自绘阴影在 Serial Studio 中NativeWindow 作为 QML 暴露的单例按平台决定是否启用 CSD 装饰Windows 10 与 Linux 上绘制自己的窗口边框与标题栏而 Windows 11 与 macOS 已经使用系统原生装饰。过去为了让 CSD 窗口也有投影效果应用会自己绘制一层 drop shadow——代价是每个 CSD 窗口都被创建得比可见内容更大窗口四周环绕一圈不可见阴影带所有与窗口几何相关的代码都要补偿这圈边距spec 中记载的阴影膨胀量约为每轴 48px、垂直 4832px。这种补偿逻辑在长期使用中反复引入四类缺陷见 spec.md对话框尺寸算错尺寸公式必须把不可见边距加回去偶尔加错导致窗口比内容大一圈或内容被裁剪窗口无法贴边不可见阴影带先碰到屏幕边缘留下可见缝隙调整大小热区偏移resize 光标在阴影带上就激活用户在空白区就触发拖拽下拉弹窗被裁剪combobox/menu 弹窗可能延伸到阴影带内被裁剪或不可点击。除此之外每次窗口 chrome 重绘都要渲染一次自绘阴影浪费 GPU 合成开销。而合成器Windows DWM、Linux 窗口管理器本来就擅长绘制原生窗口阴影把阴影交给系统可以整体消除这一整类缺陷而不是逐个修补症状。总体设计窗口几何 可见内容改造的核心原则对应 spec 的 R1是CSD 窗口的几何尺寸严格等于可见内容任何平台、任何方向都不再存在不可见边距。具体拆分删除阴影层9-slice 阴影图集、ShadowImageProvider、透明表面的BorderImage帧、shadowMargin()偏移全部移除窗口表面改为不透明不再请求 alpha buffer不再填充Qt::transparentsetupFrame()整个删除窗口从透明自绘阴影变成不透明系统阴影/细边框Windows 10 专用恢复 DWM 原生阴影在装饰器应用Qt::FramelessWindowHint之后向 HWND 重新设置WS_THICKFRAME并附带WS_CAPTION并通过QAbstractNativeEventFilter应答WM_NCCALCSIZE返回完整窗口矩形这样 DWM 会绘制原生阴影且 aero-snap、最小化动画全部免费获得——我们自己一行阴影都不用画LinuxX11/Wayland退化为细边框保留现有的 1px 细边框不实现任何 WM 检测或_KDE_NET_WM_SHADOW客户端阴影协议plan 中明确决定延迟因为它需要客户端提供阴影纹理等于把删除的代码重新引回来一半移除设置项csdShadowEnabled属性与 Settings 中的 Window Shadow 开关一起删除阴影是否出现变成纯平台决策。窗口装饰的外观标题栏颜色、最小化/最大化/关闭按钮、拖拽/双击行为保持不变spec 的 Goals 与非目标。8 个任务的任务清单tasks.md 核心内容任务清单 把 plan 拆解为 8 个小、有序、可独立验证的单元每个任务都是一个评审者能单独阅读的 diff。约定如下一个任务 一次聚焦的可评审修改若一个任务涉及超过 3 个文件或需要一段话来描述就拆分Verify验证是确认该单元完成的方式通常是python scripts/code-verify.py --check files必要时加一个测试或读回检查Deps依赖列出必须先落地的任务 ID任务顺序保证每步之后概念上工程可编译。T1 — 从 CSD 装饰器中剥离阴影层文件app/src/Platform/CSD.h、app/src/Platform/CSD.cpp内容删除ShadowRadius、阴影图集生成器generateShadowCorner、buildShadowAtlas、ShadowImageProvider、setupFrame()、updateFrameGeometry()、m_frame、m_shadowEnabled、shadow构造函数参数以及 alpha buffer /Qt::transparent表面设置shadowMargin()一并移除。验证python scripts/code-verify.py --check app/src/Platform/CSD.h app/src/Platform/CSD.cpp并 grep 确认ShadowRadius、shadowMargin、m_frame、csdshadow在CSD.*中再无引用。依赖无。状态已完成[x] done。T2 — 将 CSD 布局重新锚定到真实窗口矩形文件app/src/Platform/CSD.cpp内容标题栏位于 (0,0) 并横跨窗口宽度内容容器位于 (0, titleBarHeight()) 填满剩余区域边框尺寸等于完整窗口矩形edgeAt()的 resize 热区 从真实边缘起算的ResizeMarginupdateMinimumSize()/onMinimumSizeChanged()去掉2 * margin项windowStateChanged处理器在最大化/全屏时继续隐藏边框并调整标题栏尺寸不再有任何 frame 簿记。验证python scripts/code-verify.py --check app/src/Platform/CSD.cpp读回检查CSD.cpp中不再有引用除ResizeMargin以外边距的几何表达式。依赖T1。状态已完成。T3 — 从 NativeWindow 清除阴影偏好与 frameMargin文件app/src/Platform/NativeWindow.h、app/src/Platform/NativeWindow_CSD.cpp、app/src/Platform/NativeWindow_macOS.mm内容删除csdShadowEnabled的 Q_PROPERTY、getter/setter、信号、成员以及Window/CSDShadowEnabled的 QSettings 读写从头文件和两个实现中删除frameMargin()frameTopInset()现在只返回装饰器标题栏高度显示前回退值为CSD::TitleBarHeightCSD::Window构造不再传入 shadow 参数。验证对三个文件运行python scripts/code-verify.py --checkgrep 确认csdShadowEnabled与frameMargin在app/src/下不再出现。依赖T1。状态已完成。T4 — Windows 10 DWM 阴影启用器文件app/src/Platform/NativeWindow_CSD.cpp内容新增Q_OS_WIN专属的原生阴影路径一个enableNativeShadow(QWindow*)辅助函数在 HWND 上恢复WS_THICKFRAME | WS_CAPTION并发出SWP_FRAMECHANGED外加一个懒安装的进程级QAbstractNativeEventFilter跟踪所有已装饰的 HWND对WM_NCCALCSIZE返回完整窗口矩形最大化时按SM_CXSIZEFRAME/SM_CYSIZEFRAMESM_CXPADDEDBORDER内缩全屏时保留监视器矩形行为。从addWindow()对装饰非 Win11窗口接线窗口销毁时注销。验证python scripts/code-verify.py --check app/src/Platform/NativeWindow_CSD.cpp对照 plan 中最大化/全屏风险笔记读回过滤器AC5/AC8 冒烟测试由维护者在运行时把关。依赖T2、T3。状态已完成。T5 — 从设置中移除 Window Shadow 开关文件app/qml/Dialogs/Settings.qml内容删除 Window Shadow 的 Label Switch 行即csdShadowEnabled绑定Custom Window Decorations 开关与重启提示保留。验证python scripts/code-verify.py --check app/qml/Dialogs/Settings.qmlgrep 确认csdShadowEnabled在app/qml/下不再出现。依赖T3。状态已完成。注意当前仓库中该设置项位于 app/qml/Dialogs/Settings/SettingsGeneralPage.qml其中仅保留了 Custom Window Decorations 开关及其重启后生效提示window decoration changes apply after restartingWindow Shadow 行已不存在与 T5 的目标一致。T6 — 从 SmartDialog 尺寸计算中移除 frameMargin文件app/qml/Widgets/SmartDialog.qml内容删除frameMargin属性、Cpp_NativeWindow.frameMargin(root)刷新以及它在宽/高/最小/最大公式中的项titlebarHeightframeTopInset保留且现在精确描述窗口 chrome。验证python scripts/code-verify.py --check app/qml/Widgets/SmartDialog.qmlgrep 确认frameMargin在app/qml/下不再出现。依赖T3。状态已完成。当前 SmartDialog.qml 的尺寸公式已确认不再包含frameMargin只剩preferredHeight contentPadding * 1.50 titlebarHeight frameTopInset并通过Cpp_NativeWindow.frameTopInset(root)动态获取标题栏高度。T7 — 文档同步文件doc/claude/directory-map.md、doc/claude/specs/0004-native-csd-shadow/tasks.md内容更新Platform/的角色说明CSD 不再绘制阴影Win10 使用 DWM 原生阴影Linux 使用细边框并保持本清单最新。CLAUDE.md 无需改动没有修改任何规则/架构契约。验证通过 sanitize 流水线运行python scripts/documentation-verify.py文档 lint 为只读读回检查。依赖T1-T6。状态已完成。当前 directory-map.md 中的Platform/条目已更新为true-size CSD windows: no painted shadow; Win10 gets a DWM-drawn shadow via WM_NCCALCSIZE filter, Linux the 1px border即 T7 的产物。T8 — 自审、静态门禁与交接文件无新编辑——评审通过内容对照 plan 的文件表重读完整 diff泳道检查在Platform/的 C diff 上运行qt-cpp-review处理或记录发现然后运行python scripts/sanitize-commit.py。在交接消息中列出维护者的运行时检查清单AC1-AC8 观察项 在 CSD 平台上运行pytest tests/integration/ -v。验证sanitize 流水线干净diff 只包含 plan 文件表中的文件。qt-cpp-review 发现OWN-1broken visibleChanged deferral —— 通过在 addWindow 中直接调用 enableNativeShadow 修复、A4-1/EH-1per-monitor DPI 指标 —— 用GetSystemMetricsForDpi修复、stale-HWND 簇通过在 QSet 中跟踪 QWindow* 并在过滤器中匹配实时 winId() 修复。所有修复重新 lint 通过。依赖T1-T7。状态已完成。完成标准Definition of Donespec.md 中的每个验收标准都已满足或明确交给维护者作为运行时观察项交接中列出 AC1-AC9所有变更文件上python scripts/code-verify.py --check干净无新错误在 C diff 上运行qt-cpp-review三个发现簇已修复见 T8其余干净热路径未触及plan 声明无涉及——无需运行--benchmark-hotpath已为维护者识别相关 pytest 目标CSD 平台上的tests/integration/python scripts/sanitize-commit.py已运行工作树无 lint 欠账diff 正是所要求的、且仅此而已——无范围蔓延、无外来文件被触碰ModuleManager.cpp 是维护者自己的待处理编辑未动维护者运行时验证后spec.md状态置为done。源码级实现印证CSD 装饰器真尺寸布局T1/T2 的产物core/Ui/Platform/CSD.h 中CSD::Window的公开面已经非常精简构造函数只有Window(QWindow* window, const QString color QString(), QObject* parent nullptr)——shadow 参数已不存在成员变量只剩m_border、m_titleBar、m_contentContainer等m_frame、m_shadowEnabled均已删除。常量TitleBarHeight 32与TitleBarHeightMaximized 28被注释为Chrome dimensions shared between CSD.cpp and NativeWindow_CSD.cpp (pre-show fallback)即 T3 中frameTopInset()显示前回退值的来源。在 core/Ui/Platform/CSD.cpp 中可以看到新布局的关键几何标题栏updateTitleBarGeometry()将m_titleBar定位在(0, 0)尺寸为(window-width(), titleBarHeight())内容容器updateContentContainerGeometry()将m_contentContainer定位在(0, tbHeight)尺寸为(width, height - tbHeight)边框setupBorder()用内联 QML 构造四条 1pxRectangle边无 painter、无 FBOupdateBorderGeometry()让边框覆盖完整窗口矩形(0,0,width,height)z 值设为 1000000 保持在最上层最大化/全屏时由windowStateChanged处理器隐藏resize 热区edgeAt()以CSD::ResizeMargin8px从真实边缘计算ResizeEdge事件过滤器中在MouseButtonPress时调用startSystemResize(qtEdgesFromResizeEdge(...))最小尺寸updateMinimumSize()只叠加titleBarHeight()没有任何2 * margin项。所有几何表达式都只引用ResizeMargin与标题栏高度——T2 要求的读回检查在源码层面成立。Win32 原生阴影一个进程级过滤器T4 的产物core/Ui/Platform/NativeWindow_CSD.cpp 完整实现了 T4 描述的两件套enableNativeShadow(QWindow*)Q_OS_WIN分支通过window-winId()拿到 HWND用SetWindowLongPtr在GWL_STYLE上 OR 回WS_THICKFRAME | WS_CAPTION | WS_MINIMIZEBOX | WS_MAXIMIZEBOX调用DwmExtendFrameIntoClientArea扩展 1px 上边距到客户区最后SetWindowPos带SWP_FRAMECHANGED刷新 frame同时把窗口插入静态集合s_shadowWindows并懒安装唯一的进程级CsdNativeShadowFilter。CsdNativeShadowFilter::nativeEventFilter继承QAbstractNativeEventFilter只消费windows_generic_MSG中的WM_NCCALCSIZE且wParam TRUE先用集合中 QWindow* 的实时winId()与msg-hwnd相等来确认窗口被跟踪——这正是 T8 中修复 stale-HWND 簇的方式平台窗口销毁/重建后winId()可能被回收复用跟踪QWindow*而非裸 HWND 可避免误命中对IsZoomed最大化的窗口用GetSystemMetricsForDpi(SM_CXSIZEFRAME / SM_CYSIZEFRAME, dpi) SM_CXPADDEDBORDER按当前监视器 DPI 内缩客户矩形防止内容越出监视器A4-1/EH-1 的修复最后返回 0 并 consume 消息让 DWM 只画阴影、不画原生 frame。接线与注销addWindow()在创建CSD::Window装饰器后非 Win11 且m_csdEnabled时直接调用enableNativeShadow(w)——T8 记录的 OWN-1 修复原先的 deferral 方式在visibleChanged延迟调用不可靠removeWindow()与QObject::destroyed处理器中同步从s_shadowWindows移除。窗口状态回调windowStateChanged仍由 CSD 装饰器负责隐藏边框与标题栏Win32 侧不重复簿记。NativeWindow 的公共面T3 的产物core/Ui/Platform/NativeWindow.h 的 Q_PROPERTY 只剩csdAvailable与csdEnabledcsdShadowEnabled已消失。frameTopInset()仍保留对话框确实需要 CSD 标题栏高度但frameMargin()已删除——plan 的权衡表解释了这个决策只有一个 QML 消费者保留一个恒为 0 的死参数会招致未来误用。QSettings 中旧的Window/CSDShadowEnabled键成为永不读写、也不删除的残留满足 spec R8/AC7携带旧键的settings.ini启动既不崩溃也不改变行为Window/CSDEnabled不受影响。验收标准如何确认改造生效spec.md 的 AC1-AC9 是这次改造的运行时验证清单与 tasks.md T8 的交接要求一一对应验收项验证内容对应需求AC1Win10/Linux 打开主窗口可达的所有对话框Settings、About、Donate、Project Editor 对话框、CSV player、examples browser尺寸贴合内容、无空白边带、无裁剪R1、R2AC2将主窗口与对话框拖到四边屏幕边缘可见边框贴边Win10 aero-snap 手势仍工作R3AC3沿所有边/角悬停 CSD 窗口resize 光标恰好出现在可见边框处且可正常调整大小窗口外不出现光标变化R4AC4在窗口底部/右侧边缘附近打开长 combobox如 Setup 中的驱动选择、Settings 中的主题选择每一项都渲染且可点击R5AC5Win10 上窗口有 DWM 绘制的、与原生应用视觉一致的阴影KDE/KWin X11 上若 WM 提供机制则由 WM 绘制否则出现细边框回退R6、R7AC6GNOME Wayland 与 KDE Wayland 上窗口显示细边框回退移动/缩放/贴靠正常弹窗行为正确R5、R7AC7Settings 不再显示 shadow 开关用携带旧键的settings.ini启动既不崩溃也不改变行为R8AC8Win11 与 macOS 冒烟装饰行为、标题栏颜色、退出/最小化/最大化与当前版本完全一致R9AC9在至少一个 CSD 平台上对运行中的应用执行pytest tests/integration/ -v确认无 API/窗口管理器交互回归R9其中 AC5/AC8 明确是维护者的运行时把关点plan 的风险章节也强调若 Qt 6.11 的 Windows 集成与过滤器冲突逐窗口回退到跳过 style 恢复、使用 Linux 风格细边框是一行代码的退路。旧数据兼容与已知权衡旧窗口几何此前版本保存的SmartWindow几何各分类的 x/y/w/h 别名是按带阴影的膨胀尺寸约 48px 每轴、4832 垂直保存的。改造后可缩放窗口首次恢复时约比原来大 48px由 SmartWindow 现有的屏幕边界钳制逻辑兜底固定尺寸对话框从内容绑定重新计算。这是一次性视觉偏移而非漂移下次保存即写入真实尺寸因此无需迁移代码plan 的权衡表迁移需要猜测某个存储尺寸是否被阴影膨胀过可能猜错自动在第一轮保存自愈。设置键Window/CSDShadowEnabled变为残留键不读、不写、不删Window/CSDEnabled语义不变。平台取舍KWin X11 的_KDE_NET_WM_SHADOW明确延迟需要客户端阴影纹理违背删除阴影代码的目标Linux 统一用 1px 细边框从而完全免去 WM 检测Wayland 不实现服务器端装饰保持 CSD 标题栏 无阴影 回退边框。无新依赖、无外部 API 变化平台检测只用 Qt 与 OS 现成能力NativeWindow是 QML 上下文单例而非外部 API/SDK 面frameMargin与csdShadowEnabled的消费者只有上面两个 QML 文件plan 中 grep 确认删除不影响任何 API/Handlers 或 EnumLabels。总结spec 0004 通过 8 个有序任务完成了 Serial Studio CSD 窗口阴影体系的彻底切换删除自绘阴影与全部几何补偿T1-T3在 Windows 10 用恢复 WS_THICKFRAME WM_NCCALCSIZE 过滤器换取 DWM 原生阴影与 aero-snapT4同步清理 QML 设置项与对话框尺寸公式T5-T6并完成文档同步、静态门禁与运行时验收交接T7-T8。最终结果是CSD 窗口几何严格等于可见内容阴影存在与否成为纯平台决策Win10 获得原生观感Linux 退化为干净的 1px 细边框而用户可见的标题栏外观与交互零变化。【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考