✎Qt-doc—QPainter绘图系统&坐标系统

Paint System
Coordinate System

绘图系统(Paint System)

Qt的绘画系统可以使用相同的API在屏幕和打印设备上进行绘画,并且主要基于QPainter、QPaintDevice和QPaintEngine类。

  • QPainter用于执行绘图操作;
  • QPaintDevice是二维空间的抽象,可以使用QPainter在其上绘制;
  • QPaintEngine提供了QPainter用于在不同类型的设备上绘制的接口,QPaintEngine类在内部由QPainter和QPaintDevice使用。

    这种方法的主要优点是,所有绘制都遵循相同的绘制管道,从而可以轻松添加对新功能的支持,并为不受支持的功能提供默认实现。

绘图坐标系统(Paint Coordinate System)

坐标系由QPainter类控制。QPainterQPaintDeviceQPaintEngine类一起构成了Qt绘画系统的基础。QPaint用于执行绘图操作,QPaintDevice是二维空间的抽象,可以使用QPainter在其上绘制,而QPaintEngine提供了QPainter用于在不同类型的设备上绘制的接口。

QPaintDevice类是可以绘制的对象的基类:其绘制功能由QWidget、QImage、QPixmap、QPicture和QOpenGLPaintDevices类继承。绘制设备的默认坐标系的原点位于左上角。x值向右增加,y值向下增加。默认单位是基于像素的设备上的一个像素和打印机上的一点(1/72英寸)。

逻辑QPainter坐标到物理QPaintDevice坐标的映射由QPainter的变换矩阵(transformation)、视口(viewport)和“窗口(window)”处理。默认情况下,逻辑坐标系和物理坐标系重合。QPainter还支持坐标变换(例如旋转和缩放)。

渲染(Rendering)

逻辑表示(Logical Representation)

图形基本体的大小(宽度和高度)始终与其数学模型相对应,而忽略了渲染时使用的QPen的宽度:

走样绘制(Aliased Painting)

绘制时,像素渲染由QPainter::Antialiasing render hint控制。

RenderHint枚举用于指定QPainter的标志,这些标志可能会被任何给定引擎所遵循,也可能不会被任何给定的引擎所遵循。QPainter::Antialiasing值指示引擎应尽可能对基本体的边缘进行抗锯齿,即使用不同的颜色强度平滑边缘。

但默认情况下,painter是走样的(aliased),这时的规则是:当使用1像素宽的笔进行渲染时,像素将被渲染到数学定义的点的右下方。例如:(QPen:1像素宽)

当使用像素为偶数的笔进行渲染时,像素将围绕数学定义的点对称渲染;而使用像素为奇数的笔渲染时,多余的像素将渲染到数学点的右侧和下方,就像在一个像素的情况下一样。具体示例请参见下面的QRectF图。

请注意,由于历史原因,对于QRect(而非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()函数

抗锯齿绘制(Anti-aliased Painting)

如果设置QPainter的抗锯齿(anti-aliasing)渲染提示(render hint),像素将在数学定义的点的两侧对称渲染:

变换(Transformations)

默认情况下,QPainter在关联设备自己的坐标系上操作,但它也完全支持仿射坐标变换。

可以使用QPainter::scale()函数按给定偏移量缩放坐标系,可以使用QPainter::rotate()函数顺时针旋转坐标系,也可以使用QPainter::translate()函数平移坐标系

还可以使用QPainter::shear()函数围绕原点扭曲坐标系。所有转换操作都是在操作QPainter的转换矩阵(transformation matrix),你可以使用QPainter::worldTransform()函数获取该矩阵。矩阵将平面中的一个点转换为另一个点。

如果需要反复进行相同的变换,还可以使用QTransform对象以及QPainter::worldTransform()QPainter::setWorldTransorm()函数。你可以随时通过调用QPainter::save()函数保存QPainter的转换矩阵,该函数将矩阵保存在内部堆栈上。QPainter::restore()函数将其弹出。

转换矩阵的一个常见需求是在各种绘制设备上重用相同的绘图代码。在没有变换的情况下,结果将与绘制设备的分辨率紧密相关。打印机具有高分辨率,例如每英寸600点,而屏幕通常每英寸72到100点。

Analog Clock Example

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);
    /* codes continue -> [绘制时针] */

我们转换坐标系,使点(0,0)位于小部件的中心,而不是位于左上角。我们还按side/200缩放系统,其中side是小部件的宽度或高度,以最短的为准。我们希望时钟是正方形的,即使设备不是正方形的。
这将给我们一个200x200方形区域,原点(0,0)在中心,我们可以在上面绘制。我们绘制的内容将显示在小部件中可以容纳的最大的方形中。

/* -> [绘制时针] */
p->setPen(Qt::NoPen);
p->setBrush(hourColor);
QTime time = QTime::currentTime();
p->save();
p->rotate(30.0 * ((time.hour() + time.minute() / 60.0)));
p->drawConvexPolygon(hourHand, 3);
p->restore();
/* codes continue -> [绘制分针] */

