ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++ Qt嵌入式上位机开发:SQLite数据库存储实战指南

C++ Qt嵌入式上位机开发:SQLite数据库存储实战指南 C Qt开发嵌入式上位机项目推荐SQLite 数据库篇之前接了一个工业设备的上位机项目核心需求是实时采集设备数据、生成曲线、保存历史记录。刚开始最头疼的不是界面怎么写而是数据怎么存设备一秒钟来几十条数据不可能一直在内存里堆着等程序崩了全没了也不适合用 MySQL 这类需要独立服务的数据库毕竟用户的工控机上不会专门给你装一个数据库环境。最后选了 SQLite。它是嵌入式数据库直接以文件形式存在不需要安装服务、不需要账号密码Qt 还自带 SQLite 驱动一套代码直接本地读写非常合适。这篇文章就把这个项目里的关键经验整理出来为什么嵌入式上位机选 C/Qt SQLite项目结构怎么搭核心代码怎么写以及我在实际开发中踩过哪些坑。如果你正准备做类似的设备管理、数据采集、工控监控上位机或者只是想把 C/Qt 的数据库能力掌握扎实这篇文章可以直接照着做。1. 嵌入式上位机与 SQLite 的基础认知1.1 什么是嵌入式上位机嵌入式系统通常分成两层底层是单片机、ARM 或其他嵌入式处理器负责采集传感器数据、执行控制指令上层是上位机运行在 PC 或触控终端上负责与底层设备通信、展示数据、下发指令、存储记录。上位机在工业场景里非常常见比如设备参数监控界面实时显示温度、压力、转速。操作员登录与操作日志记录。历史数据查询和报表导出。设备配置参数的备份与恢复。这类软件的运行环境往往是工控机操作系统可能是 Windows 7/10也可能是 Linux 发行版。开发语言选择 C主要原因是性能可控、能直接对接设备通信协议、运行时依赖相对干净。Qt 则是目前 C 桌面软件开发中最主流的选择之一。它提供了完整的 GUI 框架也封装了元对象系统、信号槽、多线程、网络模块、数据库模块做上位机开发非常高效。1.2 为什么本地存储推荐 SQLiteSQLite 是一个 C 语言实现的轻量级关系型数据库整个数据库就是一个文件。它的核心特点是无需独立服务进程程序通过库函数直接读写.支持标准 SQL 的大部分语法。跨平台Windows、Linux、macOS 都能用。单文件数据库方便备份、迁移。Qt 内置支持不需要额外安装驱动。对比一下常见的几种数据存储方式方案优点缺点适用场景文本文件 CSV/TXT实现简单肉眼可见并发访问差查询不方便数据量大后性能差临时导出MySQL / PostgreSQL功能强大支持并发支持远程访问需要安装服务运维复杂服务器端数据管理SQLite免安装、单文件、SQL 查询、跨平台不擅长高并发写入不适合大规模网络访问上位机本地存储对于嵌入式上位机来说数据量一般不会达到海量级别但对访问稳定性、迁移方便性要求很高SQLite 是综合性价比最好的方案。1.3 Qt 中的 SQLite 支持Qt 通过 Qt SQL 模块访问数据库其中QSQLITE驱动是 Qt 自带编译好的不需要额外下载。开发者只需要在项目文件里添加 SQL 模块QT core gui sql然后通过QSqlDatabase建立连接通过QSqlQuery执行 SQL 语句或者通过QSqlTableModel把数据库表直接绑定到视图控件上。要注意的是这里需要区分两个概念SQLite 是底层的数据库引擎Qt SQL 是上层封装接口。不懂 SQLite 底层原理也能用 Qt 访问但如果你对 SQL、事务、索引有一定了解写出程序的稳定性和性能会高很多。2. 环境准备与项目结构设计2.1 开发环境本文示例以 Qt 6 Qt Creator 为例代码思路在 Qt 5.12 及以上版本同样适用。由于不同电脑上的 Qt 版本可能不一样建议你以自己实际环境为准不需要强行升级。操作系统Windows 10/11 或 Ubuntu 20.04编译器MSVC 2019Windows或 GCCLinux开发工具Qt Creator 或 Visual Studio Qt 插件Qt 版本Qt 6.2 以上构建工具CMake 或 qmake数据库模块Qt SQL使用 SQLite 驱动Qt 安装的时候在组件选择页面里务必勾选 Qt SQL 相关的模块一般只要勾选了对应编译器版本的 Qt 库都会默认带上 SQLite 驱动。2.2 CMake 项目结构我这里以 CMake 为例。相比 qmakeCMake 在跨平台和大型项目组织方面更通用如果你做的是公司级项目CMake 几乎是标配。推荐的项目结构embedded_host_sqlite/ ├── CMakeLists.txt ├── main.cpp ├── mainwindow.h ├── mainwindow.cpp ├── dbmanager.h ├── dbmanager.cpp └── ...CMakeLists.txt 示例cmake_minimum_required(VERSION 3.16) project(EmbeddedHostSqlite VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets Sql) qt_add_executable(embedded_host_sqlite main.cpp mainwindow.cpp mainwindow.h dbmanager.cpp dbmanager.h ) target_link_libraries(embedded_host_sqlite PRIVATE Qt6::Widgets Qt6::Sql )如果使用 Qt 5find_package里把Qt6改成Qt5即可。2.3 数据库层设计思路在写代码前建议先把数据库访问单独封装成一个类。不要在主窗口里到处写QSqlDatabase否则项目复杂后会出现连接混乱、资源不释放、SQL 语句分散等问题。我习惯的设计方式DbManager负责打开数据库、初始化表结构、提供查询接口。MainWindow只负责界面交互调用DbManager的方法。业务层与界面层分离后续替换数据库或增加逻辑时改动的范围会小很多。封装数据库还有一个好处未来如果上位机需要上报数据到服务器你可以把这一层替换成 MySQL 或者通过 HTTP API 上传界面和业务代码不需要大规模改动。3. SQLite 核心知识点与应用场景3.1 SQLite 连接与打开Qt 中打开 SQLite 数据库核心是设置数据库类型和数据库文件路径。// 文件路径dbmanager.cpp #include dbmanager.h #include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug #include QDir DbManager::DbManager(const QString dbPath, QObject *parent) : QObject(parent) , m_dbPath(dbPath) { } bool DbManager::openDatabase() { // QSqlDatabase::addDatabase 的第二个参数是连接名 // 如果没有特殊需求使用默认连接名即可 m_db QSqlDatabase::addDatabase(QSQLITE); m_db.setDatabaseName(m_dbPath); if (!m_db.open()) { qCritical() 打开数据库失败: m_db.lastError().text(); return false; } // 设置 WAL 模式提升并发读写性能 QSqlQuery query(m_db); query.exec(PRAGMA journal_modeWAL;); // 设置外键约束 query.exec(PRAGMA foreign_keysON;); return true; }这里有几个细节需要注意。QSqlDatabase::addDatabase(QSQLITE)中的QSQLITE是驱动名称Qt 自带支持。如果连接名不指定会自动使用默认连接名qt_sql_default_connection。PRAGMA journal_modeWAL;是 SQLite 提供的性能优化指令。WAL 模式允许在写入数据时不阻塞读取适合上位机一边写入数据一边查询展示的场景。PRAGMA foreign_keysON;用于启用外键约束。注意SQLite 的外键约束默认是关闭的不打开的话建表语句里的FOREIGN KEY不会生效。3.2 创建表结构设备上位机常见的数据表包括设备信息表保存设备 ID、名称、型号、通信参数。实时数据表保存采集到的实时数据包括时间戳和各个通道的值。用户操作日志表记录操作员登录、参数修改、启动停止等行为。告警记录表保存设备异常报警记录。下面是一个设备数据表和一个操作日志表的建表示例CREATE TABLE IF NOT EXISTS device_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, timestamp TEXT NOT NULL, temperature REAL, pressure REAL, speed REAL, status INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS operation_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT NOT NULL, action TEXT NOT NULL, detail TEXT, created_at TEXT NOT NULL );通过 Qt 代码执行建表bool DbManager::initTables() { QSqlQuery query(m_db); const QString createDeviceDataSql R( CREATE TABLE IF NOT EXISTS device_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, timestamp TEXT NOT NULL, temperature REAL, pressure REAL, speed REAL, status INTEGER DEFAULT 0 ); ); const QString createLogSql R( CREATE TABLE IF NOT EXISTS operation_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT NOT NULL, action TEXT NOT NULL, detail TEXT, created_at TEXT NOT NULL ); ); if (!query.exec(createDeviceDataSql)) { qWarning() 创建设备数据表失败: query.lastError().text(); return false; } if (!query.exec(createLogSql)) { qWarning() 创建操作日志表失败: query.lastError().text(); return false; } return true; }使用CREATE TABLE IF NOT EXISTS可以避免程序重复启动时因表已存在而报错。这是上位机程序非常实用的写法因为上位机往往要每天开机、关机的。3.3 SQLite 的 UPSERT 操作在日常开发中你经常会遇到这样一个需求如果记录存在就更新不存在就新增。在工业上位机中典型场景是保存设备参数配置或者保存设备最近一次的通信状态。例如设备每次启动后上报一个状态值我们希望一个设备只保留一条最新状态记录有了就更新没有就插入。SQLite 在 3.24.0 版本开始支持UPSERT语法官方写法是ON CONFLICT DO UPDATE。INSERT INTO device_config (device_id, config_value, update_time) VALUES (device_01, {baud:9600}, datetime(now)) ON CONFLICT(device_id) DO UPDATE SET config_value excluded.config_value, update_time excluded.update_time;为了让这个语法生效device_id字段必须建立唯一约束或主键约束。Qt 的执行代码bool DbManager::upsertDeviceConfig(const QString deviceId, const QString configValue) { QSqlQuery query(m_db); query.prepare(R( INSERT INTO device_config (device_id, config_value, update_time) VALUES (:device_id, :config_value, datetime(now)) ON CONFLICT(device_id) DO UPDATE SET config_value excluded.config_value, update_time excluded.update_time; )); query.bindValue(:device_id, deviceId); query.bindValue(:config_value, configValue); if (!query.exec()) { qWarning() UPSERT 失败: query.lastError().text(); return false; } return true; }这里需要注意一个细节如果device_id没有唯一约束ON CONFLICT(device_id)是不会触发冲突的结果就是不断插入新记录。3.4 事务与批量插入上位机实时采集数据时如果每秒写入几十条数据逐条执行 insert 会导致性能下降而且中途若程序崩溃已插入的数据也不完整。解决办法是把一段时间内的数据放在一个事务中统一提交。SQLite 在同一个事务中写入数据时速度会大幅提升。bool DbManager::insertBatchData(const QVectorDeviceDataItem items) { if (items.isEmpty()) { return true; } // 开启事务 if (!m_db.transaction()) { qWarning() 开启事务失败: m_db.lastError().text(); return false; } QSqlQuery query(m_db); query.prepare(R( INSERT INTO device_data (device_id, timestamp, temperature, pressure, speed, status) VALUES (:device_id, :timestamp, :temperature, :pressure, :speed, :status); )); for (const DeviceDataItem item : items) { query.bindValue(:device_id, item.deviceId); query.bindValue(:timestamp, item.timestamp); query.bindValue(:temperature, item.temperature); query.bindValue(:pressure, item.pressure); query.bindValue(:speed, item.speed); query.bindValue(:status, item.status); if (!query.exec()) { qWarning() 插入数据失败: query.lastError().text(); // 回滚事务 m_db.rollback(); return false; } } // 提交事务 if (!m_db.commit()) { qWarning() 提交事务失败: m_db.lastError().text(); m_db.rollback(); return false; } return true; }要点所有插入操作在同一个transaction()与commit()之间完成。如果中间某条数据失败整体回滚避免半截数据。批量数据条数建议控制在 500~1000 条以内太长了也会占用内存。3.5 数据查询与排序历史数据查询是上位机最常见的功能。比如用户选择一个时间段查看设备在这段时间内的温度变化。QVectorDeviceDataItem DbManager::queryHistoryData(const QString deviceId, const QString startTime, const QString endTime) { QVectorDeviceDataItem result; QSqlQuery query(m_db); query.prepare(R( SELECT device_id, timestamp, temperature, pressure, speed, status FROM device_data WHERE device_id :device_id AND timestamp :start_time AND timestamp :end_time ORDER BY timestamp ASC; )); query.bindValue(:device_id, deviceId); query.bindValue(:start_time, startTime); query.bindValue(:end_time, endTime); if (!query.exec()) { qWarning() 查询历史数据失败: query.lastError().text(); return result; } while (query.next()) { DeviceDataItem item; item.deviceId query.value(0).toString(); item.timestamp query.value(1).toString(); item.temperature query.value(2).toDouble(); item.pressure query.value(3).toDouble(); item.speed query.value(4).toDouble(); item.status query.value(5).toInt(); result.append(item); } return result; }这里要注意时间字段的格式。为了便于比较和排序建议统一存储为yyyy-MM-dd HH:mm:ss字符串格式因为这种格式下字符串字典序就是时间顺序SQL 中的、可以直接使用。4. 完整实战设备数据上位机下面我们把前面这些知识点组合起来开发一个完整的小型上位机界面包含一个表格用于显示设备名称与状态一个按钮用于写入测试数据一个按钮用于刷新显示并带有时间范围查询能力。4.1 需求与功能拆分这个演示项目主要实现程序启动时自动创建数据库和表结构。用户点击“写入测试数据”向数据库插入一组模拟采集数据。用户点击“查询数据”从数据库中读取数据并显示在 QTableView 中。支持按设备 ID 和时间范围过滤。4.2 数据库类完整实现先定义头文件// 文件路径dbmanager.h #ifndef DBMANAGER_H #define DBMANAGER_H #include QObject #include QSqlDatabase #include QString #include QVector struct DeviceDataItem { QString deviceId; QString timestamp; double temperature 0.0; double pressure 0.0; double speed 0.0; int status 0; }; class DbManager : public QObject { Q_OBJECT public: explicit DbManager(const QString dbPath, QObject *parent nullptr); bool openDatabase(); bool initTables(); bool insertTestData(); QVectorDeviceDataItem queryHistoryData(const QString deviceId, const QString startTime, const QString endTime); private: QSqlDatabase m_db; QString m_dbPath; }; #endif // DBMANAGER_H实现文件// 文件路径dbmanager.cpp #include dbmanager.h #include QSqlQuery #include QSqlError #include QDebug #include QDateTime DbManager::DbManager(const QString dbPath, QObject *parent) : QObject(parent) , m_dbPath(dbPath) { } bool DbManager::openDatabase() { m_db QSqlDatabase::addDatabase(QSQLITE); m_db.setDatabaseName(m_dbPath); if (!m_db.open()) { qCritical() 打开数据库失败: m_db.lastError().text(); return false; } QSqlQuery query(m_db); query.exec(PRAGMA journal_modeWAL;); query.exec(PRAGMA foreign_keysON;); return true; } bool DbManager::initTables() { QSqlQuery query(m_db); const QString sql R( CREATE TABLE IF NOT EXISTS device_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, timestamp TEXT NOT NULL, temperature REAL, pressure REAL, speed REAL, status INTEGER DEFAULT 0 ); ); if (!query.exec(sql)) { qWarning() 创建设备数据表失败: query.lastError().text(); return false; } return true; } bool DbManager::insertTestData() { QSqlQuery query(m_db); query.prepare(R( INSERT INTO device_data (device_id, timestamp, temperature, pressure, speed, status) VALUES (:device_id, :timestamp, :temperature, :pressure, :speed, :status); )); QString nowStr QDateTime::currentDateTime().toString(yyyy-MM-dd HH:mm:ss); query.bindValue(:device_id, QStringLiteral(DEV-001)); query.bindValue(:timestamp, nowStr); query.bindValue(:temperature, 36.5); query.bindValue(:pressure, 101.3); query.bindValue(:speed, 1500.0); query.bindValue(:status, 1); if (!query.exec()) { qWarning() 插入测试数据失败: query.lastError().text(); return false; } return true; } QVectorDeviceDataItem DbManager::queryHistoryData(const QString deviceId, const QString startTime, const QString endTime) { QVectorDeviceDataItem result; QSqlQuery query(m_db); query.prepare(R( SELECT device_id, timestamp, temperature, pressure, speed, status FROM device_data WHERE device_id :device_id AND timestamp :start_time AND timestamp :end_time ORDER BY timestamp ASC; )); query.bindValue(:device_id, deviceId); query.bindValue(:start_time, startTime); query.bindValue(:end_time, endTime); if (!query.exec()) { qWarning() 查询历史数据失败: query.lastError().text(); return result; } while (query.next()) { DeviceDataItem item; item.deviceId query.value(0).toString(); item.timestamp query.value(1).toString(); item.temperature query.value(2).toDouble(); item.pressure query.value(3).toDouble(); item.speed query.value(4).toDouble(); item.status query.value(5).toInt(); result.append(item); } return result; }4.3 主窗口界面与交互主窗口包含一个 QLineEdit设备 ID、两个 QDateTimeEdit开始和结束时间、三个按钮写入测试数据、查询、清空表格以及一个 QTableView。// 文件路径mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include dbmanager.h class QLineEdit; class QDateTimeEdit; class QPushButton; class QTableView; class QStandardItemModel; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private slots: void onInsertDataClicked(); void onQueryClicked(); private: void setupUi(); void setupModel(); DbManager *m_dbManager; QLineEdit *m_deviceIdEdit; QDateTimeEdit *m_startTimeEdit; QDateTimeEdit *m_endTimeEdit; QPushButton *m_insertBtn; QPushButton *m_queryBtn; QTableView *m_tableView; QStandardItemModel *m_model; }; #endif // MAINWINDOW_H// 文件路径mainwindow.cpp #include mainwindow.h #include QLineEdit #include QDateTimeEdit #include QPushButton #include QTableView #include QStandardItemModel #include QVBoxLayout #include QHBoxLayout #include QDateTime MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_dbManager new DbManager(device_data.db, this); if (!m_dbManager-openDatabase()) { return; } m_dbManager-initTables(); setupModel(); setupUi(); setWindowTitle(QStringLiteral(嵌入式上位机 - 数据管理演示)); resize(800, 500); } void MainWindow::setupUi() { auto *centralWidget new QWidget(this); auto *mainLayout new QVBoxLayout(centralWidget); auto *filterLayout new QHBoxLayout(); filterLayout-addWidget(new QLabel(QStringLiteral(设备ID:))); m_deviceIdEdit new QLineEdit(centralWidget); m_deviceIdEdit-setText(DEV-001); filterLayout-addWidget(m_deviceIdEdit); filterLayout-addSpacing(10); filterLayout-addWidget(new QLabel(QStringLiteral(开始:))); m_startTimeEdit new QDateTimeEdit(centralWidget); m_startTimeEdit-setDisplayFormat(yyyy-MM-dd HH:mm:ss); m_startTimeEdit-setDateTime(QDateTime::currentDateTime().addDays(-1)); filterLayout-addWidget(m_startTimeEdit); filterLayout-addSpacing(10); filterLayout-addWidget(new QLabel(QStringLiteral(结束:))); m_endTimeEdit new QDateTimeEdit(centralWidget); m_endTimeEdit-setDisplayFormat(yyyy-MM-dd HH:mm:ss); m_endTimeEdit-setDateTime(QDateTime::currentDateTime()); filterLayout-addWidget(m_endTimeEdit); mainLayout-addLayout(filterLayout); m_insertBtn new QPushButton(QStringLiteral(写入测试数据), centralWidget); m_queryBtn new QPushButton(QStringLiteral(查询数据), centralWidget); auto *buttonLayout new QHBoxLayout(); buttonLayout-addWidget(m_insertBtn); buttonLayout-addWidget(m_queryBtn); buttonLayout-addStretch(); mainLayout-addLayout(buttonLayout); m_tableView new QTableView(centralWidget); m_tableView-setModel(m_model); m_tableView-horizontalHeader()-setStretchLastSection(true); mainLayout-addWidget(m_tableView); setCentralWidget(centralWidget); connect(m_insertBtn, QPushButton::clicked, this, MainWindow::onInsertDataClicked); connect(m_queryBtn, QPushButton::clicked, this, MainWindow::onQueryClicked); } void MainWindow::setupModel() { m_model new QStandardItemModel(this); m_model-setHorizontalHeaderLabels({ QStringLiteral(设备ID), QStringLiteral(时间), QStringLiteral(温度), QStringLiteral(压力), QStringLiteral(转速), QStringLiteral(状态) }); } void MainWindow::onInsertDataClicked() { if (m_dbManager-insertTestData()) { statusBar()-showMessage(QStringLiteral(插入成功), 2000); } else { statusBar()-showMessage(QStringLiteral(插入失败请查看日志), 3000); } } void MainWindow::onQueryClicked() { QString deviceId m_deviceIdEdit-text().trimmed(); QString startTime m_startTimeEdit-dateTime().toString(yyyy-MM-dd HH:mm:ss); QString endTime m_endTimeEdit-dateTime().toString(yyyy-MM-dd HH:mm:ss); QVectorDeviceDataItem items m_dbManager-queryHistoryData(deviceId, startTime, endTime); m_model-removeRows(0, m_model-rowCount()); for (const DeviceDataItem item : items) { QListQStandardItem * rowItems; rowItems new QStandardItem(item.deviceId); rowItems new QStandardItem(item.timestamp); rowItems new QStandardItem(QString::number(item.temperature, f, 2)); rowItems new QStandardItem(QString::number(item.pressure, f, 2)); rowItems new QStandardItem(QString::number(item.speed, f, 2)); rowItems new QStandardItem(QString::number(item.status)); m_model-appendRow(rowItems); } statusBar()-showMessage(QStringLiteral(查询完成共 %1 条记录).arg(items.size()), 3000); }4.4 主函数入口// 文件路径main.cpp #include QApplication #include mainwindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }4.5 运行与验证程序运行后点击“写入测试数据”屏幕上会提示插入成功。多次点击后再点击“查询数据”表格中能显示刚插入的数据。修改开始、结束时间范围查询结果会随之变化。打开项目目录下的device_data.db文件可以看到数据确实以文件形式存在。如果你没有合适的图形化工具查看 SQLite 文件可以用 DB Browser for SQLite 等工具打开。需要提醒的是工具类的安装请通过官方渠道获取避免来源不明的压缩包。5. 常见问题与排查思路5.1 Qt 提示 Driver not loaded现象运行程序时QSqlDatabase: QSQLITE driver not loaded。原因Qt 安装时没有包含对应编译器的 SQL 驱动插件或者部署发布时没有把sqldrivers目录拷贝到可执行文件目录。解决步骤检查本机 Qt 安装目录下的plugins/sqldrivers目录是否存在qsqlite.dll或libqsqlite.so。如果不存在回到 Qt 安装器补装组件或者重新编译 SQLite 插件。发布程序时把sqldrivers目录整体拷贝到可执行文件同级目录下的plugins文件夹中。5.2 数据库文件路径不对现象程序启动后找不到数据库表或者数据写在别的地方。原因使用相对路径时工作目录不一定和可执行文件所在目录一致。尤其在 Qt Creator 中运行程序时默认工作目录可能是构建目录而不是源码目录。解决建议不要直接使用device_data.db这种裸相对路径。更稳妥的方式是拼接路径QString dbPath QCoreApplication::applicationDirPath() /data/device_data.db; QDir().mkpath(QFileInfo(dbPath).absolutePath());5.3 程序崩溃或断电导致数据库损坏现象设备突然断电后数据库文件打不开甚至程序无法启动。原因SQLite 虽然很稳定但频繁写入时突然断电仍有小概率产生未完成的事务。解决建议开启 WAL 模式减少数据库文件被破坏的概率。定期备份数据库文件上位机重启时检查文件是否可正常打开。对关键配置数据采用“先写入临时库文件确认成功后替换正式文件”的方式。5.4 时间字段无法正确过滤现象查询结果包含了不期望的时间范围。原因时间字段的存储格式不统一比如有的是yyyy-MM-dd HH:mm:ss有的是yyyy/MM/dd HH:mm:ss字符串比较就会出错。解决建议在程序层统一格式化时间字符串不要直接存储QDateTime的整数值也不要在不同模块里用不同格式。5.5 多个页面同时对数据库操作现象部分界面卡顿甚至出现database is locked。原因多个连接同时写入或者在一个连接中长时间占用事务。解决建议尽量使用全局唯一的数据库连接访问都通过同一个DbManager。写入操作放在事务中批量进行避免频繁小事务。长耗时查询不要放在 GUI 主线程中建议使用 Qt 线程或 QtConcurrent。6. 项目工程经验与最佳实践6.1 数据库访问尽量收口到独立类前面代码中已经体现了这一点。数据库文件路径、连接初始化、建表、SQL 语句都集中在DbManager中。这样带来几个好处以后要迁移到 MySQL只需要改DbManager内部实现。其他模块不直接依赖任何 SQL 语句代码可读性更好。出现数据库问题时可以从单点开始排查。6.2 注意线程安全SQLite 默认连接在单线程中访问是安全的但如果你在多个线程里同时使用同一个QSqlDatabase连接就可能出现万问题。正确做法每个线程使用独立的数据库连接。或者统一通过一个数据库管理线程把所有读写操作排队处理。界面线程只接收结果刷新 UI不做真实的数据库写入。如果你的上位机需要在后台采集线程中写数据推荐为读写单独建立一个工作线程通过 Qt 的信号槽把数据传给DbManager。6.3 定期清理历史数据上位机长时间运行数据库文件会不断增大。设备数据如果按每秒一条存储一天就是 86400 条一个月就是 200 多万条。查询性能会逐渐下降。建议做法在配置中允许用户设置历史数据保留天数。每次程序启动时删除超出保留期限的数据。删除操作同样建议放在事务中避免大范围删除中间失败。DELETE FROM device_data WHERE timestamp datetime(now, -30 day);这里需要提醒涉及删除操作时务必先确认删除条件正确再执行。可以在测试数据库中验证不要在生产环境直接执行不确定的删除命令。6.4 使用QSqlTableModel简化列表展示如果只是简单表格展示可以不手写QStandardItemModel而是把QSqlTableModel绑定到表格上。例如m_tableModel new QSqlTableModel(this, m_db); m_tableModel-setTable(device_data); m_tableModel-setFilter(device_id DEV-001); m_tableModel-select(); ui-tableView-setModel(m_tableModel);这种方式适合开发快速展示界面。但如果要做复杂格式化、合并单元格、自定义排序仍然建议使用QStandardItemModel或自定义模型。6.5 发布程序时别漏插件与依赖Qt 程序发布时需要把 Qt 的 DLL/SO 和插件目录都准备好。这里有几个常用做法使用windeployqt工具自动提取依赖。确保sqldrivers目录中包含qsqlite.dll。如果有自定义样式或中文支持注意字体和翻译文件。在目标机器上做一次最小化测试确认数据库能正常打开和写入。6.6 日志与错误处理上位机是面向工业现场的软件出错时需要能快速定位问题。建议在DbManager中把关键操作都加上日志输出比如qInfo() 数据库打开成功路径: m_dbPath; qWarning() SQL 执行失败: sql query.lastError().text();在正式项目中可以接入 Qt 的日志重定向机制把日志输出到本地文件便于现场排查问题。7. 总结与下一步学习建议这篇文章从嵌入式上位机的实际需求出发给出了一个基于 C、Qt 和 SQLite 的完整数据存储方案。重点内容包括为什么嵌入式上位机选择 SQLite而不是 MySQL 或文本文件。Qt SQL 模块的基本使用方式包括连接、建表、插入、查询、事务。SQLite 的 UPSERT 语法和批量插入事务处理。一个完整的设备数据上位机演示项目。如果你准备继续深入这个方向下一步可以重点学习Qt 网络模块用 TCP/UDP 或 Modbus 协议与下位机通信。Qt 图表模块把数据库里的历史数据绘制成曲线。Qt 多线程把数据采集、数据库写入、界面刷新彻底解耦。SQLite 高级特性索引优化、WAL 日志模式分析、数据库加密。在开发过程中最容易踩的坑往往不是 SQLite 本身而是项目结构和线程模型设计不合理。建议你先把代码按照“界面层—业务层—数据层”分离做一个简单的版本再逐步叠加功能。亲手写完一个从数据库到界面的闭环项目比看多少资料都有效。
返回列表