Qt6.8.3结合CMake与SQLite实现数据展示与刷新

0 次阅读

一、技术方案概述

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│