Shadcn/ui × Qt 6/QML:一次从 Web UI 到桌面 UI 的组件化实践

Qt 6/QML 组件库从 0 到 1:架构、踩坑和 AI 协同开发

开源:github.com/QtShadcn/qtshadcn

写 Qt 界面写久了,多少会嫌弃默认那套东西。Qt Quick Controls 的默认样式在 macOS 上还能看,换到 Windows、Linux 上直接露馅。想改好看点,QSS 是全局手术刀,给一个按钮调 hover 都要写一坨选择器,改完这个控件别处跟着遭殃。

我一直想要 shadcn/ui 那种感觉:颜色是语义化的 token,组件能像乐高一样组合,明暗主题一键切换。Web 生态早就有了,Qt 这边没人做。没人做,那就自己造一个。项目叫 qtshadcn,Qt 6 / QML 的可组合 UI 组件库,今天正好是它从 M3 收尾、M4 Icon 系统上线、到文档站和自动截图流水线落地的关键一天,40 多个 commit,从早上 8 点干到晚上 8 点。

这篇把从 0 到 1 的过程完整拆一遍:架构怎么定、组件怎么对齐规范、AI 怎么配合开发、以及文档和截图这些"软件工程里最容易被偷懒的部分"怎么自动化。

第 0 步:先想清楚架构,别急着写代码

造组件库最忌讳上来就画按钮。先把哲学定下来,我抄的是 shadcn/ui 的四层:Design Token → Component → Composition → Theme。语义化 token 定义颜色圆角间距,组件消费 token,组件之间靠组合而不是继承,主题靠切换 token 实现。

定了哲学之后,有两个关键决策,直接决定这个库是"能用"还是"好用":

决策一:QML 管 UI,C++ 管能力。 Theme 引擎、Icon 注册、Model 这些能力层全放 C++。不然它就退化成一个 QML 样式库,主题切换、图标缓存这类事在 QML 里写会非常别扭。

决策二:复用 Qt Quick Controls,不自己从零画。 键盘导航、Focus 管理、无障碍这些 Qt 花了几十年打磨的东西,白拿。我只做一件事:基于 QQC 基类,只替换 background 和 contentItem。注意要用 QtQuick.Controls.Basic,这是 token 自绘的前提,macOS 默认的 native style 会拒绝自定义 contentItem。

组件命名统一加 Shadcn 前缀,跟 QQC 基类区分开。用户那边一行 import 就能用:

qml 复制代码
import QtQuick
import QtQuick.Controls as QQC
import QtShadcn

Window {
    width: 640
    height: 480
    visible: true

    QtShadcnTheme { id: theme }   // 主题入口,必做

    ShadcnButton {
        anchors.centerIn: parent
        text: qsTr("Deploy")
        iconName: "rocket"
        onClicked: console.log("clicked")
    }
}

Token 系统:颜色不能写死

组件里所有颜色走 theme.tokens[token名] 查询,theme.mode = "dark" 就能全局随动。这个设计有个容易踩的坑:如果你把颜色取出来存成静态值,mode 切换时它就不变了,绑定链断了。颜色必须始终经 token 查询,保持绑定。

variant 到 token 的映射集中在 VariantTokens.qml 一个文件里管。新增一个 variant 要做两件事:枚举里加一项、映射表里加一行,顺序要对齐。这个文件就是整个库的"调色板宪法",改样式只动它,组件代码不用碰。

组件实现铁律:先研究,再实现

这个铁律是拿返工换来的。M2 阶段写 Button,我凭印象直接开写,结果尺寸整体小一档、漏了 link variant、圆角用了 8px(实际应该是 6px)。全部做完对照官方才发现,回头返工。

从那以后定下规矩,每个组件开发前必须过三关:

第一关,抓官方文档。 拿 Button 举例,要确认 variants 完整列表(default/secondary/destructive/outline/ghost/link)、全部 sizes 的像素值(xs=32 / sm=36 / default=40 / lg=44 / icon=40)、交互状态(hover/pressed/disabled/loading/focus ring)。

第二关,抓官方源码。 shadcn-ui/ui 仓库 2026 年重构过好几轮,路径经常变。v2/v3 的稳定路径是 apps/www/registry/default/ui/<name>.tsx,v4 挪到了 apps/v4/registry/__components__/base-luma.tsx。raw.githubusercontent 的直链经常 404,正确姿势是用 gh api 列目录确认路径再取内容,认证了免限流。

第三关,输出对照表再写码。 variants、sizes、交互状态、样式细节、API 命名、无障碍行为,逐项列成表格,确认完才开始动手。后面照表实现,改的就是 QQC 的 background/contentItem 而已。

AI 协同开发:把规范写进仓库,让 AI 自己遵守

这个库的规范特别多:token 体系、QQC 覆盖的各种坑、命名约定、构建验证流程。每次让 AI 帮忙写组件,都要重新交代一遍,交代漏了它就按自己的印象来,等于 M2 的返工再来一次。

解法是把规范固化进仓库:参考 slidev/skills 的格式,在仓库里放了一个 skills/qtshadcn/ 目录,里面有 SKILL.md 主文件加九个 references 分册。SKILL.md 包含什么?When to Use、Quick Start、一张完整的组件总览表(每个组件的说明 + 对应参考文件),最狠的是那张关键坑表,全是踩出来的:

