Qt开发实战:数据库、MV 模式、QProcess与串口通信全攻略

1. 引言

Qt 作为跨平台 C++ 应用开发框架,在桌面应用、嵌入式系统和工业控制领域应用广泛。本文围绕 Qt 开发中四个核心主题展开:数据库访问、模型/视图(MV)模式、进程管理(QProcess)以及串口通信模块,帮助读者系统掌握这些常用组件的原理与实战用法。

2. Qt数据库

2.1 模块概述

Qt 通过 Qt SQL 模块提供统一的数据库访问接口,支持 SQLite、MySQL、PostgreSQL、ODBC 等多种数据库。核心类包括:

  • QSqlDatabase:数据库连接管理类,负责建立和管理数据库连接。
  • QSqlQuery:执行 SQL 语句并处理查询结果。
  • QSqlError:封装数据库错误信息。
  • QSqlRecord:表示一条记录的字段信息。
  • QSqlTableModel:基于单张表的可编辑数据模型,常与视图组件配合使用。

2.2 连接示例

下面以 SQLite 为例,演示建立数据库连接并执行查询的基本流程。

cpp 复制代码
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QVariant>
// 添加并打开数据库连接
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("example.db");
if (!db.open()) {
qDebug() << "数据库打开失败:" << db.lastError().text();
return;
}
// 创建表
QSqlQuery query;
query.exec("CREATE TABLE IF NOT EXISTS users ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL,"
"age INTEGER)");
// 插入数据
query.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
query.addBindValue("张三");
query.addBindValue(28);
query.exec();
// 查询数据
query.exec("SELECT id, name, age FROM users");
while (query.next()) {
int id = query.value(0).toInt();
QString name = query.value(1).toString();
int age = query.value(2).toInt();
qDebug() << "用户:" << id << name << age;
}

在实际开发中,数据库操作往往伴随着各种异常情况。下面针对连接失败、SQL 执行失败和事务回滚三类常见场景,介绍对应的排查思路与错误处理代码。

连接失败

连接失败是最常见的问题,通常由数据库文件路径错误、驱动未加载或数据库文件被占用导致。排查时可先检查 QSqlDatabase::lastError() 返回的错误信息,再确认驱动是否已通过 QSqlDatabase::drivers() 正确加载。示例代码如下:

cpp 复制代码
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("example.db");
if (!db.open()) {
QSqlError err = db.lastError();
qDebug() << "连接失败,错误类型:" << err.type();
qDebug() << "错误文本:" << err.text();
qDebug() << "数据库驱动:" << QSqlDatabase::drivers();
return;
}

SQL 执行失败

SQL 语句语法错误、表结构不匹配或字段约束冲突都会导致执行失败。此时应通过 QSqlQuery::lastError() 获取具体错误码和错误文本,并结合 QSqlQuery::lastQuery() 定位出错的语句。示例代码如下:

cpp 复制代码
QSqlQuery query;
if (!query.exec("SELECT * FROM non_exist_table")) {
    QSqlError err = query.lastError();
    qDebug() << "SQL 执行失败,错误码:" << err.nativeErrorCode();
    qDebug() << "错误文本:" << err.text();
    qDebug() << "出错语句:" << query.lastQuery();
}

事务回滚

事务中任一条语句失败都会破坏原子性,此时必须回滚以保证数据一致。建议在事务内逐条检查执行结果,一旦失败立即回滚并记录错误信息。示例代码如下:

cpp 复制代码
if (!db.transaction()) {
    qDebug() << "开启事务失败:" << db.lastError().text();
    return;
}
QSqlQuery q;
bool ok = q.exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
if (ok) {
ok = q.exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
}
if (ok) {
if (!db.commit()) {
qDebug() << "提交事务失败:" << db.lastError().text();
db.rollback();
}
} else {
qDebug() << "事务执行失败,执行回滚:" << q.lastError().text();
db.rollback();
}

2.3 事务处理

