目录

写在前面

一、效果预览

二、Item图形绘制代理

2.1 QStyledItemDelegate介绍

2.2 创建自定义Delegate

2.3 重写paint接口

2.4 Item自适应大小(重写sizeHint)

2.5 编辑框自适应(重写updateEditorGeometry)

2.6 编辑框内容同步(重写setModelData和setEditorData)

三、Item事件响应

参考文章


写在前面

在做项目的过程中遇到一个需求:要实现一个美观的List,具体让我自己把握。。。

要实现预期的效果,必须要重绘QListItem,而网上相关资料很少,又大多不符合预期效果,无奈只能手撸。

要实现自定义的List有两种方法,即修改QListView和QListWidget的Item,本文采用第一种方式。第二种方式可移步博主Adamearth文章:

Qt之实现好友列表_Adamearth的博客-CSDN博客_qt 列表https://blog.csdn.net/u010519432/article/details/26988515后续也会出一篇为Item设置Widget的文章,各位看官自行选择。

编写了一个初步的demo,大致效果如下。

一、效果预览

预期的效果如下图:

初期的demo效果图如下:

 ListItem在鼠标悬停、单击和无操作状态下呈现不同背景颜色。单击Item按钮可发射对应信号,在应用时编写对应槽函数并捕捉此信号即可实现响应事件。

二、Item图形绘制代理

应用QListView控件时,可通过更改给QListView设置的QStandardItemModel完成列表的维护,十分方便。QStandardItemModel的维护则通过更改其设置的QStandardItem完成。本文通过为QStandardItem设置新的QStyledItemDelegate实现自定义的Item。

2.1 QStyledItemDelegate介绍

QStyledItemDelegate类为模型中的数据项提供显示和编辑功能,各个项目由代理绘制,在创建数据项时默认安装在它们上面。通俗的说它就是为你的控件提供渲染服务的UI绘制代理。

QStyledItemDelegate继承自QAbstractItemDelegate,同样继承自QAbstractItemDelegate基类的还有QItemDelegate,而QStyledItemDelegate是默认的Delegate。不同于QItemDelegate,QStyledItemDelegate在绘制Item时,会沿用你所设置的样式。因此,我们要自己重新创建Delegate最好是继承QStyledItemDelegate。

QStyledItemDelegate在绘制数据项时会调用paint进行渲染。QStyledItemDelegate 中 paint 函数原型如下。

//QStyledItemDelegate 中 paint 函数原型
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const

其中的三个参数功能如下表:

参数描述
painter数据项的绘制全部由它完成
option数据项在视图小部件中绘制的各项参数
index数据项的数据模型

painter就不多介绍了,重点来看另外两个参数。

option中包含了许多信息如坐标、状态、图标... 较为重要的(我们要用的)就是坐标和状态,其中坐标信息包含在rect中,状态信息包含在state中。

index用于定位数据模型中的数据项,也就是Item。换句话说,它拥有一个友元类QAbstractItemModel,所以我们可以通过它来读取Item的数据。

Item的数据存储在QVariant类型的对象中。

因此可以通过data和setData接口读取Item数据和设置Item数据。

很重要的一点是,QVariant是数据类型的集合,也就意味着我们可以为Item定义自己的数据结构。

如果想要完成一些其它功能,除paint以外,还需要重写一些其它接口:

接口名称默认功能
sizeHint返回Item窗口大小,重写以单独设置Item大小
createEditor双击Item时创建一个编辑框,返回编辑框的QWidget指针
updateEditorGeometry顾名思义,在编辑框显示时会调用它为编辑框设置大小和位置
setEditorData为编辑框设置内容
setModelData完成编辑后,将编辑内容同步给数据模型

这些接口可以重写,以完成除默认功能外的其它自定义功能。

2.2 创建自定义Delegate

首先自定义一个我们需要的数据结构并完成注册,可根据自己需求定义,重要的是要将自定义的数据结构注册,否则不可设置为Item的数据。Qt提供了注册自定义数据的宏。

Q_DECLARE_METATYPE(WItemData)    //自定义结构体WItemData

