Qt 树形控件 QTreeView QTreeWidget深入剖析

众众众所大家们周知,QTreeWidget性能差、QTreeView配合QStandardItemModel性能也差、不够灵活等等,需要自定义Model来配合QTreeView使用。那么为何这么多问题Qt官方却不进行改进?本文结合Qt源码,经过一周的深入分析,对如何设计自定义Model提出了自己的一些想法。

1. QTreeWidget分析

QTreeWidget使用起来比较简单,代码就不在这里罗列了。QTreeWidget使用时所依赖的类:

QTreeWidgetItem 没有从任何类继承
QTreeModel 继承自QAbstractItemModel
QTreeWidget 继承自QTreeView

在QTreeWidget的构造中,调用了QTreeView::setModel(new QTreeModel(1, this));,这样我们在使用时,其实只关心QTreeWidgetQTreeWidgetItem这两个类即可。那么接下来,分析一下这三个类的成员变量,基本就能看出是如何存储我们的数据,也就知道了为何效率比较低。

1.2 QTreeWidget成员

只有一个用Q_DECLARE_PRIVATE(QTreeWidget)修饰的p指针,成员都在QTreeWidgetPrivate中,经过查看里面没什么有用的东西。

1.3 QTreeModel

成员简化如下:

private:
    QTreeWidgetItem *rootItem;
    QTreeWidgetItem *headerItem; //这两个和根节点有关
    
private:
    Q_DECLARE_PRIVATE(QTreeModel) //p指针

存储了根节点的信息,别的也没啥东西。

1.4 QTreeWidgetItem

关键成员如下:

    QList<QList<QWidgetItemData>> values; //关键!
    QTreeWidget *view = nullptr;
    QTreeWidgetItemPrivate *d; //d指针,关键!
    QTreeWidgetItem *par = nullptr; //父Item
    QList<QTreeWidgetItem*> children; //子Item列表
    
    //d指针的成员:
    QTreeWidgetItem *q;
    QVariantList display; //关键!
    uint disabled : 1;
    uint selected : 1;
    uint hidden : 1;
    int rowGuess;
  • values关键成员,存储了各个Role相关相关的数据,比如背景Qt::BackgroundColorRole、对齐Qt::TextAlignmentRole等等,当然,当你设置的时候,才会存储这些信息。但每个Item都存储了一遍,这个占用了大量空间。
  • QVariantList display关键成员,它会存储你树上要显示的所有内容,也就是说,如果你的原始数据存储在Qt::UserRole中,但显示的数据会存储在这个成员中,相当于存了至少两份内容!所有占用空间大很大一部分原因也在这里。

这两个问题就是占用空间大,效率低的决定性因素,那如何解决这个问题呢?

干掉这两个成员就完事了!重载QAbstractItemModel的data()函数,用到的时候动态返回,这个后面详细说。

2. QTreeView配合QStandardItemModel使用分析

使用时所依赖的类:

QStandardItem 没有从任何类继承
QStandardItemModel 继承自QAbstractItemModel
QTreeView

这里直接看使用示例:

    // 创建一个标准项目模型
    QStandardItemModel model;
    // 添加顶级节点
    QStandardItem* itemRootTop = new QStandardItem("Top");
    model->appendRow(itemRootTop);
    // 添加子节点
    QList<QStandardItem*> items;
    QStandardItem* item0 = new QStandardItem("item0");
    QStandardItem* item1 = new QStandardItem("item1");
    itemRootTop->appendRow(items);
    
    // 创建一个QTreeView对象
    QTreeView treeView;
    treeView.setModel(&model);
    treeView.show();

可以看到,这里new了大量个QStandardItem,比treewidget都多,treewidget每个行只对应了一个TreeWidgetItem对象啊!
看一下成员:

QStandardItemPrivate的:

    QStandardItem *parent;
    QList<QStandardItemData> values; //使用std::pair存储了role相关数据
    QList<QStandardItem *> children; //存储了当前行所有Item