事务用于保证一组操作的原子性。Qt 中通过 transaction() 和 commit() 管理事务,失败时调用 rollback() 回滚。

cpp 复制代码
db.transaction();
QSqlQuery q;
bool ok = q.exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
ok &&= q.exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
if (ok) {
    db.commit();
} else {
    db.rollback();
}

3. 模型/视图(MV)模式

3.1 MV模式概述

模型/视图(Model/View)模式将数据存储与界面显示分离,是 Qt 界面开发的核心架构。它由三部分组成:

  • 模型(Model):负责数据存储、访问和更新,不关心界面展示。
  • 视图(View):负责数据的可视化呈现,如列表、表格、树形结构。
  • 委托(Delegate):负责数据的编辑和渲染细节,可自定义显示样式。

这种分离带来的好处是:同一份数据可以同时用多种视图展示,且数据变化时视图自动刷新,无需手动同步。

为了更直观地理解 MV 模式的优势,下面从数据流方向、组件职责、耦合度和适用场景四个维度,对比传统 MVC 模式与 Qt MV 模式。

对比维度 传统 MVC 模式 Qt MV 模式
数据流方向 控制器接收用户输入,更新模型,再通知视图刷新,数据流经控制器中转。 视图直接监听模型变化信号,模型数据变更后自动通知视图刷新,数据流更直接。
组件职责 模型、视图、控制器三者职责分离,控制器承担输入处理和业务逻辑协调。 模型负责数据,视图负责展示,委托负责编辑渲染,职责划分更清晰。
耦合度 控制器与视图、模型之间耦合较紧,视图变更往往需要同步调整控制器。 模型与视图通过信号槽解耦,同一模型可绑定多个视图,耦合度更低。
适用场景 适合业务逻辑复杂、需要集中控制的应用,如传统桌面软件。 适合数据展示与编辑频繁、需要多视图联动的应用,如数据库表格、列表、树形界面。

3.2 常用模型类

模型类 用途 适用场景
QStringListModel 字符串列表模型 简单列表数据
QStandardItemModel 通用项模型 树形或表格数据
QSqlTableModel 数据库表模型 数据库表格展示与编辑
QSortFilterProxyModel 排序过滤代理模型 对源模型排序、过滤

3.3 模型与视图绑定

下面演示如何将 QSqlTableModel 与 QTableView 绑定,实现数据库数据的表格展示。

cpp 复制代码
#include <QSqlTableModel>
#include <QTableView>
QSqlTableModel *model = new QSqlTableModel;
model->setTable("users");
model->setEditStrategy(QSqlTableModel::OnManualSubmit);
model->select();
QTableView *view = new QTableView;
view->setModel(model);
view->resizeColumnsToContents();
view->show();

3.4 自定义模型

当内置模型无法满足需求时,可继承 QAbstractTableModel 实现自定义模型,核心是重写以下虚函数:

  • rowCount():返回行数。
  • columnCount():返回列数。
  • data():返回指定索引处的数据。
  • headerData():返回表头信息。
  • setData():设置数据(可编辑模型需要)。
  • flags():返回项的属性标志。

3.5 自定义模型示例

下面通过一个完整的示例,演示如何继承 QAbstractTableModel 实现一个员工信息表模型,并绑定到 QTableView 展示数据。该模型包含姓名、部门和工龄三列,核心函数实现如下:

