众众众所大家们周知,QTreeWidget性能差、QTreeView配合QStandardItemModel性能也差、不够灵活等等,需要自定义Model来配合QTreeView使用。那么为何这么多问题Qt官方却不进行改进?本文结合Qt源码,经过一周的深入分析,对如何设计自定义Model提出了自己的一些想法。
QTreeWidget使用起来比较简单,代码就不在这里罗列了。QTreeWidget使用时所依赖的类:
QTreeWidgetItem 没有从任何类继承
QTreeModel 继承自QAbstractItemModel
QTreeWidget 继承自QTreeView
在QTreeWidget的构造中,调用了QTreeView::setModel(new QTreeModel(1, this));
,这样我们在使用时,其实只关心QTreeWidget 和QTreeWidgetItem这两个类即可。那么接下来,分析一下这三个类的成员变量,基本就能看出是如何存储我们的数据,也就知道了为何效率比较低。
只有一个用Q_DECLARE_PRIVATE(QTreeWidget)修饰的p指针,成员都在QTreeWidgetPrivate中,经过查看里面没什么有用的东西。
成员简化如下:
private:
QTreeWidgetItem *rootItem;
QTreeWidgetItem *headerItem; //这两个和根节点有关
private:
Q_DECLARE_PRIVATE(QTreeModel) //p指针
存储了根节点的信息,别的也没啥东西。
关键成员如下:
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;
Qt::BackgroundColorRole
、对齐Qt::TextAlignmentRole
等等,当然,当你设置的时候,才会存储这些信息。但每个Item都存储了一遍,这个占用了大量空间。Qt::UserRole
中,但显示的数据会存储在这个成员中,相当于存了至少两份内容!所有占用空间大很大一部分原因也在这里。这两个问题就是占用空间大,效率低的决定性因素,那如何解决这个问题呢?
干掉这两个成员就完事了!重载QAbstractItemModel的data()函数,用到的时候动态返回,这个后面详细说。
使用时所依赖的类:
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还差劲呢。
通过上面的分析,用QTreeWidget在数据量不是特别大的时候,用着是没什么问题的。QTreeWidget为了兼容各种情况,已经写的很灵活了,但如何更高效呢?答案就是定制化,这样就减少了一部分灵活性。
如何定制化呢?核心就是重写QAbstractItemModel,Qt已经给了我们一个很好的实例,可以自己看一下editabletreemodel这个例子,笔者这里实现的也是在这个例子基础上改的,添加了TreeItem和TreeModel两个类。
看这个例子之前,你得先明白QAbstractItemModel的几个关键原理,否则也是一脸懵逼。
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)
{
}
}
答案是在TreeItem中只定义原始数据,显示的内容在data()接口中动态计算填充。
比如笔者这里的成员函数的定义:
private:
//父节点
TreeItem *parentItem;
//子节点
QList<TreeItem*> childItems;
//各个列的原始数据 - 非显示数据
QVector<QVariant> itemData;
这里用一个vector存储了每一列的原始数据,如果你想更进一步减少内存,可以用void*指向你本来的原始数据即可,不过这样在更新数据时可能没那么方便。
这个问题折磨了一段时间,来看看创建它的接口:
inline QModelIndex QAbstractItemModel::createIndex(int arow, int acolumn, void *adata) const
{ return QModelIndex(arow, acolumn, adata, this); }
接口很简单,输入行列和要绑定的数据即可,adata我们这里传入对应的TreeItem指针。
但是这里的行有坑,这里高亮一下:
arow行表示TreeItem所在父节点的第几行!
列就是正常的列,无法计算的时候就赋值0。
在笔者的代码中,重写了两个接口来对Item和Index进行互相转换。
在这个例子中,只需要向模型中更新我们的数据即可,但你会发现插入不会立即刷新,你得切换下页面才刷新,这是因为你得调用beginInsertRows和endInsertRows接口来激发刷新的信号。
不同的树可能显示的display不同,难道每个树都得写个TreeModel?
笔者通过回调函数的方式,通过registerDisplayRule、registerBackgroundColorRule等接口,将规则lambda注入到TreeModel中,这样就实现了用同一套代码,实现不同类的方式。
编辑应该是可以的,但排序、筛选没有添加支持。
TreeModel.h
#ifndef TREEMODEL_H
#define TREEMODEL_H
#include
#include
#include
#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
#include
#include
#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
#include
#include
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
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;
}