上节课我们完成了新建项目对话框的搭建,本节课将聚焦其核心功能逻辑的实现。此前我们已在对话框中配置了保存路径设置、项目名称定义等基础交互项,接下来的核心目标是:当用户点击 "确定" 按钮时,系统将自动为该项目创建专属的 db 文件,用于统一存储项目全量数据。这也是本节课实战的核心任务。
我们会在本节课引入SQLite数据库,SQLite 是一款轻量级、嵌入式关系型数据库,核心特点是 无需独立服务器、零配置、文件级存储,堪称 "数据库界的 U 盘"------ 一个完整数据库就是单个文件(后缀通常为 .db),无需安装服务、无需账号密码,复制文件即可迁移,适配 Windows、Linux、macOS 等全平台。
Qt 5 及以上版本已内置 SQLite 支持,可直接调用相关接口使用,无需额外配置 ------ 既不用单独安装数据库服务,也无需进行复杂的环境部署与管理。更便捷的是,一个完整的 SQLite 数据库会以单个文件的形式存储,且该文件具备跨平台兼容性,使用起来十分灵活。
创建DBOperator项目
创建一个空项目
我们在VS的解决方案下创建一个新的空项目,主要用来处理数据库操作的逻辑,项目类型"空项目",项目名称"DBOperator"
Debug和Release下配置属性→常规:
- 配置类型改为:动态库(.dll)
- C++语言标准改为:ISO C++17 标准 (/std:c++17)
创建数据库管理类
添加数据库连接、初始化的类DBManager。
++注意:VS 不自动生成构造函数和析构函数是有意为之,符合 C++ 的 "零开销原则" 和现代编程实践,编译器会给生成默认版本。实际根据自己的需要来决定是否手动添加。++
手动添加构造函数、析构函数,如下:
cpp
class DBManager
{
public:
DBManager();
~DBManager();
};
创建一个头文件DBOperator.h,并声明DLL导出的函数
cpp
# if defined(DBOPERATOR_LIB)
# define DBOPERATOR_API __declspec(dllexport)
# else
# define DBOPERATOR_API __declspec(dllimport)
# endif
这段代码是 Windows 平台下 DLL 项目中常用的宏定义技巧,用于控制函数和类的导入 / 导出行为。
++注意:当前项目是作为导出函数,我们我们在 DLL 项目的属性页(配置属性 → C/C++ → 预处理器 → 预处理器定义)中添加DBOPERATOR_LIB。++
回到数据库管理类,引用DBOperator.h,并将该类设置为导出类。
cpp
#include "DBOperator.h"
class DBOPERATOR_API DBManager
{
public:
DBManager();
~DBManager();
};
数据库初始化
利用SQLite3创建数据库的过程如下图,①创建数据库文件;②创建表结构;③关闭数据库连接; 其中数据库文件的路径是我们在创建项目的对话框中设置的路径。

