一、抗锯齿渲染
1.1 逻辑绘图
图形基元的大小(宽度和高度)始终与其数学模型相对应,下图示意了忽略其渲染时使用的画笔的宽度的样子。
1.2 物理绘图(默认情况)
在默认的情况下,绘制会产生锯齿,并且使用这样的规则进行绘制: 当使用宽度为一个像素的画笔进行渲染时,像素会在数学定义的点的右边和下边进行渲染,如下图1所示。当使用一个拥有偶数像素的画笔进行渲染时,像素会在数学定义的点的周围对称渲染;而当使用一个拥有奇数像素的面笔进行渲染时,首先按照偶数对称绘制,最后一个像素会被渲染到数学定义的点的右边和下边,如下图2所示。
所以看起来图像不是很平滑,像是有锯齿,所以为了消锯齿,就要用到抗锯齿绘图。
1.3 抗锯齿绘图
抗锯齿( Anti-aliased)又称为反锯齿或者反走样,就是对图像的边缘进行平滑处理,使其看起来更加柔和流畅的一种技术。QPaint er 进行绘制时可以使用QPainter ::RenderHint 渲染提示来指定是否要使用抗锯齿功能,RenderHint 取值分为以下三种。
如果在绘制时使用了抗锯齿渲染提示,即使用 QPainter:: setRenderHint(RenderHint hint, bool on = true) 函数,将参数 hint 设置为了 QPainter:: Antialiasing。那么像素就会在数学定义的点的两侧对称的进行渲染,如下图所示。
示例程序为:
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); //抗锯齿和使用平滑转换算法
二、坐标系统
2.1 坐标系统简介
Qt的坐标系统是由QPainter类控制的,而QPainter是在绘图设备上绘制的。一个绘图设备的默认坐标系统中原点(0, 0)在其左上角,x坐标向右增长,y坐标向下增长。在基于像素的设备上,默认的单位是一个像素,而在打印机上默认的单位是一个点(1/72英寸)。
下面仍然在上一节的程序中进行代码演示,更改paintEvent()的内容如下:
void Widget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setBrush(Qt::red);
painter.drawRect(0, 0, 100, 100);
painter.setBrush(Qt::yellow);
painter.drawRect(-50, -50, 100, 100);
}
我们先在原点(0,0)绘制了一个长宽都是100像素的红色矩形,又在(-50,-50)点绘制了一个同样大小的黄色矩形。可以看到,我们只能看到黄色矩形的四分之一部分。运行程序,效果如下图所示。
2.2 坐标系统变换
默认情况下,QPainter在指定设备的坐标系统上进行绘制,在进行绘图时,使用QPainter::translate()函数平移坐标系统;可以使用QPainter::scale()函数缩放坐标系统;使用QPainter::rotate()函数顺时针旋转坐标系统;还可以使用QPainter::shear()围绕原点来扭曲坐标系统。如下图所示。
我们可以使用前面提到的那些便捷函数进行坐标系统变换,也可以通过QTransform类实现。
(1)平移变换
将paintEvent()函数内容更改如下:
void Widget::paintEvent(QPaintEvent *)
{
//平移坐标系统
QPainter painter(this);
painter.setBrush(Qt::yellow);
painter.drawRect(0, 0, 50, 50);
//将坐标系原点向右、向下平移100像素点,即使原点坐标变为(100,100)
painter.translate(100, 100);
painter.setBrush(Qt::red);
painter.drawRect(0, 0, 50, 50);
//将坐标系原点向左、向上平移100像素点,即重新使原点坐标变为(0,0)
painter.translate(-100, -100);
painter.drawLine(0, 0, 20, 20);
}
这里先在原点(0, 0)绘制了一个宽、高均为50的正方形,然后使用translate()函数将坐标系统进行了平移,使(100, 100)点成为了新原点,所以我们再次进行绘制的时候,虽然drawRect()中的逻辑坐标还是(0, 0)点,但实际显示出来的却是在(100, 100)点的红色正方形。可以再次使用translate()函数进行反向平移,使原点重新回到窗口左上角。运行程序,效果如下图所示。
(2)缩放变换
将paintEvent()函数中的内容更改如下:
void Widget::paintEvent(QPaintEvent *)
{
//缩放坐标系统
QPainter painter(this);
painter.setBrush(Qt::yellow);
painter.drawRect(0, 0, 100, 100);
//将坐标系统的横、纵坐标都放大两倍
painter.scale(2, 2);
painter.setBrush(Qt::red);
painter.drawRect(50, 50, 50, 50);
}
可以看到,当我们使用scale()函数将坐标系统的横、纵坐标都放大两倍以后,逻辑上的(50,50)点变成了窗口上的(100, 100)点,而逻辑上的长度50,绘制到窗口上的长度却是100。运行程序,效果如下图所示。
(3)旋转变换
将paintEvent()函数更改如下:
void Widget::paintEvent(QPaintEvent *)
{
//旋转坐标系统
QPainter painter(this);
painter.drawLine(0, 0, 100, 0);
//以原点为中心,顺时针旋转30度
painter.rotate(30);
painter.drawLine(0, 0, 100, 0);
painter.translate(100, 100);
painter.rotate(30);
painter.drawLine(0, 0, 100, 0);
}
这里先绘制了一条水平的直线,然后将坐标系统旋转了30度,又绘制了一条直线。可以看到,默认是以原点(0, 0)为中心旋转的。如果想改变旋转中心,可以使用translate()函数,比如这里将中心移动到了(100, 100)点,然后旋转了30度,又绘制了一条直线。运行程序,效果如下图所示。
(4)扭曲变换
将paintEvent()函数更改如下:
void Widget::paintEvent(QPaintEvent *)
{
//扭曲坐标系统
QPainter painter(this);
painter.setBrush(Qt::yellow);
painter.drawRect(0, 0, 50, 50);
//纵向扭曲变形
painter.shear(0, 1);
painter.setBrush(Qt::red);
painter.drawRect(50, 0, 50, 50);
}
shear()有两个参数,第一个是对横向进行扭曲,第二个是对纵向进行扭曲,而取值就是扭曲的程度。比如程序中对纵向扭曲值为1,那么就是红色正方形左边的边下移一个单位,右边的边下移两个单位,值为1就表明右边的边比左边的边多下移一个单位。大家可以更改取值,测试效果。运行程序,效果如下图所示。
2.3 ”窗口-视口”转换
在使用QPainter进行绘制时,会使用逻辑坐标进行绘制,然后再转换为绘图设备的物理坐标。逻辑坐标到物理坐标的映射由QPainter的worldTransform()函数和QPainter的viewport()以及window()函数进行处理。其中视口(viewport)表示物理坐标下指定的一个任意矩形,而窗口(window,与以前讲的窗口部件的概念不同)表示逻辑坐标下的相同的矩形。默认的,逻辑坐标和物理坐标是重合的,它们都相当于绘图设备上的矩形。
使用”窗口-视口”转换可以使逻辑坐标系统适合应用的要求,这个机制也可以用来让绘图代码独立于绘图设备。 下面来看一个例子。
创建一个Widget窗口应用(其默认宽400像素,高300像素,左上角为原点)。首先正常绘制一个正方形:
void Widget::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setPen(Qt::blue);
p.drawRect(0, 0, 100, 100);
}
这个是没有问题的,因为现在的绘图设备就是Widget,其左上角就是原点(0, 0)点。效果如下图所示。
现在我们使用setWindow来设置逻辑坐标矩形:
void Widget::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setPen(Qt::blue);
p.drawRect(0, 0, 100, 100);
p.setWindow(-50, -50, 100, 100);
p.setPen(Qt::red);
p.drawRect(0, 0, 100, 100);
}
这时,效果如下图所示。
现在来说p.setWindow(-50, -50, 100, 100)的作用,它将逻辑坐标矩形(后面提到的术语window窗口)与我们现在的设备物理坐标矩形(后面提到的术语viewport视口)进行了线性映射,这里所说的设备物理坐标矩形就是我们可见的Widget的坐标,就是左上角为(0, 0)点,宽400,高300这样的矩形,线性映射的示意图如下:
也就是说,调用p.setWindow(-50, -50, 100, 100)之后,再次使用p进行绘制,那么坐标原点就不再是Widget的左上角了,而是到了其中心,以前绘制的宽100、高100的正方形,现在也会按比例变为宽400, 高300,也就是我们看到的这个红色矩形。
再来修改代码:
void Widget::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setPen(Qt::blue);
p.drawRect(0, 0, 100, 100);
p.setWindow(-50, -50, 100, 100);
p.setPen(Qt::red);
p.drawRect(0, 0, 20, 20);
}
运行效果如下图所示:
我们将绘制的红色矩形变小,可以明显看到,本应该是个正方形,现在却变成了长方形。就是因为上面说的比例变换造成的,那么怎么才能让它显示应有的形状呢,我们来设置视口:
void Widget::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setPen(Qt::blue);
p.drawRect(0, 0, 100, 100);
int side = qMin(width(), height());
p.setViewport((width() - side)/2, (height() - side) /2, side, side);
p.setWindow(-50, -50, 100, 100);
p.setPen(Qt::red);
p.drawRect(0, 0, 20, 20);
}
现在使用setViewport设置视口为一个正方形,就是Widget可是区域上最大的正方形,这样逻辑坐标和物理坐标进行比例变换的时候,红色矩形的宽和高就不会因为缩放的比例不同而发生变形了,如下图所示。
问题提出:那么为什么要修改这个逻辑坐标矩形?
这是为了便于我们绘图,因为我们一般绘图时只是想在标准的坐标系中应该绘制成什么样子,不会考虑不同绘图设备的具体坐标系(比如有的设备坐标原点在其左上角,有的在中心等等),也不会考虑窗口的大小不同而使用不同的代码(比如我们只想在一个宽100、高100的绘图区域的中心绘制一个高20、宽20的正方形,到底实际绘图设备的单位是像素、还是英寸、还是厘米,我们不用考虑)。
引申术语:窗口、视口
这里说的我们想象中的宽100、高100、原点在中心的绘图区域,就是逻辑坐标下的矩形,也就是使用setWindow设置的所谓的窗口;而实际的绘图设备,比如这里的Widget部件,其可视化的区域上设置的一个矩形被称为视口(英文为viewport),默认就是可视化区域的大小,但是可以通过setViewport来设置。窗口与视口相对应,可以进行线性变换,这样,我们就可以通过先设置视口,再设置对应的窗口的方法,来确保我们的代码在标准的想象中的坐标系中绘制的图形,可以准确地显示在不同的绘图设备界面上。
参考: