4. 嵌入式通信基础
上一章我们聊了按钮和输入框这些基础控件。这一章,咱们来点更实用的——那些能装数据的控件。说白了,就是下拉框、列表、表格、树和标签页。这些控件在串口调试、网络监控这类工具里,几乎天天都要打交道。
我个人习惯把这类控件叫做「数据容器」。它们不光是展示数据,更关键的是怎么高效地往里塞数据、怎么让界面不卡顿。嗯,这里面的坑,我踩过不少。
核心要点:这五个控件的性能瓶颈,90% 出在「频繁插入/清空数据」和「大量 Item 的渲染」上。理解了这两点,调优就成功了一半。
4.1 QComboBox:下拉框的「轻」与「重」
QComboBox 看起来简单,就是个下拉选择。但你在串口波特率选择、端口号列表刷新时,如果处理不当,界面会直接卡死。
4.1.1 基本用法与数据填充
// 最直接的添加方式
QComboBox *combo = new QComboBox(this);
combo->addItem("9600");
combo->addItem("19200");
combo->addItem("115200");
// 带用户数据的添加
combo->addItem("波特率 9600", QVariant(9600));
combo->addItem("波特率 115200", QVariant(115200));
// 获取当前选中值
int baudRate = combo->currentData().toInt();
这里有个细节:addItem 的第二个参数可以存 QVariant。我在做串口调试工具时,就把波特率数值、校验位枚举值都存进去,界面显示中文,后台直接拿数值用,省去了一次转换。
4.1.2 性能陷阱:频繁清空与填充
你想想看,如果每隔 1 秒就要刷新一次串口列表,你会怎么做?
// 错误做法:每次清空再全部添加
void refreshPorts() {
combo->clear(); // 触发大量信号和重绘
QStringList ports = scanPorts();
for (const auto &port : ports) {
combo->addItem(port); // 每加一个又触发一次
}
}
我曾经在一个项目中这样写,结果串口热插拔检测时,界面直接卡住 2 秒。后来发现 clear() 和 addItem() 每次都会触发 currentIndexChanged 信号和布局重算。
正确的做法是:
// 优化做法:批量操作前阻塞信号
void refreshPorts() {
combo->blockSignals(true); // 先关信号
combo->clear();
QStringList ports = scanPorts();
combo->addItems(ports); // 批量添加,只触发一次重绘
combo->blockSignals(false); // 恢复信号
// 手动触发一次,通知外部状态更新
emit combo->currentIndexChanged(combo->currentIndex());
}
| 操作 | 触发信号次数 | 重绘次数 | 适用场景 |
|---|---|---|---|
| 逐个 addItem | N 次 | N 次 | 少量静态数据(< 20 项) |
| addItems 批量 | 1 次 | 1 次 | 中等量动态数据(20-200 项) |
| blockSignals + addItems | 0 次(手动触发) | 1 次 | 高频刷新场景 |
4.2 QListWidget:列表的「虚拟」之道
QListWidget 是展示日志、设备列表的常用控件。但当日志行数超过 1000 条时,你会发现滚动越来越卡。
4.2.1 基础操作与 Item 管理
QListWidget *list = new QListWidget(this);
// 添加带图标的项
QListWidgetItem *item = new QListWidgetItem(QIcon(":/icons/device.png"), "COM3 - 已连接");
list->addItem(item);
// 设置自定义数据
item->setData(Qt::UserRole, "COM3");
item->setData(Qt::UserRole + 1, true); // 连接状态
// 排序
list->sortItems(Qt::AscendingOrder);
4.2.2 性能瓶颈:大量 Item 的噩梦
为什么 QListWidget 在数据量大时会卡?因为它默认会为每个 Item 创建真实的 Widget 或绘制对象。10000 条日志,就是 10000 个对象。
我的经验:如果列表项超过 500 条,就别用 QListWidget 了。改用 QListView + 自定义 Model,配合 setUniformItemSizes(true) 开启统一尺寸优化,性能能提升 10 倍以上。
// 日志列表的优化方案
QListView *logView = new QListView(this);
logView->setUniformItemSizes(true); // 关键:所有项等高等宽
logView->setLayoutMode(QListView::Batched); // 批量布局
QStringListModel *model = new QStringListModel(this);
logView->setModel(model);
// 追加日志时,只更新 Model
void appendLog(const QString &msg) {
QStringList logs = model->stringList();
logs.append(msg);
if (logs.size() > 10000) {
logs.removeFirst(); // 限制最大行数
}
model->setStringList(logs);
// 自动滚动到底部
logView->scrollToBottom();
}
4.3 QTableWidget:表格的「分页」思维
表格控件在数据显示中很常见。比如网络抓包工具里的报文列表、串口数据监控的十六进制视图。
4.3.1 基本表格构建
QTableWidget *table = new QTableWidget(0, 4, this);
table->setHorizontalHeaderLabels({"时间", "方向", "数据长度", "报文内容"});
// 添加一行数据
int row = table->rowCount();
table->insertRow(row);
table->setItem(row, 0, new QTableWidgetItem("14:23:05.123"));
table->setItem(row, 1, new QTableWidgetItem("TX"));
table->setItem(row, 2, new QTableWidgetItem("64"));
table->setItem(row, 3, new QTableWidgetItem("AA BB CC DD ..."));
// 设置列宽自适应
table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
4.3.2 性能调优:分页与虚拟滚动
QTableWidget 和 QListWidget 有同样的问题——Item 太多就卡。但表格更复杂,因为每行有多个单元格。
我建议的做法是:
- 数据量 < 1000 行:直接用 QTableWidget,但开启
setSortingEnabled(false)避免排序时卡顿 - 数据量 1000-10000 行:改用 QTableView + QStandardItemModel,并设置
setBatchSize() - 数据量 > 10000 行:必须实现分页加载,或者用 QAbstractTableModel 自定义 Model
我曾经在做一个网络抓包工具时,直接把所有报文塞进 QTableWidget,结果 5000 条数据后,每次插入都要等 3 秒。后来改成 QTableView + 自定义 Model,插入 10 万条数据都流畅。
// 自定义 Model 的核心实现(片段)
class PacketTableModel : public QAbstractTableModel {
Q_OBJECT
public:
int rowCount(const QModelIndex &) const override { return packets.size(); }
int columnCount(const QModelIndex &) const override { return 4; }
QVariant data(const QModelIndex &index, int role) const override {
if (role != Qt::DisplayRole) return {};
const Packet &pkt = packets.at(index.row());
switch (index.column()) {
case 0: return pkt.timestamp;
case 1: return pkt.direction;
case 2: return pkt.length;
case 3: return pkt.hexData;
}
return {};
}
void appendPacket(const Packet &pkt) {
beginInsertRows(QModelIndex(), packets.size(), packets.size());
packets.append(pkt);
endInsertRows();
}
private:
QVector<Packet> packets; // 用 QVector 而非 QList,内存更紧凑
};
4.4 QTreeWidget:树形结构的「懒加载」
树形控件常用于设备拓扑、协议层级展示。比如 Modbus 的寄存器分组、网络设备的端口树。
4.4.1 树的基本构建
QTreeWidget *tree = new QTreeWidget(this);
tree->setHeaderLabels({"设备名称", "状态", "IP 地址"});
// 根节点
QTreeWidgetItem *root = new QTreeWidgetItem(tree);
root->setText(0, "PLC 设备组");
// 子节点
QTreeWidgetItem *child = new QTreeWidgetItem(root);
child->setText(0, "PLC-01");
child->setText(1, "在线");
child->setText(2, "192.168.1.100");
tree->expandAll(); // 展开所有节点
4.4.2 性能关键:懒加载与节点复用
树形控件最容易踩的坑,就是一次性展开所有子节点。如果一个父节点下有 10000 个子节点,展开时界面会直接卡死。
我的做法:在 itemExpanded 信号里动态加载子节点。只加载当前展开的这一层,下层数据等用户点击展开时再加载。
// 懒加载实现
void onItemExpanded(QTreeWidgetItem *item) {
// 检查是否已经加载过
if (item->childCount() > 0 && item->child(0)->text(0) != "加载中...") {
return; // 已加载,跳过
}
// 清除占位节点
item->takeChildren();
// 异步加载子节点(比如从数据库或网络)
QList<QTreeWidgetItem*> children = loadChildrenAsync(item->data(0, Qt::UserRole).toString());
for (auto *child : children) {
item->addChild(child);
}
}
4.5 QTabWidget:标签页的「按需创建」
QTabWidget 本身不涉及大量数据,但它管理的是多个页面的生命周期。如果每个标签页里都放了复杂的控件,切换时就会卡。
4.5.1 基本用法
QTabWidget *tab = new QTabWidget(this);
// 添加标签页
QWidget *page1 = new QWidget();
QVBoxLayout *layout1 = new QVBoxLayout(page1);
layout1->addWidget(new QLabel("串口配置"));
tab->addTab(page1, "串口");
QWidget *page2 = new QWidget();
tab->addTab(page2, "网络");
// 设置当前页
tab->setCurrentIndex(0);
4.5.2 性能优化:延迟创建与缓存
我见过不少项目,启动时就把所有标签页的控件都创建好了。如果程序有 10 个标签页,每个里面都有表格、图表,启动时间会非常长。
正确的做法是:
- 延迟创建:只在标签页第一次被选中时,才创建里面的控件
- 页面缓存:切换时不要销毁页面,而是隐藏/显示,避免重复创建
- 清理不可见页:如果内存紧张,可以在切换时销毁不可见页的临时数据
// 延迟创建标签页
void onTabChanged(int index) {
if (tabWidget->widget(index) == nullptr) {
QWidget *page = createTabPage(index); // 按需创建
tabWidget->insertTab(index, page, tabNames[index]);
}
}
4.6 本章知识体系总览
下面这张图,把这五个控件的核心要点串起来了。你可以把它当作一个快速参考。
4.7 避坑总结
最后,我把这些年用这些控件踩过的坑,整理成一张表。你写代码时对照着看,能省不少调试时间。
| 控件 | 常见坑 | 我的解决方案 |
|---|---|---|
| QComboBox | clear() 后 addItem 导致界面闪烁 | blockSignals + addItems 批量操作 |
| QListWidget | 大量 Item 导致滚动卡顿 | 超过 500 项改用 QListView + Model |
| QTableWidget | insertRow 频繁调用性能差 | 用 QAbstractTableModel 的 beginInsertRows/endInsertRows |
| QTreeWidget | 展开父节点时一次性加载所有子节点 | itemExpanded 信号里动态加载 |
| QTabWidget | 启动时创建所有标签页,初始化慢 | currentChanged 信号里按需创建页面 |