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 本章知识体系总览

下面这张图,把这五个控件的核心要点串起来了。你可以把它当作一个快速参考。

Qt 基础控件(二)知识体系 数据容器控件 QComboBox QListWidget QTableWidget QTreeWidget QTabWidget 关键点 • blockSignals 批量操作 • addItems 替代逐个 addItem • QVariant 存储用户数据 关键点 • >500 项改用 QListView • setUniformItemSizes(true) • 限制最大行数防内存溢出 关键点 • >1000 行用 QTableView • 自定义 Model 提升性能 • 分页加载大数据量 关键点 • itemExpanded 懒加载 • 占位节点防重复加载 • 异步加载子节点 关键点 • 延迟创建标签页 • 页面缓存避免重复创建 • 切换时清理临时数据 核心原则:数据量大时,用 Model/View 架构替代 Widget 架构 blockSignals + 批量操作 + 懒加载 + 分页 = 流畅的 UI 体验 性能调优口诀 下拉框,批量加,信号阻塞防卡顿 列表项,超五百,改用 View 加 Model 表格数据若太多,分页加载不会错 树形节点要展开,懒加载来保顺畅

4.7 避坑总结

最后,我把这些年用这些控件踩过的坑,整理成一张表。你写代码时对照着看,能省不少调试时间。

控件 常见坑 我的解决方案
QComboBox clear() 后 addItem 导致界面闪烁 blockSignals + addItems 批量操作
QListWidget 大量 Item 导致滚动卡顿 超过 500 项改用 QListView + Model
QTableWidget insertRow 频繁调用性能差 用 QAbstractTableModel 的 beginInsertRows/endInsertRows
QTreeWidget 展开父节点时一次性加载所有子节点 itemExpanded 信号里动态加载
QTabWidget 启动时创建所有标签页,初始化慢 currentChanged 信号里按需创建页面