TableView 是 Qt Quick 里用来展示二维数据的表格组件,比 ListView 多了列的概念,适合展示结构化内容。默认的 TableView 很难看,但通过自定义 delegate、表头、列宽控制这些属性,可以做出效果很不错的表格。
这篇文章介绍2个例子:第一个例子做一个商品价格表,展示样式的应用;第二个例子在它的基础上加上排序功能。
自定义样式表格
一个商品价格表,有蓝色表头、行悬停高亮、点击选中、交替背景色,价格列用醒目色加粗显示。

演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.qmlmodels
FadeInAnimation {
property int selectedRow: -1
property int hoveredRow: -1
readonly property int rowHeight: 40
readonly property color headerColor: "#1296FF"
readonly property color selectedColor: "#BCE6FF"
readonly property color hoverColor: "#E5F3FF"
readonly property color priceColor: "#E91E63"
readonly property color borderColor: "#e0e0e0"
TableModel {
id: customTableModel
TableModelColumn { display: "product" }
TableModelColumn { display: "price" }
TableModelColumn { display: "stock" }
rows: [
{ product: "笔记本电脑", price: "6999", stock: "15" },
{ product: "手机", price: "3999", stock: "25" },
{ product: "平板电脑", price: "4599", stock: "10" },
{ product: "智能手表", price: "1999", stock: "30" }
]
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 表头
Rectangle {
Layout.fillWidth: true
height: rowHeight
color: headerColor
radius: 4
Row {
anchors.fill: parent
Repeater {
model: ["产品", "价格", "库存"]
Rectangle {
width: parent.width / 3
height: parent.height
Text {
anchors.centerIn: parent
text: modelData
color: "white"
font.bold: true
font.pixelSize: 14
}
}
}
}
}
// 表格内容
TableView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: customTableModel
columnSpacing: 1
rowSpacing: 1
delegate: Rectangle {
implicitWidth: 120
implicitHeight: rowHeight
color: row === selectedRow ? selectedColor :
row === hoveredRow ? hoverColor :
row % 2 ? "#f5f5f5" : "white"
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: selectedRow = row
onEntered: hoveredRow = row
onExited: hoveredRow = -1
}
Rectangle {
width: parent.width
height: 1
color: borderColor
anchors.bottom: parent.bottom
}
Text {
anchors.centerIn: parent
text: column === 1 ? "¥" + display : display
color: column === 1 ? priceColor : "#333333"
font.pixelSize: column === 1 ? 14 : 13
font.bold: column === 1
}
}
columnWidthProvider: function(col) { return (width - 2) / 3 }
rowHeightProvider: function(row) { return rowHeight }
}
Text {
Layout.fillWidth: true
text: selectedRow >= 0
? "已选中第 " + (selectedRow + 1) + " 行"
: "悬停查看高亮 · 点击行可选中"
color: "#666"
font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter
}
}
}
与Qt Widget 那一套组件不同的是,TableView 本身不带头表,所以表头是单独用一个 Rectangle + Repeater 画出来的,放在 TableView 上方,列数和宽度跟下面的表格对齐。这样做的好处是表头不会跟着滚动,始终固定在顶部。
delegate 是表格样式的核心。行背景色用了一个三元表达式链:选中行 > 悬停行 > 交替色,优先级从高到低。选中行用 selectedRow 变量记录,点击哪行就把哪行的 row 赋值过去;悬停用 hoveredRow,鼠标移进来设为当前行,移出去设为 -1。交替色就更简单了,row % 2 判断奇偶行交替显示浅灰色和白色。
列宽由 columnWidthProvider 控制,这里是三等分。注意减去了 2 个像素(两条列间距),不然最后一列的右边框会被裁掉。行高同理,用 rowHeightProvider 统一设为 40。
可排序表格
在自定义样式的基础上,表头变成可点击的排序组件,再点一下切换升降序,表头旁边还会显示 ▲▼ 箭头提示方向。

