复现了一下Qt自带的项目示例,学习下有关树形视图的知识。这里只说了这个示例代码的部分,如果不了解qt模型视图的部分,需要先去查一下相关的基础知识。
一、示例整体框架及接口分析
1.分析代码结构
工程里的代码如下图所示:
下面这张图,简单的介绍了这个示例的代码结构
2.功能和运行结果
实现功能为打开一个窗口,以树形显示文档的内容。提供插入行列、删除行列、添加子节点,显示操作信息的功能,运行结果如下:
3.接口函数设计
可以看到在不同的代码文件中有很多函数是重名或者重复功能,这个就是整体的设计,利用函数调用可以更清晰的实现相关功能,方便之后的修改管理
二、功能实现
这里列出了一些我做了注释的代码,注释就是一些函数的功能或者需要注意的点,所以注释是比较重要的地方,整个工程代码(含注释)我都会放在文末的链接里(也可以去qt平台上直接搜这个示例,有全部代码)。
1.编辑UI文件
双击“在这里输入”,输入菜单名称,鼠标移到新添加的菜单项,可以添加菜单中的Action。
从左边器件窗口拖动一个treeView控件,再设置布局
其中要注意画红框部分控件的命名,在mainwindow代码中需要用信号链接控件和槽函数实现相关功能,控件名称就是这里的名称。
2. mainwindow代码
mainwindow类构造函数代码,其中file.txt是存储显示的数据的文档,我会放在文末的链接中。
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setupUi(this);
const QStringList headers({tr("Title"), tr("Description")});
//读取数据文件
QFile file("D:/vsproject/tree/src/file.txt");
file.open(QIODevice::ReadOnly);
TreeModel *model = new TreeModel(headers, file.readAll());
file.close();
// treeView是UI中设置的插件名称
treeView->setModel(model);
for (int column = 0; column < model->columnCount(); ++column)
treeView->resizeColumnToContents(column);
// 下面这些控件的名称都是UI中设置,只有名称正确才能正常编译运行
connect(Exit, &QAction::triggered, qApp, &QCoreApplication::quit);
connect(treeView->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &MainWindow::updateActions);
connect(menuActions, &QMenu::aboutToShow, this, &MainWindow::updateActions);
connect(insertRowAction, &QAction::triggered, this, &MainWindow::insertRow);
connect(insertColumnAction, &QAction::triggered, this, &MainWindow::insertColumn);
connect(removeRowAction, &QAction::triggered, this, &MainWindow::removeRow);
connect(removeColumnAction, &QAction::triggered, this, &MainWindow::removeColumn);
connect(insertChildAction, &QAction::triggered, this, &MainWindow::insertChild);
// 更新Action的属性(可用性、文本、图标等)
updateActions();
}
插入子节点接口实现
void MainWindow::insertChild()
{
/*selectionModel()用于获取与视图关联的选择模型(QItemSelectionModel)。
选择模型用于管理视图中的选择操作,它跟踪了用户在视图中选中的项(或单元格),并允许程序对选中项进行操作。
在使用 Qt 中的视图类展示数据时,常常需要处理用户的选择操作,
例如选择一行或一列,或者选择多个单元格。选择模型可以帮助程序轻松管理这些选择操作,同时还可以通过选择模型获取有关所选项的信息。*/
//获取当前选中项的索引
const QModelIndex index = treeView->selectionModel()->currentIndex();
QAbstractItemModel *model = treeView->model();
//如果选中项model中列为空,则添加一列(特殊情况,没有数据)
if (model->columnCount(index) == 0)
{ //insertColumn()在指定父项中的子项,给定列前插入一列。
//在父项为index的子项的0列前插入一列
if (!model->insertColumn(0, index))
return;
}
//在子项中插入一行数据(这一行的列项会自动匹配)
if (!model->insertRow(0, index))
return;
//为新插入行设置数据
for (int column = 0; column < model->columnCount(index); ++column)
{ //由于是新添加的子项行标为0
const QModelIndex child = model->index(0, column, index);
model->setData(child, QVariant(tr("[No data]")), Qt::EditRole);
//isValid() 是 QVariant 类的成员函数,用于检查一个 QVariant 是否有效。
//Qt::Horizontal表示设置水平标题,Qt::EditRole表示设置该项为可编辑
if (!model->headerData(column, Qt::Horizontal).isValid())
model->setHeaderData(column, Qt::Horizontal, QVariant(tr("[No header]")), Qt::EditRole);
}
//切换当前选择项为新添加的子项
treeView->selectionModel()->setCurrentIndex(model->index(0, 0, index),
QItemSelectionModel::ClearAndSelect);
updateActions();
}
插入行接口实现
1.action调用插入列槽函数
bool MainWindow::insertColumn()
{
//这里注意区别treeView->selectionModel()这个是获取选中项的model,
//treeView->model()这个是获取视图里的整个model
QAbstractItemModel *model = treeView->model();
int column = treeView->selectionModel()->currentIndex().column();
// Insert a column in the parent item.
bool changed = model->insertColumn(column + 1);
if (changed)
model->setHeaderData(column + 1, Qt::Horizontal, QVariant("[No header]"), Qt::EditRole);
updateActions();
return changed;
}
2.槽函数获取model,再调用model中的插入列函数
bool TreeModel::insertColumns(int position, int columns, const QModelIndex &parent)
{
beginInsertColumns(parent, position, position + columns - 1);
//这里是插入列所以直接在根节点的后面插入,如果是行的话就是在父节点的后面插入
const bool success = rootItem->insertColumns(position, columns);
endInsertColumns();
return success;
}
在 Qt 的数据模型中,插入列是一个涉及到多个步骤的操作,需要在数据插入之前通知视图(View)开始插入,然后在数据插入完成后通知视图插入结束。这就是为什么在代码中要使用 beginInsertColumns()
和 endInsertColumns()
函数对插入列操作进行包裹。
具体来说,这两个函数的作用是:
-
beginInsertColumns(parent, position, position + columns - 1)
:这个函数通知视图在父索引parent
下的position
到position + columns - 1
列之间要开始插入列。这为视图做好了插入准备,使其可以在插入数据之前进行布局和准备。 -
endInsertColumns()
:这个函数通知视图插入列操作已经完成,视图可以根据新的数据重新布局和更新显示。
在整个插入列的过程中,这两个函数的调用是必要的,因为它们确保了数据模型和视图之间的同步。如果没有这些函数的调用,插入列的操作可能会导致视图显示不正确或不一致的数据,因为视图不知道何时开始和结束插入操作。
总之,beginInsertColumns()
和 endInsertColumns()
函数的作用是确保在插入列操作期间,数据模型与视图之间的状态同步,以保证视图正确地显示插入的列数据。
3.model获取要插入的父节点项,然后调用treeitem中的插入列函数,进行具体的数据操作
bool TreeItem::insertColumns(int position, int columns)
{
if (position < 0 || position > itemData.size())
{
return false;
}
for (int column = 0; column < columns; ++column)
{
itemData.insert(position, columns);
}
return true;
}
整个插入操作的函数调用过程,就是整个工程代码层级调用的体现,删除操作和这个几乎一样。
更新action状态接口实现
void MainWindow::updateActions()
{
//setEnabled()设置action是否能够交互(是否置灰),判断条件是否有项被选中
const bool hasSelection = !treeView->selectionModel()->selection().isEmpty();
removeRowAction->setEnabled(hasSelection);
removeColumnAction->setEnabled(hasSelection);
//当前选中项是否存在
const bool hasCurrent = treeView->selectionModel()->currentIndex().isValid();
insertRowAction->setEnabled(hasCurrent);
insertColumnAction->setEnabled(hasCurrent);
if (hasCurrent)
{
/*持久编辑器是用于编辑表格或树状视图中特定单元格或项的小部件。
当用户开始编辑一个单元格或项时,会打开一个持久编辑器,允许用户进行编辑操作。
closePersistentEditor() 函数则用于关闭这个持久编辑器,将编辑后的数据保存到数据模型中。*/
treeView->closePersistentEditor(treeView->selectionModel()->currentIndex());
const int row = treeView->selectionModel()->currentIndex().row();
const int column = treeView->selectionModel()->currentIndex().column();
//状态栏显示相关操作信息,会显示是否为首行
if (treeView->selectionModel()->currentIndex().parent().isValid())
statusBar()->showMessage(tr("Position: (%1,%2)").arg(row).arg(column));
else
statusBar()->showMessage(tr("Position: (%1,%2) in top level").arg(row).arg(column));
}
}
3.treemodel代码
treemodel构造函数
TreeModel::TreeModel(const QStringList &headers, const QString &data, QObject *parent)
: QAbstractItemModel(parent)
{
QVector<QVariant> rootData;
for (const QString &header : headers)
rootData << header;
rootItem = new TreeItem(rootData);
//data.split('\n') 表示对这个字符串进行以换行符 ('\n') 为分隔符的拆分操作,将字符串分割成多个子字符串的列表。
setupModelData(data.split('\n'), rootItem);
}
setupModelData初始化数据到模型
void TreeModel::setupModelData(const QStringList &lines, TreeItem *parent)
{ //parents存储所有的父节点
QVector<TreeItem*> parents;
//indentations存储文本的缩进级别
QVector<int> indentations;
parents << parent;
indentations << 0;
//number用来统计处理的文本的行数
int number = 0;
while (number < lines.count()) {
int position = 0;
while (position < lines[number].length()) {
if (lines[number].at(position) != ' ')
break;
++position;
}
/*mid()是字符串函数,用来从指定位置开始到字符串结尾,提取字符串的子字符串(字符串位置从1开始,子字符串不包含起始位置上的字符)
trimmed()是字符串函数,用来去除字符串末尾和开头的空格、制表符、换行符等*/
//lineData 就是一行的数据 如lineData "Getting Started\t\t\t\tHow to familiarize yourself with Qt Designer"
const QString lineData = lines[number].mid(position).trimmed();
if (!lineData.isEmpty()) {
// Qt::SkipEmptyParts表示只保留非空字符串
//columnStrings 将一行数据的 左右两部分拆开 存放进字符串列表中
const QStringList columnStrings =
lineData.split(QLatin1Char('\t'), Qt::SkipEmptyParts);
/*qvector reserve方法的作用是预分配需要的内存,例如参数为2,则预分配足够容纳两个变量的内存空间
使用reserve的好处是减少内存重新分配次数、提高性能(多次内存分配消耗时间),避免内存碎片化(可能会多次分配小内存),并更精确地管理内存
*/
QVector<QVariant> columnData;
columnData.reserve(columnStrings.size());
for (const QString &columnString : columnStrings)
columnData << columnString;
//如果当前的缩进值大于记录的上一行缩进值说明此为子节点
if (position > indentations.last()) {
/*当前父节点的最后一个子节点是新的父节点,除非当前父节点没有子节点*/
if (parents.last()->childCount() > 0) {
parents << parents.last()->child(parents.last()->childCount()-1);
indentations << position;
}
} else {
//当前缩进值小于大于记录的上一行缩进值说明此为同级节点 或 父节点,弹出节点,直到找到父节点
while (position < indentations.last() && parents.count() > 0) {
parents.pop_back();
indentations.pop_back();
}
}
// 将新的项目追加到当前父节点的子节点列表中.
TreeItem *parent = parents.last();
/*parent->childCount()表示插入到子节点的位置(末尾),
第二个参数是插入子节点的数量,第三个参数为节点的列数(与根节点保持一致)*/
parent->insertChildren(parent->childCount(), 1, rootItem->columnCount());
//将数据设置到子节点中
for (int column = 0; column < columnData.size(); ++column)
parent->child(parent->childCount() - 1)->setData(column, columnData[column]);
}
++number;
}
}
获取特定项索引
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (parent.isValid() && parent.column() != 0)
return QModelIndex();
TreeItem *parentItem = getItem(parent);
if (!parentItem)
return QModelIndex();
TreeItem *childItem = parentItem->child(row);
if (childItem)
return createIndex(row, column, childItem);
return QModelIndex();
}
索引的作用和为什么要传入父对象
在一个树状的数据模型中,数据通常是以层级结构组织的,其中父项包含子项。每个项(或节点)可以有零个或多个子项。为了在这样的结构中唯一标识一个特定的数据项,我们需要一个标识,这就是索引(QModelIndex
)。
为什么获取索引需要传入父对象呢?这涉及到树状数据模型的组织方式和索引的唯一性。
当我们在树状结构中获取一个特定项的索引时,需要指定这个项所在的位置。在一个给定的层级中,每个项都有一个唯一的位置,通常由行号(Row)和列号(Column)来表示。但是,在整个数据模型中,多个层级的行号和列号可能会重叠,导致无法唯一标识一个项。
为了解决这个问题,我们引入了父对象的概念。父对象代表了包含当前项的上一层级项,它可以唯一地标识当前项的位置。通过传入父对象,我们能够在特定的层级中唯一标识一个项,从而获取到正确的索引。
换句话说,传入父对象可以帮助我们在层级结构中准确定位一个项的位置,确保获取的索引是唯一且准确的。这在树状数据模型中非常重要,因为它允许我们在复杂的数据结构中准确地导航和操作数据。
为什么是createIndex一个索引而不是直接获取索引?
`createIndex()` 函数的作用是创建一个 `QModelIndex` 对象,用于在数据模型中标识特定的数据项。虽然名字中包含了 "create",但它实际上并不是在数据模型中新建数据项,而是创建一个用于标识数据项的索引。
为什么不是获取索引而是创建一个索引?这是因为 `QModelIndex` 对象的设计目的是用于标识数据模型中的特定项,而不仅仅是获取已存在的索引。这种设计使得我们可以在数据模型中灵活地标识不同的数据项,而不受数据的实际组织方式限制。
具体来说,`createIndex()` 函数的作用是根据给定的行号、列号和数据指针创建一个 `QModelIndex` 对象。这个索引对象可以唯一地标识数据模型中的一个特定项。在树状结构中,每个项都可以通过其父项、行号和列号来唯一标识,因此 `createIndex()` 函数通过这些信息创建了一个唯一标识。
这种设计的好处是,我们可以在数据模型中使用这些索引来进行导航、查找和操作数据,而不需要直接获取已存在的索引。通过创建索引,我们可以更方便地在数据模型中定位和操作数据项,而不需要考虑索引的实际构造方式。这使得数据模型的使用变得更加灵活和高效。
设置数据
设置数据函数大同小异,就只拿出一个,了解一下设置数据到模型的过程
bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
//这里判断选中项是否是可编辑的状态
if (role != Qt::EditRole)
return false;
TreeItem *item = getItem(index);
//调用treeItem的setData函数将数据写入
bool result = item->setData(index.column(), value);
/*如果设置成功,就发送数据改变的信号,
与数据模型关联的视图会接收到这个信号,视图会根据信号传递的索引范围和数据角色,更新对应的单元格或项的显示内容。*/
if (result)
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return result;
}
顺带看一下treeItem怎么设置数据的,这样模型的数据(存储数据的变量)就被改了,之后需要发送信号,视图显示才会同步,否则之后存储的变量改了,显示的不会变。
bool TreeItem::setData(int column, const QVariant &value)
{ //判断column是否越界
if (column < 0 || column > itemData.size())
return false;
//改变QVector<QVariant> itemData;值
itemData[column] = value;
return true;
}
自己觉得比较不理解的代码,已经贴出来了,做个记录,如果大家发现什么问题欢迎指正。
下面是整个工程代码包括文件的链接。
https://download.csdn.net/download/m0_72125162/88214972?spm=1001.2014.3001.5503