QStandardItemModelPrivate的:

    QList<QStandardItem *> columnHeaderItems; 
    QList<QStandardItem *> rowHeaderItems; //存储顶级的Item成员
    QHash<int, QByteArray> roleNames;

由此可见,这个比QTreeWidget还差劲呢。

3.自定义Model/Item

通过上面的分析,用QTreeWidget在数据量不是特别大的时候,用着是没什么问题的。QTreeWidget为了兼容各种情况,已经写的很灵活了,但如何更高效呢?答案就是定制化,这样就减少了一部分灵活性。
如何定制化呢?核心就是重写QAbstractItemModel,Qt已经给了我们一个很好的实例,可以自己看一下editabletreemodel这个例子,笔者这里实现的也是在这个例子基础上改的,添加了TreeItem和TreeModel两个类。

  • TreeItem:对应了树的一行,每一列的数据存储在成员中,同时还存储父和子
  • TreeModel:重新实现关键函数

看这个例子之前,你得先明白QAbstractItemModel的几个关键原理,否则也是一脸懵逼。

3.1 数据刷新机制

qt本身已经实现了一种非常高效的刷新机制,即只刷新在窗口中显示的那部分数据,这个功能是通过在合适的时机调用data()接口来实现的,你自己可以在重写的data函数中加个打印,看看是不是只刷新了窗口显示的那部分数据。
data函数伪代码:

QVariant TreeModel::data(const QModelIndex &index, int role) const
{
    TreeItem *item = indexToItem(index);
    
    if (role == Qt::EditRole)
    {
        //根据行列提取数据并进行转化
        return QVariant("编辑时显示的内容")
    }
    else if (role == Qt::DisplayRole)
    {
        //xxx
        return QVariant("正常显示的内容");
    }
    else if (role == Qt::TextAlignmentRole)
    {
        return QVariant(Qt::AlignCenter); //固定居中对齐
    }
    else if (role == Qt::BackgroundColorRole)
    {
    }
}

3.2 如何减少存储容量

答案是在TreeItem中只定义原始数据,显示的内容在data()接口中动态计算填充。
比如笔者这里的成员函数的定义:

private:
    //父节点
    TreeItem *parentItem;
    //子节点
    QList<TreeItem*> childItems;
    //各个列的原始数据 - 非显示数据
    QVector<QVariant> itemData;

这里用一个vector存储了每一列的原始数据,如果你想更进一步减少内存,可以用void*指向你本来的原始数据即可,不过这样在更新数据时可能没那么方便。

3.3 QModelIndex怎么算

这个问题折磨了一段时间,来看看创建它的接口:

inline QModelIndex QAbstractItemModel::createIndex(int arow, int acolumn, void *adata) const
{ return QModelIndex(arow, acolumn, adata, this); }

接口很简单,输入行列和要绑定的数据即可,adata我们这里传入对应的TreeItem指针。
但是这里的行有坑,这里高亮一下:

arow行表示TreeItem所在父节点的第几行!
列就是正常的列,无法计算的时候就赋值0。

在笔者的代码中,重写了两个接口来对Item和Index进行互相转换。

3.4 插入数据页面不刷新?

在这个例子中,只需要向模型中更新我们的数据即可,但你会发现插入不会立即刷新,你得切换下页面才刷新,这是因为你得调用beginInsertRowsendInsertRows接口来激发刷新的信号。

3.5 如何写一个更通用的类

不同的树可能显示的display不同,难道每个树都得写个TreeModel?
笔者通过回调函数的方式,通过registerDisplayRuleregisterBackgroundColorRule等接口,将规则lambda注入到TreeModel中,这样就实现了用同一套代码,实现不同类的方式。

3.6 排序、编辑、筛选的支持

编辑应该是可以的,但排序、筛选没有添加支持。

4. 源码如下

