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 &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return m_names.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return 3;
}
QVariant data(const QModelIndex &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(&serial, &QSerialPort::readyRead, & {
QByteArray data = serial.readAll();
QString valueStr = QString::fromUtf8(data).trimmed();
bool ok;
double value = valueStr.toDouble(&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() << "已写入:" << value;
}
});
return app.exec();
}
7. 总结
本文系统梳理了 Qt 开发中四个重要主题:数据库访问通过 Qt SQL 模块提供统一接口,MV 模式实现数据与界面解耦,QProcess 负责外部进程管理,串口模块则支撑工业通信场景。实际项目中,这些模块常组合使用,例如串口采集数据后经 MV 模式展示并写入数据库。建议读者结合官方文档和实际项目逐步深入,掌握各模块的细节与最佳实践。