变换示例

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

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

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

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

首先,我们将快速查看 Operation 枚举,然后我们将回顾 RenderArea 类,以了解形状是如何渲染的。最后,我们将查看在 Window 类中实现的变换应用程序的功能。

变换操作

通常,QPainter 在关联设备的坐标系上操作,但它也很好地支持坐标变换。

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

所有变换操作都在 QPainter 的变换矩阵上操作,您可以使用 QPainter::worldTransform() 函数检索该矩阵。矩阵将平面上的点转换到另一个点。有关变换矩阵的更多信息,请参阅 Coordinate SystemQTransform 文档。

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

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

RenderArea 类定义

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 小部件的形状以及变换形状在其内部渲染的坐标系。

我们重新实现了QWidget的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小部件保留了一个应用操作列表、其形状的引用,以及我们将在渲染坐标时使用的几个便利变量。

RenderArea类实现

RenderArea小部件通过重新实现QWidget::paintEvent()事件处理程序来控制给定形状的渲染,包括坐标系的变换。但在做之前,我们将简要地看看构造函数和提供对RenderArea小部件访问功能的函数。

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()函数返回为小部件设置的当前字体。如果没有设置特殊字体,或者在调用QWidget::setFont()之后,这可能是小部件类的特殊字体、父的字体或(如果此小部件是顶级小部件)应用程序的默认字体。

在确保字体大小为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()函数中,我们通过存储新值或值并调用QWidget::update()槽(它安排在Qt回到主事件循环时处理绘制事件)来更新RenderArea小部件。

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

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

我们重新实现了QWidget的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小部件的绘制事件。绘制事件是对重绘小部件全部或部分内容的要求。它可能是由于QWidget::repaint()或QWidget::update()的结果,也可能是由于小部件被遮挡然后又曝光,或者由于很多其他原因。

首先为RenderArea小部件创建一个QPainter。QPainter::Antialiasing渲染提示指示如果可能的话,引擎应对原语边缘进行抗锯齿。然后我们使用QPainter::fillRect()函数清除需要重绘的区域。

我们还将坐标系与一个常量偏移量一起转换,以确保原始形状以合适的边距渲染。

    painter.save();
    transformPainter(painter);
    drawShape(painter);
    painter.restore();

在我们开始渲染形状之前,我们调用 QPainter::save() 函数。

QPainter::save() 保存当前的画笔状态(即将状态推入堆栈),包括当前的坐标系。保存画笔状态的原因是,接下来的对 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 的默认坐标系渲染形状,其他小部件除了左侧已应用的转换外,还以选择的变换渲染形状。

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 小部件时使用,并声明了指向小部件各种组件的指针。我们选择将可用的形状保留在 QList 中,其中的元素是 QPainterPath。此外,我们声明了一个私用枚举,用于计算除了在 QPainter 的默认坐标系中渲染形状的小部件之外,显示的 RenderArea 小部件的数量。

窗口类实现

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

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小部件。默认情况下,应用的操作是无变换,即形状在默认坐标系中渲染。我们创建并初始化与各种全局Operation枚举中描述的变换操作对应的相关的QComboBox

我们还连接了QComboBoxactivated()信号到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()槽在用户更改选择的操作时调用。

我们通过查询相关的QComboBox获取每个已变换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参考文档提供了多个绘图示例。其中之一是仿射变换示例,该示例展示了Qt在绘图操作中执行变换的能力。该示例还允许用户尝试各种变换操作。

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。此处包含的文档贡献均为其各自所有者的版权。所提供的文档根据Free Software Foundation发布的GNU自由文档许可证1.3版本的条款进行许可。Qt及其相关标志是芬兰及/或全球其他地区Qt公司有限公司的商标。所有其他商标均为其各自所有者的财产。