定义initDatabase()函数
我们在DBManager类中,添加一个函数initDatabase()函数用来创建数据库文件、创建表结构,在DBManager类的析构函数中关闭数据库连接;
定义initDatabase函数,引入#include <QString>
cpp
void initDatabase(QString strFilePath);
注意:此处会提示找不到QString。
原因是:我们建立的空项目未引入Qt库。
解决方案:以下内容Debug/Release都配置
配置属性 → C/C++ → 常规 →附加包含目录:$(QTDIR)\include
$(QTDIR)\include\QtCore
$(QTDIR)\include\QtWidgets
$(QTDIR)\include\QtSql
配置属性→链接器→常规→附加库目录:$(QTDIR)\lib #表示 Qt 安装目录下的 lib 文件夹,用于引用Qt 官方库文件(如 Qt5Core.lib等)
$(OutDir) #表示当前项目的输出目录(通常是 Debug 或 Release 文件夹)
配置属性→链接器→输入→附加依赖项:(debug下带d后缀)Qt6Cored.lib
Qt6Widgetsd.lib
Qt6Sqld.lib
其中:顶层目录(即$(QTDIR)\include):这里存放着一些通用的头文件和模块索引文件。
如果编译报错:"Qt requires a C++17 compiler, and a suitable value for __cplusplus. On MSVC, you must pass the /Zc:__cplusplus option to the compiler."原因是:MSVC 编译器默认不会正确定义__cplusplus宏,需要手动添加/Zc:__cplusplus选项
解决方案:配置属性→c/c++→命令行→其他选项框里添加 /Zc:__cplusplus
编译通过后,我们来实现initDatabase函数,首先需要导入头文件,导入SQL操作涉及的头文件:
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlQueryModel>
#include <QSqlError>
定义全局变量QSqlDatabase sql_db :负责数据库连接的建立和管理。定义全局变量智能指针sql_query:负责执行 SQL 语句和处理结果集。智能指针自动管理内存,防止内存泄漏。之所以定义成指针的形式,是因为sql_query对象在后续SQL的处理时会频繁使用。
cpp
private:
QSqlDatabase sql_db;
std::unique_ptr<QSqlQuery> sql_query;
数据库连接管理
首先定义连接名称。在 Qt 里,默认连接名为 "qt_sql_default_connection",我们此处定义为cfgConnection。 对名称为cfgC的数据库连接是否存在进行检查,若存在则将其移除。原因是每个连接名必须是独一无二的,要是多次运行添加连接,就会因为重复添加同名连接而产生错误。先移除已有的连接,能够保证后续添加连接时不会出现冲突。
cpp
// 定义连接名称
const QString connectionName = "cfgConnection";
// 移除已存在的连接
if (QSqlDatabase::contains(connectionName))
{
QSqlDatabase::removeDatabase(connectionName);
}
创建数据库连接
通过addDatabase函数新建数据库连接,第一个参数"QSQLITE" 表明使用 SQLite 嵌入式数据库,第二个参数如果不指定连接名,就会使用默认连接名,也可以指定自定义的连接名称。
通过setDatabaseName函数设置数据库文件的路径:
cpp
// 创建数据库连接
sql_db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
sql_db.setDatabaseName(strFilePath);
打开数据库连接
尝试打开数据库文件。如果文件不存在,则会自动创建该文件。
cpp
// 打开数据库
if (!sql_db.open())
{
qDebug() << "连接数据库失败:" << sql_db.lastError().text();
return;
}
qDebug() << "连接数据库成功";
启用外键约束
在 SQLite 中,外键约束默认是关闭的,在实际项目中,外键约束的使用存在明显的 "场景分化" ------ 并非 "一刀切" 地使用或禁用,而是取决于项目的架构设计、数据一致性要求、性能需求、团队协作模式等核心因素。可通过如下代码开启外键约束:
cpp
QSqlQuery query(sql_db);
if (!query.exec("PRAGMA foreign_keys=ON"))
{
qDebug() << "启用外键约束失败:" << query.lastError().text();
return;
}
查询对象动态构造
创建 QSqlQuery 智能指针对象,QSqlQuery 对象的作用是执行 SQL 语句。sql_db 作为参数被传入,意味着这个查询对象会使用 sql_db的连接来执行 SQL 语句。
cpp
// 使用栈上对象替代new操作
sql_query = std::make_unique<QSqlQuery>(sql_db);
关闭数据库
在该类的析构函数中关闭数据库。
cpp
DBManager::~DBManager()
{
if (sql_db.isOpen())
{
sql_db.close();
}
}
创建数据库表结构
我们定义个新的方法,专门用来创建表结构,后续有表的增加,也统一在这个方法中维护。我们以创建一个设备表为例,首先定义创建表的SQL语句,然后利用我们定义的智能指针sql_query来执行这个sql语句:
cpp
void DBManager::createTables()
{
//设备表:ID、设备名称、类型、型号ID、间隔ID
QString strProjectDevice = QString("CREATE TABLE project_device(\
device_id VARCHAR(32) PRIMARY KEY NOT NULL,\
device_name VARCHAR(32) NOT NULL,\
device_type VARCHAR(32),\
model_id VARCHAR(32),\
bay_id VARCHAR(32),\
create_time TIMESTAMP DEFAULT(DATETIME('now','localtime')))");
if (!sql_query->exec(strProjectDevice))
{
qDebug() << "Error:Fail to create table project_device." << sql_query->lastError();
}
else
{
qDebug() << "table project_device created!";
}
}
定义全局访问
因为这个DBManager类后续会经常使用,所以我们使用单例模式构建一个该类的一个实例,以便提供一个全局访问点来获取这个实例。
先在头文件定义一个该类的静态对象:
cpp
private:
static DBManager* instance;
然后在cpp文件中进行对象的初始化:
cpp
DBManager* DBManager::instance = nullptr;
再定义个获取实例的方法:
cpp
static DBManager* getInstance();
通过该方法返回instance对象,如果为空则重新创建,如果不为空直接返回已经创建的实例:
cpp
DBManager* DBManager::getInstance()
{
if (instance == nullptr)
{
instance = new DBManager;
}
return instance;
}
创建工程的逻辑
我们回到上节课的新建项目对话框,补全逻辑。新建项目的时候,设置了项目名称和项目位置,点击确定之后,我们获取到db文件的完整路径,所以在对话框的类中增加以下逻辑:
cpp
void NewProjectDlg::accept()
{
// 获取db文件的完整路径
QString strProFilePath = lineEdtProDirPath->text() + lineEdtProName->text() + ".db";
DBManager::getInstance()->initDatabase(strProFilePath);
DBManager::getInstance()->createTables();
QDialog::accept();
}
然后就可以调用DBManager类的静态函数getInstance,需要引入头文件:#include "DBManager.h",并需要在配置属性中添加上引用。
配置属性→VC++目录→包含目录:
../DBOperator 注:../ 代表上一级目录
配置属性→链接器→常规→附加库目录:
$(QTDIR)\lib #表示 Qt 安装目录下的 lib 文件夹,用于引用Qt 官方库文件(如 Qt5Core.lib等)
$(OutDir) #表示当前项目的输出目录(通常是 Debug 或 Release 文件夹)
配置属性→链接器→输入→附加依赖项:
DBOperator.lib
编译程序,发现报错。
提示错误:
E1696 无法包括源文件 "QSqlDatabase"
E1696 无法.......
解决方案:配置主项目SubCfgTool的信息
配置属性 → C/C++ → 常规 →附加包含目录:
$(QTDIR)\include
$(QTDIR)\include\QtCore
$(QTDIR)\include\QtWidgets
$(QTDIR)\include\QtSql
配置属性→链接器→输入→附加依赖项:(debug下带d后缀)
Qt6Cored.lib
Qt6Widgetsd.lib
Qt6Sqld.lib
重新编译成功。测试创建效果,可以通过菜单栏创建一个项目,并生成一个db文件,用Navicat或DBeaver打开即可看到新建的表。
至此新建项目的核心逻辑就算完成了,接下来后续的开发中产生的业务数据都将存放在该db文件中,且随着业务的不断复杂,表结构会越来越丰富。