Qt绘制跟画画一样,首先我们需要以为画家(QPainter)和一块画板(QPainterDevice), 以及绘制时需要的画笔(QPen)和画刷(QBrush),
为了更方便绘制,还需要调色板(QPalette),如果绘制一些几何图形,还需要画尺,在Qt中,归类于路径(QPainterPath)。
下面我们,就挨个的来讲述这些类,并给出一些简单的Demo。
QPainter
QPainter提供了高度优化的API来完成大多数GUI程序所需的绘图。可以使用它绘制简单的线条,也可以绘制一些复杂的图形,例如:圆弧和曲线。
同时也可以用来绘制对齐文本和图片。通常,它采用的是“自然”坐标系,但也可以进行视图坐标与全局坐标转换。QPainter可以对所有继承自QPaintDevice类的对象进行绘制。
QPainter的常见用法,就是在窗口的绘制回调的分发函数中:构造并自定义(设置画笔、画刷、字体)QPainter,然后进行绘制:
void SimpleExampleWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setPen(Qt::blue);
painter.setFont(QFont("Arial", 30));
painter.drawText(rect(), Qt::AlignCenter, "Qt");
}
QPainter的核心功能是绘图,但它提供了一些其他功能,例如设置渲染质量(setRenderHint),这个接口是一个非常常用的接口,它主要的作用设置更复杂绘制算法用来做一些抗锯齿处理,提高渲染质量,具体的用法,参考一下帮助文档,这里需要提一嘴的是,使用这个接口,有时候效果并不是很理想。
例如: 使用抗锯齿绘制圆角矩形

QPainter painter(&widget);
painter.setRenderHint(QPainter::Antialiasing);
//这种方法,效果不对
QPainterPath path1;
path1.addRoundedRect(QRect(20, 20, 40, 40), 6, 6);
painter.setPen(Qt::red);
painter.drawPath(path1);
//这种绘制圆角矩形的方式,才是正确的。
QPainterPath path2;
path2.addRoundedRect(QRect(80, 20, 40, 40), 6, 6);
path2.addRoundedRect(QRect(80, 20, 40, 40).adjusted(1, 1, -1, -1), 5, 5);
painter.fillPath(path2, Qt::red);
截图的差异不是很明显,可以自行拷贝代码运行查看效果,实际效果非常的明显,同样在绘制圆形、绘制矩形等等,都可以考虑使用方案2。另外在进行拉伸绘制图片时,使用:
QPainter::SmoothPixmapTransform
其实效果并不好,而是考虑使用Image.scale方法,先进行平滑的缩放,然后再绘制,效果才是最佳的。但记住,不要把缩放的代码,放在绘制函数中。
CompositionMode
Qt还提供了组合模式的设置:CompositionMode

其中第三种模式是默认模式,就是黄色的原本绘制的或者原本就有的,绿色是后绘制的。
这个组合模式使用,是非常的好玩的,下面举几个例子:
1.修改图片的前景有颜色
void turnColor(QImage& image, const QColor& color)
{
QPainter painter(&image);
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.fillRect(painter.viewport(), color);
}
2. 实现drop-shadow
Qt有自带的Drop Shadow的实现,但是它只能以窗口为对象,如果你手绘了一个文字,想给文字加一个阴影,那就做不到了。
其实做一个drop-shadow分为一下几步:
1. 模糊化图片,模糊算法一般是高斯模糊,Qt提供了一个API: qt_blurImage
2. 将模糊化的图片进行灰化处理。(当然先灰化,后模糊也一样),总之阴影有了。
3. 使用DestinationOver的组合方式,将阴影做一定偏移角度并绘制上去。
QImage image(200, 200, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
QPainter painter(&image);
QFont font = painter.font();
font.setPixelSize(32);
painter.setFont(font);
painter.setPen(Qt::green);
painter.drawText(painter.viewport(), "ABCDEFG", Qt::AlignHCenter | Qt::AlignVCenter);
painter.end();
image.save("D:\\aaa1.png");
QImage blurImage = image;
QImage tmp(200, 200, QImage::Format_ARGB32_Premultiplied);
tmp.fill(0);
QPainter blurPainter(&tmp);
qt_blurImage(&blurPainter, blurImage, 16, true, false);
blurPainter.setCompositionMode(QPainter::CompositionMode_SourceIn);
blurPainter.fillRect(blurPainter.viewport(), Qt::gray);
blurPainter.end();
tmp.save("D:\\aaa2.png");
painter.begin(&image);
painter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
painter.drawImage(QPoint(8, 8), tmp);
painter.end();
image.save("D:\\aaa3.png");
效果图:

是不是有点那个意思在里面呢?
3. 现在有一个这样需求,一块区域,从上至下颜色渐变,从左至右透明度渐变。

假定背景是灰色。
千万不要耍小聪明,一个上至下,一个左至右,然后组合变成左上至右下。
例如上诉的例子是,从上至下,绿色渐变为蓝色,从左至右,透明度渐变为0。
如果组合成左上至右下的模式,则是左上是纯绿色,右下为蓝色并透明度为0。
当你以为这样斜角渐变方式能达到效果,但是实际上左下角并非纯蓝色,(具体原因可以自行思考一下)
因此两个维度渐变,必须分开渐变, 这就需要用到组合模式:
代码如下所示:
QImage image(QSize(200, 100), QImage::Format_ARGB32_Premultiplied);
image.fill(0);
/* 在图片上从左至右,白色→纯透明渐变, 这里主要是完成透明度渐变,至于颜色不重要, 随便什么颜色*/,
QPainter painter(&image);
QLinearGradient gradient1;
gradient1.setColorAt(0, Qt::white);
gradient1.setColorAt(1, Qt::transparent);
gradient1.setStart(QPoint(0, 0));
gradient1.setFinalStop(QPoint(200, 0));
painter.fillRect(painter.viewport(), gradient1);
/*
这里设置组合模式, SourceIn这种组合模式,会将填充区域内的颜色重新更换,但是透明度不变
SourceOut,则是透明度全部再反过来。
*/
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
QLinearGradient gradient2;
gradient2.setColorAt(0, Qt::green);
gradient2.setColorAt(1, Qt::blue);
gradient2.setStart(QPoint(0, 0));
gradient2.setFinalStop(QPoint(0, 100));
painter.fillRect(painter.viewport(), gradient2);
组合模式,有很多种,每一种模式,都能达到特别效果,还是蛮好玩的,这些组合模式,
用好之后,能够很好的处理各种好玩的效果。
切记组合模式,对于QWidget绘制,会有一些差异,这是因为QWidget的绘制系统比较复杂。
有兴趣,可以了解一下QWidget的整个绘制系统(调试源码)。
QTransform
QPainter还有一个好用的东西,就是坐标体系转换,而这我们需要用到QTransform:

这是一些常用的坐标转换公式,记得使用矩阵对坐标体系转换以后,把自己头也扭过去,一定记得记得新的坐标系
原点是没有变的,所以需要调用translate移动一下坐标系,否则可能,你绘制的东西,在视图之外,导致你看不到。
当然也有一个万能转换手段。
- 调用translate将坐标系移动到视图的正中心。
- 做任意转换
- 再讲坐标系移动的视图的左上角。
合理的使用这个绘制手段,能够减少一些UI切图,甚至能够让代码变的简单。
例1
例如, filmora中有一个这样的绘制:

在阿拉伯语下,需要翻过来。 绘制成这样:

如果我们熟练的使用坐标体系转换的算法,就能够更快速达到此效果。
例2
现在有一个单位渐变画刷, 从左至右颜色渐变,现在需要将该画刷拉伸填充到指定区域中,并且另外
一个区域要反过来,例如下过如下所示。

除了QPainter有Transform, QBrush也有Transform。
Filmora中,颜色体系是记录在一个单独的文件中,不同的皮肤,颜色不一样,而一些渐变效果,我们能提取到
的渐变画刷都是单位画刷,如何将单位画刷填充到指定区域了,我们假定获取到单位画刷如下:
QLinearGradient gradient;
gradient.setColorAt(0, QColor(0, 0, 0, 0));
gradient.setColorAt(1, Qt::blue);
gradient.setStart(QPoint(0, 0));
gradient.setFinalStop(QPoint(1, 1));
QBrush brush(gradient);
第一步,我们需要将画刷填充到QRect(100, 100, 200, 100)的区域:
QBrush newBrush = brush;
auto trans = newBrush.transform();
trans.translate(100, 100);
trans.scale(200, 100);
newBrush.setTransform(trans);
然后使用该画刷填充QRect(100, 100, 200, 100)的区域,就能达到上图1的效果。
第二步,我们需要将画刷左右镜像之后填充到QRect(100, 300, 200, 100)的区域:
QBrush brush2 = brush;
auto trans2 = brush2.transform();
trans2.setMatrix(-1, 0, 0, 0, 1, 0, 0, 0, 1); //这是单位画刷镜像矩阵,参考上面的转换图
trans2.translate(-300, 300);
/* 我们如何再去理解画刷的坐标体系。
转换之前,坐标的原点在左上角,从左至右x值增长,从上至下y值增长
转换之后,坐标的原点不变, 从右至左x值增长, 从上至下y值增长。
因此这个时候,我们需要将原点,移植目标区域右上角,也就是(300, 300)的位置,
由于x是反的,所以x值得反过来移动。所以需要将转换之后的原点,进行(-300, 300)的移动
当然如果你先执行translate,在执行矩阵转换,可以写成如下所示:
trans2.translate(300, 300); // 先移动原点,就是正向移动
trans2.setMatrix(-1, 0, 0, 0, 1, 0, 0, 0, 1);
*/
trans2.scale(200, 100); //这里是拉伸,就没啥可讲的了
brush2.setTransform(trans2);
那么使用上面这个画刷,就可以填充QRect(100, 300, 200, 100)的区域,并达到上图2的效果。
其它的功能
QPainter还有两个常用接口,save和restore,具体的作用是将当前绘制状态进行压栈和出栈,这里不细说。
另外使用QPainter进行绘制,我们可以直接绘制带透明度的效果出来,但有时候,我们需要整体透明,如果挨个绘制
去调整透明度,就显得非常的麻烦,我们直接使用QPainter.setOpacity。
例如:一些控件的Disabled态,一般就是整体调整透明度,比如Filmora中的这个控件:

在Filmora11时,禁用态没有任何处理,显得格外的显眼,这是一个自绘控件,我们在只需要在disabled时,
设置QPainter的透明度为0.2-0.6就能达到置灰的效果。
QPaintDevice
这是在二维空间可以使用QPainter绘制的抽象设备对象。其默认坐标系的原点位于左上角。QPaintDevice的绘图功能目前由
QWidget、QImage、QPixmap、QGLPixelBUffer、QPicture和QPrinter子类实现。
在不同设备下,使用QPainter有一些不同区别。我常用的绘制,是绘制在窗口上,而绘制在窗口和绘制其他的设备上,是有
一个典型区别的。
当我们以窗口为绘制设备时,我们必须且只能在绘制的消息循环中进行绘制,Qt的帮助文档中,说只能在绘制的回调函数paintEvent
中绘制,是错误的。
paintEvent只是绘制消息的一个常规回调函数,只是整个绘制消息循环的一部分,我们还可以重载更高等级的分发函数进行绘制处理,
当然这是非常规的手段。
但还有一个常规手段,就是使用事件过滤器过滤绘制消息,然后进行绘制,总而言之,只需要在绘制回调中,绘制都是没有毛病的。
那这里有一个问题,就是为什么,我们不能在其它的消息循环中对窗口进行绘制呢?
这就需要讲一下窗口绘制的一个标准过程:
为了保证绘制的流畅和顺滑性,通常情况我们需要保证可以高频的进行绘制(前提是当其需要高频变化时),而人的肉眼可查的频率是60nps,也是
一般视频的1s的播放帧数,而绘制本身是一件耗性能的事情,所以既需要高频绘制,又需要对其做一些限制,因为超高频绘制是没有意义的,
除了浪费性能以外。
所以基本所有UI库的GUI绘制,都有这么一个标准库过程。就是使用专门的绘制消息,对窗口进行绘制,其它的所有消息,仅仅可以做一些辅助,
在修改时,通知刷新,而这个通知刷新,并不会立马刷新,而是需要等待统一的时机,这个时机,就是下一个可刷新的帧率点。
而一次完整GUI重绘过程,包括两步:1. 重新清理好需要刷新的区别,2. 将新的需要展示的东西绘制上去。
而我们用Qt进行窗口绘制时,似乎都没有想过有第一步,清空,这是因为,Qt在开始执行一个窗口的绘制回调时,已经帮你清理好了,这也是为什么
我们不能在其它回调中,进行绘制,因为这些绘制,都是无效,最终会被绘制回调清空掉。
我们总结一下:
- 绘制过程是一个耗性能的事情。
- 为了保证绘制的顺滑性,绘制回调可能会被高频的执行。
- Qt的绘制回调会主动帮你清空,需要刷新的区域,你只需要绘制即可。
- 因为绘制回调的主动清空行为,所以你不能在其它回调中,对窗口进行绘制。
上面有说到,肉眼可分辨的nps是60nps,也就是1秒60帧,反过来表示单帧的事件为167ms左右,如果单次的绘制事件高于这个时间,那么
我们就不可能做到60nps,再加上我们还需要处理其他的消息,例如:鼠标按下、移动、定时器等等消息,考虑我们还有留一些事件给其他的消息,
所以单次绘制的事件,必须远远低于167ms,否则界面的表现,一定会有卡顿感,具体的时间,一般控制在30ms以内。
因此,我们在进行绘制时,需要要遵循一个原则:只在绘制回调中,处理界面绘制相关的逻辑。不要再绘制中处理一些复杂且重复的事情,这些可以
提前做好准备,常见的错误,有两个:
- 在绘制回调绘制图片,直接使用IO加载本地图片
- 在绘制回调中,对图片进行平滑的缩放处理。
在QPainter的设置渲染质量中讲到,使用平滑绘制图片的效果,远远低于image.scale的算法,这是因为scale算法本身非常的耗时,它不适合封装为
QPainter的函数,同时Qt默认的渲染质量是最差的,目的就是因为渲染本身就是非常耗时的。
但新的问题来了,如果单次绘制的事件就是超过了100ms,导致画面非常卡顿了,怎么办呢?
其实最常见的手段,就是使用双缓存,我在优化Filmora时间线的绘制时,就是用了贴纸的策略,可以理解为双缓存的进阶版本。
具体的细节,这里就不说了。
QPen与QBrush
QPen是画笔,用于定义线条类型、颜色等,当然它可以作用于绘制文字的颜色(仅仅只是颜色哈),其它的就没啥作用了,而在讲绘制圆角边框时,
使用QPen绘制,效果并不理想。
QPen的具体用法,参考一下帮助文档,还是蛮简单的。
QBrush是画刷,主要主用于填充,比如:纯色填充、纹理填充、渐变填充和图片填充。当然线条这种玩意,也可以填充,线条不过是最低宽度为1
的区域,也是可以进行填充,甚至填充效果,比绘制效果,有时候更佳哦。
其中纯色填充比较简单,纹理填充,Qt内置一些类型。

详细的参考帮助文档,有人可能会问,除了这些内置的纹理类型,可不可以自定义一些纹理呢?当然可以,这就需要使用到图片填充,做到自定义
纹理的效果,其中图片填充,会以循环的形式进行填充,例如:

具体的用法,也挺简单单的,只不过,我们需要注意的是,画刷本身也是有坐标系,坐标系为QPainter的坐标系,而非填充区域,即便你所需要的填充
区域是一个矩形,有明确的左上角,它的原点仍然是QPainter的原点。
当然你可以单独的修改QBrush的坐标系,而不是去调整QPainter的坐标系。
尤其是当我们已图片为画刷,进行填充时,尤其是需要考虑这一点。
QPainterPath
绘制路径是一些基础的几何图形块的组合体,比如:矩形、椭圆、直线线和曲线。部分基础图形可以合成一个封闭的路径,例如:矩形和椭圆。一个封闭
路径通常由一个相同的起点和终点。当然,也可以使用直线和曲线这种未封闭的子路径组合而成。
绘制路径可以用于填充、勾勒和裁剪,如果需要给指定的绘制路径生成填充轮廓,请使用QPainterPathStroker。
绘制路径有很多好用的功能,可以参考帮助文档,这里主要讲其中一个非常使用的功能,就是多个绘制路径之间进行合并或者交叉的函数。
例如,我们填充以下的区域:

实现这个效果,有3中方法:
1.勾勒
使用线条和弧线去勾勒这样一个封闭区域,然后再填充,代码就补充了,这种用法很蠢,以前我就这么玩过,老多代码,还需要调半天。
2.分区域填充
先填充一个矩形区域,然后再填充圆角矩形区域,这个方法简单,但是有一个致命缺陷,就是如果想要半透明效果,那么交叉区域的颜色
会变深。
3. 使用QPainterPath的组合方式,
参考以下代码:
QPainter painter(&widget);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(QRect(30, 30, 60, 60), 10, 10);
QPainterPath path2;
path2.addRect(QRect(30, 40, 60, 50));
path = path.intersected(path2);
painter.fillPath(path, Qt::blue);
使用不同的绘制路径进行叠加,能够快速达到预想的效果。
Update和Repaint
QEvent- Paint、UpdateRequest、UpdateLater
总结
使用QPainter,能非常方便的去绘制一些质量很高的东西,为了保证更好的灵活性,想要达到指定效果,就需要我们去挖掘,基本上绝大数GUI绘制想要的效果,使用Qt的绘制都是可以达到的。但是我们一定要弄清楚,绘制本身是非常耗性能的,因此在绘制时,一定要注意这点,不要再绘制做一些重复的事情,而是需要考虑做一个提前的准备。另外,还有调色板(QPalette),这玩意是用来辅助样式绘制的,渐变(QGradient),也能辅助绘制很多好玩的玩意。以及定时器和动画,可以用于辅助绘制一些动画效果。
评论(0)