变换示例

变换示例展示了变换如何影响QPainter 渲染图形基元的方式。

该应用程序允许用户通过改变QPainter 的坐标系的平移、旋转和缩放来操作图形的渲染。

该示例由两个类和一个全局枚举组成:

  • RenderArea 类控制给定形状的渲染。
  • Window 类是应用程序的主窗口。
  • Operation 枚举描述了应用程序中可用的各种变换操作。

首先,我们将快速浏览Operation 枚举,然后查看RenderArea 类,了解形状是如何呈现的。最后,我们将看看Window 类中实现的 "变换 "应用程序功能。

变换操作

通常情况下,QPainter 会在相关设备自身的坐标系上进行操作,但它对坐标变换也有很好的支持。

喷涂设备的默认坐标系原点位于左上角。x 值向右增加,y 值向下增加。您可以使用QPainter::scale() 函数按给定偏移量缩放坐标系,使用QPainter::rotate() 函数顺时针旋转坐标系,使用QPainter::translate() 函数平移坐标系(即在各点上添加给定偏移量)。您还可以使用QPainter::shear() 函数将坐标系绕原点扭转(称为剪切)。

所有变换操作都是在QPainter 的变换矩阵上进行的,您可以使用QPainter::worldTransform() 函数检索该矩阵。矩阵将平面中的一点变换为另一点。有关变换矩阵的更多信息,请参阅坐标系QTransform 文档。

enum Operation { NoTransformation, Translate, Rotate, Scale };

全局Operation 枚举在renderarea.h 文件中声明,描述了 "变换 "应用程序中可用的各种变换操作。

渲染区域类定义

RenderArea 类继承于QWidget ,用于控制给定形状的渲染。

class RenderArea : public QWidget
{
    Q_OBJECT

public:
    RenderArea(QWidget *parent = nullptr);

    void setOperations(const QList<Operation> &operations);
    void setShape(const QPainterPath &shape);

    QSize minimumSizeHint() const override;
    QSize sizeHint() const override;

protected:
    void paintEvent(QPaintEvent *event) override;

我们声明了两个公共函数setOperations()setShape() ,以便指定RenderArea widget 的形状,并转换形状渲染的坐标系。

我们重新实现了QWidgetminimumSizeHint() 和sizeHint() 函数,使RenderArea 部件在我们的应用程序中具有合理的大小,我们还重新实现了QWidget::paintEvent() 事件处理程序,以应用用户的变换选择来绘制呈现区域的形状。

private:
    void drawCoordinates(QPainter &painter);
    void drawOutline(QPainter &painter);
    void drawShape(QPainter &painter);
    void transformPainter(QPainter &painter);

    QList<Operation> operations;
    QPainterPath shape;
    QRect xBoundingRect;
    QRect yBoundingRect;
};

我们还声明了几个方便函数,用于绘制形状、坐标系轮廓和坐标,并根据所选变换对绘制器进行变换。

此外,RenderArea widget 还保留了当前应用的变换操作列表、形状引用和几个方便变量,我们将在渲染坐标时使用这些变量。

渲染区域类的实现

RenderArea 部件通过重新实现QWidget::paintEvent() 事件处理程序来控制给定形状的渲染,包括坐标系的变换。不过,我们首先要快速了解一下构造函数和提供访问RenderArea widget 的函数:

RenderArea::RenderArea(QWidget *parent)
    : QWidget(parent)
{
    QFont newFont = font();
    newFont.setPixelSize(12);
    setFont(newFont);

    QFontMetrics fontMetrics(newFont);
    xBoundingRect = fontMetrics.boundingRect(tr("x"));
    yBoundingRect = fontMetrics.boundingRect(tr("y"));
}

在构造函数中,我们将父参数传递给基类,并自定义用于呈现坐标的字体。QWidget::font() 函数会返回当前为 widget 设置的字体。只要没有设置特殊字体,或者在调用QWidget::setFont() 之后,该字体要么是 widget 类的特殊字体,要么是父类的字体,要么是默认的应用程序字体(如果该 widget 是顶级 widget)。