TreeModel.h

#ifndef TREEMODEL_H
#define TREEMODEL_H

#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>
#include "treeitem.h"

class TreeItem;

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    TreeModel(const QStringList &headers, QObject *parent = 0);
    ~TreeModel();

    //清空所有数据
    void clear();

    //注册显示规则 - 函数对象第二个参数是列下标,返回值是显示的字符串QString
    void registerDisplayRule(std::function<QString(TreeItem*, int)> funcRule);

    //注册背景色规则 - 函数对象第二个参数是列下标,返回值是要显示的颜色
    void registerBackgroundColorRule(std::function<QColor(TreeItem*, int)> funcRule);

    //添加顶级条目,返回添加的条目
    TreeItem* addTopLevelItem(QVector<QVariant> vecValue);

    //添加子条目
    TreeItem* addChildItem(TreeItem *pParent, QVector<QVariant> vecValue);
    TreeItem* insertChileItem(TreeItem *pParent, int irow, QVector<QVariant> vecValue); //在指定行row后面插入 TODO未实现

    //返回顶级条目个数
    int topLevelItemCount();

    //返回指定的顶级条目
    TreeItem *topLevelItem(int index);

    //删除顶级条目及子条目
    void deleteToplevelItems(TreeItem *pTopItem);

protected:
    QVariant data(const QModelIndex &index, int role) const override;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const override;

    QModelIndex index(int row, int column,
                      const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &index) const override;

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

    Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role = Qt::EditRole) override;
    bool setHeaderData(int section, Qt::Orientation orientation,
                       const QVariant &value, int role = Qt::EditRole) override;

    bool insertColumns(int position, int columns,
                       const QModelIndex &parent = QModelIndex()) override;
    bool removeColumns(int position, int columns,
                       const QModelIndex &parent = QModelIndex()) override;
    bool insertRows(int position, int rows,
                    const QModelIndex &parent = QModelIndex()) override;
    bool removeRows(int position, int rows,
                    const QModelIndex &parent = QModelIndex()) override;

private:
    //数据修改
    void emitDataChanged(TreeItem *pItem, int iCol);
    //index转item
    TreeItem *indexToItem(const QModelIndex &index) const;
    //指定item的index
    QModelIndex itemToIndex(TreeItem *item, int iCol) const;

    TreeItem *m_pRootItem;

    //显示规则
    std::function<QString(TreeItem*, int)> m_funcDisplayRule;
    //颜色规则
    std::function<QColor(TreeItem*, int)> m_funcBackgroundColorRule;

    friend class TreeItem;
};

#endif // TREEMODEL_H

TreeModel.cpp

#include <QtWidgets>
#include <QDebug>
#include <QDateTime>

#include "treeitem.h"
#include "treemodel.h"
#include "commonlib/GlogWrapper.h"

TreeModel::TreeModel(const QStringList &headers, QObject *parent)
    : QAbstractItemModel(parent)
{
    m_pRootItem = new TreeItem(headers.size());

    for(int i=0; i<headers.size(); i++)
    {
        m_pRootItem->setData(i, headers.at(i));
    }
}

TreeModel::~TreeModel()
{
    delete m_pRootItem;
}

void TreeModel::clear()
{
    int irowCnt = rowCount();
    if (irowCnt != 0)
    {
       removeRows(0, irowCnt);
    }
}

void TreeModel::registerDisplayRule(std::function<QString(TreeItem *, int)> funcRule)
{
    m_funcDisplayRule = funcRule;
}

void TreeModel::registerBackgroundColorRule(std::function<QColor(TreeItem *, int)> funcRule)
{
    m_funcBackgroundColorRule = funcRule;
}