cpp 复制代码
#include <QAbstractTableModel>
#include <QStringList>
#include <QVariant>
class EmployeeModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit EmployeeModel(QObject *parent = nullptr)
: QAbstractTableModel(parent)
{
// 初始化示例数据
m_names << "张三" << "李四" << "王五";
m_departments << "研发部" << "测试部" << "产品部";
m_years << 5 << 3 << 8;
}
int rowCount(const QModelIndex &amp;parent = QModelIndex()) const override
{
    Q_UNUSED(parent);
    return m_names.size();
}
int columnCount(const QModelIndex &amp;parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return 3;
}
QVariant data(const QModelIndex &amp;index, int role = Qt::DisplayRole) const override
{
if (!index.isValid() || role != Qt::DisplayRole)
return QVariant();
switch (index.column()) {
case 0:
    return m_names.at(index.row());
case 1:
    return m_departments.at(index.row());
case 2:
    return m_years.at(index.row());
default:
    return QVariant();
}
}
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override
{
if (role != Qt::DisplayRole)
return QVariant();
if (orientation == Qt::Horizontal) {
    switch (section) {
    case 0: return "姓名";
    case 1: return "部门";
    case 2: return "工龄";
    default: return QVariant();
    }
}
return QString("第 %1 行").arg(section + 1);
}
private:
QStringList m_names;
QStringList m_departments;
QList<int> m_years;
};

模型定义完成后,在界面中创建 QTableView 并调用 setModel() 绑定模型,即可自动完成数据展示:

cpp 复制代码
#include <QTableView>
EmployeeModel *model = new EmployeeModel;
QTableView *view = new QTableView;
view->setModel(model);
view->resizeColumnsToContents();
view->show();

运行上述代码后,表格会显示三行三列的员工数据,表头分别为「姓名」「部门」「工龄」。由于模型通过 data() 按行列索引返回数据,视图组件无需关心数据的具体存储方式,这正是 MV 模式解耦优势的体现。

4. QProcess进程

4.1 QProcess 概述

QProcess 是 Qt 提供的跨平台进程管理类,用于启动外部程序、与其交互并监控其状态。它属于 Qt Core 模块,常用于调用系统命令、启动子程序或与其他进程通信。

QProcess 的核心能力包括:启动外部可执行程序、向进程标准输入写入数据、读取进程的标准输出与标准错误、等待进程结束并获取退出码,以及通过信号槽机制实时监控进程状态变化。它封装了底层平台相关的进程 API(如 Windows 的 CreateProcess、Unix 的 fork/exec),使开发者可以用统一的接口完成跨平台进程管理。

4.2 启动外部程序

QProcess 支持两种启动方式:分离式启动和阻塞式启动。

cpp 复制代码
#include <QProcess>
QProcess process;
// 方式一:启动后立即返回,异步执行
process.start("ping", QStringList() << "127.0.0.1");
// 方式二:等待程序执行完成(阻塞)
process.start("dir");
process.waitForFinished(5000); // 最多等待 5 秒
QString output = process.readAllStandardOutput();

除了 start(),QProcess 还提供了 startDetached() 方法,用于启动一个与当前进程完全分离的独立进程。分离后的进程不受父进程生命周期影响,即使父进程退出,子进程仍会继续运行,适合启动后台服务或独立工具。示例代码如下:

cpp 复制代码
// 分离式启动:进程独立运行,不随父进程退出而终止
qint64 pid = 0;
bool ok = QProcess::startDetached("notepad.exe", QStringList(), QString(), &pid);
if (ok) {
    qDebug() << "已分离启动记事本,进程 ID:" << pid;
}

此外,start() 还支持通过 QProcessEnvironment 设置子进程的环境变量,以及通过 setWorkingDirectory() 指定子进程的工作目录,满足更复杂的启动需求:

cpp 复制代码
QProcess process;
process.setWorkingDirectory("/tmp");
process.setProcessEnvironment(QProcessEnvironment::systemEnvironment());
QProcessEnvironment env = process.processEnvironment();
env.insert("MY_VAR", "hello");
process.setProcessEnvironment(env);
process.start("./my_script.sh");

4.3 读取进程输出

通过信号 readyReadStandardOutput() 和 readyReadStandardError() 异步读取标准输出和错误输出。

cpp 复制代码
QProcess process;
QObject::connect(&process, &QProcess::readyReadStandardOutput, [&]() {
    QByteArray data = process.readAllStandardOutput();
    qDebug() << "标准输出:" << QString::fromUtf8(data);
});
process.start("cmd", QStringList() << "/c" << "echo hello");

