Model/View模块中Delegate的扩展:持久Delegate(一)

注:以下内容需要熟悉Qt的Model/View模块,如尚未掌握该模块的基础知识,不建议阅读。

 

在处理大规模,结构化数据的时候,Qt的Model/View非常好用,但其界面形式较为单一,不太符合日益增长的审美需求。有时候,Model/View在处理数据时,虽然可以完成相应的功能,但界面处理上总好像差点意思。

在Model/View中,Delegate负责数据项item的显示和交互,我们可以继承相关的Delegate类,来提供自定义的显示和交互。然而,在实际上,一般而言能实现的总是相对简单的显示和交互。比如让Table中的某一列显示按钮,复选框等,就难倒了不少人,在网络上搜索自定义Delegate时,似乎最复杂的实现也就是显示一个复选框和按钮,而一些更为复杂的则实现不了(起码笔者没有见过更复杂的,如下图的Delegate)。

我们考察下面的图:

复杂重复项界面

 可以看到,它的三个项是重复的,底层的数据结构是相同的,如果从Model/View的角度分析,数据和显示分离,这样的界面我们应该也是可以实现的。然而现有的Delegate不足以实现这样的界面,这是因为它有一定的缺陷:

  • Delegate中,数据项的显示是由paint函数实现的,它在内部使用QPainter来完成绘制,只能绘制图像,不能绘制控件。
  • 在paint中,虽然可以使用通过使用底层的QStyle接口来完成控件的绘制,但由于其复杂性,我们一般只能完成简单控件的绘制,如QCheckbox,QProgressBar等,不能绘制如上图一样的复杂Widget。
  • Delegate的编辑器可以是任意Widget,但它只在item编辑时显示,并不能持久显示,且一般而言,一个View上最多只会有一个Delegate的编辑器显示。
  • 方便类Widget中,有setItemWidget或者setCellWidget,可以为一个item设置一个窗口,但这种方法需要我们手动将数据传递给窗口,非常麻烦,最重要的是:它不能通过设置的Widget和底层的数据交互,仅适合静态展示数据。且View中并没有类似的接口,因此这种方法灵活性上也很差。
  • 方便类Widget中,有openPersistentEditor函数,这个函数可以使item处于编辑状态,且永久性地处于编辑状态,也就是说编辑窗口会一直存在,一直到我们调用对应的closePersistentEditor为止。但当Model底层有增删的情况发生时,我们也必须相在相对应的item上去open或者close操作,这无疑增大了代码开发的难度。且,同上一条一样,在View中同样没有这个接口。

因此,这次扩展的目标就是:实现一个可支持任意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 <QStyledItemDelegate>
#include <QWidget>
#include <QMap>
#include <QPersistentModelIndex>

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<QPersistentModelIndex,QWidget *> m_iWidgets;
};

#endif // QPERSISTENTSTYLEDITEMDELEGATE_H
#include "QPersistentStyledItemDelegate.h"
#include <QDebug>
#include <QPainter>
#include <QItemSelection>

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<QAbstractItemModel *>(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<int>)),this,SLOT(updateWidget(QModelIndex,QModelIndex)));

    if(!m_iWidgets.contains(perIndex))
    {
        QWidget *parentWidget = static_cast<QWidget *>(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<QWidget *>(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<QModelIndex>(perIndex);
    if(!index.isValid())
        return;

    QAbstractItemModel *model = const_cast<QAbstractItemModel *>(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 <QSpinBox>
#include <QDebug>
#include <QPushButton>
#include <QPainter>
#include <QHBoxLayout>

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<QModelIndex>(perIndex);
        //const成员里,不能修改对象,因此不能emit信号
        auto temp = const_cast<CPushButtonItemDelegate *>(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读写数据的持久代理示例。

后言:工作以来,在学习方面一直比较懈怠,因此想着要不要写点什么东西鞭策一下自己。因为既然要写博客,必然对自己和读者都要负责,我也不想千篇一律写一些网络上很容易找到的东西。因此就有了这篇博客,这也是我第一篇博客,希望对诸君有所帮助。

H&A
发布了7 篇原创文章 · 获赞 4 · 访问量 839

猜你喜欢

转载自blog.csdn.net/qq_34305316/article/details/90298426