警告

本节包含自动将 C++ 代码片段翻译成 Python 语言的片段,可能存在错误。

坐标系统#

有关绘图系统所使用的坐标系统信息。

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

QPaintDevice类是可以进行绘制的对象的基础类:其绘图功能由QWidgetQImageQPixmap,和 QPicture,以及QOpenGLPaintDevice类继承。绘图设备的默认坐标系统以其左上角为原点。在基于像素的设备上,默认单元为单像素;在打印机上,默认单位为点(1 英寸的 1/72)。

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

渲染

逻辑表示

图形原语的大小(宽度和高度)始终与其数学模型相对应,忽略其绘制时使用的笔宽。

coordinatesystem-rect1

coordinatesystem-line2

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)

抗锯齿绘图

在绘图时,像素渲染受抗锯齿渲染提示的控制。

RenderHint枚举用于指定可能或可能不被任何给定引擎尊重的标志位给QPainterAntialiasing值表示如果可能,引擎应抗锯齿原语边缘,即通过使用不同的颜色强度来平滑边缘。

但是,默认情况下画家是模糊的,并应用其他规则:当使用宽度为单像素的笔绘制时,像素将被渲染到数学定义点的右侧和下方。例如

coordinatesystem-rect-raster3

coordinatesystem-line-raster4

painter = QPainter(self)
painter.setPen(Qt.darkGreen)
# Using the (x  y  w  h) overload
painter.drawRect(1, 2, 6, 4)
painter = QPainter(self)
painter.setPen(Qt.darkGreen)
painter.drawLine(2, 7, 6, 1)

当使用具有偶数像素宽度的笔绘制时,像素将围绕数学定义点对称渲染,而当使用具有奇数像素宽度的笔绘制时,额外的像素将被渲染到数学点的右侧和下方,就像单像素的情况一样。下面是QRectF图的具体示例。

QRectF

qrect-diagram-zero5

qrectf-diagram-one6

逻辑表示

单像素宽笔

qrectf-diagram-two7

qrectf-diagram-three8

双像素宽笔

三像素宽笔

请注意,出于历史原因,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()函数。

抗锯齿绘图

如果您设置QPainteranti-aliasing绘制提示,则像素将在数学定义点的两侧以对称方式渲染。

coordinatesystem-rect-antialias9

coordinatesystem-line-antialias10

painter = QPainter(self)
painter.setRenderHint(
    QPainter.Antialiasing)
painter.setPen(Qt.darkGreen)
# Using the (x  y  w  h) overload
painter.drawRect(1, 2, 6, 4)
painter = QPainter(self)
painter.setRenderHint(
    QPainter.Antialiasing)
painter.setPen(Qt.darkGreen)
painter.drawLine(2, 7, 6, 1)

转换

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

您可以使用 scale() 函数通过对坐标系乘以给定偏移量来进行缩放,使用 rotate() 函数进行顺时针旋转,以及使用 translate() 函数进行平移(即在点上加给定偏移量)。

您还可以使用 shear() 函数将坐标系围绕原点旋转。所有变换操作都作用于 QPainter 的转换矩阵,您可以使用 worldTransform() 函数来检索它们。矩阵将平面上的一个点转换到另一个点。

如果您需要重复使用相同的变换,也可以使用 QTransform 对象和 worldTransform() 以及 setWorldTransform() 函数。您可以在任何时候通过调用 save() 函数来保存 QPainter 的转换矩阵,该函数在内部堆栈上保存矩阵。使用 restore() 函数将其弹出。

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

模拟时钟示例

coordinatesystem-analogclock15

模拟时钟示例展示了如何使用 QPainter 的转换矩阵绘制自定义小部件的内容。

我们建议在继续阅读之前编译并运行此示例。特别是,尝试调整窗口为不同的大小。