演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.qmlmodels
FadeInAnimation {
property int selectedRow: -1
property int hoveredRow: -1
readonly property int rowHeight: 40
readonly property color headerColor: "#1296FF"
readonly property color selectedColor: "#BCE6FF"
readonly property color hoverColor: "#E5F3FF"
readonly property color priceColor: "#E91E63"
readonly property color borderColor: "#e0e0e0"
property int sortColumn: -1
property bool ascending: true
property var columns: [
{ title: "产品", role: "product" },
{ title: "价格", role: "price" },
{ title: "库存", role: "stock" }
]
TableModel {
id: originalModel
TableModelColumn { display: "product" }
TableModelColumn { display: "price" }
TableModelColumn { display: "stock" }
rows: [
{ product: "笔记本电脑", price: 6999, stock: 15 },
{ product: "手机", price: 3999, stock: 25 },
{ product: "平板电脑", price: 4599, stock: 10 },
{ product: "智能手表", price: 1999, stock: 30 }
]
}
TableModel {
id: sortedModel
TableModelColumn { display: "product" }
TableModelColumn { display: "price" }
TableModelColumn { display: "stock" }
}
function sortModel() {
let items = []
for (let i = 0; i < originalModel.rowCount; i++) {
items.push({
product: originalModel.getRow(i).product,
price: originalModel.getRow(i).price,
stock: originalModel.getRow(i).stock
})
}
if (sortColumn !== -1) {
let role = columns[sortColumn].role
items.sort((a, b) => {
let aValue = a[role]
let bValue = b[role]
if (typeof aValue === "string") {
return ascending ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue)
} else {
return ascending ? aValue - bValue : bValue - aValue
}
})
}
sortedModel.rows = items
}
Component.onCompleted: { sortModel() }
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 表头(可点击排序)
Rectangle {
Layout.fillWidth: true
height: rowHeight
color: headerColor
radius: 4
Row {
anchors.fill: parent
Repeater {
model: columns
Rectangle {
width: parent.width / 3
height: parent.height
Row {
anchors.centerIn: parent
spacing: 5
Text {
text: modelData.title
color: "white"
font.bold: true
font.pixelSize: 14
anchors.verticalCenter: parent.verticalCenter
}
Text {
visible: sortColumn === index
text: ascending ? "▲" : "▼"
color: "white"
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (sortColumn === index) {
ascending = !ascending
} else {
sortColumn = index
ascending = true
}
sortModel()
}
}
}
}
}
}
// 表格内容
TableView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: sortedModel
columnSpacing: 1
rowSpacing: 1
delegate: Rectangle {
implicitWidth: 120
implicitHeight: rowHeight
color: row === selectedRow ? selectedColor :
row === hoveredRow ? hoverColor :
row % 2 ? "#f5f5f5" : "white"
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: selectedRow = row
onEntered: hoveredRow = row
onExited: hoveredRow = -1
}
Rectangle {
width: parent.width
height: 1
color: borderColor
anchors.bottom: parent.bottom
}
Text {
anchors.centerIn: parent
text: column === 1 ? "¥" + display : display
color: column === 1 ? priceColor : "#333333"
font.pixelSize: column === 1 ? 14 : 13
font.bold: column === 1
}
}
columnWidthProvider: function(col) { return (width - 2) / 3 }
rowHeightProvider: function(row) { return rowHeight }
}
Text {
Layout.fillWidth: true
text: sortColumn === -1
? "点击表头排序"
: "当前按【" + columns[sortColumn].title + "】" + (ascending ? "升序" : "降序")
color: "#666"
font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter
}
}
}
排序的核心思路是「双模型」:originalModel 存原始数据不动,sortedModel 用来展示排序后的结果。点表头时把原始数据拷贝出来,用 JS 的 sort() 方法排好序,再赋值给 sortedModel.rows,TableView 就自动刷新了。
表头列定义成了一个数组,每项包含标题和对应的字段名,这样排序的时候知道按哪个字段排。点击表头列的逻辑也很直接:如果点的是当前排序列,就切换升降序;如果点的是新列,就设为排序列并默认升序。然后调一下 sortModel() 重新排序。
排序函数里有个细节要注意:字符串和数字的排序方式不一样。字符串用 localeCompare(),这样中文排序也能正常工作;数字直接相减就行。通过 typeof 判断类型后分别处理,不会出现"10 排在 2 前面"的字符串排序坑。
这个方案适合数据量不大的场景,如果数据量很大,纯 JS 排序会有点卡,那时候就该用 C++ 的 QSortFilterProxyModel 来做排序了。
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_tableview