在确保字体大小为 12 点后,我们使用QFontMetrics 类提取包围坐标字母 "x "和 "y "的矩形。

QFontMetrics 该类提供了一些函数,用于访问字体、字体字符和以字体渲染的字符串的各个指标。 () 函数返回给定字符相对于基线最左点的边界矩形。QFontMetrics::boundingRect

void RenderArea::setOperations(const QList<Operation> &operations)
{
    this->operations = operations;
    update();
}

void RenderArea::setShape(const QPainterPath &shape)
{
    this->shape = shape;
    update();
}

setShape()setOperations() 函数中,我们通过存储一个或多个新值来更新RenderArea widget,然后调用QWidget::update() 槽,该槽会调度一个绘制事件,以便在 Qt 返回主事件循环时进行处理。

QSize RenderArea::minimumSizeHint() const
{
    return QSize(182, 182);
}

QSize RenderArea::sizeHint() const
{
    return QSize(232, 232);
}

我们重新实现了QWidgetminimumSizeHint() 和sizeHint() 函数,使RenderArea 部件在我们的应用程序中具有合理的大小。如果该部件没有布局,这些函数的默认实现将返回无效尺寸,否则将分别返回布局的最小尺寸或首选尺寸。

void RenderArea::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.fillRect(event->rect(), QBrush(Qt::white));

    painter.translate(66, 66);

paintEvent() 事件处理程序接收RenderArea widget 的绘制事件。绘制事件是重新绘制全部或部分部件的请求。它可能是QWidget::repaint() 或QWidget::update() 的结果,也可能是因为部件被遮挡而现在又被揭开,还可能是其他许多原因。

首先,我们为RenderArea widget 创建一个QPainterQPainter::Antialiasing 渲染提示指出,引擎应尽可能对基元的边缘进行反锯齿处理。然后,我们使用QPainter::fillRect() 函数擦除需要重新绘制的区域。

此外,我们还使用恒定偏移量平移坐标系,以确保在渲染原始形状时留出适当的余量。

    {
        QPainterStateGuard guard(&painter);
        transformPainter(painter);
        drawShape(painter);
    }

在开始渲染形状之前,我们会实例化一个QPainterStateGuard 来保存当前的绘制器状态(即把状态推入堆栈),包括作用域中的当前坐标系。保存绘制器状态的理由是,接下来调用transformPainter() 函数时,坐标系将根据当前选择的变换操作进行变换,因此我们需要一种方法来返回原始状态以绘制轮廓。

变换坐标系后,我们绘制RenderArea 的形状,然后使用QPainter::restore() 函数恢复绘制器状态(即从堆栈中弹出保存的状态)。

    drawOutline(painter);

然后绘制正方形轮廓。

    transformPainter(painter);
    drawCoordinates(painter);
}

由于我们希望坐标与渲染形状时的坐标系一致,因此必须再次调用transformPainter() 函数。

对于共享像素来说,绘制操作的顺序至关重要。我们之所以不在坐标系已转换为渲染形状时渲染坐标,而是将渲染推迟到最后,是因为我们希望坐标显示在形状及其轮廓之上。

由于绘制坐标是最后一个绘制操作,因此这次无需保存QPainter 状态。

void RenderArea::drawCoordinates(QPainter &painter)
{
    painter.setPen(Qt::red);

    painter.drawLine(0, 0, 50, 0);
    painter.drawLine(48, -2, 50, 0);
    painter.drawLine(48, 2, 50, 0);
    painter.drawText(60 - xBoundingRect.width() / 2,
                     0 + xBoundingRect.height() / 2, tr("x"));

    painter.drawLine(0, 0, 0, 50);
    painter.drawLine(-2, 48, 0, 50);
    painter.drawLine(2, 48, 0, 50);
    painter.drawText(0 - yBoundingRect.width() / 2,
                     60 + yBoundingRect.height() / 2, tr("y"));
}