TreeItem *TreeModel::addTopLevelItem(QVector<QVariant> vecValue)
{
    //检查列数是否一致
    if (vecValue.size() != m_pRootItem->columnCount())
    {
        return nullptr;
    }

    if (!insertRow(m_pRootItem->childCount(), QModelIndex())) //会回调insertrows
        return nullptr;
    TreeItem *pItem = m_pRootItem->child(m_pRootItem->childCount()-1);
    pItem->itemData = vecValue;
    return pItem;

    //下面这个页面无法及时更新
    /*m_pRootItem->insertChildren(m_pRootItem->childCount(), 1, m_pRootItem->columnCount(), this);
    TreeItem *pItem = m_pRootItem->child(m_pRootItem->childCount() - 1);
    pItem->itemData = vecValue;
    return pItem;*/

}

TreeItem* TreeModel::addChildItem(TreeItem *pParent, QVector<QVariant> vecValue)
{
    if (vecValue.size() != m_pRootItem->columnCount())
    {
        return nullptr;
    }

    QModelIndex indexParent = itemToIndex(pParent, 0);
    if (!insertRow(pParent->childCount(), indexParent))
        return nullptr;
    TreeItem *pItem = pParent->child(pParent->childCount()-1);
    pItem->itemData = vecValue;
    return pItem;

    //下面这个页面无法及时更新
    /*pParent->insertChildren(pParent->childCount(), 1, m_pRootItem->columnCount(), this);
    TreeItem *pItem = pParent->child(pParent->childCount()-1);
    pItem->itemData = vecValue;
    return pItem;*/
}

int TreeModel::topLevelItemCount()
{
    return m_pRootItem->childCount();
}

TreeItem *TreeModel::topLevelItem(int index)
{
    if (index > m_pRootItem->childCount()-1)
    {
        return nullptr;
    }

    return m_pRootItem->child(index);
}

void TreeModel::deleteToplevelItems(TreeItem *pTopItem)
{
    if (pTopItem == nullptr)
    {
        return;
    }
    int irow = pTopItem->inParentRow(); //pTopItem所在父的第几行
    removeRow(irow, QModelIndex()); //会回调removeRows()!第二个参数默认根Item为空,后面会在indexToItem自动转成根
}

int TreeModel::columnCount(const QModelIndex & /* parent */) const
{
    return m_pRootItem->columnCount();
}

QVariant TreeModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    TreeItem *item = indexToItem(index);

    //LOG_INFO_WIN(QString("%1, row:%2, col:%3,dataptr:%4").arg(CToolUtile::GetCurrentTimeSec()).arg(index.row()).arg(index.column()).arg(*(int*)index.internalPointer()));

    if (role == Qt::EditRole)
    {
        //TODO
    }
    else if (role == Qt::DisplayRole)
    {
        //根据注入的规则进行显示
        QString strDisplay = m_funcDisplayRule(item, index.column());
        return strDisplay;
    }
    else if (role == Qt::TextAlignmentRole)
    {
        return QVariant(Qt::AlignCenter); //居中对齐
    }
    else if (role == Qt::BackgroundColorRole)
    {
        QColor color = m_funcBackgroundColorRule(item, index.column());
        if (!color.isValid())
        {
            return QVariant();
        }
        else
        {
            return color;
        }
    }

    return QVariant();
}

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return 0;

    //return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
    return QAbstractItemModel::flags(index); //不可编辑
}

TreeItem *TreeModel::indexToItem(const QModelIndex &index) const
{
    if (index.isValid()) {
        TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
        if (item)
            return item;
    }
    return m_pRootItem;
}

//局部更新中,需要输入行列,这里列已经知道,但是行是多少呢?难道是Item所在整个表格的行?
//答案是非也!经过调试发现,本信号调用完毕后,会激发data()调用去获取pItem的数据,和行的关系不大,所以到底该设置什么数值?
//查看源码(QTreeModel中的settext接口),它这里的行设置的是所在父项子列表中的第几个,照着抄就完了。
void TreeModel::emitDataChanged(TreeItem *pItem, int iCol)
{
    QModelIndex index = itemToIndex(pItem, iCol);
    //LOG_INFO_WIN(QString("%1, row:%2, col:%3,dataptr:%4").arg("emitDataChanged").arg(index.row()).arg(index.column()).arg(*(int*)index.internalPointer()));
    emit dataChanged(index, index, {Qt::DisplayRole});
}

