一、技术方案概述
Qt 6.8.3提供了较为完善的桌面应用开发能力,配合CMake进行项目管理,再利用SQLite保存业务数据,可以快速搭建一个轻量级的数据管理程序。整个实现过程不需要额外部署数据库服务器,SQLite数据库文件可以直接随应用程序使用,非常适合桌面工具、配置管理、设备信息维护以及本地数据查询等场景。
本文以“数据库查询 → Qt界面展示 → 数据修改 → 刷新列表”为主线,介绍Qt 6.8.3、CMake和SQLite之间如何配合使用,并给出一个可以直接扩展的项目结构和核心代码。
整个程序可以划分为四个部分:
CMake:负责项目配置、编译和SQLite相关链接。
SQLite:负责本地数据存储。
Qt SQL模块:负责连接数据库、执行SQL语句。
Qt Widgets:负责表格展示、按钮操作和数据刷新。
其中,Qt的SQL模块已经提供了SQLite数据库驱动,因此通常不需要单独安装SQLite客户端程序。
二、创建Qt 6.8.3与CMake项目
Qt 6推荐使用CMake管理项目。一个简单的数据展示程序可以采用如下目录结构:
QtSQLiteDemo/
├── CMakeLists.txt
├── main.cpp
├── mainwindow.h
├── mainwindow.cpp
├── mainwindow.ui
└── data/
└── app.db如果程序采用纯代码创建界面,也可以不使用mainwindow.ui,直接在C++中创建控件。
CMake配置文件可以写成:
cmake_minimum_required(VERSION 3.16)
project(QtSQLiteDemo VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 6.8 REQUIRED COMPONENTS Widgets Sql)
qt_standard_project_setup()
qt_add_executable(QtSQLiteDemo
main.cpp
mainwindow.cpp
mainwindow.h
)
target_link_libraries(QtSQLiteDemo PRIVATE
Qt6::Widgets
Qt6::Sql
)这里最重要的是:
find_package(Qt6 6.8 REQUIRED COMPONENTS Widgets Sql)Widgets用于构建桌面界面,Sql用于数据库操作。
随后通过:
target_link_libraries(QtSQLiteDemo PRIVATE
Qt6::Widgets
Qt6::Sql
)将两个模块链接到程序中。
如果使用Qt Creator创建项目,可以直接选择CMake作为构建系统,然后选择已经安装的Qt 6.8.3 Kit进行编译。
三、创建SQLite数据库
SQLite最大的特点是数据库本身就是一个文件。例如:
data/app.db程序第一次启动时,可以检测数据库文件是否存在,如果不存在则创建数据库和数据表。
推荐通过QStandardPaths获取应用程序数据目录,而不是把数据库永久写入程序安装目录。例如:
QString dbPath =
QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation
) + "/app.db";然后创建SQLite连接:
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(dbPath);
if (!db.open()) {
qDebug() << "数据库打开失败:" << db.lastError().text();
return;
}打开成功后,可以创建数据表:
QSqlQuery query;
QString sql = R"(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
email TEXT
)
)";
if (!query.exec(sql)) {
qDebug() << "创建数据表失败:" << query.lastError().text();
}这里创建了一个简单的users表,包括编号、姓名、年龄和邮箱四个字段。
四、Qt连接SQLite时需要注意什么
Qt通过数据库驱动访问SQLite,因此连接数据库之前,可以先检查SQLite驱动是否可用:
qDebug() << QSqlDatabase::drivers();正常情况下可以看到:
("QSQLITE", ...)如果没有QSQLITE,程序可能会出现类似:
QSqlDatabase: QSQLITE driver not loaded
QSqlDatabase: available drivers:这类问题通常不是SQL语句错误,而是Qt SQLite驱动没有正确部署或当前运行环境缺少对应驱动。
开发阶段如果Qt安装完整,一般不需要额外处理;发布程序时则需要重点检查Qt SQL插件是否随程序正确部署。
五、使用QTableWidget展示SQLite数据
如果数据量不大,QTableWidget是实现数据库列表展示比较直观的方式。
首先在头文件中定义界面和刷新函数:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
void loadData();
private:
Ui::MainWindow *ui;
};
#endif 然后在构造函数中初始化数据库并加载数据:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
#include
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
loadData();
}
MainWindow::~MainWindow()
{
delete ui;
} 数据查询函数可以这样实现:
void MainWindow::loadData()
{
QSqlQuery query;
if (!query.exec(
"SELECT id, name, age, email "
"FROM users ORDER BY id DESC")) {
qDebug() << "查询失败:"
<< query.lastError().text();
return;
}
ui->tableWidget->setRowCount(0);
int row = 0;
while (query.next()) {
ui->tableWidget->insertRow(row);
ui->tableWidget->setItem(
row, 0,
new QTableWidgetItem(
query.value("id").toString()));
ui->tableWidget->setItem(
row, 1,
new QTableWidgetItem(
query.value("name").toString()));
ui->tableWidget->setItem(
row, 2,
new QTableWidgetItem(
query.value("age").toString()));
ui->tableWidget->setItem(
row, 3,
new QTableWidgetItem(
query.value("email").toString()));
++row;
}
}界面中的表格可以设置为4列:
ID | 姓名 | 年龄 | 邮箱如果使用Qt Designer,可以提前设置表头;也可以通过代码完成:
ui->tableWidget->setColumnCount(4);
ui->tableWidget->setHorizontalHeaderLabels({
"ID", "姓名", "年龄", "邮箱"
});为了让表格更加实用,还可以设置列宽自动适应:
ui->tableWidget->horizontalHeader()
->setSectionResizeMode(QHeaderView::Stretch);六、实现新增数据
SQLite数据展示完成后,实际项目通常还需要增加数据。
例如界面中准备三个输入框:
姓名:[ ]
年龄:[ ]
邮箱:[ ]
[添加] [刷新]点击“添加”按钮时,可以使用参数绑定,而不是直接拼接SQL字符串:
void MainWindow::on_addButton_clicked()
{
QString name = ui->nameEdit->text().trimmed();
int age = ui->ageEdit->text().toInt();
QString email = ui->emailEdit->text().trimmed();
if (name.isEmpty()) {
return;
}
QSqlQuery query;
query.prepare(
"INSERT INTO users (name, age, email) "
"VALUES (:name, :age, :email)"
);
query.bindValue(":name", name);
query.bindValue(":age", age);
query.bindValue(":email", email);
if (!query.exec()) {
qDebug() << "插入失败:"
<< query.lastError().text();
return;
}
loadData();
}这里的关键点是:
query.prepare(...)
query.bindValue(...)参数绑定不仅能够让SQL代码更加清晰,还可以避免手工拼接用户输入带来的SQL注入风险。
七、实现数据刷新
“数据展示与刷新”是整个程序的核心。
很多初学者在新增数据之后,只修改数据库,却发现界面上的表格没有变化。原因很简单:数据库中的数据和界面中的控件是两个不同的数据层。
执行:
INSERT INTO users ...只会改变SQLite数据库,不会自动修改QTableWidget。
因此新增、修改或删除数据库记录之后,需要再次执行查询并重新加载表格。
例如刷新按钮:
void MainWindow::on_refreshButton_clicked()
{
loadData();
}新增数据后同样调用:
loadData();于是整个刷新流程就变成:
用户操作
↓
执行INSERT / UPDATE / DELETE
↓
SQLite数据库发生变化
↓
重新执行SELECT
↓
清空QTableWidget
↓
重新填充数据
↓
界面显示最新结果这种实现方式简单、稳定,非常适合数据量较小的桌面应用。
八、修改和删除数据后如何刷新
如果需要实现完整的数据管理功能,可以增加修改和删除操作。
例如根据ID删除:
QSqlQuery query;
query.prepare("DELETE FROM users WHERE id = :id");
query.bindValue(":id", userId);
if (!query.exec()) {
qDebug() << "删除失败:"
<< query.lastError().text();
return;
}
loadData();修改同样如此:
QSqlQuery query;
query.prepare(
"UPDATE users "
"SET name = :name, age = :age, email = :email "
"WHERE id = :id"
);
query.bindValue(":name", name);
query.bindValue(":age", age);
query.bindValue(":email", email);
query.bindValue(":id", id);
if (!query.exec()) {
qDebug() << "更新失败:"
<< query.lastError().text();
return;
}
loadData();可以把loadData()理解成界面的统一刷新入口。
无论是新增、修改、删除还是用户主动点击刷新按钮,最终都调用同一个函数:
loadData();这样能够避免在多个事件处理函数中重复编写查询和表格填充代码。
九、main.cpp完整示例
程序入口非常简单:
#include "mainwindow.h"
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QSqlDatabase db =
QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("app.db");
if (!db.open()) {
qDebug() << "无法打开数据库:"
<< db.lastError().text();
return -1;
}
QSqlQuery query;
query.exec(R"(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
email TEXT
)
)");
MainWindow window;
window.show();
return app.exec();
} 如果数据库文件放在当前工作目录,那么程序运行后会生成:
app.db实际发布项目时,更建议使用明确的应用数据目录,避免因为程序启动路径变化导致数据库文件位置发生变化。
十、QTableWidget与QTableView如何选择
如果只是实现一个简单的数据展示工具,QTableWidget上手更加容易。它直接提供单元格管理,不需要额外的数据模型。
但如果数据量较大,或者程序后期需要支持分页、排序、筛选、编辑等功能,更推荐使用:
QTableView + QSqlTableModel例如:
QSqlTableModel *model =
new QSqlTableModel(this);
model->setTable("users");
model->select();
ui->tableView->setModel(model);刷新时:
model->select();相比手动清空QTableWidget再逐行插入数据,QSqlTableModel更加符合Qt的Model/View设计模式。
简单的数据工具可以采用:
QTableWidget功能复杂的数据库应用则可以考虑:
QTableView
↓
QSqlTableModel
↓
SQLite这种架构在后期维护时通常更加方便。
十一、解决数据库路径问题
Qt项目中一个非常容易忽略的问题就是数据库相对路径。
例如:
db.setDatabaseName("app.db");它依赖的是程序当前工作目录,而当前工作目录不一定等于可执行文件所在目录。
开发阶段可能表现正常,但打包后就可能发现数据库生成到了意料之外的位置。
可以通过:
QCoreApplication::applicationDirPath()获取程序所在目录:
QString dbPath =
QCoreApplication::applicationDirPath()
+ "/app.db";不过如果程序安装到受保护目录,例如Windows下的某些系统目录,直接写入程序目录可能遇到权限问题。
因此,更合理的做法是将用户数据库放到:
QStandardPaths::AppDataLocation对应的应用数据目录中。
例如:
QString dataPath =
QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation);
QDir().mkpath(dataPath);
QString dbPath = dataPath + "/app.db";这样可以减少因权限和运行目录造成的问题。
十二、数据库初始化建议独立封装
当项目功能越来越多时,不建议把数据库初始化代码全部放在main.cpp。
可以单独创建一个数据库管理类:
class DatabaseManager
{
public:
static bool initialize();
static QSqlDatabase database();
};例如:
bool DatabaseManager::initialize()
{
QSqlDatabase db =
QSqlDatabase::addDatabase("QSQLITE");
QString path =
QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation);
QDir().mkpath(path);
db.setDatabaseName(path + "/app.db");
if (!db.open()) {
qDebug() << db.lastError().text();
return false;
}
QSqlQuery query;
return query.exec(R"(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
email TEXT
)
)");
}然后在程序启动时:
if (!DatabaseManager::initialize()) {
return -1;
}这种结构可以让UI代码只负责界面逻辑,而数据库类负责连接、初始化和数据库相关操作。
十三、刷新机制中的常见错误
1. 只执行INSERT,不重新查询
这是最常见的问题。
数据库已经增加记录,但表格没有变化:
query.exec("INSERT ...");解决方法:
query.exec("INSERT ...");
loadData();2. 刷新时没有清空旧数据
如果每次刷新都直接追加:
insertRow(...)那么用户点击一次刷新就多一份数据。
因此重新加载前需要:
ui->tableWidget->setRowCount(0);或者:
ui->tableWidget->clearContents();
ui->tableWidget->setRowCount(0);3. 数据库连接没有成功
不要忽略:
db.open()的返回值。
应该始终检查错误:
if (!db.open()) {
qDebug() << db.lastError().text();
}4. SQLite驱动缺失
如果出现:
QSQLITE driver not loaded应该优先检查Qt SQL插件部署,而不是修改SQL语句。
5. 数据库路径不正确
程序可能连接到了另一个app.db,导致看到的内容与预期不一致。
调试时建议输出实际数据库路径:
qDebug() << db.databaseName();这样可以快速确认程序到底打开了哪个数据库文件。
十四、Qt 6.8.3 + CMake + SQLite的推荐项目架构
一个相对清晰的桌面项目可以按照以下方式组织:
QtSQLiteDemo/
├── CMakeLists.txt
├── main.cpp
├── database/
│ ├── databasemanager.h
│ └── databasemanager.cpp
├── model/
│ ├── usermodel.h
│ └── usermodel.cpp
├── ui/
│ ├── mainwindow.h
│ └── mainwindow.cpp
└── resources/对应的数据流可以理解为:
Qt Widgets
│
▼
MainWindow
│
▼
数据操作层
│
▼
Qt SQL模块
│
▼
SQLite当用户新增一条数据时:
输入框
↓
INSERT
↓
SQLite
↓
SELECT
↓
Model / TableWidget
↓
界面刷新当用户修改或删除数据时,也采用同样的数据流。
十五、性能与稳定性优化
对于几十、几百甚至几千条本地数据,直接重新查询并刷新表格通常已经足够。但数据规模继续增长后,需要考虑性能问题。
首先应该为经常用于查询和排序的字段建立索引。例如:
CREATE INDEX IF NOT EXISTS idx_users_name
ON users(name);批量插入数据时,不建议每插入一条就刷新一次界面。可以使用事务:
db.transaction();
for (...) {
query.exec(...);
}
db.commit();
loadData();也就是说,批量操作结束后只刷新一次,而不是:
插入 → 刷新
插入 → 刷新
插入 → 刷新
插入 → 刷新这样能够明显减少UI更新次数。
对于大型数据集,则可以进一步采用QSqlTableModel、分页查询或者Model/View架构,避免一次性将全部数据加载到界面。
十六、最终实现效果
完成上述配置后,一个基本的Qt 6.8.3 SQLite数据管理程序可以具备以下功能:
┌─────────────────────────────────────┐
│ 姓名:[________] 年龄:[____] │
│ 邮箱:[____________________] │
│ │
│ [添加] [修改] [删除] │
│ [刷新] │
├──────┬────────┬──────┬─────────────┤
│ ID │ 姓名 │ 年龄 │ 邮箱 │
├──────┼────────┼──────┼─────────────┤
│ 3 │ 张三 │ 25 │ xxx@test.com │
│ 2 │ 李四 │ 31 │ abc@test.com │
│ 1 │ 王五 │ 28 │ test@demo.com│