void RenderArea::drawOutline(QPainter &painter)
{
    painter.setPen(Qt::darkGreen);
    painter.setPen(Qt::DashLine);
    painter.setBrush(Qt::NoBrush);
    painter.drawRect(0, 0, 100, 100);
}

void RenderArea::drawShape(QPainter &painter)
{
    painter.fillPath(shape, Qt::blue);
}

drawCoordinates()drawOutline()drawShape()paintEvent() 事件处理程序调用的方便函数。有关QPainter 的基本绘制操作以及如何显示基本图形基元的更多信息,请参阅基本绘制示例。

void RenderArea::transformPainter(QPainter &painter)
{
    for (int i = 0; i < operations.size(); ++i) {
        switch (operations[i]) {
        case Translate:
            painter.translate(50, 50);
            break;
        case Scale:
            painter.scale(0.75, 0.75);
            break;
        case Rotate:
            painter.rotate(60);
            break;
        case NoTransformation:
        default:
            ;
        }
    }
}

transformPainter() 方便函数也从paintEvent() 事件处理程序中调用,并根据用户的变换选择变换给定的QPainter 坐标系。

窗口类定义

Window 类是变换应用程序的主窗口。

应用程序显示四个RenderArea 部件。最左侧的窗口小部件以QPainter 的默认坐标系渲染形状,其他窗口小部件则以所选变换以及应用于其左侧RenderArea 窗口小部件的所有变换渲染形状。

class Window : public QWidget
{
    Q_OBJECT

public:
    Window();

public slots:
    void operationChanged();
    void shapeSelected(int index);

我们声明了两个公共槽,使应用程序能够响应用户交互,根据用户的变换选择更新显示的RenderArea 部件。

operationChanged() 插槽应用当前选择的变换操作更新每个RenderArea 部件,每当用户更改所选操作时都会被调用。每当用户更改首选形状时,shapeSelected() 槽就会更新RenderArea 部件的形状。

private:
    void setupShapes();

    enum { NumTransformedAreas = 3 };
    RenderArea *originalRenderArea;
    RenderArea *transformedRenderAreas[NumTransformedAreas];
    QComboBox *shapeComboBox;
    QComboBox *operationComboBoxes[NumTransformedAreas];
    QList<QPainterPath> shapes;
};

我们还声明了一个私有便捷函数setupShapes() ,用于构建Window 部件,并声明了指向该部件各个组件的指针。我们选择将可用形状保存在QListQPainterPaths 中。此外,我们还声明了一个私有枚举,用于计算显示的RenderArea 部件的数量,但以QPainter 的默认坐标系渲染形状的部件除外。

窗口类的实现

在构造函数中,我们创建并初始化应用程序的组件:

Window::Window()
{
    originalRenderArea = new RenderArea;

    shapeComboBox = new QComboBox;
    shapeComboBox->addItem(tr("Clock"));
    shapeComboBox->addItem(tr("House"));
    shapeComboBox->addItem(tr("Text"));
    shapeComboBox->addItem(tr("Truck"));

    QGridLayout *layout = new QGridLayout;
    layout->addWidget(originalRenderArea, 0, 0);
    layout->addWidget(shapeComboBox, 1, 0);

首先,我们创建RenderArea 部件,它将在默认坐标系中呈现形状。我们还创建了相关的QComboBox ,允许用户从四种不同的形状中进行选择:时钟、房子、文字和卡车。形状本身是在构造函数末尾使用setupShapes() 方便函数创建的。

    for (int i = 0; i < NumTransformedAreas; ++i) {
        transformedRenderAreas[i] = new RenderArea;

        operationComboBoxes[i] = new QComboBox;
        operationComboBoxes[i]->addItem(tr("No transformation"));
        operationComboBoxes[i]->addItem(tr("Rotate by 60\xC2\xB0"));
        operationComboBoxes[i]->addItem(tr("Scale to 75%"));
        operationComboBoxes[i]->addItem(tr("Translate by (50, 50)"));

        connect(operationComboBoxes[i], &QComboBox::activated,
                this, &Window::operationChanged);

        layout->addWidget(transformedRenderAreas[i], 0, i + 1);
        layout->addWidget(operationComboBoxes[i], 1, i + 1);
    }

然后,我们创建RenderArea 小部件,通过坐标变换来呈现形状。默认情况下,应用的操作是No Transformation ,即在默认坐标系中渲染形状。我们创建并初始化相关的QComboBoxes,其中包含与全局Operation 枚举描述的各种变换操作相对应的项目。

我们还将QComboBoxes 的activated() 信号连接到operationChanged() 插槽,以便在用户更改所选变换操作时更新应用程序。

    setLayout(layout);
    setupShapes();
    shapeSelected(0);

    setWindowTitle(tr("Transformations"));
}

