目录
2.5 编辑框自适应(重写updateEditorGeometry)
2.6 编辑框内容同步(重写setModelData和setEditorData)
写在前面
在做项目的过程中遇到一个需求:要实现一个美观的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条就要考虑优化性能了。