除了异步读取,QProcess 也支持同步读取方式。调用 waitForReadyRead() 可以阻塞等待数据到达,配合 readAllStandardOutput() 或 readAllStandardError() 获取完整输出。这种方式适合在脚本或命令行工具中按顺序处理进程输出:

cpp 复制代码
QProcess process;
process.start("ping", QStringList() << "-n" << "3" << "127.0.0.1");
// 等待进程结束并同步读取全部输出
if (process.waitForFinished(10000)) {
QByteArray stdoutData = process.readAllStandardOutput();
QByteArray stderrData = process.readAllStandardError();
qDebug() << "标准输出:" << QString::fromUtf8(stdoutData);
qDebug() << "标准错误:" << QString::fromUtf8(stderrData);
qDebug() << "退出码:" << process.exitCode();
}

向进程写入数据同样重要。通过 write() 方法可以向进程的标准输入发送数据,常用于与交互式命令行程序通信:

cpp 复制代码
QProcess process;
process.start("python", QStringList() << "-i");
// 等待进程启动完成
process.waitForStarted(3000);
// 向 Python 交互式解释器发送命令
process.write("print('Hello from QProcess')\n");
process.write("exit()\n");
process.waitForFinished(5000);

4.4 进程状态与信号

信号 触发时机
started() 进程成功启动
finished(int exitCode) 进程正常结束
errorOccurred(QProcess::ProcessError) 进程发生错误
stateChanged(QProcess::ProcessState) 进程状态变化

QProcess 的进程状态通过枚举 QProcess::ProcessState 表示,包含三种取值:NotRunning(未运行)、Starting(正在启动)和 Running(运行中)。通过 state() 方法可以查询当前状态,通过 stateChanged() 信号可以监听状态变化。

错误处理方面,errorOccurred() 信号携带 QProcess::ProcessError 枚举值,常见错误包括:FailedToStart(启动失败,如程序不存在)、Crashed(进程崩溃)、Timedout(等待超时)和 WriteError(写入失败)。建议在启动后连接该信号,及时捕获并处理异常:

cpp 复制代码
QProcess process;
QObject::connect(&process, &QProcess::errorOccurred, [&](QProcess::ProcessError error) {
    switch (error) {
    case QProcess::FailedToStart:
        qDebug() << "启动失败,请检查程序路径";
        break;
    case QProcess::Crashed:
        qDebug() << "进程崩溃";
        break;
    case QProcess::Timedout:
        qDebug() << "操作超时";
        break;
    default:
        qDebug() << "其他错误:" << process.errorString();
        break;
    }
});
process.start("nonexistent_program");

最后,kill() 和 terminate() 方法用于结束进程。terminate() 会先尝试优雅地请求进程退出(发送 WM_CLOSE 或 SIGTERM),而 kill() 则强制终止进程(发送 SIGKILL)。在超时或需要强制结束场景下,可结合 waitForFinished() 使用:

cpp 复制代码
QProcess process;
process.start("some_long_task");
// 等待 3 秒,若未结束则强制终止
if (!process.waitForFinished(3000)) {
qDebug() << "进程超时,强制终止";
process.kill();
process.waitForFinished(1000);
}

5. Qt串口

5.1 串口模块概述

Qt 通过 Qt Serial Port 模块提供串口通信支持,核心类是 QSerialPort 和 QSerialPortInfo。前者用于串口读写操作,后者用于枚举系统可用串口及其属性。

5.2 枚举可用串口

cpp 复制代码
#include <QSerialPortInfo>
#include <QSerialPort>
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
qDebug() << "端口名:" << info.portName();
qDebug() << "描述:" << info.description();
qDebug() << "制造商:" << info.manufacturer();
}

5.3 打开并配置串口