最后,我们使用QWidget::setLayout() 函数设置应用程序窗口的布局,使用私有的setupShapes() 方便函数构建可用的形状,并在设置窗口标题前使用公共的shapeSelected() 插槽使应用程序在启动时显示时钟形状。

void Window::setupShapes()
{
    QPainterPath truck;
    QPainterPath clock;
    QPainterPath house;
    QPainterPath text;
    ...
    shapes.append(clock);
    shapes.append(house);
    shapes.append(text);
    shapes.append(truck);

    connect(shapeComboBox, &QComboBox::activated,
            this, &Window::shapeSelected);
}

构造函数会调用setupShapes() 函数,并创建代表应用程序中使用的形状的QPainterPath 对象。有关构造的详细信息,请参阅painting/transformations/window.cpp 示例文件。这些形状存储在QList 中。QList::append() 函数会在列表末尾插入给定的形状。

我们还将相关的QComboBoxactivated() 信号连接到shapeSelected() 槽,以便在用户更改首选形状时更新应用程序。

void Window::operationChanged()
{
    static const Operation operationTable[] = {
        NoTransformation, Rotate, Scale, Translate
    };

    QList<Operation> operations;
    for (int i = 0; i < NumTransformedAreas; ++i) {
        int index = operationComboBoxes[i]->currentIndex();
        operations.append(operationTable[index]);
        transformedRenderAreas[i]->setOperations(operations);
    }
}

每当用户更改所选操作时,公共operationChanged() 槽就会被调用。

我们通过查询相关的QComboBoxes 来获取每个已转换的RenderArea 部件所选择的转换操作。转换后的RenderArea 部件除了应用于其左侧RenderArea 部件的所有转换应该用其相关组合框指定的转换来呈现形状。因此,对于我们查询的每个部件,我们都会将相关操作附加到QList 的变换中,然后再应用到下一个部件。

void Window::shapeSelected(int index)
{
    QPainterPath shape = shapes[index];
    originalRenderArea->setShape(shape);
    for (int i = 0; i < NumTransformedAreas; ++i)
        transformedRenderAreas[i]->setShape(shape);
}

每当用户更改首选形状时,shapeSelected() 槽就会被调用,并使用公共setShape() 函数更新RenderArea 部件。

总结

变换示例展示了变换如何影响QPainter 渲染图形基元的方式。通常情况下,QPainter 在设备自身的坐标系上运行,但它对坐标变换也有很好的支持。通过 "变换 "应用程序,您可以缩放、旋转和平移QPainter 的坐标系。这些变换的应用顺序对结果至关重要。

所有变换操作都在QPainter 的变换矩阵上进行。有关变换矩阵的更多信息,请参阅坐标系QTransform 文档。

Qt 参考文档提供了多个绘画示例。其中的Affine Transformations示例展示了 Qt 在绘画操作中执行变换的能力。该示例还允许用户尝试各种变换操作。

示例项目 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.