坐标系

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

QPaintDevice 类是可绘图对象的基础类:其绘图能力由 QWidgetQImageQPixmapQPicture,和 QOpenGLPaintDevice 类继承。绘图设备的默认坐标系其原点位于左上角。在基于像素的设备上,默认单位是一个像素;在打印机上,默认单位是一个点(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::Antialiasing 表示引擎应尽可能对原型的边缘进行抗锯齿处理,即通过使用不同的颜色强度来平滑边缘。

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

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() 函数的返回值偏离了矩形的真正底部右角。

QRectright() 函数返回 left() + width() - 1,而 bottom() 函数返回 top() + height() - 1。图示中的底部绿色点显示了这些函数的返回坐标。

我们建议您直接使用 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() 函数平移它(即将给定的偏移量添加到点)。

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

如果您需要重复使用相同的变换,您还可以使用 QTransform 对象以及 QPainter::worldTransform() 和 QPainter::setWorldTransform() 函数。您可以随时通过调用 QPainter::save() 函数来保存 QPainter 的变换矩阵。它将矩阵存储在一个内部堆栈上。《a href="qpainter.html#restore" translate="no">QPainter::restore() 函数将其弹出。

变换矩阵的一个常见需求是在多种绘画设备上重复使用相同的绘图代码时。没有变换,结果将严格依赖于绘图设备的分辨率。例如,打印机的分辨率很高,例如每英寸600点,而屏幕通常在每英寸72到100点之间。

模拟时钟示例
模拟时钟示例显示了如何使用 QPainter 的变换矩阵来自定义组件的内容。

我们建议在阅读任何其他内容之前编译并运行此示例。特别是,尝试调整窗口的大小。

void AnalogClock::paintEvent(QPaintEvent *)
{
    static const QPoint hourHand[4] = {
        QPoint(5, 14),
        QPoint(-5, 14),
        QPoint(-4, -71),
        QPoint(4, -71)
    };
    static const QPoint minuteHand[4] = {
        QPoint(4, 14),
        QPoint(-4, 14),
        QPoint(-3, -89),
        QPoint(3, -89)
    };

    static const QPoint secondsHand[4] = {
       QPoint(1, 14),
       QPoint(-1, 14),
       QPoint(-1, -89),
       QPoint(1, -89)
    };

    const QColor hourColor(palette().color(QPalette::Text));
    const QColor minuteColor(palette().color(QPalette::Text));
    const QColor secondsColor(palette().color(QPalette::Accent));

    int side = qMin(width(), height());

    QPainter painter(this);
    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();

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

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

使用 QPainter::save() 和 QPainter::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 (int j = 0; j < 60; ++j) {
        painter.drawLine(92, 0, 96, 0);
        painter.rotate(6.0);
    }

最后,我们绘制时钟面的分针标记,由60条6度间隔的短线组成。我们跳过了每个第五分钟标记,因为我们不想覆盖小时标记。之后,绘图器以一种不太有用的方式旋转,但我们已经完成了绘图,所以这无关紧要。

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

窗口视图转换

当使用 QPainter 绘制时,我们使用逻辑坐标来指定点,这些点随后被转换成绘图设备的物理坐标。

逻辑坐标到物理坐标的映射由 QPainter 的全局变换 worldTransform()(在变换部分中描述)以及QPainterviewport() 和 window() 处理。视口表示指定任意矩形的物理坐标。同样,“窗口”描述的是逻辑坐标中的同一个矩形。默认情况下,逻辑坐标系统与物理坐标系统相叠,等效于绘图设备的矩形。

使用窗口视口转换,您可以调整逻辑坐标系统以适应您的偏好。该机制还可以用于使绘图代码与绘图设备独立。例如,您可以通过调用QPainter::setWindow() 函数将逻辑坐标从 (-50, -50) 扩展到 (50, 50),其中(0, 0)位于中心。

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

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

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

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

painter.setViewport(x, y, side, side);

如果我们使逻辑坐标系为正方形,我们也应该使用QPainter::setViewport() 函数将视口也设置为正方形。在上面的示例中,我们使其与包含在绘图设备矩形中的最大正方形等效。通过在设置窗口或视口时考虑绘图设备的尺寸,可以保持绘图代码与绘图设备的独立性。

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

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

另请参阅模拟时钟

© 2024 Qt 公司。本文件包含的文档贡献属于其各自所有者的版权。本文件提供的文档是根据自由软件基金会发布的GNU自由文档许可证版本1.3的条款许可的。Qt 和相关标志是芬兰以及/或全球其他国家的 The Qt Company Ltd. 的商标。所有其他商标均为其各自所有者的财产。