void QPainter::drawConvexPolygon(const QPointF *points, int pointCount)
Draws the convex polygon(凸多边形) defined by the first [pointCount] points in the array [points] using the current pen.

我们通过旋转坐标系并调用QPainter::drawConvexPolygon()来绘制时钟的时针。感谢旋转,它画的方向是正确的。
多边形被指定为x,y交替值的数组,存储在hourHand静态变量中,对应于四个点(2,0)、(0,2)、(-2,0)和(0,-25)。
围绕代码对QPainter::save()和QPainter::restore()的调用保证了后面的代码不会受到我们使用的转换的干扰。

/* -> [绘制分针] */
p->setPen(Qt::NoPen);
p->setBrush(minuteColor);
p->save();
p->rotate(6.0 * (time.minute() + time.second() / 60.0));
p->drawConvexPolygon(minuteHand, 3);
p->restore();
/* codes continue -> [绘制钟面] */

我们对钟面的分针也这样做,它由四个点(1,0)、(0,1)、(-1,0)和(0,-40)定义。这些坐标指定的钟指针比分针更细、更长。

/* -> [绘制钟面] */
p->setPen(hourColor);
p->setBrush(hourColor);
p->save();
for (int i = 0; i < 12; ++i) {
	p->drawLine(88, 0, 96, 0);
	p->rotate(30.0);
}
p->resore();
p->setPen(minuteColor);
p->setBrush(minuteColor);
for (int j = 0; j < 60; ++j) {
	if ((j % 5) != 0)
		p->drawLine(92, 0, 96, 0);
	p->rotate(6.0);
}
}// end render()

最后,我们画出钟面,它由12条间隔30度的短线组成。最后,painter以一种不太有用的方式旋转,但我们已经完成了绘画,所以这并不重要。(因为旋转的是坐标系而不是整个画布,所以原来画上去的一切图形的位置是保持原样不变的。)

窗口-视口转换(Window-Viewport Conversion)

使用QPainter绘制时,我们使用逻辑坐标(logical coordinates)指定点,然后将这些点转换为绘制设备(paint device)的物理坐标(physical coordinates)。

逻辑坐标到物理坐标的映射由QPainter的worldTransform()以及viewport()window()处理。视口(viewport)表示指定任意矩形的物理坐标。“窗口(window)”以逻辑坐标描述相同的矩形。默认情况下,逻辑坐标系和物理坐标系重合,并相当于绘制设备的矩形。

使用窗口-视口转换,可以使逻辑坐标系符合你的首选项。该机制还可用于使绘图代码独立于绘制设备。例如,你可以通过调用QPainter::setWindow()函数,使逻辑坐标从(-50,-50)扩展到(50,50),中间为(0,0):

QPainter painter(this);
painter.setWindow(QRect(-50, -50, 100, 100));

void QPainter::setWindow(const QRect &rectangle)
Sets the painter’s window to the given [rectangle], and enables view transformations.
The window rectangle is part of the view transformation. The window specifies the logical coordinate system. Its sister, the viewport(), specifies the device coordinate system.
The default window rectangle is the same as the device’s rectangle.

现在,逻辑坐标(-50,-50)对应于绘制设备的物理坐标(0,0)。独立于绘制设备,绘制代码将始终在指定的逻辑坐标上运行。

通过设置“窗口”或视口矩形,可以执行坐标的线性变换。请注意,“窗口”的每个角都映射到视口的相应角,反之亦然。因此,通常最好让视口和“窗口”保持相同的纵横比,以防止变形:

int side = qMin(width(), height());
int x = (width() - side / 2);
int y = (height() - side / 2);
painter.setViewport(x, y, side, side);

void QPainter::setViewport(int x, int y, int width, int height)
Sets the painter’s viewport rectangle to be the rectangle beginning at ([x], [y]) with the given [width] and [height].

如果将逻辑坐标系设置为正方形,还应使用QPainter::setViewport()函数将视口设置为正方形。在上面的示例中,我们将其等效为适合绘制设备矩形的最大正方形。通过在设置窗口或视口时考虑绘制设备的大小,可以使图形代码独立于绘制设备。
请注意,窗口-视口转换只是一个线性转换,即它不执行剪裁。这意味着,如果在当前设置的“窗口”外绘制,绘制仍会使用相同的线性代数方法转换到视口。

视口、“窗口”和变换矩阵确定逻辑QPainter坐标如何映射到绘制设备的物理坐标。默认情况下,世界变换矩阵是单位矩阵,“窗口”和视口设置与绘制设备的设置等效,即世界、“窗口”与设备坐标系等效,但正如我们所见,可以使用变换操作和窗口-视口转换来操纵系统。上图描述了该过程。

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值