cpp 复制代码
QSerialPort serial;
serial.setPortName("COM3");
serial.setBaudRate(QSerialPort::Baud9600);
serial.setDataBits(QSerialPort::Data8);
serial.setParity(QSerialPort::NoParity);
serial.setStopBits(QSerialPort::OneStop);
serial.setFlowControl(QSerialPort::NoFlowControl);
if (!serial.open(QIODevice::ReadWrite)) {
qDebug() << "串口打开失败:" << serial.errorString();
return;
}

5.4 串口数据读写

串口数据读取采用异步方式,通过 readyRead() 信号触发读取。

cpp 复制代码
QObject::connect(&serial, &QSerialPort::readyRead, [&]() {
    QByteArray data = serial.readAll();
    qDebug() << "收到数据:" << data.toHex();
});
// 发送数据
serial.write("AT\r\n");

5.5 串口配置注意事项

  • 打开串口前务必确认端口号存在且未被其他程序占用。
  • 波特率、数据位、校验位和停止位必须与对端设备一致,否则通信失败。
  • 读取数据时应考虑粘包问题,可结合帧头帧尾或长度字段进行数据分包。
  • 长时间无数据时,可配合定时器实现超时重发或连接检测。

6. 综合实战:串口数据入库

下面综合运用串口和数据库模块,实现一个简单的数据采集程序:从串口读取数据并写入 SQLite 数据库。

cpp 复制代码
#include <QCoreApplication>
#include <QSerialPort>
#include <QSqlDatabase>
#include <QSqlQuery>
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
// 打开数据库
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("sensor.db");
db.open();
QSqlQuery query;
query.exec("CREATE TABLE IF NOT EXISTS sensor_data ("
           "id INTEGER PRIMARY KEY AUTOINCREMENT,"
           "timestamp TEXT,"
           "value REAL)");
// 配置串口
QSerialPort serial;
serial.setPortName("COM3");
serial.setBaudRate(QSerialPort::Baud9600);
serial.open(QIODevice::ReadOnly);
// 收到数据后写入数据库
QObject::connect(&amp;serial, &amp;QSerialPort::readyRead, &amp; {
QByteArray data = serial.readAll();
QString valueStr = QString::fromUtf8(data).trimmed();
bool ok;
double value = valueStr.toDouble(&amp;ok);
if (ok) {
query.prepare("INSERT INTO sensor_data (timestamp, value) VALUES (?, ?)");
query.addBindValue(QDateTime::currentDateTime().toString(Qt::ISODate));
query.addBindValue(value);
query.exec();
qDebug() &lt;&lt; "已写入:" &lt;&lt; value;
}
});
return app.exec();
}

7. 总结

本文系统梳理了 Qt 开发中四个重要主题:数据库访问通过 Qt SQL 模块提供统一接口,MV 模式实现数据与界面解耦,QProcess 负责外部进程管理,串口模块则支撑工业通信场景。实际项目中,这些模块常组合使用,例如串口采集数据后经 MV 模式展示并写入数据库。建议读者结合官方文档和实际项目逐步深入,掌握各模块的细节与最佳实践。

相关推荐
小米里的大麦2 小时前
16 MySQL 事务
数据库·mysql
西柚小萌新2 小时前
【LLM&&AI应用开发 八股文】--4.3.Agent智能体(下)
java·开发语言·数据库
时间的拾荒人3 小时前
Qt 界面优化:绘图详解 —— 从基础图形到图片处理
开发语言·qt·面试
MayZork3 小时前
Qt 文件操作:QSaveFile 与 QTemporaryFile(含 QTemporaryDir)实战
开发语言·qt
邓工说电3 小时前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技
adinnet20263 小时前
企业微调大模型(Fine-tuning):先吃透 RAG 还是先训自己的模型
大数据·数据库
上位机妹子4 小时前
EF Core 两种继承映射:每层次一张表(TPH)与每类型一张表(TPT)
数据库·c#·.netcore·ef core
小马同学-4 小时前
Redis消息队列与客户端编程
数据库·redis
for_ever_love__4 小时前
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
java·数据库·spring boot·mysql·spring cloud·面试·总结