当前位置: 首页 > news >正文

[Qt]QListView 重绘实例之二:列表项覆盖的问题处理

0 环境

  1. Windows 11
  2. Qt 5.15.2 MinGW x64

1 系列文章

简介:本系列文章,是以纯代码方式实现 Qt 控件的重构,尽量不使用 Qss 方式。

《[Qt]QListView 重绘实例之一:背景重绘》

《[Qt]QListView 重绘实例之二:列表项覆盖的问题处理》

《[Qt]QListView 重绘实例之三:滚动条覆盖的问题处理》

《[Qt]QListView 重绘实例之四:效果一讲解》

《[Qt]QListView 重绘实例之四:效果二讲解》

2 问题开始

继上文《之一》,绘制圆角矩形背景时,遗留了两个主要问题:

  • 列表项覆盖破坏背景效果;
  • 滚动条覆盖破坏背景效果;

其中,对于滚动条的问题,留作下一文《之三》讲解。参考《[Qt]QListView 重绘实例之三:滚动条的处理》。

本文先解决列表项覆盖的问题。

QListView-residual2

至少有两个思路解决列表项的覆盖问题:

  • 使用委托。如子类化 QItemDelegate,然后进行列表项的重绘;
  • 使用代理样式。如子类化 QProxyStyle,然后进行列表项的重绘;

本文选择第二种思路,原因有二:

  • 其一,承接上文《之一》已经使用了子类化 QProxyStyle 的方法,继续实现对列表项的重绘功能即可,没有必要再另外添加一个新委托子类实现;
  • 其二,样式 QStyle /代理样式 QProxyStyle,本身即包含委托实现的功能。而且它们更多强大,可以对某类或全局样式进行控件;

具体的重绘过程分为两个部分:

  1. 对于视口最上行/最下行,需要处理上半部分/下半部分的圆角绘制;
  2. 对于视口的其它中间行,则进行默认绘制(或按期望样式绘制);

→→→ 解决方案直达 ←←←

3 重绘列表项背景

代理样式中,与列表项相关的主要有一个样式类和两个元素类型:

  • 样式类:QStyleOptionViewItem
  • 元素类型:
    • QStyle::CE_ItemViewItem - void drawControl() const
    • QStyle::PE_PanelItemViewItem - void drawPrimitive() const

Qt 文档关于 QStyle 类有如下说明:

Styles in Item Views

The primitive element PE_PanelItemViewItem is responsible for painting the background of items, and is called from QCommonStyle’s implementation of CE_ItemViewItem.

(译:QCommonStyle 子类实现中的控制类型 CE_ItemViewItem 会调用原始类型 PE_PanelItemViewItem,其中,原始类型 PE_PanelItemViewItem 负责绘制列表项的背景。)

即,在 QStyle::PE_PanelItemViewItem 中绘制列表项的背景,在 QStyle::CE_ItemViewItem 中绘制列表项的内容。

3.1 保存视口大小信息

Qt 明确说明 QListView 是垂直型的列表。

首先,需要判断视口中的最上/最下行位置。而判断位置则需要知道整个列表的高度。具体实现是在绘制 QFrame 时保存数据。

