QSqlTableModel实战:不写SQL完成数据库增删改查
2026/9/17 16:41:29 网站建设 项目流程

QSqlTableModel这个类,说白了一句话:让你不写一条SQL语句,就能把一张数据库表完整地搬到界面里,并且用户直接在上面改、增、删,改完再一次性提交回数据库。我做Qt开发这些年,最早接触数据库操作也是从QSqlQuery一行一行写SQL开始的,等用到QSqlTableModel之后,才意识到很多常规的“表单维护界面”根本不需要那么费劲。这篇文章就把我实际项目里用QSqlTableModel做数据展示、视图绑定、增删改查的完整思路和踩过的坑都写出来,适合刚接触Qt数据库编程的初学者,也适合那些已经会QSqlQuery但觉得效率低、想换种更工程化写法的开发者。

QSqlTableModel之所以值得拿出来单独聊,是因为它把你和底层SQL之间加了一个“模型层”。你操作的是行列数据、是QModelIndex,而不是拼接字符串。这种范式转换,意味着代码里不再塞满SELECT、INSERT、UPDATE这类语句,界面和数据库逻辑真正做到了解耦。下面我按自己的项目经验,从初始化配置、视图绑定、增删改查、进阶扩展到问题排查,一条线全讲透。

1. QSqlTableModel的核心价值与适用边界

1.1 不写SQL也能操作表的底层原理

QSqlTableModel继承自QAbstractTableModel,是Qt Model/View框架在数据库领域的一个标准实现。它在内部维护了一张数据表的缓存,当你调用select()时,它会自动执行类似于SELECT * FROM 表名的操作,把结果集拉进内存,并且按行列组织成模型数据结构。之后你通过index(row, col)拿到单元格,再通过data()读取、setData()修改,底层都映射到对应字段值的暂存区里,只有调用submitAll()时才真正组装成UPDATE、INSERT、DELETE语句发往数据库。

这个过程里最妙的点是:你完全不需要关心SQL方言的细节。无论是SQLite、MySQL还是PostgreSQL,只要驱动没问题,上层代码几乎一模一样。我第一次从SQLite切到MySQL时,最大的感觉就是省心,数据库换了一个,模型层代码一行没动。

1.2 什么场景下该用它,什么场景该避开

任何以“单表维护”为核心的业务界面,都是QSqlTableModel的主场。举几个我做过的例子:后台管理系统的用户列表、商品列表、设备参数配置表,甚至是医疗项目里的病人基础信息维护页。这类界面高度雷同:顶部是表格、旁边有“新增/删除/保存/刷新”按钮,表格能直接编辑或弹出对话框编辑。用QSqlTableModel,三十分钟就能出一个能跑的版本。

但有些场景不适合硬套:

  • 多表复杂联查:例如三张表join以后还带GROUP BY汇总,这种查询请直接上QSqlQueryModel,或者干脆用QSqlQuery手动封装。
  • 大数据量实时分页:几万行以上还要支持流式分页加载,QSqlTableModel默认是一次性全量塞进内存,扛不住。这时候需要自己实现分页逻辑或考虑QSqlQueryModel配合LIMIT。
  • 字段级权限控制:例如不同用户看到不同的列、不同的按钮。虽然能通过setColumnHidden()做,但模型本身不擅长做细粒度权限。

我自己划分的原则是:一个页面对应一张主表,外键关联不超过两层,数据量预计在几千行以内,就用QSqlTableModel;反之,老老实实写SQL。这个判断标准在多个项目里验证下来都很稳。

2. 环境准备与数据库连接

2.1 项目配置里的隐藏雷区

在动手写代码之前,先把.pro文件或者CMakeLists.txt弄对。Qt的SQL模块是单独的QtSql,不添加就编译报“unknown module(s) in qt: sql”。我这里用qmake写一下:

QT += core gui sql greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

如果你还打算做表格控件的美化、自定义委托之类,widgets模块千万别漏。CMake方式就对应地加find_package(Qt6 COMPONENTS Core Gui Sql Widgets)。这里有个坑是我印象很深的:Qt5和Qt6在SQL模块的用法上几乎没有变,但如果你在.pro里漏了sql,错误提示却可能晚到运行时才暴露,非常迷惑。我建议新手把这一行当成比代码更前置的步骤来检查。