然后创建自己的 Delegate并继承QStyledItemDelegate,声明要重写的接口。。。

#ifndef WITEMDELEGATE_H
#define WITEMDELEGATE_H
#include <QStyledItemDelegate>
#include <QPainter>
#include <QWidget>
#include <QItemDelegate>
#include "WGlobal.h"
class WItemDelegate : public QStyledItemDelegate
{
    Q_OBJECT

public:
    WItemDelegate(QListView *parent = nullptr);


    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;

    QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
    void setEditorData(QWidget *editor, const QModelIndex &index) const override;
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;

    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;

    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;

    //设置 Item 矩形区域圆角
    void setBorderRadius(int borderRadius);

    //设置按钮大小
    void setButWidth(int but_Width);
public slots:
    void on_but_clicked(QPoint mousePoint, const QModelIndex &index);

private:
    int border_radius;
    int butWidth;
signals:
    void itemButClicked(int,int) const;    //按钮点击信号
};

#endif // WITEMDELEGATE_H

2.3 重写paint接口

paint是完成自定义必须要重写的一个接口,话不多说上代码:

void WListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    if (index.isValid()) {
        painter->save();
        painter->setRenderHints(QPainter::Antialiasing);
        painter->setFont(QFont("Microsoft YaHei", 10));
        int rowLineNum,rowHeight;
        QVariant var = index.data(Qt::UserRole+1);
        WItemData itemData = var.value<WItemData>();

        // Item 矩形区域
        QRectF rect = toLocalRect(option.rect);
        //绘制Item阴影
        painter->setPen(QPen(QColor("#c7c7c7")));
        painter->setBrush(QColor("#c7c7c7"));
        painter->drawRoundedRect(QRectF(rect.x()+4,rect.y()+4,rect.width(),rect.height()),border_radius,border_radius);
        // 鼠标悬停或者选中时改变背景色
        if (option.state.testFlag(QStyle::State_MouseOver)) {
            painter->setPen(QPen(QColor("#c4f0ff")));
            painter->setBrush(QColor("#c4f0ff"));
            painter->drawRoundedRect(rect,border_radius,border_radius);
        }else if (option.state.testFlag(QStyle::State_Selected)) {
            painter->setPen(QPen(QColor("#e4f0ff")));
            painter->setBrush(QColor("#e4f0ff"));
            painter->drawRoundedRect(rect,border_radius,border_radius);
        }else {
            painter->setPen(QPen(QColor("#BCF2F5")));
            painter->setBrush(QColor("#BCF2F5"));
            painter->drawRoundedRect(rect,border_radius,border_radius);
        }
        QPainterPath rowPath;
        //判断选项卡是否折叠
        if(!itemData.isFold){
            //计算行高和行数
            rowLineNum = itemData.row_Name.length() + 1;
            rowHeight = rect.height()/(itemData.row_Name.length() + 3);
            for(int i = 1;i <= rowLineNum;i++){
                rowPath.moveTo(QPointF(rect.x(),rect.y() + rowHeight * i));
                rowPath.lineTo(QPointF(rect.width() + 5,rect.y() + rowHeight * i));
            }
            //绘制item
        }else {
            rowLineNum = 1;
            rowHeight = rect.height()/3;
            rowPath.moveTo(QPointF(rect.x(),rect.y() + rowHeight));
            rowPath.lineTo(QPointF(rect.width() + 5,rect.y() + rowHeight));
        }
        // 鼠标悬停或者选中时改变背景色
        if (option.state.testFlag(QStyle::State_MouseOver)) {
            painter->setPen(QPen(QColor("#000000")));
        }else if (option.state.testFlag(QStyle::State_Selected)) {
            painter->setPen(QPen(QColor("#000000")));
        }else {
            painter->setPen(QPen(QColor("#000000")));
        }
        painter->drawPath(rowPath);        //绘制行线
        for(int i = 1;i < rowLineNum;i++){

            QRectF textRect(rect.x(),rect.y() + rowHeight * i,rect.width(),rowHeight);
            painter->drawText(textRect, Qt::AlignVCenter
                             ,itemData.row_Name.value(i - 1));
            QRectF dataRect(textRect.width()/ 3,textRect.y(),textRect.width(),textRect.height());
            painter->drawText(dataRect, Qt::AlignVCenter
                             ,itemData.row_Data.value(i - 1));
        }
        //绘制标题
        painter->drawText(QRectF(rect.x(),rect.y(),rect.width(),rowHeight), Qt::AlignCenter ,itemData.itemName);
        painter->setFont(QFont("Microsoft YaHei", 11));
        painter->drawText(QRectF(rect.x(),rect.y() + rowHeight * rowLineNum,rect.width(),rowHeight*1.2)
                          , Qt::AlignVCenter ,itemData.indexName);
        painter->setFont(QFont("Microsoft YaHei", 9));
        painter->drawText(QRectF(rect.x(),rect.y() + rowHeight * rowLineNum + rowHeight*1.2,rect.width(),rowHeight*0.8)
                          , Qt::AlignVCenter ,itemData.date);
        //绘制按钮
        int butHeight = (rowHeight * 2 - 15)/2;
        for(int i = 0;i < itemData.butNames.size();i++){
            //绘制按钮阴影
            QRectF butShadowRect(rect.width() - butWidth - 8
                                 ,rect.y() + rowHeight * rowLineNum +(butHeight + 5)*i + 7
                                 ,50
                                 ,butHeight);
            painter->setPen(QPen(QColor("#c7c7c7")));
            painter->setBrush(QColor("#c7c7c7"));
            painter->drawRoundedRect(butShadowRect,border_radius,border_radius);


            //绘制按钮
            QRectF butRect(rect.width() - butWidth - 10
                           ,rect.y() + rowHeight * rowLineNum +(butHeight + 5)*i + 5
                           ,50
                           ,butHeight);
            //根据按钮选中状态绘制按钮
            if(itemData.butSelected[i]){
                painter->setPen(QPen(QColor("#8f9ae6")));
                painter->setBrush(QColor("#8f9ae6"));
            }else{
                painter->setPen(QPen(QColor("#FFFFFF")));
                painter->setBrush(QColor("#FFFFFF"));
            }
            painter->drawRoundedRect(butRect,border_radius,border_radius);

            //添加按钮文字
            painter->setPen(QPen(QColor("#000000")));
            painter->drawText(butRect,Qt::AlignCenter,itemData.butNames.value(i));
        }
        painter->restore();
    }
}