//返回指定TreeItem的index,其实返回的是所在父的行以及指定列创建的index
QModelIndex TreeModel::itemToIndex(TreeItem *item, int iCol) const
{
    //executePendingSort();

    if (!item || (item == m_pRootItem))
    {
        return QModelIndex();
    }
    int row = item->inParentRow();
    return createIndex(row, iCol, item);
}

QVariant TreeModel::headerData(int section, Qt::Orientation orientation,
                               int role) const
{
    if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
        return m_pRootItem->data(section);

    return QVariant();
}

QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if (parent.isValid() && parent.column() != 0)
        return QModelIndex();

    TreeItem *parentItem = indexToItem(parent);

    TreeItem *childItem = parentItem->child(row);
    if (childItem)
        return createIndex(row, column, childItem);
    else
        return QModelIndex();
}

bool TreeModel::insertColumns(int position, int columns, const QModelIndex &parent)
{
    bool success;

    beginInsertColumns(parent, position, position + columns - 1);
    success = m_pRootItem->insertColumns(position, columns);
    endInsertColumns();

    return success;
}

bool TreeModel::insertRows(int position, int rows, const QModelIndex &parent)
{
    TreeItem *parentItem = indexToItem(parent);
    bool success;

    beginInsertRows(parent, position, position + rows - 1);
    success = parentItem->insertChildren(position, rows, m_pRootItem->columnCount(), this);
    endInsertRows();

    return success;
}

//当前index在父中所处的行构成的父index
QModelIndex TreeModel::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return QModelIndex();

    TreeItem *childItem = indexToItem(index);
    if (childItem == m_pRootItem)
    {
        return QModelIndex();
    }

    TreeItem *parentItem = childItem->parent();

    return itemToIndex(parentItem, 0);
}

bool TreeModel::removeColumns(int position, int columns, const QModelIndex &parent)
{
    bool success;

    beginRemoveColumns(parent, position, position + columns - 1);
    success = m_pRootItem->removeColumns(position, columns);
    endRemoveColumns();

    if (m_pRootItem->columnCount() == 0)
        removeRows(0, rowCount());

    return success;
}

bool TreeModel::removeRows(int position, int rows, const QModelIndex &parent)
{
    TreeItem *parentItem = indexToItem(parent);
    bool success = true;

    beginRemoveRows(parent, position, position + rows - 1);
    success = parentItem->removeChildren(position, rows);
    endRemoveRows();

    return success;
}

int TreeModel::rowCount(const QModelIndex &parent) const
{
    TreeItem *parentItem = indexToItem(parent);

    return parentItem->childCount();
}

bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    TreeItem *item = indexToItem(index);
    bool result = false;
    if (role == Qt::EditRole)
    {
        result = item->setData(index.column(), value);
        if (result)
        {
            emit dataChanged(index, index, {role});
        }
    }
    return result;
}

bool TreeModel::setHeaderData(int section, Qt::Orientation orientation,
                              const QVariant &value, int role)
{
    if (role != Qt::EditRole || orientation != Qt::Horizontal)
        return false;

    bool result = m_pRootItem->setData(section, value);

    if (result)
        emit headerDataChanged(orientation, section, section);

    return result;
}

TreeItem.h

#ifndef TREEITEM_H
#define TREEITEM_H

#include <QList>
#include <QVariant>
#include <QVector>

class TreeModel;
class TreeItem
{
public:
    explicit TreeItem(int iColumns, TreeItem *parent = 0);
    ~TreeItem();