2.2 打开数据库连接的两种姿势

连接数据库最常用的是静态方法QSqlDatabase::addDatabase(),但很多人不知道它返回的是按连接名存储的实例。默认连接名是QSqlDatabase::defaultConnection,如果你只连一个库,直接这么写:

QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("demo.db"); if (!db.open()) { qWarning() << "数据库打开失败:" << db.lastError().text(); return -1; }

SQLite比较简单,不用账密、不用端口,设个文件路径就完事。MySQL则要额外配置主机、端口等:

QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("127.0.0.1"); db.setPort(3306); db.setDatabaseName("qt_demo"); db.setUserName("root"); db.setPassword("123456"); if (!db.open()) { qWarning() << "数据库连接失败:" << db.lastError().text(); }

我遇到过最典型的问题是驱动没编进去:执行程序时提示QSqlDatabase: QMYSQL driver not loaded。这种情况去Qt安装目录的plugins/sqldrivers下看有没有qsqlmysql.dlllibqsqlmysql.so,没有就确认是否装了对应数据库的开发库、还是需要自己编插件。很多人卡在这一步半天,排查思路其实是先看驱动文件存不存在,再看数据库服务能不能用第三方客户端连上。

2.3 模型初始化的顺序很重要

数据库连接打开之后,模型初始化的代码,我的习惯是固定四步:

QSqlTableModel *model = new QSqlTableModel(this, db); model->setTable("student"); model->setEditStrategy(QSqlTableModel::OnManualSubmit); model->select();

顺序不能乱。先setTable告诉模型要操作哪张表,再设置编辑策略,最后select()真正把数据从表里拉出来。很多新手会漏掉select(),结果表格界面上一行数据都没有,还以为是数据库地址错了。其实模型在setTable之后只是“知道”了表的元信息,并没有真正加载数据。你可以把setTable理解为打开文件对话框选好了文件,而select()才是双击文件把内容读出来。

表名一定要和数据库里实际的名字完全一致,大小写也严格匹配。Windows上SQLite可能比较宽容,但MySQL在Linux环境经常因为大小写对不上导致找不到表。这个细节我是在一次生产环境事故里记牢的。

3. 视图绑定与显示控制实战

3.1 QTableView表格控件绑上模型后立刻能看见数据

模型和视图的绑定是Qt框架最顺手的地方,核心就一行代码:

QTableView *view = new QTableView; view->setModel(model); view->show();

绑完之后,模型里的数据会一行一列地渲染到表格里。默认情况下,表格的列头显示的往往是字段名,例如idnamescore,而实际业务里通常要显示“学号”“姓名”“成绩”。这个场景需要自定义表头文字。很多人会去搜怎么改TableView的表头,其实直接操作模型就行:

model->setHeaderData(0, Qt::Horizontal, "学号"); model->setHeaderData(1, Qt::Horizontal, "姓名"); model->setHeaderData(2, Qt::Horizontal, "成绩");

注意一点:setHeaderData只影响显示,不影响模型数据本身。而且如果你在select()之后又对模型做了一次setTable之类的操作,可能需要重新设置表头。

如果某些列是不需要让业务人员看到的,比如数据表的主键,那可以用隐藏的方式。在TableView上操作:

view->setColumnHidden(0, true);

隐藏的是视图层,模型里该列的数据仍然在,增删改的时候不会影响。

3.2 三种编辑策略怎么选

setEditStrategy这个接口决定了用户修改的数据什么时候同步到数据库,有三种可选策略,我用过之后总结成一句话:新手用OnManualSubmit,老手也多偏爱OnManualSubmit,只有极少数场景才用自动提交。

策略同步时机适用场景问题风险
OnManualSubmit手动调用submitAll()大多数桌面端表单维护界面最安全,可以先校验再提交
OnRowChange光标离开当前行时提交行粒度编辑,逻辑简单的小工具行内编辑了但没换行,数据其实还没提交
OnFieldChange任意单元格编辑完成立即提交类似Excel的即时保存场景出错难回滚,高频调用数据库压力大

