1. 模型-视图-委托(Model-View-Delegate)架构
QML 沿用 Qt 经典的 MVC 思想,但把 Controller 弱化(信号/绑定承担职责):
| 角色 | 职责 | 例子 |
|---|---|---|
| Model | 提供数据(行/字段/顺序) | ListModel、XmlListModel、C++ QAbstractListModel、JS 数组 |
| View | 遍历模型,按需创建委托 | ListView、GridView、TableView、PathView、Repeater |
| Delegate | 渲染单条数据,处理交互 | 自定义 Component |
关键点:View 只创建"可见区域 + 缓存区"的委托(虚拟化),所以百万条数据也能流畅滚动——这是 QML 列表性能的根基。
2. Repeater —— 最简单的"视图"
Repeater 不滚动、不虚拟化,适合少量静态数据:
Column { Repeater { model: ["苹果", "香蕉", "橙子"] delegate: Text { text: "水果:" + modelData } } }Repeater 的 delegate 中可用变量:
| 变量 | 含义 |
|---|---|
| index | 当前序号(从 0 开始) |
| modelData | 当前数据项(数组/整数模型时) |
| model | 整个模型(或对象模型时表示当前项) |
数据量大(>50)时别用 Repeater——它一次性创建全部委托,无虚拟化。
3. ListView —— 列表视图(重点)
3.1 最小完整示例
import QtQuick ListView { width: 300 height: 400 model: ListModel { ListElement { name: "张三"; age: 20 } ListElement { name: "李四"; age: 22 } ListElement { name: "王五"; age: 24 } } delegate: Rectangle { width: parent.width height: 40 color: index % 2 === 0 ? "#f8f8f8" : "#ffffff" border.color: "#e0e0e0" Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: name + ",年龄 " + age // 直接访问模型字段 } } }delegate 里直接按字段名访问模型数据(name、age),这是 QML 的便捷机制。
3.2 常用属性
ListView { model: myModel delegate: myDelegate orientation: ListView.Vertical // 或 Horizontal spacing: 4 // 项间距 cacheBuffer: 400 // 预渲染缓冲(像素) clip: true // 裁剪,防止滚动时露出内容 snapMode: ListView.SnapToItem // 吸顶/吸附 highlightRangeMode: ListView.ApplyRange // 高亮跟随 preferredHighlightBegin: 0 preferredHighlightEnd: 100 currentIndex: 0 // 当前项 // 信号 onCurrentIndexChanged: console.log("current", currentIndex) onMovementEnded: console.log("滚动了") }3.3 highlight 与滚动定位
ListView { id: list model: 20 delegate: Rectangle { width: list.width; height: 40 color: index === list.currentIndex ? "#d0e8ff" : "white" Text { anchors.centerIn: parent; text: "第 " + index + " 项" } MouseArea { anchors.fill: parent onClicked: list.currentIndex = index } } highlight: Rectangle { color: "#d0e8ff" } // 高亮背景(可动画) highlightMoveDuration: 200 // 高亮移动动画时长 highlightFollowsCurrentItem: true }3.4 数据操作(ListModel 增删改)
ListModel { id: model ListElement { name: "A" } ListElement { name: "B" } } // 追加 / 插入 / 修改 / 删除 Button { onClicked: model.append({ name: "C" }) } Button { onClicked: model.insert(0, { name: "Z" }) } Button { onClicked: model.setProperty(1, "name", "B2") } Button { onClicked: model.remove(0) } Button { onClicked: model.clear() }ListModel 的每个元素是 ListElement,字段用 role 表示。增删改后 View 自动刷新——这就是"数据驱动 UI"。
3.5 动态模型(JS 数组作 model)
ListView { model: [10, 20, 30] // JS 数组 delegate: Text { text: "值:" + modelData // 数组项用 modelData } }model: 5(整数)时 delegate 只拿到 index,常用于占位/分页。
3.6 ListView 性能要点
| 优化 | 说明 |
|---|---|
| delegate 用 Item 代替复杂 Rectangle | 减少绘制 |
| 图片设 sourceSize | 缩小解码内存 |
| 少用阴影/透明效果 | 滚动时开销大 |
| cacheBuffer 合理设置 | 太大浪费,太小滚动白屏 |
| 不要放重计算绑定 | delegate 的绑定每项都会求值 |
| 大量复杂项用 Recycle | 见后文"委托复用" |
3.7 委托复用(Recycle,Qt 5.15+/Qt6)
委托对象会被 View 复用(item 移动而非重建),需要在 onReused 里重置状态:
Component { id: delegateComp Rectangle { width: 200; height: 40 property int itemIndex Text { text: itemIndex } Component.onCompleted: console.log("created") // 只会执行有限次 onReused: { // 复用回调 color = "#ffffff" // 重置残留状态 itemIndex = model.index } } } ListView { model: 10000 delegate: delegateComp reuseItems: true // 开启委托复用(默认 false) }开启 reuseItems 后,不要再依赖 Component.onCompleted 做每项初始化,改在 onReused 处理。
4. GridView / PathView / TableView
4.1 GridView —— 网格
GridView { width: 320; height: 320 cellWidth: 100 cellHeight: 100 model: 20 delegate: Rectangle { width: 90; height: 90 radius: 8 color: "hsl(" + (index * 18) + ", 60%, 60%)" Text { anchors.centerIn: parent; text: index } } }4.2 PathView —— 路径滚动(封面流)
PathView { width: 400; height: 200 model: 10 path: Path { startX: 0; startY: 100 PathQuad { x: 200; y: 100 } PathQuad { x: 400; y: 100 } } delegate: Rectangle { width: 120; height: 120 radius: 8 color: "steelblue" Text { anchors.centerIn: parent; text: index; color: "white" } } }PathView 适合"卡片轮播"效果,性能比 ListView 差,数据不宜过多。
4.3 TableView(Qt5.12+ / Qt6)
import QtQuick import QtQuick.Controls TableView { width: 400; height: 300 model: myTableModel // 建议 C++ QAbstractTableModel delegate: Rectangle { implicitHeight: 36 color: row === tableView.currentRow ? "#e0f0ff" : (row % 2 === 0 ? "#fafafa" : "white") Text { anchors.fill: parent anchors.margins: 8 verticalAlignment: Text.AlignVCenter text: display // 表模型 role:display } } }复杂表格(排序/编辑/多列)强烈建议用 C++ QAbstractTableModel + TableView,详见 04 章。
5. Component 与自定义组件
5.1 什么是 Component
Component 是"可复用模板"。两种形态:
- 独立 .qml 文件:MyButton.qml 本身就是一个 Component。
- 内联 Component 元素:在一个文件里定义可复用模板。
// 内联 Component Component { id: myText Text { font.pixelSize: 18; color: "#333" } } Column { Repeater { model: 3 delegate: Loader { sourceComponent: myText; onLoaded: item.text = "条目 " + index } } }5.2 自定义组件文件
MyCard.qml:
// MyCard.qml import QtQuick Rectangle { id: root width: 200 height: 80 radius: 10 color: "#f0f4f8" border.color: "#d0d7de" // 对外暴露属性(供使用方设置) property string title: "" property string subtitle: "" property color accentColor: "#3d5a80" // 对外暴露信号 signal clicked() Column { anchors.centerIn: parent spacing: 4 Text { anchors.horizontalCenter: parent.horizontalCenter text: root.title font.pixelSize: 18 font.bold: true color: root.accentColor } Text { anchors.horizontalCenter: parent.horizontalCenter text: root.subtitle font.pixelSize: 12 color: "gray" } } MouseArea { anchors.fill: parent onClicked: root.clicked() } }使用:
import QtQuick Column { MyCard { title: "标题 A" subtitle: "副标题说明" onClicked: console.log("点击 A") } MyCard { title: "标题 B" accentColor: "#e63946" } }5.3 组件规范(团队最佳实践)
| 规范 | 说明 |
|---|---|
| 文件首字母大写 | MyCard.qml |
| 根对象必须 id: root | 内部引用统一用 root.xxx |
| 对外 API 用 property + signal | 不要暴露内部实现细节 |
| 对外属性命名语义化 | title、subtitle、accentColor |
| 内部元素 id 不对外 | 使用方不能访问内部 id |
| 复杂组件加文档注释 | /// 卡片组件:显示标题与副标题 |
| 提供合理默认值 | 属性都给默认值,避免使用方必须设置 |
| 用 Qt.styleHints / 主题统一风格 | 颜色/字体从主题读取 |
5.4 property alias —— 属性转发
// MyButton.qml Rectangle { id: root property alias text: label.text // 把 label.text 暴露为 text property alias textColor: label.color Text { id: label anchors.centerIn: parent text: "" color: "white" } }alias 限制:只能转发属性,不能转发绑定表达式;别名不能重复命名;别名不能参与循环(property alias a: b + property alias b: a 禁止)。
5.5 Loader —— 按需加载
Loader { id: contentLoader anchors.fill: parent source: showPage ? "Page2.qml" : "Page1.qml" // 或 sourceComponent: someComponent onLoaded: item.someProperty = 1 }Loader 适用:按需加载大组件、延迟初始化、多页面切换。item 是加载出的根对象。
5.6 常用组件模式:封装 vs 继承
QML 没有传统继承,组合优先:
- 需要"换皮":给组件加 property + 内部 delegate。
- 需要"扩展":用 Loader + default property 嵌入子内容:
// Panel.qml Rectangle { id: root default property alias content: contentItem.data // 子对象直接进 contentItem Rectangle { id: contentItem anchors.fill: parent anchors.margins: 10 } } // 使用 Panel { Text { text: "我直接放在 Panel 里" } }6. 样式与主题
6.1 Controls 2 的样式体系
Qt Quick Controls 2 通过"控件风格(Style)"管理外观:
- Default:默认风格。
- Basic:基础风格(可定制)。
- Fusion:桌面风(跨平台一致)。
- Material:Material Design(Android 风)。
- Universal:Windows 10 风。
- Imagine:纯图片驱动风格(可换肤)。
运行时选择(C++):
qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion");或配置文件 qtquickcontrols2.conf:
[Controls] Style=Fusion6.2 自定义控件外观的三种方式
方式 A:修改内建 Style(推荐入门)
import QtQuick.Controls.Fusion Button { text: "Fusion 风格" }方式 B:写自定义 Style 文件(Qt6):
QtQuick/Controls.2/<Style>/Button.qml
方式 C:直接用 contentItem / background 属性定制(灵活)
Button { text: "自定义按钮" background: Rectangle { radius: 8 color: parent.hovered ? "#3d5a80" : (parent.pressed ? "#1d3557" : "#457b9d") } contentItem: Text { text: parent.text color: "white" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } }6.3 主题变量统一(工程实践)
用单例管理颜色/字体:
// Theme.qml pragma Singleton import QtQuick QtObject { readonly property color primary: "#3d5a80" readonly property color primaryDark: "#1d3557" readonly property color accent: "#e63946" readonly property color bg: "#f0f4f8" readonly property color textPrimary: "#2c3e50" readonly property int fontSizeBase: 14 }qmldir 中声明 Singleton,使用时:
import "." Rectangle { color: Theme.primary } Text { color: Theme.textPrimary; font.pixelSize: Theme.fontSizeBase }7. 数据模型选择决策表
| 场景 | 推荐 |
|---|---|
| 静态少量数据(<50) | JS 数组 / 内联 ListModel |
| 中等量可增删(数百) | ListModel |
| 海量数据(万+) | C++ QAbstractListModel |
| 表格 | C++ QAbstractTableModel + TableView |
| 树 | TreeView(Qt6.4+)/ C++ QAbstractItemModel |
| 实时数据流 | C++ 模型 + 信号增量更新 |
| 本地 XML | XmlListModel(旧)或 C++ 解析 |
| 数据库 | C++ 模型包 SQL 查询(04 章) |
经验:QML 侧 ListModel 超过几千条就明显变慢(每次修改全量通知);数据量大的业务模型必须放 C++。
8. 综合示例:可复用的"标签云 + 列表"组件
// TagCloud.qml import QtQuick Flow { id: root spacing: 6 property var tags: [] // 外部传入标签数组 property color tagColor: "#457b9d" signal tagClicked(string tag) Repeater { model: root.tags delegate: Rectangle { width: tagText.implicitWidth + 24 height: 28 radius: 14 color: root.tagColor Text { id: tagText anchors.centerIn: parent text: modelData color: "white" font.pixelSize: 12 } MouseArea { anchors.fill: parent onClicked: root.tagClicked(modelData) } } } }// 使用 TagCloud { tags: ["Qt", "QML", "C++", "动画", "布局"] tagColor: "#2a9d8f" onTagClicked: console.log("选中标签", tag) }9. 本章小结
- MVC:Model 提供数据、View 负责滚动/布局、Delegate 渲染每项。
- ListView 是核心:虚拟化、highlight、currentIndex、增删改自动刷新。
- 大数据必须用 C++ 模型;QML 侧模型适合中小数据。
- Component + 独立 .qml 文件实现复用;property/signal/alias 定义对外 API。
- 样式:Controls 2 风格体系 + background/contentItem 定制 + 主题单例。
10. 自测题
- ListView 为什么能滚动百万条数据?
- reuseItems: true 后 Component.onCompleted 还可靠吗?
- modelData 和 name(字段名)在 delegate 中分别什么时候用?
- property alias 能转发绑定表达式吗?
- 什么时候应该把模型从 QML 挪到 C++?
(答案见正文。)