跳转到内容
新建笔记

Qt 列表数量变化:模型信号、空状态与展开逻辑

“机器模板”界面中,一个 QListWidget 保存测点。新增、删除、清空测点后,界面需要更新数量;最后一个测点被移除时,还要显示空状态。应监测列表的数据模型变化,然后重新读取 count()。

先区分两种业务含义:

  • 当前为空:初始化和刷新时都可以据此显示空提示。
  • 从非空变为空:仅在上一次数量大于零、这一次数量等于零时通知一次。

设上一次数量为 noldn_{old}、当前数量为 nnewn_{new},清空通知条件是:

becameEmpty⁡=(nold>0)∧(nnew=0)\operatorname{becameEmpty}=(n_{old}>0)\land(n_{new}=0)

一直为空并不等于不断发生“清空”事件。将这两个概念混在一起,会导致重复日志或重复业务请求。

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 类,不必复制两套构造函数。保留创建标题、添加按钮、列表和布局的代码,只做三件事:

  1. 将 previousCount 放入类成员,在控件建立后连接三个模型信号。
  2. 在一个刷新函数中更新数量、空提示以及 isExpanded 对应的可见性。
  3. 添加和删除操作继续调用 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。保留“机器模板—测点列表”的功能意图,改正将对象子事件当作条目变化事件的说明,并用完整可编译示例替换重复构造代码。