QWidget抗锯齿圆角窗口的一个实现方案
由于 QWidget::setMask 接口设置圆角不支持抗锯齿,所以通常会使用透明窗口加圆角背景,但圆角背景不能满足对子控件的裁剪,子控件与圆角区域重叠的部分还是能显示出来。当然对于大多数窗口,留出足够的边距也是可以接受。
对一些特殊场景,比如QComboBox的列表框,UI设计师强烈要求圆角,列表与它的容器不能有边距,常规办法就很难做到。笔者在经过长时间的研究,有了一个可能的方案。
最终实现效果如下图,可以看到,列表项区域,滚动条区域也能够正常显示圆角。
注意,该方案可能不适用一些场景:
- 特殊的平台或Qt配置
- 复杂窗口且有性能要求
- 窗口尺寸较大(可以优化)
- 有嵌入式窗口、OpenGL、QWindow等
方案
基本原理
Qt的每个独立窗口,默认都是在一张图片上,层叠绘制所有子控件。通常我们自绘控件时,几乎不会使用QPainter::setCompositionMode设置其他混合模式,会出现比较奇怪的效果。但如果使用透明背景窗口,使用混合模式其实跟在QPixmap或QImage上绘制一样。
另外一点,当一个控件重绘时,由于底层的绘制会影响到上层透明合成,所以Qt会从下到上按顺序绘各个控件的脏区域。
所以理论上,如果在一个窗口上增加一个全尺寸的遮罩,重绘时使用混合模式就可以实现对一些像素的清除,且支持抗锯齿。
代码步骤
-
创建一个QWidget作为遮罩
遮罩置于顶层,鼠标设置透传(WA_TransparentForMouseEvents)
遮罩跟随窗口尺寸大小同步变化,保持一致。安装事件过滤器即可(installEventFilter)
-
重写paintEvent,利用混合模式清除圆角像素
以下绘制逻辑比较直接,建议优化
//创建一个图片,填充透明色
QPixmap pix(this->size());
pix.fill(QColor(0,0,0,0));
// 在改图片上填充一个圆角区域,需要设置抗锯齿
QPainter painter(&pix);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
//这里圆角区域需要根据dpi、size调整
path.addRoundedRect(QRectF(pix.rect()).adjusted(0.5, 0.5, -0.5, -0.5), 10, 10);
painter.fillPath(path, Qt::white);
painter.end();
// 在窗口上绘制该圆角图片
painter.begin(this);
painter.setRenderHint(QPainter::Antialiasing);
// 该混合模式会根据source像素的透明度,调整目标的透明度
painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
painter.drawPixmap(0, 0, pix);
// 恢复默认混合模式,绘制边框,如果没有则不用
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.setPen(QPen(QColor(0xCA64EA), 1.0));
painter.drawPath(path);
优化方向
-
性能优化
上面的示例,使用了一整张图片对窗口像素进行混合模式的运算,且每次子控件重绘都会引起遮罩的重绘,性能比较差。可以考虑仅在四周设置圆角的遮罩。
-
圆角绘制优化
本文使用了一个不透明的圆角区域对窗口设置裁剪,圆角的参数是固定在代码里的。实际QSS是可以设置窗口的圆角,因此可以借助QSS来生成一张圆角图片,就避免代码里包含固定数值。
具体实现原理可以参考之前的文章,这里不具体展示了:
QComboBox文字居中的一种解决办法
Qt实现一个支持QSS的Switch Button
Qt借助隐藏控件和QSS绘制重复元素