简介:本系列文章,是以纯代码方式实现 Qt 控件的重构,尽量不使用 Qss 方式。
《[Qt]QListView 重绘实例之二:列表项覆盖的问题处理》
《[Qt]QListView 重绘实例之三:滚动条覆盖的问题处理》
《[Qt]QListView 重绘实例之四:效果一讲解》
《[Qt]QListView 重绘实例之五:效果二讲解》
继上文《之二》,继续处理绘制圆角矩形背景时,遗留了另一个主要问题:滚动条覆盖的问题。
实际上,本文不仅仅只是解决滚动条覆盖的问题,还会进一步重绘一个简单的滚动条,以实现较好的整体效果。
补充内容:
为
QListView
设置代理样式,并不会对其子控件生效,如水平/垂直滚动条不会受代理样式影响,即使其中实现了滚动条的新样式。必须单独给QListView
的自带滚动条设置代理样式。
关于滚动条,需要先对一下暗号!啊不!是术语,抱歉!
参考上图所示,Qt 中滚动条的相关定义如下:
关于滚动条的重绘,直观的感觉就是尝试重构一个新的滚动条类,然后设置为 QListView
的滚动条,以此来解决滚动条覆盖的问题。
void PScrollBar::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::green);
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(rect(), 15, 15);
}
只打算给滚动条先画一个圆角矩形外边框,但却出现了如下图所示的效果。
无填充的圆角矩形是绘制出来了,可以看到绿色的边框。但是,背景却成了黑色。而右图中,则是将填充色设置成白色,可以看到四个角上仍旧有黑色的区域。
补充说明:
关于滚动条背景黑色的问题,从上文代码中可以看出,在
paintEvent()
方法中重绘肯定是无法解决。尝试过
setAttribute(Qt::WA_TranslucentBackground)
设置,也没有效果。尝试过在国内/国外网站查找资料,也没有找到相关的内容。
(这过程中,还是花不不少的时间/精力的。)
……
最后,只好在
QScrollBar
的源码中寻找答案。发现QScrollBar
内部初始化时,有给其添加Qt::WA_OpaquePaintEvent
属性。然后果断去除此属性,测试效果即可满足要求。万幸啊!~~……
其实,是有尝试 Qss 的,最开始确实也一直没有找到合适的纯代码解决方案。相对来说,使用 Qss 确实简单、直接,可以很方便的设置滚动条的各个元素。而纯代码方式却没办法,甚至有些地方都做不到。而修改源码,或者复制/修改源码为另外的版本,都比 Qss 方案开销大。(可谁让 Qss 会有性能问题呢!当然,少量嵌入式代码应用还好。)
归根结底,还是对源码、对 Qt 的底层实现不熟吧!而我的思路更多的是想在尽量不动 Qt 底层的基础上,实现自己想要的功能。也有相互隔离的意思吧。(额,略过略过……)
Qt::WA_OpaquePaintEvent
Qt 帮助文档说明:
Indicates that the widget paints all its pixels when it receives a paint event. Thus, it is not required for operations like updating, resizing, scrolling and focus changes to erase the widget before generating paint events. The use of WA_OpaquePaintEvent provides a small optimization by helping to reduce flicker on systems that do not support double buffering and avoiding computational cycles necessary to erase the background prior to painting. Note: Unlike WA_NoSystemBackground, WA_OpaquePaintEvent makes an effort to avoid transparent window backgrounds. This flag is set or cleared by the widget’s author.
表示当控件收到绘制事件时,绘制控件的所有像素。因此,在生成绘制事件之前,诸如更新、调整大小、滚动和更改焦点等操作则不再要求需要擦除控件。使用此标志可以提供一些小优化,有助于减少在不支持双缓冲的系统上会出现的界面闪烁问题,还能减少在绘制前擦除背景所需的计算时间。注意:与
WA_NoSystemBackground
不同,WA_OpaquePaintEvent
应尽量避免窗口需要透明背景的情况。此标志由控件的作者设置或清除。理解:
这里的重点是控件收到绘制事件时,会绘制控件的所有像素。如此,就像本文代码实现一样,在矩形圆角外侧,也要进行绘制。那么,绘制成什么样呢?大概就绘制成了黑色了吧!(就像,对
QPainter
设置Qt::NoBrush
也是这样的黑色效果。)又明确说明此标志应该避免使用在需要窗口透明背景的情况。而在
QScrollBar
中刚好设置了该标志。所以,应用于透明背景的情况,就应该删除此标志。
PScrollBar::PScrollBar(QWidget *parent) : QScrollBar(parent)
{
/* 设置透明背景,因为 QScrollBar 源码中设置了此标志 */
setAttribute(Qt::WA_OpaquePaintEvent, false);
}
再来看一下效果图,这下背景黑框问题解决了。
有了上面的基础,现在开始就需要绘制滚动条的各个组成元素了。虽然绘制滚动条背景时,只画了一个白底绿边的圆角矩形,但也同时说明了几个问题点:
本文打算绘制如下图所示的效果,这种效果也是现今比较常见的效果,也正好不需要滚动条两端的上一行/下一行按钮,方便利用滚动条的整个区域。
然后,就是选择重绘的实现方式了。
本文一开始采用了重写 paintEvent()
的方法,但后来发现,这样就只能通过 Qss 才能隐藏上一行/下一行两个按钮,没办法通过纯代码的方式实现。
相反,采用 QProxyStyle
代码样式的方法,却可以很好的控制滚动条的各个元素,以及绘制效果。最后还是改为了代理样式的方法。
对滚动条各个元素作如下设置:
SC_ScrollBarSubLine
:设置大小为 0SC_ScrollBarAddLine
:设置大小为 0SC_ScrollBarSubPage
:上移/左移 1 个按钮的高度/宽度SC_ScrollBarAddPage
:下称/右移 1 个按钮的高度/宽度SC_ScrollBarSlider
:上称/左移 1 个按钮的高度/宽度,增加 2 个按钮的高度/宽度SC_ScrollBarGroove
:上称/左移 1 个按钮的高度/宽度,增加 2 个按钮的高度/宽度这样做,主要是在原样式的基础上进行调整,可以确保 Qt 计算滚动时,滚动条依旧可以正常响应。
但是,这个实现还是使用了一组经验数据。暂时没有找到更合适的解决办法,如果更改了滚动条的默认宽度/高度,估计会出问题,需要同步修改这些经验值。
这样做主要是为了解决初始状态下,获取不到上一行/下一行按钮大小的问题。因为实测发现,subControlRect()
函数在滚动条初始化时,根本不会调用 SC_ScrollBarSubLine
和 SC_ScrollBarAddLine
两处。
/* 水平滚动条的高度,垂直滚动条的宽度 */
const int ScrollBarExtent = 21;
PScrollBarStyle::PScrollBarStyle()
{
/* TODO: 经验值,调试 QProxyStyle 过程可以获取滚动条两个按钮的默认大小的 21x21 */
/* NOTED: 必须设置一个合适值,否则滚动条初始化状态可能效果不正常 */
mSubLineRect = QRect(0, 0, ScrollBarExtent, ScrollBarExtent);
mAddLineRect = QRect(0, 0, ScrollBarExtent, ScrollBarExtent);
}
QRect PScrollBarStyle::subControlRect(QStyle::ComplexControl cc,
const QStyleOptionComplex *option,
QStyle::SubControl sc,
const QWidget *widget) const
{
int x, y, w, h;
QRect rect = QProxyStyle::subControlRect(cc, option, sc, widget);
rect.getRect(&x, &y, &w, &h);
switch(cc)
{
case QStyle::CC_ScrollBar:
{
const QStyleOptionSlider *opt = qstyleoption_cast<const QStyleOptionSlider *>(option);
if(nullptr == opt) { break; }
switch(sc)
{
/* NOTED: 实测发现,初始状态,滚动条不会触发这两个消息 */
case QStyle::SC_ScrollBarSubLine:
mSubLineRect = rect;
return QRect(x, y, 0, 0);
case QStyle::SC_ScrollBarAddLine:
mAddLineRect = rect;
return QRect(x, y, 0, 0);
case QStyle::SC_ScrollBarSubPage:
{
if(Qt::Horizontal == opt->orientation)
{
rect = QRect(x - mSubLineRect.width(), y, w, h);
}
else
{
rect = QRect(x, y - mSubLineRect.height(), w, h);
}
return rect;
}
case QStyle::SC_ScrollBarAddPage:
{
if(Qt::Horizontal == opt->orientation)
{
rect = QRect(x + mAddLineRect.width(), y, w, h);
}
else
{
rect = QRect(x, y + mAddLineRect.height(), w, h);
}
return rect;
}
case QStyle::SC_ScrollBarSlider:
{
if(Qt::Horizontal == opt->orientation)
{
rect = QRect(x - mSubLineRect.width(), y,
w + mSubLineRect.width() + mAddLineRect.width(), h);
}
else
{
rect = QRect(x, y - mSubLineRect.height(),
w, h + mSubLineRect.height() + mAddLineRect.height());
}
return rect;
}
case QStyle::SC_ScrollBarGroove:
{
if(Qt::Horizontal == opt->orientation)
{
rect = QRect(x - mSubLineRect.width(), y,
w + mSubLineRect.width() + mAddLineRect.width(), h);
}
else
{
rect = QRect(x, y - mSubLineRect.height(),
w, h + mSubLineRect.height() + mAddLineRect.height());
}
return rect;
}
default:
break;
}
break;
}
default:
break;
}
return rect;
}
效果图如下。可以看到,滚动条的两侧按键都被移除掉了,使用鼠标测试滚动功能也都正常。
需要绘制两个部分:
其中,利用滑块大小判断,还实现了滚动条无效时不绘制滑块的效果。这一点与默认滚动条表现一致。
void PScrollBarStyle::drawComplexControl(QStyle::ComplexControl control,
const QStyleOptionComplex *option,
QPainter *painter,
const QWidget *widget) const
{
switch(control)
{
case QStyle::CC_ScrollBar:
{
const QStyleOptionSlider *opt = qstyleoption_cast<const QStyleOptionSlider *>(option);
if(nullptr == opt) { break; }
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
/* Track */
painter->setPen(Qt::NoPen);
painter->setBrush(QBrush(QColor(0xCE, 0xCE, 0xCE)));
painter->drawRoundedRect(opt->rect, Radius, Radius);
/* Slider */
QRect sliderRect = subControlRect(control, opt, SC_ScrollBarSlider, widget);
/* 不需要滚动条时,滑块大小等于滑轨大小,或滚动条大小 */
/* 滚动条无效时,不绘制滑块,只绘制背景/滑轨,与滚动条默认行为一致 */
if((Qt::Horizontal == opt->orientation && opt->rect.width() != sliderRect.width())
|| (Qt::Vertical == opt->orientation && opt->rect.height() != sliderRect.height()))
{
painter->setBrush(QBrush(QColor(0, 0xBC, 0xD4)));
painter->drawRoundedRect(sliderRect, Radius, Radius);
}
painter->restore();
return;
}
default:
break;
}
QProxyStyle::drawComplexControl(control, option, painter, widget);
}
效果图如下:
上图的效果已经实现基本的目标,但是,滚动条还是太宽了,整体显得不太好看。所以额外实现一下缩小滚动条的效果。
在 QProxyStyle
代理样式中,可以使用 QStyle::PM_ScrollBarExtent
标志设置水平滚动条的高度/垂直滚动条的宽度。而且,既然要设置滚动条的宽度/高度,那么初始化时上一行/下一行按钮的大小也就不再是经验值,而是目标值了。
但是,本文中不打算使用这种方法,因为这里还有另一个重要的知识点。
处理方法:
在 QListView
中监听滚动条的事件,然后在 QEvent::Resize
和 QEvent::Move
事件下调整滚动条的位置/大小。
/* plistview.h */
class PListView : public QListView
{
private:
/* 保存初始状态下滚动条的宽度/高度 */
int mInitHScrollBarHieght;
int mInitVScrollBarWidth;
}
/* plistview.cpp */
PListView::PListView()
{
//...
setVerticalScrollBar(mVScrollBar);
mHScrollBar->setOrientation(Qt::Horizontal);
setHorizontalScrollBar(mHScrollBar);
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
/*
* TODO: 注意,必须在滚动条设置到列表后,才能正确地获取到滚动条的宽度/高度。
* 此处理可能会有问题,暂未发现。
*/
mInitHScrollBarHeight = mHScrollBar->height();
mInitVScrollBarWidth = mVScrollBar->width();
mVScrollBar->installEventFilter(this);
mHScrollBar->installEventFilter(this);
//...
}
bool PListView::eventFilter(QObject *obj, QEvent *event)
{
if(obj->inherits("QScrollBar"))
{
QScrollBar *scrollBar = qobject_cast<QScrollBar *>(obj);
if(nullptr == scrollBar) { return QListView::eventFilter(obj, event); }
switch(event->type())
{
case QEvent::Resize:
case QEvent::Move:
{
const int Margins = 5;
QRect r(scrollBar->rect());
if(Qt::Vertical == scrollBar->orientation())
{
if(mInitVScrollBarWidth == r.width())
{
scrollBar->setGeometry(r.adjusted(Margins, Margins, -Margins, -Margins));
}
}
else
{
if(mInitHScrollBarHeight == r.height())
{
scrollBar->setGeometry(r.adjusted(Margins, Margins, -Margins, -Margins));
}
}
break;
}
default:
break;
}
}
return QListView::eventFilter(obj, event);
}
注意:
经过实际测试,(在此场景下)如果尝试移动滚动条,结果可能是找不到滚动条了。
只能在滚动条的区域内,进行滚动条的大小、以及相对位置的调整。因为还在原位置处,所以做一些微调,还是可以正常被绘制出来的。
最后再来看一个效果图:
滚动条的宽度/高度变小了,与边框的间距也加大了。
意思到了,具体的效果,则可以根本实际需求优化,本文不再深入了。
到此,系列的主要内容就基本完成了。
写文章之前,其实还有一些 Qss 的代码。但是随着写文章过程中的整理,又查了一些资料,看了看源码,最终还是消除了 Qss 代码实现,达到了纯代码实现的目的。
本文实现了这个方案的整体思路与效果。对 QProxyStyle
代理应用,也进行了深入,有助于其它相似功能的扩展开发。
当然,本文肯定还有未考虑到的内容,以后有其它发现,再做补充。
系列后两文,会再写两个不一样的效果实例,具体实现有不少差别,也有不同的亮点。感兴趣的看官,还请移驾。
《[Qt]QListView 重绘实例之四:效果一讲解》
《[Qt]QListView 重绘实例之五:效果二讲解》