目录
概述
坐标系统由QPainter类控制。与QPaintDevice和QPaintEngine类一起,QPainter构成了Qt绘画系统的基础。QPainter用于执行绘制操作,QPaintDevice是一个二维空间的抽象,可以使用QPainter在其上进行绘制,QPaintEngine提供了画家用于在不同类型设备上绘制的界面。
QPaintDevice类是可绘制对象的基类:它的绘制功能由QWidget、QImage、QPixmap、QPicture和QOpenGLPaintDevice类继承。绘制设备的默认坐标系统起源于左上角。x值向右增大,y值向下增大。在基于像素的设备上,默认单位是一个像素,在打印机上是一个点(1/72英寸)。
逻辑QPainter坐标到物理QPaintDevice坐标的映射是由QPainter的转换矩阵、视口和“窗口”处理的。逻辑坐标系统和物理坐标系统默认重合。QPainter还支持坐标变换(例如旋转和缩放)。
渲染
逻辑表示
一个图元的大小(宽度和高度)总是对应于它的数学模型,忽略渲染它的笔的宽度
![]() | ![]() |
QRect(QPoint(1, 2), QPoint(7, 6)) | QLine(QPoint(2, 7), QPoint(6, 1)) |
QLine(2, 7, 6, 1) | |
QRect(QPoint(1, 2), QSize(6, 4)) | |
QRect(1, 2, 6, 4) |
锯齿绘制
绘制时,像素渲染由QPainter::Antialiasing渲染提示控制。
RenderHint枚举用于指定QPainter的标志,这些标志可能被任何给定的引擎尊重,也可能不被尊重。QPainter::Antialiasing值表示引擎应该尽可能地消除锯齿的边缘,即通过使用不同的颜色强度平滑边缘。
但在默认情况下,painter是走样(有锯齿)的,其他规则也适用:当使用一个像素宽的笔渲染时,像素将被渲染到右边,并低于数学定义的点。例如:
![]() | ![]() |
QPainter painter(this); painter.setPen(Qt::darkGreen); // Using the (x y w h) overload painter.drawRect(1, 2, 6, 4); | QPainter painter(this); painter.setPen(Qt::darkGreen); painter.drawLine(2, 7, 6, 1); |
当使用具有偶数像素的笔进行渲染时,像素将围绕数学定义的点对称地渲染,而使用具有奇数像素的笔进行渲染时,多余的像素将像在一个像素的情况下一样呈现在数学点的右侧和下方。有关具体示例,请参见下面的QRectF图。
QRectF | |
---|---|
![]() | ![]() |
逻辑表示 | 一个像素宽的笔 |
![]() | ![]() |
两像素宽的笔 | 三像素宽笔 |
请注意,由于历史原因,QRect::right()和QRect::bottom()函数的返回值偏离了矩形的真正右下角。
QRect的right()函数返回left() + width() - 1,而bottom()函数返回top() + height() - 1。图中右下角的绿色点显示了这些函数的返回坐标。
我们建议你直接使用QRectF: QRectF类使用浮点坐标在平面中定义一个矩形来保证精度(QRect使用整数坐标),而QRectF::right()和QRectF::bottom()函数则返回真正的右下角。
或者,使用QRect,应用x() + width()和y() + height()来找到右下角,避免使用right()和bottom()函数。
抗锯齿的绘画
如果你设置了QPainter的抗锯齿渲染提示,像素将在数学定义的点的两边对称地渲染:
![]() | ![]() |
QPainter painter(this); painter.setRenderHint( QPainter::Antialiasing); painter.setPen(Qt::darkGreen); // Using the (x y w h) overload painter.drawRect(1, 2, 6, 4); | QPainter painter(this); painter.setRenderHint( QPainter::Antialiasing); painter.setPen(Qt::darkGreen); painter.drawLine(2, 7, 6, 1); |
坐标转换(矩阵变换)
默认情况下,QPainter在关联设备自己的坐标系统上操作,但它也完全支持仿射坐标变换。
您可以使用QPainter::scale()函数按给定的偏移量缩放坐标系统,您可以使用QPainter::rotate()函数顺时针旋转它,并且可以使用QPainter::translate()函数平移它(即向点添加给定的偏移量)。
![]() | ![]() | ![]() | ![]() |
nop | rotate() | scale() | translate() |
你也可以使用QPainter::shear()函数围绕原点旋转坐标系统。所有的转换操作都是在QPainter的转换矩阵上进行的,你可以使用QPainter::worldTransform()函数来获取这个矩阵。矩阵将平面上的一个点变换为另一个点。
如果你需要反复使用相同的变换,你也可以使用QTransform对象以及QPainter::worldTransform()和QPainter::setWorldTransform()函数。你可以在任何时候通过调用QPainter::save()函数来保存QPainter的转换矩阵,该函数将矩阵保存在内部堆栈上。QPainter::restore()函数弹出它。
在各种绘图设备上重用相同的绘图代码时,经常需要转换矩阵。如果没有变换,结果将与绘制设备的分辨率紧密绑定。打印机的分辨率很高,例如每英寸600点,而屏幕的分辨率通常在每英寸72到100点之间。
模拟时钟示例
Analog Clock示例展示了如何使用QPainter的转换矩阵绘制自定义小部件的内容。
我们建议在进一步阅读之前编译并运行此示例。特别是,尝试将窗口大小调整为不同的大小。
Analog Clock Window Example | Qt GUI 5.15.17
void AnalogClockWindow::render(QPainter *p)
{
static const QPoint hourHand[3] = {
QPoint(7, 8),
QPoint(-7, 8),
QPoint(0, -40)
};
static const QPoint minuteHand[3] = {
QPoint(7, 8),
QPoint(-7, 8),
QPoint(0, -70)
};
QColor hourColor(127, 0, 127);
QColor minuteColor(0, 127, 127, 191);
p->setRenderHint(QPainter::Antialiasing);
p->translate(width() / 2, height() / 2);
int side = qMin(width(), height());
p->scale(side / 200.0, side / 200.0);
我们转换坐标系统,使点(0,0)位于小部件的中心,而不是位于左上角。我们还将系统按side / 100进行缩放,其中side是小部件的宽度或高度,以最短的为准。我们希望时钟是方的,即使设备不是方的。
这将给我们一个200 × 200的正方形区域,原点(0,0)在中心,我们可以在上面绘图。我们绘制的内容将显示在适合小部件的最大可能的正方形中。
QTime time = QTime::currentTime();
p->save();
p->rotate(30.0 * ((time.hour() + time.minute() / 60.0)));
p->drawConvexPolygon(hourHand, 3);
p->restore();
我们通过旋转坐标系统并调用QPainter::drawConvexPolygon()来绘制时钟的时针。多亏了旋转,它指向了正确的方向。
多边形被指定为一个交替的x、y值数组,保存在静态变量hourHand中(在函数开始时定义),它对应四个点(2,0)、(0,2)、(- 2,0)和(0,-25)。
对代码周围的QPainter::save()和QPainter::restore()的调用保证了接下来的代码不会被我们使用的转换操作干扰。
p->save();
p->rotate(6.0 * (time.minute() + time.second() / 60.0));
p->drawConvexPolygon(minuteHand, 3);
p->restore();
我们对时钟的分针也是这样做的,分针由四个点(1,0)、(0,1)、(- 1,0)和(0,-40)定义。这些坐标指定了一个比分针更细更长的指针。
p->setPen(minuteColor);
for (int j = 0; j < 60; ++j) {
if ((j % 5) != 0)
p->drawLine(92, 0, 96, 0);
p->rotate(6.0);
}
最后,我们画出钟面,它由12条30度间隔的短线组成。最后,painter会以一种不太有用的方式旋转,但我们已经完成了绘制,所以这无关紧要。
有关转换矩阵的更多信息,请参阅 QTransform 文档。
Window-Viewport转换
- world(世界)坐标:转换(移动,缩放,旋转)后的逻辑坐标(没有转换则等于逻辑坐标)
- window(窗口)坐标:逻辑坐标
- viewport(视口)坐标:物理坐标
当使用QPainter绘图时,我们使用逻辑坐标指定点,然后将其转换为绘图设备的物理坐标。
逻辑坐标到物理坐标的映射由QPainter的坐标转换worldTransform()(在上面坐标转换部分中描述)以及QPainter的viewport()和window()处理。视口表示指定任意矩形的物理坐标。“窗口”在逻辑坐标中描述了相同的矩形。默认情况下,逻辑坐标系统和物理坐标系统是一致的,并且等价于绘制设备的矩形。
使用window-viewport转换,你可以让逻辑坐标系统符合你的偏好。该机制也可以用于使绘图代码独立于绘图设备。例如,你可以调用QPainter::setWindow()函数,将逻辑坐标从(-50,-50)扩展到(50,50),中心为(0,0):
setFixedSize(100,100);//宽高100像素
QPainter painter(this);
painter.setWindow(QRect(-50, -50, 100, 100));//将(-50,-50)作为(左上角)坐标,宽高是100,那么右下角是(50,50);
painter.setPen(QColor(255,0,0));
painter.drawRect(0,0,50,50);
现在,逻辑坐标(-50,-50)对应于绘图设备的物理坐标(0,0)。独立于绘图设备,您的绘图代码将始终在指定的逻辑坐标上操作。
setFixedSize(100,100);//宽高100像素
QPainter painter(this);
painter.setPen(QColor(255,0,0));
painter.setWindow(QRect(-50,-50,50,50));//将(-50,-50)作为圆点坐标,宽高是50,那么绘制一个像素的大小缩放了(width()/50,height()/50)倍,右下角未缩放前是(50,50),缩放后是(25,25)
painter.drawRect(-50,-50,25,25);
painter.drawRect(-25,-25,25,25);
通过设置"window"或视口矩形,可以对坐标进行线性变换。注意,“窗口”的每个角落都映射到视口的相应角落,反之亦然。因此,让视口和“窗口”保持相同的宽高比以防止变形通常是一个好主意:
setFixedSize(100,100);//宽高100像素
int side = qMin(width(), height());
int x = (width() - side / 2);
int y = (height() - side / 2);
painter.setPen(QColor(0,255,0));
painter.setViewport(x, y, side, side);//将(50,50)作为物理坐标的(0,0),物理坐标坐标的宽高不变都是100px,右下角等于(100-50,100-50)
painter.drawRect(0,0,49,49);//此时0,0将会绘制在中间位置
如果我们将逻辑坐标系统设置为一个正方形,那么我们也应该使用QPainter::setViewport()函数将视口设置为一个正方形。在上面的示例中,我们使其等同于适合绘制设备矩形的最大正方形。通过在设置窗口或视口时考虑绘图设备的尺寸,可以使绘图代码独立于绘图设备。
setFixedSize(100,100);
QPainter painter(this);
painter.drawRect(0,0,100,100);
painter.save();
painter.setPen(QColor(255,0,0));
painter.setWindow(QRect(-50,-50,100,100));
painter.drawRect(0,0,50,50);
painter.setPen(QColor(0,255,0));
//从这里可以看到QRect表示的是当前(转化后)的位置
painter.setViewport(QRect(10, 10, width(), height()));//setWindow之后的(10,10)实际是在最开始(60,60)的位置
painter.drawRect(0,0,49,49);//此时0,0将会绘制在中间偏移10px
请注意,窗口-视口转换只是一个线性转换,即它不执行裁剪。这意味着,如果你在当前设置的“窗口”之外绘制,你的绘制仍然会使用相同的线性代数方法转换到视口。
视口、“窗口”和 坐标转换 决定了逻辑QPainter坐标如何映射到绘制设备的物理坐标。默认情况下,坐标转换是单位矩阵,“窗口”和视口设置等同于绘制设备的设置,即世界、“窗口”和设备坐标系是等效的,但正如我们所看到的,系统可以使用坐标转换和窗口-视口转换来操纵。上面的插图描述了这个过程。