在Qt开发过程中,树控件QTreeView使用的非常频繁。各种批量展示和编辑信息的地方,都用得上该控件。
在使用QTreeView过程中,用到各种常规、不常规的功能,并进行过各种改造。
这里将这些知识和技巧作一个总结。
一、Model/View框架介绍
1.简介
Model/View架构分为三部分:模型、视图和委托。主要目的是将数据的存储与显示分离。
- Model模型:对外提供标准接口存取数据,不关心数据如何显示。
- View视图:自定义数据的显示方式,不关心数据如何组织存储。
- Delegate委托:在视图的基础上可以自定义特殊的显示和编辑效果。
一般只有Model与内存上的原始数据打交道,通过原始数据构造一个Model,然后View从Model取数据进行展示。
两者耦合度低,一般对其中一方进行修改,另一方影响很小,因此把一种QT View控件替换为另一种View控件时,Model的代码基本不需要多少改动即可正常运行。
委托一般在我需要定制一些特殊的显示效果或输入方式时使用,提供了很大的灵活性。
这种分层架构给代码的逻辑性和可维护性带来了很大的提升。
2.常用Model和View
最常用的两种View控件为QTableView和QTreeView。其实,QTreeView一般也能满足QTableView的功能。
最常用的Model类型为QStandardItemModel,能满足大部分开发需要。
如果数据量比较大,对性能和内存要求比较高,可以使用自定义model,下文有讲。
完整的Model类型有下面这些:
- QStringListModel:存储简单的字符串列表
- QStandardItemModel:可以用于树结构的存储,提供了层次数据
- QFileSystemModel:本地系统的文件和目录信息
- QSqlQueryModel、QSqlTableModel、QSqlRelationalTableModel:存取数据库数据
QT提供了多个预定义好的视图类:
- QListView:用于显示列表
- QTableView:用于显示表格
- QTreeView:用于显示层次数据
委托一般继承自QStyledItemDelegate类进行定制开发。
二、QSS风格美化
默认的QTreeView是这样的:
用QSS来改造QTreeView的样式,一般会处理这些项:
- 表头:背景色、文字色、边框、高度
- 控件整体:背景色、文字色、边框
- 元素:背景色、文字色、边框、高度(处理normal、hover、press三态的颜色)
- 分支:颜色或图片(normal、hover、press三态)
示例代码如下:
QHeaderView::section
{
height:25px;
color:white;
background:#505050;
border-left:0px solid gray;
border-right:1px solid gray;
border-top:0px solid gray;
border-bottom:0px solid gray;
}
QTreeView
{
border:none;
background: #404040;
show-decoration-selected: 1; /* 设置整行颜色一致 */
}
QTreeView::item
{
height: 25px;
border: none;
color: white;
background: transparent;
}
QTreeView::item:hover
{
background: #2CAEFF;
}
QTreeView::item:selected
{
background: #1E90FF;
}
QTreeView::branch
{
background: transparent;
}
QTreeView::branch:hover
{
background: transparent;
}
QTreeView::branch:selected
{
background: #1E90FF;
}
QTreeView::branch:closed:has-children
{
image: url(:/QtExample/Resources/fold_normal.png);
}
QTreeView::branch:closed:has-children:hover
{
image: url(:/QtExample/Resources/fold_hover.png);
}
QTreeView::branch:open:has-children
{
image: url(:/QtExample/Resources/unfold_normal.png);
}
QTreeView::branch:open:has-children:hover
{
image: url(:/QtExample/Resources/unfold_hover.png);
}
示例效果图:
三、自定义Delegate
虽然QSS可以定制绝大多数QTreeView的样式,但是对于某些情况,QSS实现起来比较棘手。接下来介绍一些高级的用法和改造技巧,用delegate,即委托,对QTreeView的item进行改造,以实现特殊的输入方式和显示效果。
1.使用委托:定制item输入效果
这里演示一个让某列使用QComboBox进行编辑的效果:
继承QStyledItemDelegate,写一个MyDelegate类,实现里面的以下方法:
- createEditor: 当item激活编辑状态时,显示的内容。这里创建一个QComboBox
- setEditorData:用以初始化createEditor里创建的控件内容。这里直接把当前item的text设置为QComboBox选中项。
- setModelData:应用编辑后,修改model的data。这里把QComboBox的当前选中项文本设置为item的显示文本。
- updateEditorGeometry:更新控件位置状态。
示例代码如下:
MyEditDelegate.h
#include <QStyledItemDelegate>
class MyEditDelagate : public QStyledItemDelegate
{
Q_OBJECT
public:
MyEditDelagate(QObject *parent);
~MyEditDelagate();
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
MyEditDelegate.cpp
#include "MyEditDelagate.h"
#include <QComboBox>
MyEditDelagate::MyEditDelagate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
MyEditDelagate::~MyEditDelagate()
{
}
QWidget *MyEditDelagate::createEditor(QWidget *parent, const QStyleOptionViewItem &/* option */, const QModelIndex & index) const
{
/* 只对第4列采用此方法编辑 */
if (index.column() == 3)
{
QComboBox* box = new QComboBox(parent);
box->addItems({
QString::fromLocal8Bit("测试输入0"),
QString::fromLocal8Bit("测试输入1"),
QString::fromLocal8Bit("测试输入2") });
return box;
}
return NULL;
}
void MyEditDelagate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
QString value = index.model()->data(index, Qt::EditRole).toString();
QComboBox* box = static_cast<QComboBox*>(editor);
box->setCurrentText(value);
}
void MyEditDelagate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QComboBox* box = static_cast<QComboBox*>(editor);
model->setData(index, box->currentText(), Qt::EditRole);
}
void MyEditDelagate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
{
editor->setGeometry(option.rect);
}
在初始化QTreeView时,新建一个MyDelegate,设itemDelegate:
注意:要设置单元格编辑属性为可编辑(setEditTriggers),否则点击单元格不会有反应。
//ui.treeView->setEditTriggers(QTreeView::NoEditTriggers); /* 单元格不能编辑 */
ui.treeView->setSelectionBehavior(QTreeView::SelectRows); /* 一次选中整行 */
ui.treeView->setSelectionMode(QTreeView::SingleSelection); /* 单选,配合上面的整行就是一次选单行 */
//ui.treeView->setAlternatingRowColors(true); /* 每间隔一行颜色不一样,当有qss时该属性无效 */
ui.treeView->setFocusPolicy(Qt::NoFocus); /* 去掉鼠标移到单元格上时的虚线框 */
MyEditDelagate* delegate = new MyEditDelagate(ui.treeView);
ui.treeView->setItemDelegate(delegate);
2.使用委托:定制item显示效果
可能相比于控制输入,定制item显示效果不那么常用,但是有时候通过委托来绘制是比较容易实现的。比如上述QSS中定义的hover高亮效果:
QTreeView::item:hover
{
background: #2CAEFF;
}
上述QSS定义存在如下图所示无法高亮整行的问题:
可以通过在Delegate中(比如上述MyEditDelegate)中实现下面这个paint方法来轻松实现:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
想要保持原来绘制的部分可以先写如下代码:
QStyleOptionViewItem viewOption(option);
initStyleOption(&viewOption, index);
QStyledItemDelegate::paint(painter, viewOption, index);
那么如何hover高亮整行呢。
上述QSS之所以无法高亮整行,是因为item的范围就不是从最左边开始的。因此只能高亮item范围内,item前面多余的空白部分无法填充。
先设置QTreeView的属性indentation为0,让所有的column的x坐标相同。
然后绘制高亮代码如下:
QRect rectBackground = option.rect;
rectBackground.setLeft(0);
if (option.state & QStyle::State_MouseOver)
{
painter->fillRect(rectBackground, QColor("##E5F3FF"));
}
示例效果如下:
上述高亮会覆盖前面paint的部分,可以根据需要调整绘制顺序。
由于属性indentation为0,绘制原先部分时需要根据column作相应右偏移,只需要在
QStyledItemDelegate::paint(painter, viewOption, index);
前对viewOption的rect设置适当右偏移即可。或者也在委托里面定制。
在项目中使用delegate高亮部分关键字,效果如下:
可以把要显示的文本setData到model中,然后在paint的时候分别绘制高亮部分和非高亮部分。
示例代码:
QString strSearchKeywords; /* 需要高亮的关键字 */
QString strProjectName = index.data(OrderRole = Qt::UserRole + 1).toString(); /* 完整的字符串 */
if (strProjectName.contains(strSearchKeywords))
{
QStringList listSplit = strProjectName.split(strSearchKeywords);
QStringList listProjectName;
/* split默认保留分割后的空字符串,使得原字符串以关键字为分界分成多段 */
if (listSplit.size() > 0)
{
for (int i = 0; i < listSplit.size(); i++)
{
/* 第0个若empty,说明是关键字,跳过,因为到第1个时可以加上 */
if (i == 0 && listSplit[i].isEmpty())
{
continue;
}
if (i > 0)
{
/* 关键字作为分界的位置,需要将关键字也加入list以显示 */
listProjectName.append(strSearchKeywords);
}
if (!listSplit[i].isEmpty())
{
listProjectName.append(listSplit[i]);
}
}
}
else
{
listProjectName.append(strSearchKeywords);
}
int iLeftOffset = rectName.left();
for (QString strPart : listProjectName)
{
/* 如果该分段是关键字,则高亮,否则不高亮 */
painter->setPen((strPart == strSearchKeywords) ? COLOR_HIGHLIGHT : COLOR_NORMAL);
/* 根据该分段文字与名称总长的比例,获取获取该分段文字rect宽度 */
QRect rectPart = rectName;
rectPart.setLeft(iLeftOffset);
rectPart.setWidth(QFontMetrics(font).width(strPart));
painter->drawText(rectPart, Qt::AlignLeft | Qt::AlignVCenter | Qt::ElideRight, strPart);
/* 下一个的左侧距离必须加上实际显示出来的文字宽度 */
iLeftOffset += QFontMetrics(font).width(strPart);
}
}
另外,还可以在delegate里面实现以下两个函数控制点击事件和toolTips事件
/* 捕捉点击事件 */
virtual bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index);
/* 捕捉toolTips事件 */
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index);
通过delegate绘制超出treeView可见宽度的内容,会导致treeView不能自动调整横向滚动条,可以重写QTreeView的setExpanded和expandAll来调整
示例图:
超出可见宽度item展开时,显示横向滚动条;
超出可见宽度item隐藏时,隐藏横向滚动条。
代码:
setColumnWidth(0, this->width()); /* 设置item宽度 */
QModelIndex modelIndex = this->indexAt(this->rect().topLeft());
QFont font;
font.setFamily("Microsoft YaHei");
font.setPixelSize(13);
/* 遍历所有可见index(从右上角可见index开始向下找) */
while (modelIndex.isValid())
{
/* item宽度要足够宽,能够完整显示下拉箭头、名称、选中√图标 */
int iItemWidth = QFontMetrics(font).width(QString(modelIndex.data(Qt::UserRole + 1).toString()));
if (iItemWidth > this->columnWidth(0))
{
this->setColumnWidth(0, iItemWidth);
}
modelIndex = this->indexBelow(modelIndex);
}
三、自定义Model
上文已经简单介绍过Qt的Model/View框架,提到了Qt预定义的几个model类型:
- QStringListModel:存储简单的字符串列表
- QStandardItemModel:可以用于树结构的存储,提供了层次数据
- QFileSystemModel:本地系统的文件和目录信息
- QSqlQueryModel、QSqlTableModel、QSqlRelationalTableModel:存取数据库数据
一般情况下满足需求了,不过有时候需要一些定制功能,或者是大量数据下对性能和开销比较注重,觉得自带的model无用功能太多效率比较低,这时候自定义model就比较适合了。
以上述示例程序为例,当行数达到10W的数据量级时,常规QStandardItemModel 在初始化tree的过程比自定义model慢很多,而且所占用的内存开销是自定义model的数倍甚至数十倍。数据量越大内存差距越明显。如果考虑百万、千万级别的数据,常规model相比于自定义model内存也大很多。
以下为自定义model需要实现的一些虚函数,将会被Qt在查询model数据时调用:
- headerData: 获取表头第section列的数据
- data: 核心函数,获取某个索引index的元素的各种数据role决定获取哪种数据,常用有下面几种:
1)DisplayRole(默认):就是界面显示的文本数据
2)TextAlignmentRole:就是元素的文本对齐属性
3)TextColorRole、BackgroundRole:分别指文本颜色、单元格背景色
- flags: 获取index的一些标志,一般不怎么改
- index: Qt向你的model请求一个索引为parent的节点下面的row行column列子节点的元素,在本函数里你需要返回该元素的正确索引
- parent:获取指定元素的父元素
- rowCount: 获取指定元素的子节点个数(下一级行数)
- columnCount: 获取指定元素的列数
下面是对比QStandardItemModel和自定义Model的初始化tree时间和占用内存(不设置model程序本身内存:15.4MB)对比:
Model类型 |
初始化tree时间 |
占用内存 |
QStandardItemModel |
1263ms |
96.9MB |
自定义Model |
75ms |
20MB |
程序示例图:
示例代码:
模拟数据数据结构:
/* 子节点 */
typedef struct _CHILD
{
QString name;
int number1;
int number2;
int number3;
_CHILD()
{
name = "";
number1 = number2 = number3 = 0;
}
}CHILD, *PCHILDITEM;
/* 父节点 */
typedef struct _PARENT
{
QString name;
QVector<CHILD*> childs;
_PARENT()
{
name = "";
}
}PARENT;
模拟数据初始化:
/* 10个父节点,每个父节点有1W个子节点,共10W行记录 */
int nParent = 10;
int nChild = 10000;
for (int i = 0; i < nParent; i++)
{
PARENT* c = new PARENT;
c->name = QString::fromLocal8Bit("父节点%1").arg(i);
for (int j = 0; j < nChild; j++)
{
CHILD* s = new CHILD;
s->name = QString::fromLocal8Bit("子节点%1").arg(j);
s->number1 = 0;
s->number2 = 1;
s->number3 = 2;
c->childs.append(s);
}
mDatas.append(c);
}
模拟数据
QVector<PARENT*> mDatas;
常规QStandardItemModel构造tree代码:
QStandardItemModel* model = new QStandardItemModel(ui.treeView);
model->setHorizontalHeaderLabels(headers);
foreach(PARENT* p, mDatas)
{
/* 一级节点:父节点 */
QStandardItem* itemParent = new QStandardItem(p->name);
model->appendRow(itemParent);
foreach(CHILD* c, p->childs)
{
/* 二级节点:子节点 */
QList<QStandardItem*> items;
QStandardItem* item0 = new QStandardItem(c->name);
QStandardItem* item1 = new QStandardItem(QString::number(c->number1));
QStandardItem* item2 = new QStandardItem(QString::number(c->number2));
QStandardItem* item3 = new QStandardItem(QString::number(c->number3));
items << item0 << item1 << item2 << item3;
itemParent->appendRow(items);
}
}
自定义Model构造tree代码:
MyModel* model = new MyModel(headers, ui.treeView);
MyItem* root = model->root();
foreach(PARENT* p, mDatas)
{
/* 一级节点:父节点 */
MyItem* itemParent = new MyItem(root);
/* 设为一级节点,供显示时判断节点层级来转换数据指针类型 */
itemParent->setLevel(1);
/* 保存PARENT* p为其数据指针,显示时从该PARENT*取内容显示 */
itemParent->setPtr(p);
root->appendChild(itemParent);
foreach(CHILD* c, p->childs)
{
MyItem* itemChild = new MyItem(itemParent);
/* 设为二级节点,供显示时判断节点层级来转换数据指针类型 */
itemChild->setLevel(2);
/* 保存CHILD* c为其数据指针,显示时从CHILD*取内容显示 */
itemChild->setPtr(c);
itemParent->appendChild(itemChild);
}
}
自定义Model MyModel.h代码
#include <QAbstractItemModel>
#include "MyItem.h"
class MyModel : public QAbstractItemModel
{
Q_OBJECT
public:
MyModel(QStringList headers, QObject *parent = 0);
~MyModel();
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role) const override;
Qt::ItemFlags flags(const QModelIndex &index) 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;
public:
MyItem *itemFromIndex(const QModelIndex &index) const;
MyItem *root();
private:
QStringList mHeaders; /* 表头内容 */
MyItem *mRootItem; /* 根节点 */
};
自定义Model MyModel.cpp
#include "MyModel.h"
MyModel::MyModel(QStringList headers, QObject *parent)
: QAbstractItemModel(parent)
{
mHeaders = headers;
mRootItem = new MyItem;
}
MyModel::~MyModel()
{
delete mRootItem;
}
MyItem *MyModel::itemFromIndex(const QModelIndex &index) const
{
if (!index.isValid())
return NULL;
MyItem *item = static_cast<MyItem*>(index.internalPointer());
return item;
}
MyItem *MyModel::root()
{
return mRootItem;
}
QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal)
{
if (role == Qt::DisplayRole)
{
return mHeaders.at(section);
}
}
return QVariant();
}
QVariant MyModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
MyItem *item = static_cast<MyItem*>(index.internalPointer());
if (role == Qt::DisplayRole)
{
return item->data(index.column());
}
return QVariant();
}
Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return 0;
return QAbstractItemModel::flags(index);
}
QModelIndex MyModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
MyItem *parentItem;
if (!parent.isValid())
parentItem = mRootItem;
else
parentItem = static_cast<MyItem*>(parent.internalPointer());
MyItem *childItem = parentItem->child(row);
if (childItem)
return createIndex(row, column, childItem);
else
return QModelIndex();
}
QModelIndex MyModel::parent(const QModelIndex &index) const
{
if (!index.isValid())
return QModelIndex();
MyItem *childItem = static_cast<MyItem*>(index.internalPointer());
MyItem *parentItem = childItem->parentItem();
if (parentItem == mRootItem)
return QModelIndex();
return createIndex(parentItem->row(), 0, parentItem);
}
int MyModel::rowCount(const QModelIndex &parent) const
{
MyItem *parentItem;
if (parent.column() > 0)
return 0;
if (!parent.isValid())
parentItem = mRootItem;
else
parentItem = static_cast<MyItem*>(parent.internalPointer());
return parentItem->childCount();
}
int MyModel::columnCount(const QModelIndex &parent) const
{
return mHeaders.size();
}
MyItem.h
#include <QVariant>
class MyItem
{
public:
explicit MyItem(MyItem *parentItem = 0);
~MyItem();
void appendChild(MyItem *child); //在本节点下增加子节点
void removeChilds(); //清空所有节点
MyItem *child(int row); //获取第row个子节点指针
MyItem *parentItem(); //获取父节点指针
int childCount() const; //子节点计数
int row() const; //获取该节点是父节点的第几个子节点
//核心函数:获取节点第column列的数据
QVariant data(int column) const;
//设置、获取节点是几级节点(就是树的层级)
int level(){ return mLevel; }
void setLevel(int level){ mLevel = level; }
//设置、获取节点存的数据指针
void setPtr(void* p){ mPtr = p; }
void* ptr(){ return mPtr; }
//保存该节点是其父节点的第几个子节点,查询优化所用
void setRow(int row){
mRow = row;
}
private:
QList<MyItem*> mChildItems; //子节点
MyItem *mParentItem; //父节点
int mLevel; //该节点是第几级节点
void* mPtr; //存储数据的指针
int mRow; //记录该item是第几个,可优化查询效率
};
MyItem.cpp
#include "MyItem.h"
#include "defines.h"
MyItem::MyItem(MyItem *parent)
{
mParentItem = parent;
mPtr = NULL;
mLevel = 0;
mRow = 0;
}
MyItem::~MyItem()
{
removeChilds();
}
void MyItem::appendChild(MyItem *item)
{
item->setRow(mChildItems.size()); //item存自己是第几个,可以优化效率
mChildItems.append(item);
}
void MyItem::removeChilds()
{
qDeleteAll(mChildItems);
mChildItems.clear();
}
MyItem *MyItem::child(int row)
{
return mChildItems.value(row);
}
MyItem *MyItem::parentItem()
{
return mParentItem;
}
int MyItem::childCount() const
{
return mChildItems.count();
}
int MyItem::row() const
{
return mRow;
}
QVariant MyItem::data(int column) const
{
if (mLevel == 1)
{
/* 一级节点: 父节点 */
if (column == 0)
{
PARENT* c = (PARENT*)mPtr;
return c->name;
}
}
else if (mLevel == 2)
{
/* 二级节点: 子节点 */
CHILD* s = (CHILD*)mPtr;
switch (column)
{
case 0: return s->name;
case 1: return QString::number(s->number1);
case 2: return QString::number(s->number2);
case 3: return QString::number(s->number3);
default:
return QVariant();
}
}
return QVariant();
}