QML TableView:自定义样式与排序表格

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 来做排序了。


已验证环境:

相关推荐
Quz1 小时前
QML TableView:可编辑表格与 SQLite 数据持久化
qt·sqlite
Quz1 小时前
QML ListView:C++ 数据模型与 ProxyModel 实时过滤
qt
Quz1 小时前
QML ListView 入门:自定义样式与数据交互
qt
cpp_learner1 小时前
QT 隐藏式密码鉴权:一键开启管理员模式
qt
慧都小项6 天前
当边缘AI上产线:QtitanDocking 如何让工业 HMI 实现多视图协同
人工智能·qt·ui·边缘计算·qt6.3
Quz6 天前
QML SwipeDelegate 滑动委托
qt
Quz6 天前
QML 列表中的四种委托:ItemDelegate、CheckDelegate、RadioDelegate、SwitchDelegate
qt
实心儿儿6 天前
Qt — 显示类控件
qt
江湖人称菠萝包6 天前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter9-Qt Charts
笔记·qt·qt5