变换示例
“变换”示例展示了变换如何影响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 类定义
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 的minimumSizeHint()和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() 函数中,我们通过存储新值来更新RenderArea 控件,随后调用QWidget::update()槽函数,该函数会安排一个绘制事件,待Qt返回主事件循环时进行处理。
QSize RenderArea::minimumSizeHint() const
{
return QSize(182, 182);
}
QSize RenderArea::sizeHint() const
{
return QSize(232, 232);
}我们重写了QWidget 的minimumSizeHint()和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()函数清除需要重绘的区域。
我们还通过一个常量偏移量对坐标系进行平移,以确保原始形状在渲染时带有适当的边距。
{
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 控件,并声明了指向该控件各组件的指针。我们选择将可用形状保存在由QPainterPath组成的QList 中。此外,我们声明了一个私有枚举,用于统计除在QPainter 默认坐标系中渲染形状的控件之外,已显示的RenderArea 控件的数量。
Window 类的实现
在构造函数中,我们创建并初始化应用程序的组件:
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 ,即形状在默认坐标系内渲染。我们创建并初始化相关的QComboBox对象,其项对应于全局Operation 枚举中描述的各种变换操作。
我们还将QComboBox的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()函数将给定的形状插入到列表的末尾。
我们还将关联的QComboBox 对象的activated()信号连接到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() 的public槽。
我们通过查询相关的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 在绘图操作中执行变换的能力。该示例还允许用户尝试各种变换操作。
© 2026 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.