/* .h */
class PListViewStyle : public QListView
{
public:// ...
private:mutable QRect mRect;	// Need mutable
}/* .cpp */
void PListViewStyle::drawControl(QStyle::ControlElement element,const QStyleOption *option,QPainter *painter,const QWidget *widget) const
{switch(element){case QStyle::CE_ShapedFrame:{const QStyleOptionFrame *opt = qstyleoption_cast<const QStyleOptionFrame *>(option);if(nullptr == opt) { return; }mRect = opt->rect;//...return;}default:break;}QProxyStyle::drawControl(element, option, painter, widget);
}

注意:变量 mRect 必须使用 mutable 修饰。

3.2 判断视口中的最上/最下行位置

  • 判断视口中的最上行位置
if(0 == opt->rect.y())

如果没有设置内填充,最上行位置的 y 坐标应该等于 0,因此使用此条件进行判断。

  • 判断视口中的最下行位置
bool PListViewStyle::isLastRow(const QRect rect, int &rowHeight) const
{/* 列表可显示行数 */int rowCount = mRect.height() / rect.height();/* 由 y 坐标与行高计算行索引 */int index = rect.y() / rect.height();if(rowCount == index){rowHeight = mRect.height() - index * rect.height();return true;}return false;
}

3.3 绘制列表项背景

/* 添加常量定义,需要添加到 cpp 文件开头位置 */
const int Radius = 15;void PListViewStyle::drawPrimitive(QStyle::PrimitiveElement element,const QStyleOption *option,QPainter *painter,const QWidget *widget) const
{switch(element){/* PE_PanelItemViewItem 主要负责绘制列表项的背景(选中背景/高亮背景)*/case QStyle::PE_PanelItemViewItem:{const QStyleOptionViewItem *opt = qstyleoption_cast<const QStyleOptionViewItem *>(option);if(nullptr == opt) { break; }QColor c(Qt::lightGray);if(QStyle::State_MouseOver & opt->state){c = QColor(0, 0, 255, 255 * 0.2);}else if(QStyle::State_Selected & opt->state){c = QColor(0, 0, 255, 255 * 0.5);}int x, y, w, h;opt->rect.getRect(&x, &y, &w, &h);QPainterPath path;int rowHeight = 0;/* 最上一行 */if(0 == y){/* 创建最上一行,带圆的角矩形路径 */path.moveTo(x, y + h);path.arcTo(QRect(x, y, 2 * Radius, 2 * Radius), 180, -90);path.lineTo(x + w, y);path.lineTo(x + w, y + h);path.closeSubpath();}/* 最下一行 */else if(isLastRow(opt->rect, rowHeight)){/* 创建最下一行,带圆角的矩形路径 */path.moveTo(x, y);path.lineTo(x + w, y);path.lineTo(x + w, y + rowHeight);path.arcTo(QRect(x, y + rowHeight - 2 * Radius, 2 * Radius, 2 * Radius), 270, -90);path.closeSubpath();}else{path.addRect(QRect(x, y, w, h));}painter->save();painter->setRenderHint(QPainter::Antialiasing);painter->setPen(Qt::NoPen);painter->setBrush(QBrush(c));painter->drawPath(path);painter->restore();return;}default:break;}QProxyStyle::drawPrimitive(element, option, painter, widget);
}

为了显示绘图的效果,这里特地将列表项的默认白色背景,改成了浅灰色。

其中的重点是:对视口最上行和最下行进行了圆角处理,通过 QPainterPaht 实现。

效果图如下:

QListView-itembg

从上图中可以看到,绘制列表项的圆角效果确实出来了,高亮效果也正确。说明,至少这样的处理方案没有问题。

但是,还是发现,背景上有一个白色直角矩形,仍然破坏了圆角矩形背景。

这也同样说明,这个白色直角矩形并不是由于列表项产生的影响。

3.4 视口 viewport()

好了,现在问题好像又回到 QListView 的里层了。

很明显,列表项应该是表层的,因为用户是可以直接看到的。

接下来,结合上一文《之一》的内容,来看一下以下这张图。

QListView-layers

这里有一个层级关系:

  • 最底层:QFrame。在《之一》中使用 paintEvent() 进行重绘时,正是设置的这个,直接设置成了 QFrame::NoFrame
  • 上一层:viewpotr()。在《之一》中也验证过这点,而且,viewport() 区域是不包含滚动条的。如上图中列表项底下的白色矩形;
  • 最上层:列表项。上文已有说明。

现在,基本上可以确定问题应该聚焦在 viewport() 上。

查看 Qt 帮助文档,与视口相关的有两个接口:

QWidget *QAbstractScrollArea::viewport() const;
void QAbstractScrollArea::setViewport(QWidget *widget);

可知,视口即是一个 QWidget 控件。

那么,再来验证一下,重新设置视口控件,然后设置其背景色为蓝色:

PListView::PListView(QWidget *parent) : QListView(parent)
{auto *widget = new QWidget;widget->setStyleSheet("background: blue");setViewport(widget);
}

效果图如下:

QListView-viewport

由此可知,确认了之前的推论。

补充内容:

解决方法只需要一条语句:setViewport(new QWidget),而且,做过一些深入的尝试,但没有理解具体的原因。

  • 既然视口是一个 QWidget,那么,对 QWidget 进行绘制,是不是应该也可以当作背景使用?实际上的情况是,子类化 QWidget 后重写 paintEvent() 方法,再设置为新的视口控件,重绘函数根本不会被调用。经实测,可以改变这个视口控件的方式,只有通过设置 Qss 才有效。也没明白是为什么。
  • 然后,设置 QWidget 对象为新的视口控件,该对象的调色板(或者说对象样式)是会影响列表项的默认样式的。例如 QWidget 通过 Qss 设置背景颜色为蓝色,则列表项的默认背景色也会变为蓝色。当然,这也可能是 Qss 设置影响了子对象。

此外,之所以只要设置一个默认的 QWidget 对象作为新视口即可,猜测原因是:默认 QWidget 本身是一个透明的(或者是统一风格背景色的)控制,在 QListView 中即表现为透明的一层,所以不会影响圆角背景的效果。

虽然实际原因不知,但能解决问题。

4 解决方案

  • 添加新的视口控件
PListView::PListView(QWidget *parent) : QListView(parent)
{setViewport(new QWidget);setFrameStyle(QFrame::NoFrame);	// Must//...
}
  • 绘制列表项背景
void PListViewStyle::drawPrimitive(QStyle::PrimitiveElement element,const QStyleOption *option,QPainter *painter,const QWidget *widget) const
{switch(element){/* PE_PanelItemViewItem 主要负责绘制列表项的背景(以及选中背景/高亮背景) */case QStyle::PE_PanelItemViewItem:{const QStyleOptionViewItem *opt = qstyleoption_cast<const QStyleOptionViewItem *>(option);if(nullptr == opt) { break; }QColor c(Qt::white);	/* 默认背景色 */if(QStyle::State_MouseOver & opt->state){c = QColor(0, 0, 255, 255 * 0.2);}else if(QStyle::State_Selected & opt->state){c = QColor(0, 0, 255, 255 * 0.5);}int x, y, w, h;opt->rect.getRect(&x, &y, &w, &h);QPainterPath path;int rowHeight = 0;/* 最上一行 */if(0 == y){/* 创建最上一行,带圆角的矩形路径 */path.moveTo(x, y + h);path.arcTo(QRect(x, y, 2 * Radius - 5, 2 * Radius - 5), 180, -90);path.lineTo(x + w, y);path.lineTo(x + w, y + h);path.closeSubpath();}/* 最下一行 */else if(isLastRow(opt->rect, rowHeight)){/* 创建最下一行,带圆角的矩形路径 */path.moveTo(x, y);path.lineTo(x + w, y);path.lineTo(x + w, y + rowHeight);path.arcTo(QRect(x, y + rowHeight - 2 * Radius, 2 * Radius, 2 * Radius), 270, -90);path.closeSubpath();}else{path.addRect(QRect(x, y, w, h));}painter->save();painter->setRenderHint(QPainter::Antialiasing);painter->setPen(Qt::NoPen);painter->setBrush(QBrush(c));painter->drawPath(path);painter->restore();return;}default:break;}QProxyStyle::drawPrimitive(element, option, painter, widget);
}

最后来看一下效果图,达到了预期的效果目标。

QListView-items

http://www.lryc.cn/news/174672.html

相关文章:

  • Java 函数式编程思考 —— 授人以渔
  • 操作系统权限提升(二十八)之数据库提权-SQL Server 数据库安装
  • 腾讯mini项目-【指标监控服务重构-会议记录】2023-08-18
  • 如何通过axios拦截器,给除了登录请求以外,axios的所有异步请求添加JWT令牌!
  • Spring学习笔记9 SpringIOC注解式开发
  • 【新日标习题集】第13課 までのまとめ (discarded)
  • Java基础常考知识点(基础、集合、异常、JVM)
  • 虚拟机桥接模式下没有无线网卡选项
  • 设计模式笔记
  • c==ubuntu+vscode debug redis7源码
  • java字符串储存底层原理
  • c++获取当前时间的字符串
  • 【精品】通用Mapper 批量更新bug解决方案
  • 腾讯mini项目-【指标监控服务重构-会议记录】2023-07-06
  • 【React】函数式组件和类式组件的用法和逻辑
  • 题目 1061: 二级C语言-计负均正
  • 数位和(C++)
  • [牛客复盘] 牛客周赛round13 20230924
  • mybatsi-MyBatis的逆向工程
  • 转转闲鱼交易猫链接源码 支持二维码收款
  • Python爬虫基础(三):使用Selenium动态加载网页
  • Linux系统下安装Mysql
  • Jenkins学习笔记1
  • 注意力机制
  • JVM-Java字节码技术笔记
  • C++ 友元、重载、继承、多态
  • Spring Boot 日志文件
  • vulhub venom
  • 量化交易之One Piece篇 - linux - 定时任务(重启服务器、执行程序、验证)
  • Qt5开发及实例V2.0-第二十三章-Qt-多功能文档查看器实例