我强烈推荐OnManualSubmit的原因很现实:数据库操作最怕丢失数据。如果你用自动提交,用户在某个单元格输入到一半误点了其他单元格,这一条数据就直接写进数据库了。有些时候这个是期望的行为,但更多的时候,我们应该给用户一个“保存”按钮,让所有修改先留在内存里,等用户点了“保存”,再统一提交。这也是绝大多数企业级管理软件的标准交互。

选择自动提交方案,界面体验上倒是最接近Web表单的“改完即存”,但是数据库连接波动、字段校验失败、并发锁冲突这些问题处理起来会特别痛苦,因为错误往往在用户没注意到的时候已经发生。

3.3 行选择和整行操作的体验打磨

表格绑上之后,默认的选中模式是单击选中单元格。做数据维护界面时,通常改为选中整行,这样用户一目了然自己选的是哪一条记录,也方便做“删除当前行”这种操作。

view->setSelectionBehavior(QAbstractItemView::SelectRows); view->setSelectionMode(QAbstractItemView::SingleSelection);

再做一层限制:禁止用户直接在表格里乱改单元格,所有编辑都走弹窗。这样能最大程度减少误操作。把这个和OnManualSubmit配套起来,基本上就是一个非常稳健的桌面数据维护界面。

弹窗编辑的场景,代码里通常在选中某行之后取出对应记录:

int row = view->currentIndex().row(); QSqlRecord rec = model->record(row); QString name = rec.value("name").toString();

record(row)返回的是一条记录,里面按字段名取值,比用手动算列的index再取值得要清晰得多。尤其是表结构调整后,按字段名取值的代码完全不用改,这就是工程化思维的体现。

4. 增删改查的完整实操流程

4.1 新增记录:insertRecord和insertRow都不难但细节不同

新增数据,推荐的方式是先取出一个“空记录”,填充值之后再插入模型:

QSqlRecord rec = model->record(); rec.setValue("name", "王小明"); rec.setValue("score", 92); model->insertRecord(-1, rec);

record()不带参数时,返回一条与当前表结构匹配的空记录,字段名、字段类型、默认值都齐全。往里面setValue时,字段名必须是表里的真实字段名,大小写也有讲究,建议保持一致。insertRecord(-1, rec)表示在模型末尾追加这一行记录。

然后调用submitAll()提交。注意,submitAll()会提交模型里所有未提交的更改,包括插入、修改、删除,而不是只提交某一条。所以提交之前,代码里最好做一层业务校验,比如姓名不能为空、成绩范围要合法,否则几百条脏数据一次性入库,后续排查代价非常大。

如果插入的数据里含自动增长的主键,例如自增id,通常不需要手动设置,数据库会自己分配。但如果表没有自增主键而你又没有给主键赋值,提交时会失败。这是很多新手觉得“我明明写了insertRow,为什么插入不成功”的根本原因。解决方式是看数据库表设计时是否设置了主键,并且确认模型里能正确拿到primaryIndex()

4.2 修改数据:先改内存再提交,回滚也有后悔药

修改单元格数据,本质上就是setData

QModelIndex index = model->index(row, column); model->setData(index, "新值");

这里改的只是模型内存里的数据,等于是在草稿纸上改了字,但还没誊写到数据库。如果用户反悔了,想放弃所有未提交的修改,支持一键撤销:

model->revertAll();

这个设计非常像一个“事务草稿箱”。我实际项目里,界面上的“刷新”按钮,点完后也是分批执行:先记录当前选中的行,再revertAll(),然后select()重新拉取数据库最新数据。有时候用户一边开着界面,另一边别的客户端改了数据,点刷新就能把新数据拉回来。

当用户点了“保存”按钮,真正的提交逻辑是两步走:

if (model->submitAll()) { db.commit(); } else { db.rollback(); }

submitAll()返回bool,表示所有SQL语句是否执行成功。注意事务要由你手动管理,submitAll()本身并不会自动开启和提交事务。你可以在打开数据库连接之后,手动db.transaction(),然后用submitAll()执行写入,最后根据结果决定commit()还是rollback()。我遇到过最尴尬的情况是:数据库写入报错,但因为没包事务,界面上模型的数据已经清空了,数据库里还是旧数据,两边状态对不上,特别难看。后来统一改成手动事务,这个问题再没出现过。

