QML TableView:可编辑表格与 SQLite 数据持久化

表格光好看还不够,很多场景下还得能直接改数据、能存到数据库。直接在单元格里编辑比弹个框改要顺手得多,而 SQLite 作为轻量级嵌入式数据库,跟 Qt 配合起来也很方便。

这篇文章用两个例子演示:第一个例子做一个单元格可编辑的表格,点一下价格或库存就能直接改,回车确认,只允许输入数字;第二个例子用 QSqlTableModel 对接 SQLite 数据库,实现数据的增删改查和持久化。

可编辑表格

一个商品价格表,点击价格或库存单元格进入编辑状态,弹出输入框,回车确认修改,有数字校验防止输错。

演示代码

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 editingColor: "#FFE082"
    readonly property color borderColor: "#e0e0e0"

    property bool isEditing: false
    property int editingRow: -1
    property int editingColumn: -1

    TableModel {
        id: tableModel
        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: tableModel
            columnSpacing: 1
            rowSpacing: 1

            delegate: Rectangle {
                implicitWidth: 120
                implicitHeight: rowHeight
                color: (row === editingRow && column === editingColumn) ? editingColor :
                       row === selectedRow ? selectedColor :
                       row === hoveredRow  ? hoverColor :
                       row % 2             ? "#f5f5f5" : "white"

                MouseArea {
                    anchors.fill: parent
                    hoverEnabled: true
                    onClicked: {
                        selectedRow = row
                        if (column > 0) {
                            editingRow = row
                            editingColumn = column
                            textInput.text = display
                            textInput.visible = true
                            textInput.focus = true
                        }
                    }
                    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 ? "#E91E63" : "#333333"
                    font.pixelSize: column === 1 ? 14 : 13
                    font.bold: column === 1
                    visible: !(row === editingRow && column === editingColumn)
                }

                TextField {
                    id: textInput
                    anchors.fill: parent
                    anchors.margins: 2
                    visible: row === editingRow && column === editingColumn
                    text: display
                    selectByMouse: true
                    horizontalAlignment: TextInput.AlignHCenter
                    verticalAlignment: TextInput.AlignVCenter

                    onAccepted: {
                        if (/^\d+$/.test(text)) {
                            let newCell = tableModel.getRow(row)
                            newCell[column === 1 ? "price" : "stock"] = text
                            tableModel.setRow(row, newCell)
                        }
                        visible = false
                        editingRow = -1
                        editingColumn = -1
                    }

                    onFocusChanged: {
                        if (!focus) {
                            visible = false
                            editingRow = -1
                            editingColumn = -1
                        }
                    }

                    validator: IntValidator { bottom: 0; top: 999999 }
                }
            }

            columnWidthProvider: function(col) { return (width - 2) / 3 }
            rowHeightProvider: function(row) { return rowHeight }
        }

        Text {
            Layout.fillWidth: true
            text: "点击价格或库存单元格进行编辑 · 仅允许整数 · 回车确认"
            color: "#666"
            font.pixelSize: 11
            horizontalAlignment: Text.AlignHCenter
        }
    }
}

编辑的核心思路是「同一个单元格里放两个东西,编辑时显示输入框,否则显示文字」。delegate 里同时有一个 Text 和一个 TextField,通过 visible 属性控制谁显示谁隐藏。editingRow 和 editingColumn 两个变量记录当前正在编辑的单元格位置。

点击单元格时,判断是不是产品列(column > 0),是的话就进入编辑状态:把当前值填进输入框、让输入框显示、获取焦点。产品列不让改,所以点了只选中不编辑。

确认修改有两种方式:按回车或者输入框失去焦点。回车时走 onAccepted,先校验输入是不是纯数字,校验通过就把新值写回模型。这里用的是 getRow() 配合 setRow() 两个方法:先用 getRow(row) 把当前行整份取出来,改掉目标单元格,再 setRow(row, newCell) 写回。注意 rows 属性只能读、不能直接整体赋值回去,想要改数据务必走 setRow(),否则会把单元格包成 QVariantList 导致类型校验失败。

输入框加了 IntValidator 限制只能输整数,从输入层面就拦住非法值。

SQLite 表格:数据持久化

一个人员信息表,数据存在 SQLite 数据库里,支持新增、删除、修改,关闭程序再打开数据还在。表格上方有操作按钮,选中一行后可以删除或修改。

篇幅太长,代码就不贴出来了,详见文章底部的Github链接。

这个例子用 C++ 的 QSqlTableModel 来对接 SQLite,QML 这边只负责展示和调用。QSqlTableModel 是 Qt 提供的 SQL 表格模型,封装了常见的增删改查操作,不用自己写 SQL 语句就能用。

核心流程是这样的:程序启动时打开或创建数据库和 people 表,实例化 SqlTableModel 并注册到 QML 上下文,TableView 的 model 直接指向这个 C++ 模型。新增、删除、修改都通过模型的 Q_INVOKABLE 方法来操作,操作完调 saveAll() 提交到数据库,再调 select() 刷新视图。

模型里暴露了一个 count 属性,用来在 QML 显示「共 N 条记录」------因为 rowCount 在 QML 里是方法不是属性,直接绑定会拿到函数体,而且增删后不会自动刷新,包一层带 NOTIFY 的属性就解决了。

表头用了 HorizontalHeaderView,跟 TableView 通过 syncView 关联起来,列宽自动同步,还支持拖拽调整列宽。第 0 列是数据库的主键 id,宽度设为 0 隐藏掉,用户看不到但数据还在,删除修改时用得上。

适用场景:本地数据存储、需要持久化的小型管理工具、离线应用的数据表格。


已验证环境:

相关推荐
Quz1 小时前
QML ListView:C++ 数据模型与 ProxyModel 实时过滤
qt
Quz1 小时前
QML ListView 入门:自定义样式与数据交互
qt
cpp_learner1 小时前
QT 隐藏式密码鉴权:一键开启管理员模式
qt
小林ixn1 小时前
用 SQLite + 大模型做一个 Text2SQL 小助手:从建表到自然语言查询的完整实战
数据库·sqlite
慧都小项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