QT示例项目复现(1)--树视图

复现了一下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() 函数对插入列操作进行包裹。

具体来说,这两个函数的作用是:

  1. beginInsertColumns(parent, position, position + columns - 1):这个函数通知视图在父索引 parent 下的 positionposition + columns - 1 列之间要开始插入列。这为视图做好了插入准备,使其可以在插入数据之前进行布局和准备。

  2. 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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
qt-vs-addin-1.2.4-opensource是一个开源的Qt集成开发环境插件。Qt是一种跨平台的应用程序开发框架,它允许开发者使用统一的代码库编写跨平台的应用程序。Qt提供了丰富的库和工具,使得开发者可以更方便地创建GUI应用程序、网络应用程序、嵌入式应用程序等。 而qt-vs-addin-1.2.4-opensource则是为了方便使用Visual Studio开发Qt应用程序而开发的插件。Visual Studio是一种常用的集成开发环境,它提供了强大的编辑器、调试器和其他开发工具。而通过qt-vs-addin-1.2.4-opensource插件,开发者可以在Visual Studio中直接使用Qt的功能,如自动生成Qt项目、添加Qt类、设计器支持等。 qt-vs-addin-1.2.4-opensource主要的特点包括: 1. 支持多个Visual Studio版本:这个插件可以与不同版本的Visual Studio(如2010、2012、2013等)兼容,方便开发者选择自己喜欢的开发环境。 2. 丰富的项目模板:插件提供了多种项目模板,包括Qt Widgets应用程序、Qt Quick应用程序等,可以帮助开发者快速创建项目并开始开发。 3. 代码自动补全:插件可以提供Qt类的代码自动补全功能,方便编写代码并减少出错。 4. 可视化设计器:插件还提供了可视化设计器,开发者可以通过拖放控件来快速设计界面。 5. 调试支持:插件可以与Visual Studio的调试器集成,方便开发者进行调试。 总体而言,qt-vs-addin-1.2.4-opensource是一个强大且易于使用的插件,它为开发者提供了更好的开发体验和效率,使得使用Qt开发跨平台应用程序更加便捷。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

葫芦娃找爷爷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值