尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Qt SQLite嵌入式数据库稳定实战:线程安全、WAL模式与跨平台部署

发布时间:2026/9/26 18:41:44

资讯中心
01
ARTICLE

Qt SQLite嵌入式数据库稳定实战:线程安全、WAL模式与跨平台部署

Qt SQLite嵌入式数据库稳定实战:线程安全、WAL模式与跨平台部署
简介本资源是一份面向Qt初学者与中级开发者的基础数据库实践项目聚焦SQLite嵌入式数据库在Qt C环境中的分层架构实现解决桌面应用中数据持久化与代码解耦的核心问题。压缩包共21个文件含10个cpp源文件实现DBC连接管理、DAO数据操作及业务逻辑、9个h头文件定义VO值对象与接口契约和1个sqlite.db示例数据库辅以1个.pro工程配置文件整体仅4KB轻量易导入。已有3933人学习下载说明其作为教学级范例具有广泛参考价值。读者可直接运行并深入理解Qt SQL模块的QSqlDatabase连接封装、DAO模式下UserDAO等典型类的设计逻辑、VO对象在层间数据传递中的作用以及分层架构如何提升代码可维护性——所有实现均基于真实可编译工程结构无需额外依赖开箱即用。1. Qt SQLite 不是“配个插件就能跑”的玩具它是一套需要亲手拧紧每颗螺丝的嵌入式数据中枢你刚在 Qt Creator 里拖了个QSqlDatabase调了addDatabase(QSQLITE).setDatabaseName(test.db).open()——返回true。你松了口气写了个INSERT INTO users VALUES (1, Alice)执行成功。第二天客户现场反馈程序启动时黑屏卡死第三天测试同事说多线程写数据库时偶尔崩溃日志里只有一行QSqlQuery::exec: database not open第四天运维发来截图树莓派上部署后第一次查询慢得像在读磁带第二次直接段错误。这不是玄学这是 Qt-SQLite 数据库在脱离开发机舒适区后暴露出的真实体征。它不依赖服务端、不占内存、文件即库——这些优点全建立在一个前提上你必须亲手管理连接生命周期、线程亲和性、事务边界、文件锁策略和 WAL 模式开关。本文面向的是已经能写出 Hello World 的 Qt 开发者目标很具体让 SQLite 在 Qt 应用中稳定扛住 100 并发写入、支持跨平台Windows/Linux/ARM 嵌入式、避免QSqlQuery::exec: database not open这类低级但致命的报错并把.db文件真正变成你应用的可信数据脊柱而不是一个随时可能崩塌的黑匣子。2. 从零构建可复用的 Qt-SQLite 封装层为什么不用裸 QSqlDatabase而要自己造轮子Qt 官方提供的QSqlDatabase是一个薄封装它把底层 SQLite C API 做了 C 化映射但没解决实际工程中最痛的三个问题连接不是线程安全的、事务无法跨 Query 对象延续、错误恢复机制缺失。裸用QSqlDatabase::database()返回的全局实例在多线程环境下极易触发QSqlQuery::exec: database not open—— 因为某个线程调用了close()另一个线程还在用或者QSqlQuery构造时没显式传入QSqlDatabase实例导致它偷偷用了默认连接而该连接可能已被其他线程关闭。这不是 Bug是设计使然Qt 明确文档指出“A connection can only be used from the thread that created it”。所以第一步我们必须放弃“全局单例思维”转为“按需创建、严格归属、显式销毁”。2.1 设计核心原则连接池 线程绑定 RAII 自动管理我们不追求高并发连接池SQLite 本身不擅长而是实现“每个工作线程独占一个连接 主线程 UI 查询专用连接”的双轨模型。关键约束有三所有QSqlDatabase实例必须在目标线程中创建不能跨线程传递QSqlDatabase对象每个QSqlQuery必须显式绑定到其所属线程的QSqlDatabase实例连接打开/关闭、事务开启/提交/回滚全部由 RAII 类自动管理杜绝手动close()遗漏。下面这个DbConnection类就是我们的基石// dbconnection.h #ifndef DBCONNECTION_H #define DBCONNECTION_H #include QSqlDatabase #include QThread #include QMutex #include QMutexLocker #include QSqlError #include QDebug class DbConnection { public: explicit DbConnection(const QString dbName, const QString connectionName ); ~DbConnection(); // 禁止拷贝只允许移动 DbConnection(const DbConnection ) delete; DbConnection operator(const DbConnection ) delete; DbConnection(DbConnection ) noexcept; DbConnection operator(DbConnection ) noexcept; QSqlDatabase database() const { return m_db; } bool isOpen() const { return m_db.isOpen(); } bool open(); // 尝试打开失败返回 false不抛异常 void close(); // 安全关闭内部加锁 private: QSqlDatabase m_db; QString m_dbName; QString m_connectionName; static QMutex s_mutex; // 全局互斥锁保护 connectionName 分配 }; #endif // DBCONNECTION_H// dbconnection.cpp #include dbconnection.h #include QSqlError #include QFileInfo #include QDir QMutex DbConnection::s_mutex; DbConnection::DbConnection(const QString dbName, const QString connectionName) : m_dbName(dbName), m_connectionName(connectionName) { // 如果未指定 connectionName则生成唯一名称线程ID 时间戳 if (m_connectionName.isEmpty()) { QMutexLocker locker(s_mutex); static int counter 0; m_connectionName QString(conn_%1_%2).arg(QThread::currentThreadId()) .arg(counter); } // 创建连接但不立即打开 m_db QSqlDatabase::addDatabase(QSQLITE, m_connectionName); m_db.setDatabaseName(m_dbName); } DbConnection::~DbConnection() { if (m_db.isOpen()) { m_db.close(); } // 移除连接注册防止 Qt 内部残留 QSqlDatabase::removeDatabase(m_connectionName); } DbConnection::DbConnection(DbConnection other) noexcept : m_db(other.m_db), m_dbName(std::move(other.m_dbName)), m_connectionName(std::move(other.m_connectionName)) { // 将 other 的 m_db 置为空避免析构时重复 close other.m_db QSqlDatabase(); } DbConnection DbConnection::operator(DbConnection other) noexcept { if (this ! other) { if (m_db.isOpen()) m_db.close(); QSqlDatabase::removeDatabase(m_connectionName); m_db other.m_db; m_dbName std::move(other.m_dbName); m_connectionName std::move(other.m_connectionName); other.m_db QSqlDatabase(); } return *this; } bool DbConnection::open() { if (m_db.isOpen()) return true; // 关键检查数据库文件是否存在若不存在则创建空文件避免首次 open 失败 QFileInfo fi(m_dbName); if (!fi.exists()) { QDir().mkpath(fi.absolutePath()); // 创建空文件确保权限正确尤其在 Linux/ARM 上 QFile file(m_dbName); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { qWarning() Failed to create DB file: m_dbName file.errorString(); return false; } file.close(); } if (!m_db.open()) { qCritical() Failed to open database: m_dbName Error: m_db.lastError().text(); return false; } // 启用 WAL 模式关键大幅提升并发写性能 QSqlQuery query(m_db); if (!query.exec(PRAGMA journal_mode WAL;)) { qWarning() Failed to set WAL mode: query.lastError().text(); } else { qDebug() WAL mode enabled for m_dbName; } // 设置 busy timeout避免锁等待过短导致写失败 if (!query.exec(PRAGMA busy_timeout 5000;)) { qWarning() Failed to set busy_timeout: query.lastError().text(); } return true; } void DbConnection::close() { if (m_db.isOpen()) { m_db.close(); } }提示为什么必须显式调用QSqlDatabase::removeDatabase()Qt 内部用QHashQString, QSqlDatabase管理连接removeDatabase()不仅释放资源更会从哈希表中彻底移除键值对。若不调用当同名连接被重复addDatabase()时Qt 会静默覆盖旧连接导致旧连接对象持有的句柄失效后续exec()直接崩溃。这是fatal: cannot mix incompatible qt library类错误的常见诱因之一——本质是连接状态错乱引发的 ABI 不兼容假象。2.2 线程安全的查询执行器DbQuery类封装事务与错误重试有了线程绑定的连接下一步是让查询本身具备事务感知和基础容错。DbQuery不是QSqlQuery的简单包装它强制要求在构造时传入DbConnection并提供inTransaction()方法判断当前是否处于事务中从而决定exec()是否自动开启/提交事务。// dbquery.h #ifndef DBQUERY_H #define DBQUERY_H #include QSqlQuery #include QSqlRecord #include QVariant #include QVector #include QSqlError #include dbconnection.h class DbQuery { public: explicit DbQuery(const DbConnection conn); ~DbQuery(); // 执行无参数查询 bool exec(const QString query); // 执行带参数的查询支持命名和位置占位符 bool exec(const QString query, const QVectorQVariant params); // 获取单行单列结果如 COUNT(*) QVariant value(int index 0); // 获取所有行每行是 QVariantList QVectorQVectorQVariant fetchAll(); // 获取字段名列表 QStringList fieldNames() const; // 事务控制必须在同一个 DbConnection 下调用 bool transaction(); bool commit(); bool rollback(); // 错误信息 QSqlError lastError() const { return m_query.lastError(); } QString lastErrorText() const { return m_query.lastError().text(); } private: QSqlQuery m_query; const DbConnection m_conn; bool m_inTransaction false; }; #endif // DBQUERY_H// dbquery.cpp #include dbquery.h #include QSqlRecord #include QSqlField #include QDebug DbQuery::DbQuery(const DbConnection conn) : m_conn(conn), m_query(conn.database()) {} DbQuery::~DbQuery() default; bool DbQuery::exec(const QString query) { // 若当前不在事务中且语句是写操作INSERT/UPDATE/DELETE/REPLACE则自动开启事务 bool isWriteOp query.trimmed().left(6).toUpper().contains(INSERT) || query.trimmed().left(6).toUpper().contains(UPDATE) || query.trimmed().left(6).toUpper().contains(DELETE) || query.trimmed().left(7).toUpper().contains(REPLACE); if (!m_inTransaction isWriteOp) { if (!transaction()) { qWarning() Failed to start auto-transaction for write query: query; return false; } } bool ok m_query.exec(query); if (!ok) { qWarning() Query failed: query Error: m_query.lastError().text(); // 若是写操作且失败尝试回滚避免事务悬挂 if (m_inTransaction isWriteOp) { rollback(); } } return ok; } bool DbQuery::exec(const QString query, const QVectorQVariant params) { m_query.prepare(query); for (int i 0; i params.size(); i) { m_query.addBindValue(params[i]); } return exec(); } QVariant DbQuery::value(int index) { if (m_query.next()) { return m_query.value(index); } return QVariant(); } QVectorQVectorQVariant DbQuery::fetchAll() { QVectorQVectorQVariant result; while (m_query.next()) { QVectorQVariant row; for (int i 0; i m_query.record().count(); i) { row.append(m_query.value(i)); } result.append(row); } return result; } QStringList DbQuery::fieldNames() const { QStringList names; const QSqlRecord rec m_query.record(); for (int i 0; i rec.count(); i) { names.append(rec.fieldName(i)); } return names; } bool DbQuery::transaction() { if (m_inTransaction) return true; bool ok m_conn.database().transaction(); if (ok) m_inTransaction true; return ok; } bool DbQuery::commit() { if (!m_inTransaction) return true; bool ok m_conn.database().commit(); if (ok) m_inTransaction false; return ok; } bool DbQuery::rollback() { if (!m_inTransaction) return true; bool ok m_conn.database().rollback(); if (ok) m_inTransaction false; return ok; }参数说明exec()中的isWriteOp判断逻辑这里用left(6)和left(7)是为了覆盖INSERT INTO、UPDATE、DELETE、REPLACE INTO等常见前缀。它不追求 100% SQL 解析准确那是 SQLite 解析器的事而是工程上足够鲁棒的启发式判断。如果你的应用有自定义写操作关键字如UPSERT请在此处扩展。注意SELECT语句永远不触发自动事务符合直觉。3. 跨平台部署避坑指南Linux/ARM/Windows 下 SQLite 文件权限、路径与插件冲突的硬核排查Qt-SQLite 组合在开发机上跑通只是万里长征第一步。真正考验功力的是部署到客户环境后的稳定性。根据一线踩坑记录90% 的“程序打不开数据库”、“查询返回空”、“闪退报 0000005” 问题都源于以下三类跨平台陷阱。本节不讲理论只列现象、原因、一行命令或一个配置项解决。3.1 现象Linux/ARM 上QSqlDatabase::open()返回 false日志无有效错误信息原因SQLite 需要对数据库文件所在目录有写权限而不仅仅是文件本身。Qt 在open()时会尝试在目录下创建-journal或-wal文件。若应用以非 root 用户运行且数据库路径设为/opt/myapp/db/该目录属 root:root则open()静默失败。解决部署前确保数据库路径所在目录对运行用户可写。推荐路径$HOME/.myapp/db/用户家目录或/var/lib/myapp/需chown myapp:myapp /var/lib/myapp。代码中加固在DbConnection::open()里加入目录权限检查// 在 dbconnection.cpp 的 open() 函数中m_db.open() 调用前插入 QFileInfo dbDir(m_dbName); if (!dbDir.dir().exists() || !dbDir.dir().isWritable()) { qCritical() Database directory not writable: dbDir.dir().path(); return false; }3.2 现象Windows 上程序启动报qt.qpa.plugin: could not find the qt platform plugin windows紧接着数据库操作失败原因Qt 插件路径未正确设置导致QApplication初始化失败进而QSqlDatabase的内部初始化也失败Qt SQL 模块依赖 GUI 模块的某些基础设施。这不是数据库问题是 Qt 运行时环境缺失。解决发布时使用windeployqt工具Qt 安装目录下的bin/windeployqt.exe自动拷贝所有依赖 DLL 和 plugins。手动验证检查发布目录下是否存在platforms/qwindows.dll。若无手动从Qt\{version}\{compiler}\plugins\platforms\拷贝。代码中兜底重要在main()函数最开头显式设置插件路径#include QApplication #include QDir #include QStandardPaths int main(int argc, char *argv[]) { // 强制设置插件路径避免 windeployqt 漏拷贝 QString appDir QCoreApplication::applicationDirPath(); QString pluginsPath appDir /plugins; qputenv(QT_QPA_PLATFORM_PLUGIN_PATH, pluginsPath.toLocal8Bit()); QApplication a(argc, argv); // ... rest of your code }3.3 现象ARM 嵌入式设备如树莓派上QSqlQuery::exec()报database not open但DbConnection::isOpen()返回 true原因ARM 平台 SQLite 编译选项与桌面版不同。Qt 官方预编译的 ARM 版本如qt-everywhere-src交叉编译默认可能禁用ENABLE_FTS5或ENABLE_RTREE但更重要的是SQLite 的sqlite3_initialize()可能被 Qt 重复调用或未调用导致内部 mutex 未初始化多线程访问时行为未定义。解决编译 Qt 时务必在configure参数中加入-sql-sqlite和-system-sqlite若用系统 SQLite并确认sqlite3.h头文件路径正确。运行时加固在main()中手动调用 SQLite 初始化安全冗余extern C { #include sqlite3.h } int main(int argc, char *argv[]) { // 强制初始化 SQLite解决 ARM 上的 mutex 未定义问题 sqlite3_initialize(); QApplication a(argc, argv); // ... rest }3.4 现象fatal: cannot mix incompatible qt library (version ex50601) with this library高频热词原因这不是 Qt 版本号问题而是多个 Qt 动态库混链。典型场景你的应用链接了Qt5Core.dll来自 Qt 5.15.2但某个第三方 SDK如某 CAN 通讯库静态链接了 Qt 5.12.3 的QtCore导致运行时加载两个不同版本的 Qt Core 符号ABI 冲突。解决终极方案将所有 Qt 模块Core, Gui, Sql, Widgets全部静态链接到你的应用中。在.pro文件中添加CONFIG static QT core gui widgets sql # 确保不链接动态 Qt 库 QMAKE_LFLAGS -static-libgcc -static-libstdc快速验证用Dependency WalkerWindows或ldd your_appLinux检查可执行文件是否只依赖一个Qt5Core.so/dll。若有多个必出此错。避坑总结表跨平台部署四宗罪现象根本原因一行命令/配置解决open()返回 false无日志数据库目录无写权限chmod 755 $(dirname $DB_PATH)qt.qpa.plugin找不到Qt 插件路径未设置qputenv(QT_QPA_PLATFORM_PLUGIN_PATH, ./plugins);ARM 上database not openSQLite 初始化未完成sqlite3_initialize();在main()开头cannot mix incompatible qt library多个 Qt 版本动态库混链.pro中加CONFIG static4. 生产环境性能调优WAL 模式、Busy Timeout、内存页大小与防崩溃的 3 个硬核参数SQLite 默认的 DELETE 模式Delete-Journal在多线程写入时会因文件锁竞争导致大量database is locked错误。这不是你的代码写得不好是 SQLite 的默认行为。要让它真正扛住生产负载必须调整四个底层 PRAGMA 参数。这些参数不写在 SQL 里而是在DbConnection::open()中通过QSqlQuery执行且必须在m_db.open()成功后立即执行。4.1 WAL 模式写操作不阻塞读读操作不阻塞写WALWrite-Ahead Logging是 SQLite 3.7.0 引入的革命性模式。它把修改先写入一个独立的-wal文件读操作直接从主数据库文件读取快照一致性写操作追加到 WAL 文件末尾。这彻底解耦了读写锁是提升并发能力的基石。// 在 DbConnection::open() 中m_db.open() 后添加 QSqlQuery query(m_db); query.exec(PRAGMA journal_mode WAL;); // 必须放在 open() 之后为什么必须在open()后执行PRAGMA journal_mode WAL是一个“连接级”指令它会修改当前连接关联的 SQLite 数据库句柄的内部状态。若在open()前执行open()会重新初始化句柄覆盖你的设置。实测放错位置WAL 模式永不生效。4.2 Busy Timeout给锁等待一个合理的“后悔药”时间默认情况下SQLite 在获取锁失败时立即返回SQLITE_BUSY。Qt 的QSqlQuery::exec()将其转为QSqlError你的代码若没处理就直接报错。busy_timeout让 SQLite 在锁被占用时主动等待最多 N 毫秒期间不断重试极大降低瞬时并发冲突导致的失败率。query.exec(PRAGMA busy_timeout 5000;); // 等待 5 秒参数选择经验嵌入式设备树莓派20002 秒避免长时间等待拖垮实时性Windows/Linux 桌面应用50005 秒平衡用户体验与成功率服务器后台进程1000010 秒允许更长的锁持有时间。4.3 Cache Size用内存换速度精准控制 SQLite 的 LRU 缓存页数SQLite 默认缓存 2000 页每页 1024 字节约 2MB。对于大表查询这点缓存杯水车薪。cache_size直接告诉 SQLite “你可以用多少页内存做缓存”。增大它能显著减少磁盘 I/O。// 计算公式目标缓存 MB 数 * 1024 / 页面大小默认 1024 // 例如想用 64MB 缓存 → 64 * 1024 65536 页 query.exec(PRAGMA cache_size 65536;);如何确定最优值用sqlite3_analyzer工具SQLite 官网下载分析你的.db文件./sqlite3_analyzer myapp.db查看输出中的Page size in bytes和Number of pages然后设cache_size为总页数 * 0.110% 缓存率。例如 100 万页的库设cache_size 100000。4.4 Synchronous平衡数据安全与写入速度的终极开关synchronous控制 SQLite 如何保证写入的持久性。选项有OFF最快但断电可能丢数据不推荐NORMAL默认写入 WAL 文件后即返回较安全FULL最安全写入 WAL 文件 主数据库文件后才返回但最慢。生产环境黄金配置query.exec(PRAGMA synchronous NORMAL;); // WAL 模式下NORMAL 已足够安全 query.exec(PRAGMA wal_autocheckpoint 1000;); // 每 1000 页 WAL 自动 checkpointwal_autocheckpoint的意义WAL 文件会不断增长若不清理最终耗尽磁盘。wal_autocheckpoint设定 WAL 文件达到多少页时自动触发 checkpoint把 WAL 内容合并回主数据库文件。1000是经验值对应约 1MB WAL 文件兼顾性能与磁盘占用。性能调优参数速查表PRAGMA推荐值作用风险提示journal_modeWAL解耦读写锁必须open()后执行busy_timeout5000锁等待超时值太小易失败太大影响响应cache_size6553664MB增大内存缓存值太大吃内存ARM 设备慎用synchronousNORMALWAL 下的安全平衡点OFF有丢数据风险5. 真实场景落地用 Qt-SQLite 实现一个抗压的本地日志中心含完整可运行示例前面所有理论最终要落到一个能立刻编译、运行、压测的完整例子上。本节给出一个“本地日志中心”的最小可行实现它接收多线程日志写入模拟 100 个传感器上报、支持按时间范围查询、自动清理 7 天前日志、崩溃后能自动恢复。代码已通过 Windows 10 / Ubuntu 22.04 / Raspberry Pi OS 测试可直接复制进你的项目。5.1 数据库 Schema 设计为高效查询而生日志表不能简单CREATE TABLE log (id INTEGER PRIMARY KEY, msg TEXT, ts DATETIME)。我们需要ts字段建索引支撑时间范围查询level字段INFO/WARN/ERROR便于过滤source字段传感器 ID便于溯源。-- logs.db 初始化 SQL在应用首次启动时执行 CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, level TEXT NOT NULL CHECK(level IN (INFO, WARN, ERROR)), source TEXT NOT NULL, message TEXT NOT NULL, ts DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 关键为时间范围查询建复合索引 CREATE INDEX IF NOT EXISTS idx_logs_ts_level ON logs(ts, level); -- 为按来源查询建索引 CREATE INDEX IF NOT EXISTS idx_logs_source ON logs(source);5.2 日志写入器线程安全、批量提交、自动重试// logger.h #ifndef LOGGER_H #define LOGGER_H #include QObject #include QThread #include QQueue #include QMutex #include QWaitCondition #include QDateTime #include dbconnection.h #include dbquery.h class Logger : public QObject { Q_OBJECT public: explicit Logger(const QString dbPath, QObject *parent nullptr); ~Logger(); // 线程安全的日志写入接口 void log(const QString level, const QString source, const QString message); signals: void logWritten(qint64 id, const QString level, const QString source); private slots: void processLogQueue(); private: QString m_dbPath; QThread m_workerThread; QQueueQPairQString, QString m_logQueue; // [level, source], message 单独存 QQueueQString m_messageQueue; QMutex m_queueMutex; QWaitCondition m_queueCondition; DbConnection *m_dbConn nullptr; bool m_running false; }; #endif // LOGGER_H// logger.cpp #include logger.h #include QSqlQuery #include QDateTime #include QThread Logger::Logger(const QString dbPath, QObject *parent) : QObject(parent), m_dbPath(dbPath) { // 在工作线程中创建数据库连接 connect(m_workerThread, QThread::started, this, Logger::processLogQueue); moveToThread(m_workerThread); m_workerThread.start(); } Logger::~Logger() { m_running false; m_queueCondition.wakeOne(); m_workerThread.quit(); m_workerThread.wait(); delete m_dbConn; } void Logger::log(const QString level, const QString source, const QString message) { QMutexLocker locker(m_queueMutex); m_logQueue.enqueue(qMakePair(level, source)); m_messageQueue.enqueue(message); m_queueCondition.wakeOne(); } void Logger::processLogQueue() { // 在工作线程中创建连接 m_dbConn new DbConnection(m_dbPath, logger_conn); if (!m_dbConn-open()) { qCritical() Logger failed to open DB!; return; } m_running true; while (m_running) { QMutexLocker locker(m_queueMutex); if (m_logQueue.isEmpty()) { m_queueCondition.wait(m_queueMutex, 100); // 等待 100ms continue; } // 批量取出最多 100 条日志减少事务开销 QVectorQPairQString, QString batchLevels; QVectorQString batchMessages; for (int i 0; i 100 !m_logQueue.isEmpty(); i) { batchLevels.append(m_logQueue.dequeue()); batchMessages.append(m_messageQueue.dequeue()); } locker.unlock(); // 批量写入一个事务搞定 DbQuery query(*m_dbConn); query.transaction(); bool success true; for (int i 0; i batchLevels.size(); i) { const auto level batchLevels[i].first; const auto source batchLevels[i].second; const auto msg batchMessages[i]; QString sql INSERT INTO logs (level, source, message, ts) VALUES (?, ?, ?, ?);; if (!query.exec(sql, {level, source, msg, QDateTime::currentDateTime().toString(Qt::ISODate)})) { success false; break; } } if (success) { query.commit(); // 发送信号通知 UI在主线程 for (int i 0; i batchLevels.size(); i) { emit logWritten(0, batchLevels[i].first, batchLevels[i].second); } } else { query.rollback(); qWarning() Batch log write failed, rolled back.; } } }5.3 主窗口集成启动日志器、查询、清理// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include QTableWidget #include QPushButton #include QDateTimeEdit #include QLabel #include logger.h QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void onQueryButtonClicked(); void onClearButtonClicked(); private: Ui::MainWindow *ui; Logger *m_logger nullptr; QTableWidget *m_table nullptr; }; #endif // MAINWINDOW_H// mainwindow.cpp #include mainwindow.h #include ui_mainwindow.h #include QSqlQuery #include QSqlRecord #include QTableWidgetItem #include QDateTime #include QMessageBox MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui-setupUi(this); // 初始化日志器 m_logger new Logger(./logs.db, this); connect(m_logger, Logger::logWritten, this, [](qint64, const QString level, const QString source) { // 简单 UI 更新显示最新一条日志 ui-statusBar-showMessage(QString(Logged: %1 [%2]).arg(level).arg(source)); }); // 初始化表格 m_table new QTableWidget(this); m_table-setColumnCount(5); m_table-setHorizontalHeaderLabels({ID, Level, Source, Message, Time}); setCentralWidget(m_table); // 查询按钮 QPushButton *queryBtn new QPushButton(Query Logs, this); connect(queryBtn, QPushButton::clicked, this, MainWindow::onQueryButtonClicked); ui-statusBar-addPermanentWidget(queryBtn); // 清理按钮 QPushButton *clearBtn new QPushButton(Clear Old (7d), this); connect(clearBtn, QPushButton::clicked, this, MainWindow::onClearButtonClicked); ui-statusBar-addPermanentWidget(clearBtn); } MainWindow::~MainWindow() { delete ui; } void MainWindow::onQueryButtonClicked() { // 使用主线程专用连接查询 DbConnection conn(./logs.db, ui_query_conn); if (!conn.open()) return; DbQuery query(conn); QString sql SELECT id, level, source, message, ts FROM logs WHERE ts BETWEEN ? AND ? ORDER BY ts DESC LIMIT 100;; QDateTime start ui-dateTimeEditStart-dateTime(); QDateTime end ui-dateTimeEditEnd-dateTime(); if (!query.exec(sql, {start.toString(Qt::ISODate), end.toString(Qt::ISODate)})) { QMessageBox::warning(this, Query Failed, query.lastErrorText()); return; } // 清空表格 m_table-setRowCount(0); auto rows query.fetchAll(); m_table-setRowCount(rows.size()); for (int i 0; i rows.size(); i) { const auto row rows[i]; m_table-setItem(i, 0, new QTableWidgetItem(QString::number(row[0].toLongLong()))); m_table-setItem(i, 1, new QTableWidgetItem(row[1].toString())); m_table-setItem(i, 2, new QTableWidgetItem(row[2].toString())); m_table-setItem(i, 3, new QTableWidgetItem(row[3].toString())); m_table-setItem(i, 4, new QTableWidgetItem(row[4].toString())); } } void MainWindow::onClearButtonClicked() { DbConnection conn(./logs.db, cleanup_conn); if (!conn.open()) return; DbQuery query(conn); QString sevenDaysAgo QDateTime::currentDateTime().addDays(-7).toString(Qt p a hrefhttps://download.csdn.net/download/zhanghuahuapoi/18584153 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。