    //修改指定列原始数据
    void setColmData(int iColm, QVariant value);
    //返回指定列的原始数值
    const QVariant &getColmData(int iColm);
    //返回所有原始数据
    const QVector<QVariant> &getAllData();
    //返回子项个数
    int childCount() const;
    //返回子项
    TreeItem *child(int index);

private:
    void setModel(TreeModel *pModel);
    int columnCount() const;
    QVariant data(int column) const;
    //从第posion位置开始,添加count个
    bool insertChildren(int position, int count, int columns, TreeModel *pModel);
    bool insertColumns(int position, int columns);
    TreeItem *parent();
    bool removeChildren(int position, int count);
    bool removeColumns(int position, int columns);
    int inParentRow() const; //在父的子序列的第几行中
    bool setData(int column, const QVariant &value);

private:
    //父节点
    TreeItem *parentItem;
    //子节点
    QList<TreeItem*> childItems;
    //各个列的原始数据 - 非显示数据
    QVector<QVariant> itemData;

    TreeModel *m_pModel;
    friend class TreeModel;
};

#endif // TREEITEM_H

TreeItem.cpp

#include "treeitem.h"
#include "treemodel.h"

#include <QStringList>

TreeItem::TreeItem(int iColumns, TreeItem *parent)
{
    parentItem = parent;
    itemData.resize(iColumns);
    m_pModel = nullptr;
}

TreeItem::~TreeItem()
{
    qDeleteAll(childItems);
}

void TreeItem::setColmData(int iColm, QVariant value)
{
    if (iColm > itemData.size()-1)
    {
        return;
    }
    itemData[iColm] = value;

    if (m_pModel == nullptr)
    {
        assert(false);
        return;
    }

    //局部更新
    m_pModel->emitDataChanged(this, iColm);
}

const QVariant &TreeItem::getColmData(int iColm)
{
    if (iColm > itemData.size()-1)
    {
        return QVariant();
    }
    return itemData[iColm];
}

const QVector<QVariant> &TreeItem::getAllData()
{
    return itemData;
}

TreeItem *TreeItem::child(int index)
{
    if (index > childCount()-1)
    {
        return nullptr;
    }
    return childItems.value(index);
}

void TreeItem::setModel(TreeModel *pModel)
{
    m_pModel = pModel;
}

int TreeItem::childCount() const
{
    return childItems.count();
}

int TreeItem::inParentRow() const
{
    if (parentItem)
        return parentItem->childItems.indexOf(const_cast<TreeItem*>(this));

    return 0;
}

int TreeItem::columnCount() const
{
    return itemData.count();
}

QVariant TreeItem::data(int column) const
{
    return itemData.value(column);
}

bool TreeItem::insertChildren(int position, int count, int columns, TreeModel *pModel)
{
    if (position < 0 || position > childItems.size())
        return false;

    for (int row = 0; row < count; ++row) {
        QVector<QVariant> data(columns);
        TreeItem *item = new TreeItem(columns, this);
        item->setModel(pModel);
        childItems.insert(position, item);
    }

    return true;
}

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, QVariant());

    foreach (TreeItem *child, childItems)
        child->insertColumns(position, columns);

    return true;
}

TreeItem *TreeItem::parent()
{
    return parentItem;
}

bool TreeItem::removeChildren(int position, int count)
{
    if (position < 0 || position + count > childItems.size())
        return false;

    for (int row = 0; row < count; ++row)
        delete childItems.takeAt(position);

    return true;
}

bool TreeItem::removeColumns(int position, int columns)
{
    if (position < 0 || position + columns > itemData.size())
        return false;

    for (int column = 0; column < columns; ++column)
        itemData.remove(position);

    foreach (TreeItem *child, childItems)
        child->removeColumns(position, columns);

    return true;
}

bool TreeItem::setData(int column, const QVariant &value)
{
    if (column < 0 || column >= itemData.size())
        return false;

    itemData[column] = value;
    return true;
}

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值