说明
override QQC 子组件丢定位 handle/indicator/background 的 x/y/width 定义在模板默认组件内部,override 后不自动应用,必须自带定位
TextArea 内部滚动 QQC.TextArea 没有 Flickable,TextEdit 原生不响应滚轮,要用 Flickable + TextEdit + ScrollBar 重写
contentHeight 早期 undefined Math.min/max 遇到 NaN 传播会拖垮组件,要加 > 0 兜底
readonly property 引用子对象 创建期立即求值会拿到 null,要放进惰性绑定

AI 进仓库先读 SKILL.md,规范、坑、流程一次到位。配合 GitHub issues 的组件计划(每批一个组件,实现完关对应 issue),开发节奏很稳。我的体会是:AI 写代码不等于不用懂规范,把规范写进 skill 让它遵守,比自己每次盯防省心得多。

自动截图流水线:文档配图不用手截

组件做完了要验证、要配图。showcase 里每组件一页(ButtonPage.qml、SliderPage.qml......),验证完还要给文档配效果图。手动截 21 个组件窗口,截完还要裁掉左侧菜单,这种事干一次就烦了。

于是写了 scripts/screenshot.sh:showcase 加了个 --screenshot 模式,配合 Qt 的 offscreen 平台(QT_QPA_PLATFORM=offscreen,纯软件渲染,不需要开 GUI 窗口),grabWindow 直接抓整窗,一条命令批量出全部组件的 PNG:

bash 复制代码
./scripts/screenshot.sh                            # 全量,21 张
./scripts/screenshot.sh button select textarea     # 只截指定组件
CROP="190,0,790,704" ./scripts/screenshot.sh       # 自定义裁剪矩形 x,y,w,h

这里有两个坑值得说。第一,offscreen 软件渲染不支持 MultiEffect 阴影,不开关掉会导致 Card 这类组件截出来是空白,所以截图模式有个环境变量 QTSHADCN_SCREENSHOT=1 专门关 GPU 特效。第二,默认裁剪矩形 190,0,790,704 就是把左侧菜单裁掉,只留组件内容区,这个值还能按页面微调。

输出文件名跟文档 slug 自动对齐:ButtonPage.qml → button.png,ButtonGroupPage → button-group.png。文档站直接引用,一条命令跑完,21 张效果图全齐。

文档站:Docus 5,每组件一页

文档站用 Docus 5(Nuxt Content 那套)搭的,每组件一页用法文档,带代码高亮。细节坑也不少:侧边栏菜单的 lucide 图标一开始是远程加载,构建时疯狂报 failed to load icon,改成 UIcon 本地打包;frontmatter 的 title 已经由 UPageHeader 渲染了,正文里再写 H1 就重复,全部删掉。

踩坑清单:攒了四个最有代表性的

除了上面表格里的,今天 Dialog 重构还贡献了一个经典案例:ScrollView 的 implicitWidth binding loop,症状是对话框左右乱跳、内容不支持滑动。修法是固定 height: implicitHeight,别让它在视口里反复自我测量。这类坑单看都小,攒多了就是一本小册子,全记进 SKILL.md 了。

收尾

这套组织方式跟 QSS 的本质区别:QSS 是全局样式覆盖,token 是语义化契约。颜色不叫"蓝色 200",叫"primary / muted / accent",主题切换就是换 token 值。组件可以组合(Card = Header / Content / Footer),能力沉淀在 C++ 层。文档配图一条命令生成,AI 照着仓库里的规范写代码。基础设施是拿来省的,不是拿来表演的。

仓库在 GitHub 上开源:github.com/QtShadcn/qtshadcn,组件还在陆续补(Animations 开发中,Models / Table 在计划里),欢迎提 issue 和 PR。

你写 Qt 界面现在用什么方案,QSS 硬调、第三方库还是自己造轮子?评论区聊聊。


相关推荐
余额瞒着我当琳1 小时前
C++--vector第二讲:手写 C++ STL:vector 源码剖析与迭代器失效分析
android·java·c++
郝学胜-神的一滴1 小时前
[简化版 GAMES 104] 现代游戏引擎 06:从Tick时序到邮局模型,拆解确定性世界的底层密码
开发语言·c++·游戏引擎·图形渲染·软件开发·opengl
不会代码的小猴1 小时前
6. Qt网络编程
开发语言·c++·笔记·qt·算法
沐风老师1 小时前
从零开始学3dMax插件开发!
c++·3dmax插件·3dmax·maxscript
wuyk55512 小时前
98.C语言易混难点:字符数组与字符串指针的底层差异
c语言·开发语言·c++·stm32·嵌入式硬件·算法
峥无12 小时前
从0到1手撕红黑树:封装实现 my_map 与 my_set(SGI-STL 源码级深度解析)
开发语言·c++·笔记·算法·stl
波特率11520012 小时前
C++新特性---属性说明符与标准属性
开发语言·c++
不会代码的小猴13 小时前
3. 控件学习1
开发语言·c++·笔记·qt·算法
精英的英17 小时前
记一次 Qt5 Language Server 开发
开发语言·vscode·qt