Qt模型控件:QTreeView应用
QTreeView复选框
- 一、复选框
- 1.1 解决方案
- 1. `mainwindow.h`
- 2. `mainwindow.cpp`
- 1.2 进阶功能:实现父项与子项的联动
- 二、插入多列
- 2.1 解决方案
- 2.2 总结
- 三、插入多列(隐藏)
- 3.1 实现思路
- 3.2 完整代码示例
这个功能的核心是利用 Qt Model/View 架构中的**“角色(Role)”**概念。
将通过
QStandardItemModel
和
QStandardItem
来实现,复选框的状态(选中、未选中、半选中)将存储在每个项的
Qt::CheckStateRole
中。
一、复选框
1.1 解决方案
- 使用
QStandardItemModel
: 这是一个灵活的模型,可以存储QStandardItem
对象。 - 设置
Qt::ItemIsUserCheckable
标志: 对于每个需要复选框的QStandardItem
,必须设置Qt::ItemIsUserCheckable
标志,告诉QTreeView
这个项是可以被用户勾选的。 - 设置初始复选状态: 使用
QStandardItem::setData()
方法,并指定Qt::CheckStateRole
角色,来设置项的初始状态(Qt::Checked
或Qt::Unchecked
)。 - 连接信号:
QTreeView
本身不直接提供复选框状态改变的信号。我们需要连接模型的dataChanged
信号。当任何项的数据(包括复选状态)发生改变时,这个信号就会被发射。 - 处理信号: 在槽函数中,判断数据改变的角色是否为
Qt::CheckStateRole
。如果是,就执行相应的操作。
完整代码示例
下面是一个完整的示例,演示了如何创建一个带有复选框的树,并响应复选框状态的变化。
1. mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:// 槽函数:用于处理复选框状态变化void onItemChanged(QStandardItem *item);private:Ui::MainWindow *ui;QTreeView *m_treeView;QStandardItemModel *m_model;
};
#endif // MAINWINDOW_H
2. mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QStandardItem>
#include <QDebug>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);setWindowTitle("QTreeView 复选框示例");// 1. 创建视图和模型m_treeView = new QTreeView(this);m_model = new QStandardItemModel(this);m_model->setHeaderData(0, Qt::Horizontal, "可选项");m_treeView->setModel(m_model);// --- 核心步骤:创建带复选框的项 ---// 2. 创建顶层项 "水果"QStandardItem *fruitItem = new QStandardItem("水果");// 设置为可被用户勾选fruitItem->setFlags(fruitItem->flags() | Qt::ItemIsUserCheckable);// 设置初始状态为未选中fruitItem->setData(Qt::Unchecked, Qt::CheckStateRole);m_model->appendRow(fruitItem);// 3. 创建子项 "苹果"QStandardItem *appleItem = new QStandardItem("苹果");appleItem->setFlags(appleItem->flags() | Qt::ItemIsUserCheckable);appleItem->setData(Qt::Unchecked, Qt::CheckStateRole);fruitItem->appendRow(appleItem);// 4. 创建子项 "香蕉"QStandardItem *bananaItem = new QStandardItem("香蕉");bananaItem->setFlags(bananaItem->flags() | Qt::ItemIsUserCheckable);bananaItem->setData(Qt::Checked, Qt::CheckStateRole); // 初始状态为选中fruitItem->appendRow(bananaItem);// 5. 创建顶层项 "蔬菜"QStandardItem *vegItem = new QStandardItem("蔬菜");vegItem->setFlags(vegItem->flags() | Qt::ItemIsUserCheckable);vegItem->setData(Qt::Unchecked, Qt::CheckStateRole);m_model->appendRow(vegItem);// 自动展开所有节点m_treeView->expandAll();setCentralWidget(m_treeView);// --- 核心步骤:连接信号和槽 ---// 监听模型的数据变化信号// 注意:QStandardItemModel 的 itemChanged 信号比 QAbstractItemModel 的 dataChanged 信号更方便// 因为它直接传递了发生变化的 QStandardItem 指针connect(m_model, &QStandardItemModel::itemChanged, this, &MainWindow::onItemChanged);
}MainWindow::~MainWindow()
{delete ui;
}// --- 核心步骤:实现槽函数 ---
void MainWindow::onItemChanged(QStandardItem *item)
{// 确保我们只处理复选框状态的变化if (item->isCheckable()) {// 获取当前项的文本QString itemText = item->text();// 获取当前的复选状态Qt::CheckState state = item->checkState();if (state == Qt::Checked) {qDebug() << "项 '" << itemText << "' 被选中了。";// 在这里执行选中项后的操作...} else if (state == Qt::Unchecked) {qDebug() << "项 '" << itemText << "' 被取消选中了。";// 在这里执行取消选中项后的操作...}}
}
1.2 进阶功能:实现父项与子项的联动
一个非常常见的需求是:当父项被勾选时,所有子项都自动被勾选;当所有子项都被勾选时,父项自动被勾选;当子项部分被勾选时,父项显示为半选中状态(Qt::PartiallyChecked
)。
要实现这个功能,你需要修改 onItemChanged
槽函数,添加相应的逻辑。
修改后的 onItemChanged
槽函数:
void MainWindow::onItemChanged(QStandardItem *item)
{// 防止在批量修改子项状态时,重复触发 itemChanged 信号,导致无限递归// 断开信号连接disconnect(m_model, &QStandardItemModel::itemChanged, this, &MainWindow::onItemChanged);if (item->isCheckable()) {// --- 1. 如果当前项是父项,更新所有子项 ---if (item->hasChildren()) {Qt::CheckState parentState = item->checkState();for (int i = 0; i < item->rowCount(); ++i) {QStandardItem *child = item->child(i);child->setCheckState(parentState);}}// --- 2. 如果当前项是子项,更新父项 ---else if (item->parent()) {QStandardItem *parent = item->parent();int checkedCount = 0;int uncheckedCount = 0;// 统计所有子项的状态for (int i = 0; i < parent->rowCount(); ++i) {QStandardItem *sibling = parent->child(i);if (sibling->checkState() == Qt::Checked) {checkedCount++;} else if (sibling->checkState() == Qt::Unchecked) {uncheckedCount++;}}// 根据统计结果更新父项状态if (checkedCount == parent->rowCount()) {// 所有子项都被选中parent->setCheckState(Qt::Checked);} else if (uncheckedCount == parent->rowCount()) {// 所有子项都未被选中parent->setCheckState(Qt::Unchecked);} else {// 子项部分被选中parent->setCheckState(Qt::PartiallyChecked);}}// 打印调试信息qDebug() << "项 '" << item->text() << "' 的状态变为: " << (item->checkState() == Qt::Checked ? "Checked" : (item->checkState() == Qt::Unchecked ? "Unchecked" : "PartiallyChecked"));}// 重新连接信号connect(m_model, &QStandardItemModel::itemChanged, this, &MainWindow::onItemChanged);
}
- 启用复选框: 对
QStandardItem
设置Qt::ItemIsUserCheckable
标志。 - 设置状态: 使用
item->setData(Qt::CheckState, Qt::CheckStateRole)
。 - 响应变化: 连接
QStandardItemModel::itemChanged
信号到自定义槽函数。 - 联动逻辑: 在槽函数中,根据当前项是父项还是子项,编写相应的逻辑来更新其他项的状态。记得在批量更新前断开信号,完成后再重新连接,以避免无限递归。
二、插入多列
在 QTreeView
控件的一行中插入两个数据,意味着你需要一个至少包含两列的模型(Model)。QTreeView
本身支持多列显示。
2.1 解决方案
- 设置模型的列数: 在创建
QStandardItemModel
时,或者之后通过setColumnCount()
方法,将列数设置为2
。 - 创建列数据项: 对于要插入的每一行,创建一个
QStandardItem
对象的列表(QList<QStandardItem*>
),列表中的每个QStandardItem
对应一列的数据。 - 插入行: 使用
model->appendRow()
或model->insertRow()
方法,将这个包含多个QStandardItem
的列表添加到模型中。
完整代码示例
下面的示例演示了如何创建一个两列的 QTreeView
,并在一行中插入两个数据。
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;QTreeView *m_treeView;QStandardItemModel *m_model;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QStandardItem>
#include <QList>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);setWindowTitle("QTreeView 多列示例");// 1. 创建视图和模型m_treeView = new QTreeView(this);m_model = new QStandardItemModel(this);// 2. 设置模型的列数和列标题m_model->setColumnCount(2); // 设置为2列m_model->setHeaderData(0, Qt::Horizontal, "名称");m_model->setHeaderData(1, Qt::Horizontal, "值");// --- 核心步骤:插入包含两个数据的行 ---// 方法一:使用 appendRow() 添加顶层项// 为第一行创建两个 QStandardItemQStandardItem *item1Col1 = new QStandardItem("项目 A");QStandardItem *item1Col2 = new QStandardItem("100");// 创建一个 QList 来存放这一行的所有项QList<QStandardItem*> rowItems1;rowItems1 << item1Col1 << item1Col2;// 将这个列表添加到模型中,作为新的一行m_model->appendRow(rowItems1);// 方法二:使用 insertRow() 在指定位置插入行// 为第二行创建两个 QStandardItemQStandardItem *item2Col1 = new QStandardItem("项目 B");QStandardItem *item2Col2 = new QStandardItem("200");QList<QStandardItem*> rowItems2;rowItems2 << item2Col1 << item2Col2;// 在索引为 1 的位置插入一行(即第二行)m_model->insertRow(1, rowItems2);// --- 进阶:为父项添加多列子项 ---// 父项只需要第一列有内容即可QStandardItem *parentItem = new QStandardItem("父项目");m_model->appendRow(parentItem);// 为父项创建一个两列的子项QStandardItem *childItemCol1 = new QStandardItem("子项目 C");QStandardItem *childItemCol2 = new QStandardItem("300");// 使用父项的 appendRow() 方法parentItem->appendRow(QList<QStandardItem*>() << childItemCol1 << childItemCol2);// 将模型设置到视图m_treeView->setModel(m_model);// 自动调整列宽以适应内容m_treeView->resizeColumnToContents(0);m_treeView->resizeColumnToContents(1);// 展开所有节点m_treeView->expandAll();setCentralWidget(m_treeView);resize(400, 300);
}MainWindow::~MainWindow()
{delete ui;
}
model->setColumnCount(2)
: 这是最关键的一步,它告诉模型和视图,我们的数据结构是二维的,每行有两个单元格。QList<QStandardItem*> rowItems
: 这个列表是打包一行数据的容器。列表中的第一个QStandardItem
会放在第一列,第二个会放在第二列,以此类推。model->appendRow(rowItems)
: 这个函数接收一个QList<QStandardItem*>
,并将其作为一个完整的行添加到模型的末尾。parentItem->appendRow(rowItems)
: 当你要为一个父项添加子项时,同样使用appendRow()
,但这次是在QStandardItem
对象(父项)上调用。视图会自动将子项显示在父项下方,并正确对齐到相应的列。
2.2 总结
在 QTreeView
的一行中插入两个数据,本质上是操作一个多列的模型。
- 步骤:
- 设置模型列数
setColumnCount(2)
。 - 为一行的每一列创建一个
QStandardItem
。 - 将这些
QStandardItem
放入一个QList
。 - 使用
model->appendRow(list)
或parentItem->appendRow(list)
将这一行数据添加到模型中。
- 设置模型列数
这个方法同样适用于插入三列或更多列的数据,只需相应地增加 QStandardItem
的数量即可。
三、插入多列(隐藏)
实现“一行插入两列数据,但只显示第一列,同时能获取两列数据”,核心是隐藏第二列但保留其数据。这样既可以在视觉上不显示第二列,又能在需要时通过模型获取该列的数据。
3.1 实现思路
- 创建包含两列的模型(
QStandardItemModel
),确保两列都有数据。 - 通过
QTreeView
的setColumnHidden()
方法隐藏第二列,使其不在界面上显示。 - 当需要获取数据时,通过模型的
data()
方法或item()
方法访问第二列的数据。
3.2 完整代码示例
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPushButton>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:// 点击按钮时获取选中行的两列数据void onGetDataClicked();private:Ui::MainWindow *ui;QTreeView *m_treeView;QStandardItemModel *m_model;QPushButton *m_getDataBtn;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QStandardItem>
#include <QList>
#include <QVBoxLayout>
#include <QWidget>
#include <QMessageBox>
#include <QModelIndex>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);setWindowTitle("QTreeView 隐藏列但保留数据示例");// 创建主布局和中心部件QWidget *centralWidget = new QWidget(this);QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);// 1. 创建视图和模型m_treeView = new QTreeView(this);m_model = new QStandardItemModel(this);// 2. 设置模型为2列,并设置列标题(标题也会被隐藏,仅用于内部标识)m_model->setColumnCount(2);m_model->setHeaderData(0, Qt::Horizontal, "名称"); // 第一列标题m_model->setHeaderData(1, Qt::Horizontal, "值"); // 第二列标题(隐藏)// 3. 插入测试数据(每行两列)// 行1:第一列显示"项目A",第二列存储"100"(隐藏)QList<QStandardItem*> row1;row1 << new QStandardItem("项目A") << new QStandardItem("100");m_model->appendRow(row1);// 行2:第一列显示"项目B",第二列存储"200"(隐藏)QList<QStandardItem*> row2;row2 << new QStandardItem("项目B") << new QStandardItem("200");m_model->appendRow(row2);// 行3:父项+子项(子项同样包含两列数据)QStandardItem *parentItem = new QStandardItem("父项目");m_model->appendRow(parentItem);QList<QStandardItem*> childRow;childRow << new QStandardItem("子项目C") << new QStandardItem("300");parentItem->appendRow(childRow);// 4. 设置模型到视图,并隐藏第二列(关键步骤)m_treeView->setModel(m_model);m_treeView->setColumnHidden(1, true); // 隐藏索引为1的列(第二列)// 优化显示m_treeView->expandAll(); // 展开所有节点m_treeView->resizeColumnToContents(0); // 自动调整第一列宽度// 5. 添加按钮用于触发获取数据m_getDataBtn = new QPushButton("获取选中行的两列数据", this);connect(m_getDataBtn, &QPushButton::clicked, this, &MainWindow::onGetDataClicked);// 添加控件到布局mainLayout->addWidget(m_treeView);mainLayout->addWidget(m_getDataBtn);setCentralWidget(centralWidget);resize(400, 300);
}MainWindow::~MainWindow()
{delete ui;
}// 槽函数:获取选中行的两列数据
void MainWindow::onGetDataClicked()
{// 获取当前选中项的索引QModelIndex currentIndex = m_treeView->currentIndex();if (!currentIndex.isValid()) {QMessageBox::warning(this, "提示", "请先选中一项");return;}// 获取当前行的索引(row() 是当前项在父项中的行号)int row = currentIndex.row();// 获取当前项的父索引(用于处理子项的情况)QModelIndex parentIndex = currentIndex.parent();// 6. 获取第一列数据(显示的列)QVariant column1Data = m_model->data(m_model->index(row, 0, parentIndex), // 第一列的索引Qt::DisplayRole);// 7. 获取第二列数据(隐藏的列)QVariant column2Data = m_model->data(m_model->index(row, 1, parentIndex), // 第二列的索引Qt::DisplayRole);// 显示结果QString info = QString("选中项数据:\n第一列:%1\n第二列:%2").arg(column1Data.toString()).arg(column2Data.toString());QMessageBox::information(this, "数据", info);
}
-
隐藏第二列
通过m_treeView->setColumnHidden(1, true);
隐藏索引为1
的列(第二列)。setColumnHidden
只会隐藏列的视觉显示,不会删除数据。 -
插入两列数据
每行数据通过QList<QStandardItem*>
存储两列内容,例如:QList<QStandardItem*> row1; row1 << new QStandardItem("项目A") << new QStandardItem("100"); // 第一列显示,第二列隐藏 m_model->appendRow(row1);
-
获取隐藏列的数据
即使列被隐藏,仍可通过模型的data()
方法结合列索引获取数据:// 获取第二列数据(索引为1) QVariant column2Data = m_model->data(m_model->index(row, 1, parentIndex), Qt::DisplayRole);
row
:当前选中项在父项中的行号。parentIndex
:父项的索引(用于处理树形结构中的子项)。Qt::DisplayRole
:获取显示文本(第二列数据以文本形式存储)。
- 效果说明
- 界面上只显示第一列数据(如“项目A”“项目B”),第二列完全隐藏。
- 选中任意项并点击“获取选中行的两列数据”按钮,会弹出对话框显示该行列的两列数据(包括隐藏的第二列)。
这种方式既保证了界面简洁,又保留了数据的完整性,适用于需要“后台存储额外信息但不显示”的场景。