注:以下内容需要熟悉Qt的Model/View模块,如尚未掌握该模块的基础知识,不建议阅读。
在处理大规模,结构化数据的时候,Qt的Model/View非常好用,但其界面形式较为单一,不太符合日益增长的审美需求。有时候,Model/View在处理数据时,虽然可以完成相应的功能,但界面处理上总好像差点意思。
在Model/View中,Delegate负责数据项item的显示和交互,我们可以继承相关的Delegate类,来提供自定义的显示和交互。然而,在实际上,一般而言能实现的总是相对简单的显示和交互。比如让Table中的某一列显示按钮,复选框等,就难倒了不少人,在网络上搜索自定义Delegate时,似乎最复杂的实现也就是显示一个复选框和按钮,而一些更为复杂的则实现不了(起码笔者没有见过更复杂的,如下图的Delegate)。
我们考察下面的图:
可以看到,它的三个项是重复的,底层的数据结构是相同的,如果从Model/View的角度分析,数据和显示分离,这样的界面我们应该也是可以实现的。然而现有的Delegate不足以实现这样的界面,这是因为它有一定的缺陷:
因此,这次扩展的目标就是:实现一个可支持任意Widget且持久存在的Delegate。
它不仅可随着model的增删而自动增删,也要可以在model底层数据变化时立即更新其数据,而且可以充当编辑器的功能,向model中写入数据。
总而言之,就是在Qt支持的原有全部功能下,再加上两条:任意Widget代理和持久显示。
当然,由于笔者水平有限,如有错误,请留言指出,共同进步。
我抽象了一个类QPersistentStyledItemDelegate,持久代理类,它继承自QStyledItemDelegate,做了更进一步的功能性扩展。
思路:View负责为item分配一个自己窗口上的Rect,item就绘制在这个区域,编辑器事实上也是显示在这个区域的一个字Widget而已。既然如此,理论上,如果我们可以为每个item手动分配一个widget,且全部显示,不让它关闭,并负责底层model,widget相关联的多种多样的维护工作,那么是不是就可以实现一个持久代理?
回答是:可以。
好,废话不多说,上代码。代码中有详细注释,如有兴趣,可查看实现的原理,如没有,可以直接查看后续的例子。
#ifndef QPERSISTENTSTYLEDITEMDELEGATE_H
#define QPERSISTENTSTYLEDITEMDELEGATE_H
/* 持久代理类 by pp.xue
*
* 这是该需求的简易实现,使用时需注意:
* 1、View需要将EditTrigger设置为QAbstractItemView::NoEditTriggers,关闭默认的Delegate逻辑
* 2、继承这个类,实现自定义的 持久代理
*
* 继承时:不能也不必重写paint、updateEditorGeometry函数:
* 前者是因为paint本应该做的绘制工作已没有意义(被编辑器覆盖,绘制了也看不到),
* 且在本类中用paint实现了根本逻辑,不能被覆盖。
* 后者是因为本类的paint中会一直重设编辑器的尺寸
* 其他函数可随需要重写,不做要求。
*
* 3、如果需要将数据从编辑器保存到model中,那么需要连接信号槽 到本类的updateModelData,它会调用子类的
*
* void QStyledItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
*
* 因此,此时我们也需要实现这个函数。这也是最重要的额外步骤。
*/
#include
#include
#include
#include
class QPersistentStyledItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
QPersistentStyledItemDelegate(QObject *parent = Q_NULLPTR);
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const Q_DECL_OVERRIDE final;
public slots:
void updateModelData();
private slots:
void clearWidget();
void updateWidget(QModelIndex,QModelIndex);
private:
mutable QMap m_iWidgets;
};
#endif // QPERSISTENTSTYLEDITEMDELEGATE_H
#include "QPersistentStyledItemDelegate.h"
#include
#include
#include
QPersistentStyledItemDelegate::QPersistentStyledItemDelegate(QObject *parent)
:QStyledItemDelegate(parent)
{
}
/*
* 本类的本质是为每个item(对应QPersistentModelIndex)保存一个widget,且维护Model,view与之相关的关系。
*
*/
void QPersistentStyledItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QPersistentModelIndex perIndex(index);
QAbstractItemModel *model = const_cast(index.model());
//如果model有删除、重置,则删除对应的widget
//如果model有新增,view会调用paint绘制,因此不需做额外工作
connect(model,SIGNAL(rowsRemoved(QModelIndex,int,int)),
this,SLOT(clearWidget()),Qt::UniqueConnection);
connect(model,SIGNAL(columnsRemoved(QModelIndex,int,int)),
this,SLOT(clearWidget()),Qt::UniqueConnection);
connect(model,SIGNAL(destroyed(QObject*)),
this,SLOT(clearWidget()),Qt::UniqueConnection);
connect(model,SIGNAL(modelReset()),this,SLOT(clearWidget()),Qt::UniqueConnection);
//如果model有数据变化,更新变化到编辑器
connect(model,SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)),this,SLOT(updateWidget(QModelIndex,QModelIndex)));
if(!m_iWidgets.contains(perIndex))
{
QWidget *parentWidget = static_cast(painter->device());
if(nullptr == parentWidget)
return;
QWidget *tempWidget = this->createEditor(parentWidget,option,index);
this->setEditorData(tempWidget,index);
tempWidget->setGeometry(option.rect);
tempWidget->setVisible(true);
m_iWidgets.insert(perIndex,tempWidget);
}
else
{
QWidget *tempWidget = m_iWidgets.value(perIndex);
if(tempWidget)
{
tempWidget->setGeometry(option.rect);
}
}
}
//如果子类的编辑器需要将数据回写到model,则需要连接到这个槽
//它的主要作用是:调用了子类重写的setModelData
void QPersistentStyledItemDelegate::updateModelData()
{
QObject *sender = this->sender();
if(nullptr == sender)
return;
QWidget *editor = static_cast(sender);
if(nullptr == editor)
return;
if(!m_iWidgets.values().contains(editor))
return;
QPersistentModelIndex perIndex = m_iWidgets.key(editor);
if(!perIndex.isValid())
return;
QModelIndex index = static_cast(perIndex);
if(!index.isValid())
return;
QAbstractItemModel *model = const_cast(index.model());
this->setModelData(editor,model,index);
emit model->dataChanged(index,index);
}
//清理已无用的代理Widget
void QPersistentStyledItemDelegate::clearWidget()
{
auto i = m_iWidgets.begin();
while (i != m_iWidgets.end()) {
if(!i.key().isValid())
{
i.value()->setParent(nullptr);
i.value()->deleteLater();
i = m_iWidgets.erase(i);
}
else
{
++i;
}
}
}
//更新数据到delegate
void QPersistentStyledItemDelegate::updateWidget(QModelIndex begin, QModelIndex end)
{
QItemSelection selection(begin,end);
QModelIndexList list = selection.indexes();
foreach (QModelIndex index, list) {
QPersistentModelIndex perIndex(index);
if(m_iWidgets.contains(perIndex))
{
QWidget *tempWidget = m_iWidgets.value(perIndex);
if(tempWidget)
{
this->setEditorData(tempWidget,index);
}
}
}
}
这就是我这个类的实现。使用的时候,需要从这个类继承,然后重写即可。大家可以发现,因为我这个类也是继承自Qt中Model/View中Delegate,因此自定义的时候,接口和Qt之前是一样的,并没有引入很多额外的东西。
当然,还是有一些东西需要大家注意,才能实现想要的效果。具体在头文件中有注释,请查看,而且在后续我会给出两个个自定义的示例,可以参照。
好,本篇文章中,我会给出一个简单的PushButton自定义实现,任意Widget的自定义会放在下一篇文章中。
上代码:
#ifndef CPUSHBUTTONITEMDELEGATE_H
#define CPUSHBUTTONITEMDELEGATE_H
#include "QPersistentStyledItemDelegate.h"
class QPushButton;
class CPushButtonItemDelegate :public QPersistentStyledItemDelegate
{
Q_OBJECT
public:
CPushButtonItemDelegate(QString text,QObject *parent = Q_NULLPTR);
//因为按钮既不需要从model读取数据,也不需要写入,因此仅需要重写一个函数即可
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;
signals:
void BtnClicked(QModelIndex index);
private:
QString m_strText;
};
#endif // CPUSHBUTTONITEMDELEGATE_H
#include "PushButtonItemDelegate.h"
#include
#include
#include
#include
#include
CPushButtonItemDelegate::CPushButtonItemDelegate(QString text,QObject *parent)
:QPersistentStyledItemDelegate(parent)
{
m_strText = text;
}
QWidget *CPushButtonItemDelegate::createEditor(QWidget *parent,const QStyleOptionViewItem &/* option */,const QModelIndex &index) const
{
QPersistentModelIndex perIndex(index);
QWidget *widget = new QWidget(parent);
widget->setAutoFillBackground(true);
QHBoxLayout *layout = new QHBoxLayout;
layout->setMargin(2);
layout->addStretch();
QPushButton *btn = new QPushButton(m_strText);
btn->setMinimumHeight(24);
layout->addWidget(btn);
layout->addStretch();
widget->setLayout(layout);
QObject::connect(btn,&QPushButton::clicked,[=]{
QModelIndex tIndex = static_cast(perIndex);
//const成员里,不能修改对象,因此不能emit信号
auto temp = const_cast(this);
emit temp->BtnClicked(tIndex);
});
return widget;
}
大家可以看到,我这个例子中的代码,和继承QStyledItemDelegate几乎没有任何区别。
代码中,唯一复杂的就是为QPushButton的点击信号转发为新的信号,这样我们就可以在View中去写槽函数,这样这个类就更加通用了。
我们既然有了PushButton的示例,那么其他的简单控件代理仅需在上述代码中稍作修改即可得到,非常的方便。
好,下面我们看一下这个代理的效果。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QTableView *table = new QTableView;
QStandardItemModel *model = new QStandardItemModel(10, 3 );
for( int r=0; r<10; ++r )
{
QStandardItem *item = new QStandardItem( QString("Row %1").arg(r+1) );
model->setItem( r, 0, item );
model->setItem( r, 1, new QStandardItem( QString::number((r*30)%100 )) );
}
table->setModel(model ); // 正常设置模型,没有任何特殊之处
CPushButtonItemDelegate *delegate = new CPushButtonItemDelegate(tr("按钮"));
connect(delegate,SIGNAL(BtnClicked(QModelIndex)),
this,SLOT(OnDelegatePushButtonClicked(QModelIndex)));
table->setItemDelegateForColumn(2,delegate);
table->show();
}
void MainWindow::OnDelegatePushButtonClicked(QModelIndex index)
{
if(!index.isValid())
return;
QMessageBox msgBox;
msgBox.setText(QString("click happen in row:%1 col:%2").arg(index.row()).arg(index.column()));
msgBox.exec();
}
结果:
可以看到,按钮代理的窗口会持久显示,且点击按钮时,在槽函数中可以获取点击的QModelIndex,通过它我们就可以从model中获取数据,方便后续的操作。
至于按钮的美化,那是被返回的作为编辑器的Widget本身的工作,也就是为Widget做美化工作,和普通的美化工作完全一样。
好,本篇文章到此就结束了,下一篇会给出一个任意Widget,且可以从Model读写数据的持久代理示例。
后言:工作以来,在学习方面一直比较懈怠,因此想着要不要写点什么东西鞭策一下自己。因为既然要写博客,必然对自己和读者都要负责,我也不想千篇一律写一些网络上很容易找到的东西。因此就有了这篇博客,这也是我第一篇博客,希望对诸君有所帮助。