本文主要是介绍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));
,这样我们在使用时,其实只关心QTreeWidget 和QTreeWidgetItem这两个类即可。那么接下来,分析一下这三个类的成员变量,基本就能看出是如何存储我们的数据,也就知道了为何效率比较低。
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; //父ItemQList<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){//xxxreturn 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 插入数据页面不刷新?
在这个例子中,只需要向模型中更新我们的数据即可,但你会发现插入不会立即刷新,你得切换下页面才刷新,这是因为你得调用beginInsertRows和endInsertRows接口来激发刷新的信号。
3.5 如何写一个更通用的类
不同的树可能显示的display不同,难道每个树都得写个TreeModel?
笔者通过回调函数的方式,通过registerDisplayRule、registerBackgroundColorRule等接口,将规则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_OBJECTpublic:TreeModel(const QStringList &headers, QObject *parent = 0);~TreeModel();//清空所有数据void clear();//注册显示规则 - 函数对象第二个参数是列下标,返回值是显示的字符串QStringvoid 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转itemTreeItem *indexToItem(const QModelIndex &index) const;//指定item的indexQModelIndex 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())) //会回调insertrowsreturn 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);elsereturn 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;
}
这篇关于Qt 树形控件 QTreeView QTreeWidget深入剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!