要解决的问题
跳转到“要解决的问题”“机器模板”界面中,一个 QListWidget 保存测点。新增、删除、清空测点后,界面需要更新数量;最后一个测点被移除时,还要显示空状态。应监测列表的数据模型变化,然后重新读取 count()。
先区分两种业务含义:
- 当前为空:初始化和刷新时都可以据此显示空提示。
- 从非空变为空:仅在上一次数量大于零、这一次数量等于零时通知一次。
设上一次数量为 、当前数量为 ,清空通知条件是:
一直为空并不等于不断发生“清空”事件。将这两个概念混在一起,会导致重复日志或重复业务请求。
为什么监听模型信号
跳转到“为什么监听模型信号”QListWidget 内部使用模型保存条目,可通过 list->model() 访问。监听模型操作完成后的信号,再调用 count(),能统一处理不同代码路径引起的增删和重置。Qt 的模型文档定义了这些通知。
| 信号 | 何时处理 | 本文的动作 |
|---|---|---|
rowsInserted | 行插入完成后 | 重新读取数量 |
rowsRemoved | 行删除完成后 | 重新读取数量 |
modelReset | 模型重置完成后 | 丢弃旧数量假设,重新读取数量 |
这里监听的是一个平面列表。树模型需要区分 parent 和目标层级,不能把任意父节点的行变化直接解释为根节点数量变化。模型只要按 Qt 的约定正确报告修改,观察者就不必知道增删是哪个按钮或函数发起的。
以下机制不能替代数量监测:
itemChanged表示条目数据改变,例如文字被修改,并不表示增加或删除条目。currentRowChanged表示当前项变化,选中另一项也会发生。QEvent::ChildAdded、ChildRemoved面向QObject的父子对象关系;QListWidgetItem不是QObject子对象。把这两个事件当成条目增删通知是错误的。QListWidgetItem、QEvent
完整示例:数量、空状态和展开状态
跳转到“完整示例:数量、空状态和展开状态”下面程序只依赖 Qt Widgets,不需要资源文件和自定义信号。标题按钮控制展开;“+”增加测点;删除按钮移除当前选中项;清空按钮移除全部项。所有数据修改都发生在界面线程。
保存为 main.cpp:
#include <QAbstractItemModel>#include <QApplication>#include <QDebug>#include <QHBoxLayout>#include <QLabel>#include <QListWidget>#include <QObject>#include <QPushButton>#include <QVBoxLayout>#include <QWidget>
int main(int argc, char *argv[]){ QApplication app(argc, argv); int previousCount = 0; int emptyTransitions = 0; int nextNumber = 1; QWidget window; window.setWindowTitle(QStringLiteral("机器模板与测点")); window.resize(420, 320);
auto *layout = new QVBoxLayout(&window); auto *titleRow = new QHBoxLayout; auto *title = new QPushButton(QStringLiteral("新建机器模板"), &window); title->setCheckable(true); title->setChecked(true); auto *add = new QPushButton(QStringLiteral("+"), &window); add->setAccessibleName(QStringLiteral("添加测点")); titleRow->addWidget(title, 1); titleRow->addWidget(add); layout->addLayout(titleRow);
auto *status = new QLabel(&window); auto *list = new QListWidget(&window); auto *remove = new QPushButton(QStringLiteral("删除选中测点"), &window); auto *clear = new QPushButton(QStringLiteral("清空测点"), &window); layout->addWidget(status); layout->addWidget(list, 1); layout->addWidget(remove); layout->addWidget(clear);
const auto refresh = [&] { const int currentCount = list->count(); const bool becameEmpty = previousCount > 0 && currentCount == 0; previousCount = currentCount;
status->setText(currentCount == 0 ? QStringLiteral("还没有测点,点击 + 添加。") : QStringLiteral("测点数量:%1").arg(currentCount)); list->setVisible(currentCount > 0 && title->isChecked()); clear->setEnabled(currentCount > 0); remove->setEnabled(list->currentRow() >= 0);
if (becameEmpty) { ++emptyTransitions; qInfo() << "List became empty" << emptyTransitions; // 在此通知业务层;不要在这个观察函数里再次增删列表。 } };
// 后构造、先析构:在 window 开始销毁子控件前断开这些连接。 QObject observationLifetime; auto *model = list->model(); QObject::connect(model, &QAbstractItemModel::rowsInserted, &observationLifetime, refresh); QObject::connect(model, &QAbstractItemModel::rowsRemoved, &observationLifetime, refresh); QObject::connect(model, &QAbstractItemModel::modelReset, &observationLifetime, refresh); QObject::connect(title, &QPushButton::toggled, &observationLifetime, refresh); QObject::connect(list, &QListWidget::currentRowChanged, &observationLifetime, [remove](int row) { remove->setEnabled(row >= 0); });
QObject::connect(add, &QPushButton::clicked, &observationLifetime, [&] { list->addItem(QStringLiteral("测点 %1").arg(nextNumber++)); list->setCurrentRow(list->count() - 1); }); QObject::connect(remove, &QPushButton::clicked, &observationLifetime, [list] { const int row = list->currentRow(); if (row >= 0) delete list->takeItem(row); }); QObject::connect(clear, &QPushButton::clicked, &observationLifetime, [list] { list->clear(); }); refresh(); // 初始化空提示,不把初始为空算作一次清空事件。
if (app.arguments().contains(QStringLiteral("--test"))) { const auto expect = [](bool condition, const char *message) { if (!condition) qFatal("Test failed: %s", message); }; expect(previousCount == 0 && emptyTransitions == 0, "initial state"); list->addItems({QStringLiteral("A"), QStringLiteral("B")}); expect(previousCount == 2 && !list->isHidden(), "insert two items"); list->item(0)->setText(QStringLiteral("renamed")); expect(previousCount == 2 && emptyTransitions == 0, "edit is not removal"); title->setChecked(false); expect(list->isHidden() && previousCount == 2, "collapse keeps items"); title->setChecked(true); expect(!list->isHidden(), "expand restores list"); delete list->takeItem(0); expect(previousCount == 1 && emptyTransitions == 0, "remove one"); list->clear(); expect(previousCount == 0 && emptyTransitions == 1, "clear remaining item"); list->clear(); expect(emptyTransitions == 1, "clearing an empty list is not a transition"); list->addItem(QStringLiteral("C")); delete list->takeItem(0); expect(emptyTransitions == 2 && list->isHidden(), "remove last item"); expect(!clear->isEnabled(), "empty state disables clear"); qInfo() << "List count tests passed"; return 0; }
window.show(); return app.exec();}takeItem() 把条目从列表移除并交给调用方管理,所以示例显式 delete;如果要把条目移到另一个列表,应转交所有权而不是删除。clear() 已经负责销毁全部条目,之后不能再使用此前保存的条目指针。QListWidget 文档
observationLifetime 是这些连接的上下文对象。它在窗口之后构造,离开 main() 时先析构,从而在窗口拆除子控件前断开连接。否则,列表销毁过程中的模型重置信号可能回调刷新函数,而函数引用的兄弟标签控件已经销毁。把连接上下文设为窗口,并不能保证所有兄弟控件在窗口析构的全过程中仍然有效。
保存为 CMakeLists.txt:
cmake_minimum_required(VERSION 3.21)project(list_count_demo LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)find_package(Qt6 REQUIRED COMPONENTS Widgets)add_executable(list_count_demo main.cpp)target_link_libraries(list_count_demo PRIVATE Qt6::Widgets)配置 CMake 时选择与 Qt 安装匹配的编译器,并通过 CMAKE_PREFIX_PATH 指向 Qt 安装目录。运行程序可交互观察;附加 --test 参数执行状态检查。本文示例已在 Windows、Qt 6.10.2、MinGW 下编译,并以 offscreen 平台运行上述检查;这验证了列表行为,没有替代不同屏幕上的视觉检查。
合入原来的 MachineTemplate
跳转到“合入原来的 MachineTemplate”如果项目已有 MachineTemplate 类,不必复制两套构造函数。保留创建标题、添加按钮、列表和布局的代码,只做三件事:
- 将
previousCount放入类成员,在控件建立后连接三个模型信号。 - 在一个刷新函数中更新数量、空提示以及
isExpanded对应的可见性。 - 添加和删除操作继续调用
QListWidget的接口,由模型通知刷新函数。
类封装还要安排连接寿命:可将同样的无父对象 QObject 上下文作为成员,让它在基类 QWidget 开始销毁子控件前析构;或保存连接句柄,在派生类析构函数中明确断开观察连接。不要让回调访问已经开始销毁的界面成员。
原稿中的界面参数属于特定设计:总宽度 257、标题按钮 250×50、加号 25×25、标题栏高度 50、图标 25×25、文字 14/18 点、标题左内边距 25、列表左内边距 50、右边距 20,以及蓝色 rgb(18,150,219)。这些数值与监测机制无关;标题宽度、加号和边距之和还可能超过总宽度。新界面应根据字体、缩放和可用空间使用布局分配宽度,避免同时锁死所有尺寸。原 SVG 资源路径仅在项目确实包含该资源时才可使用。
后台线程可以生产测点数据,但应通过队列连接把更新交给界面线程;不能从工作线程直接调用列表或其模型接口。模型线程约束
定时轮询什么时候才有意义
跳转到“定时轮询什么时候才有意义”如果外部数据源没有变更通知,可以用定时器读取它,再把变化更新到模型。原稿每 100 ms 判断一次空列表,会在持续为空时反复执行逻辑;即使采用轮询,也要保存前后状态。定时器还受事件循环负载影响,100 ms 间隔不构成“100 ms 内必达”的实时保证。
对本例已经提供模型信号的列表,轮询只会增加延迟和重复工作。定时器机制可另读Qt 定时器。
来源与修订
跳转到“来源与修订”本页整合原始笔记 147112838 与其已合并来源 147110666。保留“机器模板—测点列表”的功能意图,改正将对象子事件当作条目变化事件的说明,并用完整可编译示例替换重复构造代码。