需要注意的是,在取得index中数据时要保持UserRole和设置数据时的一致,即下列代码中WUserRole要保持一致:

wItem->setData(QVariant::fromValue(itemData), WUserRole);    //WUserRole = Qt::UserRole + 1

QVariant var = index.data(WUserRole);        //WUserRole = Qt::UserRole + 1
WItemData itemData = var.value<WItemData>();

实现效果如下:

2.4 Item自适应大小(重写sizeHint)

因为要完成Item自适应大小,所以要重写sizeHint,没有相关需求可不重写。代码如下:

QSize WListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
//    qDebug() << "sizeHint";
    Q_UNUSED(index)
    QVariant var = index.data(Qt::UserRole+1);
    WItemData itemData = var.value<WItemData>();
    if(itemData.isFold){
        return QSize(option.rect.width(),3 * 40);
    }else {
        return QSize(option.rect.width(), (itemData.row_Name.length() + 3) * 40);
    }
}

实现效果如下:

2.5 编辑框自适应(重写updateEditorGeometry)

因为本文所绘制Item具有多行内容,因此要捕捉鼠标位置来自动调整编辑框位置,并记录下所编辑行号。

updateEditorGeometry原型如下:

void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option
            , const QModelIndex &index) const override;

updateEditorGeometry并不能直接修改类成员,也就是说无法保存最近编辑行,本文采用解决办法是将editor->setWhatsThis设置为所编辑行号。各位看官如有更好解决方式欢迎评论(抱拳)。

重写的updateEditorGeometry如下:

void WListItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const {
    qDebug() << "updateEditorGeometry";

    editor->setStyleSheet("border-width:0;border-style:outset;font:10pt Microsoft YaHei");
    int mouse_X=editor->mapFromGlobal(QCursor::pos()).x();
    int mouse_Y=editor->mapFromGlobal(QCursor::pos()).y();

    QVariant var = index.data(Qt::UserRole+1);
    WItemData itemData = var.value<WItemData>();
    // Item 矩形区域
    QRectF rect = toLocalRect(option.rect);

    int rowLineNum,rowHeight;
    if(!itemData.isFold){
        //计算行高和行数
        rowLineNum = itemData.row_Name.length() + 1;
        rowHeight = rect.height()/(itemData.row_Name.length() + 3);
        for(int i = 1;i < rowLineNum;i++){
            //文本框区域
            QRect textRect(rect.x(),rect.y() + rowHeight * i,rect.width(),rowHeight);
            //判断鼠标落点
            if(mouse_X >= textRect.x()
                    && mouse_X <= textRect.x() + textRect.width()
                    && mouse_Y >= textRect.y()
                    && mouse_Y <= textRect.y() + textRect.height()){
                //设置编辑框显示位置为对应ItemData处
                editor->setGeometry(textRect.width() /3
                                    ,textRect.y() + textRect.height() /6
                                    ,textRect.width() /3 * 2
                                    ,textRect.height() /3 * 2);
                //保存当前行
                editor->setWhatsThis(QString::number(i - 1));
                return;
            }
        }
    }
    //单击其它地方不显示编辑框
    editor->setGeometry(0,0,0,0);
    editor->setWhatsThis(QString::number(-1));
}

实现效果如下:

2.6 编辑框内容同步(重写setModelData和setEditorData)

这部分比较简单,直接上代码:

void WItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const {

    if(editor->whatsThis().toInt() < 0) return;
    //读取编辑框数据
    QLineEdit *lineBox = static_cast<QLineEdit*>(editor);
    QString str = lineBox ->text();

    if(str.length() <= 0) return;

    //编辑框数据转存
    QVariant var = index.data(Qt::UserRole+1);
    WItemData itemData = var.value<WItemData>();
    itemData.row_Data[editor->whatsThis().toInt()] = str;
    var.setValue<WItemData>(itemData);
    //更新模型数据
    model->setData(index,var,Qt::UserRole + 1);
}

void WItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const {
    if(editor->whatsThis() < 0) return;
    QStyledItemDelegate::setEditorData(editor,index);
//    qDebug() << "setEditorData";
    QLineEdit *lineEdit = static_cast<QLineEdit*>(editor);
    QVariant var = index.data(Qt::UserRole+1);
    WItemData itemData = var.value<WItemData>();
    lineEdit->setText(itemData.row_Data.value(lineEdit->whatsThis().toInt()));
}

值得注意的是这几个接口的调用顺序和时机。当双击Item时调用顺序如下:

createEditor

updateEditorGeometry

setEditorData

//完成编辑

setModelData

setEditorData

实现效果如下:

三、Item事件响应

图形绘制已经完成,在QStyledItemDelegate已经完成了编辑事件,还需完成其它事件响应。

预期效果:双击标题栏完成选项卡的折叠和展开;单击按钮发射对应信号;

QStyledItemDelegate只负责Item图形的绘制和编辑框的创建等工作,要完成其它事件响应(如鼠标事件)需要在QListView中实现。

 在QListView中已经提供了一些事件的信号,例如鼠标事件有如下信号:

//鼠标按压
void pressed(const QModelIndex &index);
//鼠标单击(左键)
void clicked(const QModelIndex &index);
//鼠标双击(左右键均可)
void doubleClicked(const QModelIndex &index);

我们只需实现对应的槽函数即可:

    connect(this,&WList::clicked,this,&WList::slotClicked);
    connect(this,&WList::doubleClicked,this,&WList::slotDoubleClicked);