def paintEvent(self, arg__0):

    QPoint hourHand[4] = {
        QPoint(5, 14),
        QPoint(-5, 14),
        QPoint(-4, -71),
        QPoint(4, -71)

    QPoint minuteHand[4] = {
        QPoint(4, 14),
        QPoint(-4, 14),
        QPoint(-3, -89),
        QPoint(3, -89)

    QPoint secondsHand[4] = {
       QPoint(1, 14),
       QPoint(-1, 14),
       QPoint(-1, -89),
       QPoint(1, -89)

    hourColor = QColor(palette().color(QPalette.Text))
    minuteColor = QColor(palette().color(QPalette.Text))
    secondsColor = QColor(palette().color(QPalette.Accent))
    side = qMin(width(), height())
    painter = QPainter(self)
    painter.setRenderHint(QPainter.Antialiasing)
    painter.translate(width() / 2, height() / 2)
    painter.scale(side / 200.0, side / 200.0)

我们将坐标系转换为点(0, 0)在部件中心,而不是在左上角。我们还通过 side / 200 的比例来缩放系统,其中 side 是部件的宽度或高度中较短的边。我们希望时钟是正方形的,即使设备不是也是如此。

这将给我们一个 200 x 200 的正方形区域,其中原点 (0, 0) 在中心,我们可以在这个区域内进行绘图。我们在部件内绘制的将显示在可容纳部件中最大的正方形内。

请参阅“窗口视口转换”部分。

painter.save()
painter.rotate(30.0 * ((time.hour() + time.minute() / 60.0)))
painter.drawConvexPolygon(hourHand, 4)
painter.restore()

我们通过旋转坐标系并调用 drawConvexPolygon() 来绘制时钟的时针。多亏了这个旋转,它指向正确的方向。

多边形通过一个交替的 x, y 值数组和存储在 hourHand 静态变量(在函数开头定义)中来指定,这些值对应于三个点 (7, 8), (-7, 8), (0, -40)。

围绕代码的 save()restore() 调用确保后续代码不会被我们使用的转换打扰。

painter.setBrush(minuteColor)
painter.save()
painter.rotate(6.0 * time.minute())
painter.drawConvexPolygon(minuteHand, 4)
painter.restore()

之后,我们绘制时钟面的时针标志,由十二条30度间隔的短线条组成。当该循环完成后,绘图器已完整旋转回到其原始状态,因此我们无需保存和恢复状态。

painter.save()
painter.rotate(6.0 * time.second())
painter.drawConvexPolygon(secondsHand, 4)
painter.drawEllipse(-3, -3, 6, 6)
painter.drawEllipse(-5, -68, 10, 10)
painter.restore()

对于时钟的时针,我们可以使用同样的方法,它由三个点 (7, 8), (-7, 8), (0, -70) 定义。这些坐标指定了一个比分钟手更细更长的时针。

for j in range(0, 60):
    painter.drawLine(92, 0, 96, 0)
    painter.rotate(6.0)

最后,我们绘制时钟面的分钟标记,由60度间隔的短线条组成。我们跳过每个第五个分钟标记,因为我们不希望覆盖小时标记。这样,绘图器以一种不是非常有用的方式旋转,但我们已经完成了绘图,所以这并不重要。

有关变换矩阵的更多信息,请参阅QTransform 文档。

窗口-视口转换casting to this heading

使用 QPainter 绘图时,我们使用逻辑坐标来指定点,然后将其转换为绘图设备的物理坐标。

逻辑坐标到物理坐标的映射由 QPainter 的世界变换 worldTransform()(在 坐标系 部分描述)以及 QPainterviewport()window() 实现。视口表示指定任意矩形的物理坐标。"窗口"描述的是相同矩形的逻辑坐标。默认情况下,逻辑坐标系统和物理坐标系统是重合的,并且等同于画设备的矩形。

使用窗口-视口转换,您可以使逻辑坐标系统适应您的偏好。此机制还可以用于使绘图代码与画设备无关。例如,您可以通过调用 setWindow() 函数使逻辑坐标从 (-50, -50) 到 (50, 50),原点在中心。

painter = QPainter(self)
painter.setWindow(QRect(-50, -50, 100, 100))

现在,逻辑坐标 (-50, -50) 对应于画设备的物理坐标 (0, 0)。与画设备无关,您的绘图代码将始终在指定的逻辑坐标上操作。

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

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

如果我们使逻辑坐标系统成为一个正方形,我们也应该使用 setViewport() 函数将视口也变成一个正方形。在上面的例子中,我们使其成为适合画设备矩形的最大正方形。在设置窗口或视口时考虑画设备的大小,可以使绘图代码独立于画设备。

请注意,窗口-视口转换仅是线性变换,即它不会执行剪裁。这意味着,如果您在当前设置的“窗口”之外绘制,您的绘图仍将使用相同的线性代数方法转换为视口。

../_images/coordinatesystem-transformations.png

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

另请参阅

Analog Clock