4.3 删除数据:removeRows批量操作也很顺手

删除当前选中的行,代码非常短:

int row = view->currentIndex().row(); model->removeRow(row); model->submitAll();

如果要做批量删除,比如用户在表格里多选了多行,就需要遍历选中模型索引。有一种常用写法是:

QModelIndexList selected = view->selectionModel()->selectedRows(); std::sort(selected.begin(), selected.end(), [](const QModelIndex &a, const QModelIndex &b) { return a.row() > b.row(); }); for (const QModelIndex &idx : selected) { model->removeRow(idx.row()); } model->submitAll();

为什么从后往前删?因为removeRow会改变模型行索引,如果先删前面的行,后面行的索引就变了,原本要删的第5行可能变成了第4行,导致删错或者漏删。从后往前删,索引变化不影响已经处理过的行。这个细节是初学者最容易踩的坑。

删除操作是不可逆的,建议在界面上加一个确认对话框。Dialog的提示文案我一般写清楚:“确定要删除选中的N条记录吗?此操作不可恢复。”防止用户手滑。

4.4 过滤和排序:不写SQL的灵活查询

QSqlTableModel内置了过滤器和排序支持,这是它比纯QTableView好用很多的地方。

过滤本质上是生成WHERE条件,用SQL语法写:

model->setFilter("score >= 60"); model->select();

注意,setFilter之后一定要再调一次select(),否则不会生效。过滤条件里如果涉及用户输入,要特别小心SQL注入。虽然QSqlTableModel本质上是原生SQL拼条件,但至少要用QSqlDatabase的转义函数处理一下输入值:

QString safeVal = model->database().escapeIdentifier(input, QSqlDatabase::FieldName); model->setFilter(QString("name = '%1'").arg(safeVal));

如果想要动态地在多个字段之间切换模糊查询,可以拼成name LIKE '%关键字%'这种形式。LIKE语句里对单引号的处理要小心,最好的做法是用参数绑定。不过QSqlTableModel的setFilter不支持像QSqlQuery那样的bindValue方式,不少人因此改用QSqlQueryModel。说实话,如果查询真的复杂到需要多条件动态拼接,用QSqlQueryModel是更合适的路子。

排序用setSort

model->setSort(2, Qt::DescendingOrder); model->select();

setSort接收列索引和排序方向,并不是字段名字符串。有些新手会写setSort("score", ...),编译都过不去。排序同样要配合select()才能生效。

想要组合筛选和排序,直接先setFiltersetSort,最后调一次select()即可。它们的执行顺序在模型内部会合成一条带WHERE和ORDER BY的SQL语句。

5. 进阶玩法:委托、关联表与信号联动

5.1 自定义委托实现更友好的编辑界面

默认的TableView编辑时,文本字段给一个QLineEdit,布尔字段可能就是一个复选框的逻辑,但这远远不够。比如成绩字段希望用SpinBox限制范围,日期字段希望弹出日历选择器。这个需求通过自定义QStyledItemDelegate来完成。

一个经典的SpinBox委托长这样:

class ScoreDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &) const override { QSpinBox *editor = new QSpinBox(parent); editor->setRange(0, 100); editor->setSuffix(" 分"); return editor; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { int value = index.model()->data(index).toInt(); qobject_cast<QSpinBox *>(editor)->setValue(value); } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { QSpinBox *spinBox = qobject_cast<QSpinBox *>(editor); spinBox->interpretText(); model->setData(index, spinBox->value(), Qt::EditRole); } };

在视图上挂上委托:

view->setItemDelegateForColumn(2, new ScoreDelegate(view));

这样用户双击成绩列时,弹出来的是只能输入0到100的SpinBox,天然规避了非法输入。我实际项目里还做过按钮委托,用于在表格内直接触发某行操作,幸好有自定义委托这条路径,否则要额外加列事件判断,代码会乱很多。

5.2 QSqlRelationalTableModel:外键字段直接显示关联表内容

比如说订单表里存着用户ID,但你界面上想显示的是用户名,而不是一串数字ID。QSqlTableModel做不到这一点,得用它的子类QSqlRelationalTableModel

