0 环境
- Windows 11
- Qt 5.15.2 MinGW x64
1 系列文章
简介:本系列文章,是以纯代码方式实现 Qt 控件的重构,尽量不使用 Qss 方式。
《[Qt]QListView 重绘实例之二:列表项覆盖的问题处理》
《[Qt]QListView 重绘实例之三:滚动条覆盖的问题处理》
《[Qt]QListView 重绘实例之四:效果一讲解》
《[Qt]QListView 重绘实例之五:效果二讲解》
2 问题开始
继上文《之一》,绘制圆角矩形背景时,遗留了两个主要问题:
- 列表项覆盖破坏背景效果;
- 滚动条覆盖破坏背景效果;
其中,对于滚动条的问题,留作下一文《之三》讲解。参考《[Qt]QListView 重绘实例之三:滚动条的处理》。
本文先解决列表项覆盖的问题。
至少有两个思路解决列表项的覆盖问题:
- 使用委托。如子类化
QItemDelegate
,然后进行列表项的重绘; - 使用代理样式。如子类化
QProxyStyle
,然后进行列表项的重绘;
本文选择第二种思路,原因有二:
- 其一,承接上文《之一》已经使用了子类化
QProxyStyle
的方法,继续实现对列表项的重绘功能即可,没有必要再另外添加一个新委托子类实现; - 其二,样式
QStyle
/代理样式QProxyStyle
,本身即包含委托实现的功能。而且它们更多强大,可以对某类或全局样式进行控件;
具体的重绘过程分为两个部分:
- 对于视口最上行/最下行,需要处理上半部分/下半部分的圆角绘制;
- 对于视口的其它中间行,则进行默认绘制(或按期望样式绘制);
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 fromQCommonStyle
’s implementation ofCE_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
实现。
效果图如下:
从上图中可以看到,绘制列表项的圆角效果确实出来了,高亮效果也正确。说明,至少这样的处理方案没有问题。
但是,还是发现,背景上有一个白色直角矩形,仍然破坏了圆角矩形背景。
这也同样说明,这个白色直角矩形并不是由于列表项产生的影响。
3.4 视口 viewport()
好了,现在问题好像又回到 QListView
的里层了。
很明显,列表项应该是表层的,因为用户是可以直接看到的。
接下来,结合上一文《之一》的内容,来看一下以下这张图。
这里有一个层级关系:
- 最底层:
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);
}
效果图如下:
由此可知,确认了之前的推论。
补充内容:
解决方法只需要一条语句:
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);
}
最后来看一下效果图,达到了预期的效果目标。