void WList::slotClicked(const QModelIndex &index){
    QVariant var = index.data(Qt::UserRole+1);
    WItemData itemData = var.value<WItemData>();
    //获取Item图形区域
    QRect itemRect = this->visualRect(index);
    //获取点击时鼠标位置
    int mouse_X=this->mapFromGlobal(QCursor::pos()).x();
    int mouse_Y=this->mapFromGlobal(QCursor::pos()).y();
    //计算行高和行数
    int rowLineNum,rowHeight;
    if(!itemData.isFold){
        rowLineNum = itemData.row_Name.length() + 1;
        rowHeight = itemRect.height()/(itemData.row_Name.length() + 3);
    }else {
        rowLineNum = 1;
        rowHeight = itemRect.height()/3;
    }
    //计算按钮范围
    int butWidth = 50,butHeight = (rowHeight * 2 - 15)/2;
    //判断鼠标落点是否在按钮上
    for(int i = 0;i < itemData.butNames.size();i++){
        QRectF butRect(itemRect.width() - butWidth - 10
                       ,itemRect.y() + rowHeight * rowLineNum +(butHeight + 5)*i + 5
                       ,50
                       ,butHeight);
        if(mouse_X >= butRect.x()
                && mouse_X <= butRect.x() + butRect.width()
                && mouse_Y >= butRect.y()
                && mouse_Y <= butRect.y() + butRect.height())
        {
            itemData.butSelected[i] = !itemData.butSelected.value(i);
            var.setValue<WItemData>(itemData);
            //更新模型数据
            model()->setData(index,var,Qt::UserRole + 1);
            emit itemButClicked(index.row(),i);     //触发按钮,发送Item序号和按钮序号
        }
    }
}
void WList::slotDoubleClicked(const QModelIndex &index){
    QVariant var = index.data(Qt::UserRole+1);
    WItemData itemData = var.value<WItemData>();
    //获取Item图形区域
    QRect itemRect = this->visualRect(index);
    //获取点击时鼠标位置
    int mouse_X=this->mapFromGlobal(QCursor::pos()).x();
    int mouse_Y=this->mapFromGlobal(QCursor::pos()).y();
    //计算行高和行数
    int rowLineNum,rowHeight;
    if(!itemData.isFold){
        rowLineNum = itemData.row_Name.length() + 1;
        rowHeight = itemRect.height()/(itemData.row_Name.length() + 3);
    }else {
        rowLineNum = 1;
        rowHeight = itemRect.height()/3;
    }
    //计算标题范围
    QRectF titleRect= QRectF(itemRect.x(),itemRect.y(),itemRect.width(),rowHeight);
    //判断鼠标落点是否在标题上
    if(mouse_X >= titleRect.x()
            && mouse_X <= titleRect.x() + titleRect.width()
            && mouse_Y >= titleRect.y()
            && mouse_Y <= titleRect.y() + titleRect.height()){
        itemData.isFold = !itemData.isFold;
        var.setValue<WItemData>(itemData);
        //更新模型数据
        model()->setData(index,var,Qt::UserRole + 1);   //只更新图形相关,不触发dataChanged
        return;
    }
}

 值得注意的是,这两个信号并不包含任何鼠标信息,我们需要在信号触发时手动获取鼠标位置,并以此来实现不同的鼠标单击事件。

我们需要信号触发时:

        1.鼠标与当前窗口的相对位置;

        2.Item当前图形绘制区域;

    //获取Item图形区域
    QRect itemRect = this->visualRect(index);
    //获取点击时鼠标位置
    int mouse_X=this->mapFromGlobal(QCursor::pos()).x();
    int mouse_Y=this->mapFromGlobal(QCursor::pos()).y();

 至此已经完成了所有预期的目标,在实际应用时只需要定义自己的信号和槽即可~

对于其它功能的实现可通过捕捉其它信号、重写其它事件响应函数和为其安装事件过滤器来实现,本篇重点介绍QStyledItemDelegate,在此不再赘述。


参考文章

https://blog.csdn.net/u010519432/article/details/26988515

QStyledItemDelegate基本使用:单元格数据渲染与编辑_龚建波的博客-CSDN博客_qstyleditemdelegate的用法

 Qt模型视图中代理(QStyledItemDelegate)的使用_qq_26480033的博客-CSDN博客


 2022-6-17        我自己测试绘制2W条以内数据所用时间1S以内,绘制10W条数据大概需要3-4S的时间,个人建议如果数据量超过10W条就要考虑优化性能了。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