QSqlRelationalTableModel *model = new QSqlRelationalTableModel; model->setTable("order"); model->setRelation(1, QSqlRelation("user", "id", "name")); model->select();

setRelation三个参数分别是:当前表的列索引、关联表名、关联表中的键字段、要显示的字段。这里的意思是:order表的第1列是user.id的外键,但显示的时候要显示的是user.name。

关联模型在使用上和QSqlTableModel几乎一致,增删改查都兼容。唯一的区别是,当用户修改关联列时,模型帮你做了一层ID和显示文本的映射。

但是有一点要特别注意:submitAll()提交时,关联列写入数据库的仍然是外键ID,而不是显示文本。这个机制让我曾经困惑了好一阵,后来看了Qt源码才明白,关系模型的转换发生在视图显示层,数据库持久化的还是原始键值,这其实是合理的。所以你在dataChanged信号里取到的数据,要想拿到显示文本,需要自己通过关联字段去查。

5.3 dataChanged信号与多界面联动

前面用了模型做数据展示,另一个很实用的功能是监听数据变化,联动更新其他界面。比如一个界面是班级列表,一个界面是学生详情,当用户在某张表格里修改了班级人数,另一个界面要实时刷新总数。

这种场景可以直接连接模型的dataChanged信号:

connect(model, &QSqlTableModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) { qDebug() << "数据变化区间:" << topLeft << bottomRight; // 重新计算并刷新其他视图 });

信号里携带的是变化区域的最小和最大索引,大多数情况下我们只需要关注topLeft.row()就够了。要注意的是,dataChanged只对模型内存中的变化触发,如果数据是外部程序改的,模型是感知不到的。外部数据变更需要定时select()刷新,或者在数据库层面做触发器再外加一些机制,但那已经超出这个类本身的职责范围了。

我习惯把“模型数据变化”和“保存成功”这两件事分开处理。dataChanged只管UI联动,比如启停保存按钮、更新状态栏;而保存成功后再触发一次select()submitAll后的刷新,去同步真实数据库状态。这样逻辑清晰,不会在信号里嵌套触发太多额外操作导致死循环。

6. 常见问题排查与经验教训

6.1 表格空白,一行数据都没有

这个问题的出现率,几乎是我被问到最多的一个QSqlTableModel问题。排查路径如下:

  1. 确认db.open()成功,打印lastError()看有没有报错。
  2. 确认表名拼写正确,大小写是否匹配。
  3. 确认调用了select(),这一步漏掉的话,表格永远空白。
  4. 打印model->rowCount(),如果返回0,看是不是查询条件把数据全过滤了。
  5. 如果是MySQL,确认当前用户对这张表有没有SELECT权限。

还有一个容易被忽略的点:如果用setFilter过滤后没有调select(),模型显示的还是旧数据。如果你看到界面数据是上一次查询的结果,而不是最新的过滤结果,十有八九是忘了重新select()

6.2 提交失败,到底有没有写进去

submitAll()返回false时,第一时间要看model->lastError()。常见几种情况:

报错场景原因分析解决思路
database is lockedSQLite被多线程或另一个进程锁住检查连接是否被占用,事务是否有未关闭的
duplicate primary key插入数据的主键重复设置自增主键,或插入前检查ID是否存在
no such column模型中列名和数据库不一致检查表字段拼写和大小写
parameter count mismatch过滤或插入的字段数与表不匹配检查record()是否拿到正确的表结构

提交失败后,建议代码里做两件事:一是rollback()回滚事务,二是调用revertAll()让模型回到提交前的内存状态。不然界面上已经更新的数据和数据库实际数据不一致,用户无法判断哪个是真实的。

6.3 主键问题的各种妖蛾子

QSqlTableModel在内部依赖于数据库主键来判断行是否已存在,进而决定生成UPDATE还是INSERT。如果一张表没有主键,模型的修改、删除操作很容易出现“不更新反而变成插入”、“删除没反应”这类诡异问题。

排查的时候,先打印model->primaryIndex().columnCount(),如果返回0,说明模型没拿到有效主键。解决办法:要么给表加主键,要么用setPrimaryKey()手动指定。注意,setPrimaryKey()只能在setTable()后、select()前调用,顺序错一点就不生效。

还有一种情况是:表的主键是联合主键,QSqlTableModel对复合主键的支持并不完全可靠。我在老项目里遇到了一个由两个字段联合组成主键的流水表,用QSqlTableModel怎么都跑不对,最后换成了QSqlQuery手写SQL,干净利落地解决了。所以,如果你的表设计就是复合主键,建议直接避开这个类。

6.4 性能优化与大数据量场景

QSqlTableModel默认把所有数据加载到内存,这在几千行时毫无压力,一旦到几万行,界面的滚动、排序就开始发飘。我在一个日志查询工具里试过把十万行记录塞进去,结果首次select()耗时好几秒,界面卡到爆。这种场景下,我总结了几个优化方向:

  • 使用setQuery()的QSqlQueryModel:它更轻量,但不可编辑。
  • 分批加载:自己实现“按页查询”,用LIMIT控制每次拉取200行左右,滚动到底部再加载下一页,需要重写模型的部分方法,工作量略大。
  • 过滤优先:在业务上尽量先缩窄数据集,比如按日期范围、按关键字过滤后再加载。
  • 关闭不必要的列:隐藏列虽然不显示,但模型仍然会携带这些数据,某些大字段(比如BLOB)会严重拖慢加载速度。这种情况下别把BLOB列直接塞到QSqlTableModel里,SQL里就排除掉才是正道。

另外,select()时如果担心大量数据阻塞UI线程,可以把模型操作放在子线程中进行,但要注意QSqlDatabase连接不能跨线程共享,子线程里必须新建独立连接。这个坑一旦踩到,就是“不能在两个线程同时使用同一个QSqlDatabase连接”之类的诡异崩溃。我个人的建议是:如果数据真的达到十万行以上,优先考虑用QSqlQueryModel做只读分页,同时把ID、时间等关键字段拿出来单独做筛选逻辑。这样性能和体验反而比强行用QSqlTableModel更好。

6.5 编译期与运行期的模块坑

文章开头提到的unknown module(s) in qt: sql,在.Pro里加sql就解决。但还有一批人会在运行时遇到driver not loaded。如果SQLite驱动没装,不要慌,查看Qt安装目录下的plugins/sqldrivers。SQLite一般默认都有,MySQL驱动则需要确认Qt安装选项里是否勾选了MySQL插件。如果你用的是MinGW版Qt,而MySQL驱动只提供了MSVC版本,那就要么换编译器,要么从源码编译驱动。这个问题非常劝退新人,提前说清楚能让很多人少走一晚上的弯路。

7. 一些项目里养成的个人习惯

最后分享几个我长期使用QSqlTableModel沉淀下来的习惯,不一定适合所有项目,但至少在我负责的几个系统里都证明了价值。

第一,给所有数据库表的增删改操作统一封装成函数,比如saveCurrentData()refreshData()appendRecord()。这样即使以后把QSqlTableModel换成自定义模型,界面层代码几乎不用动。

第二,编辑策略永远选OnManualSubmit,配合用户可见的“保存”“取消”按钮,让用户明确知道修改什么时候真正落库。我发现很多用户其实并不喜欢“不小心点一下就改掉数据库”的感觉,手动提交反而更能给人安全感。

第三,在submitAll()之后,务必检查返回值。如果提交失败,立刻回滚事务并弹出包含lastError()文本的对话框。我见过太多项目里保存失败没有任何提示,用户以为成功了,实际数据库里一条都没变,这种体验非常致命。

第四,不要嫌弃revertAll()。界面上的“不保存并刷新”功能,并不是什么高级逻辑,本质就是revertAll()select()两行代码。但要小心,如果用户之前添加了多行数据然后点了刷新,这些未提交的“新增行”会一起被回滚掉,所以在点击刷新前最好加一个确认。

QSqlTableModel不是万能的,它的边界就是单表、可编辑、中小数据量这些关键词圈出来的范围。弄清楚它擅长什么、不擅长什么,再结合你自己的项目特点去取舍,这个类一定会成为Qt数据库开发工具箱里非常顺手的工具。真哪天遇到它搞不定的场景,你至少知道该去找QSqlQuery或QSqlQueryModel,这本身就是一种进步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询