变换示例
变换示例展示了变换如何影响 QPainter 渲染图形原语的方式。
应用程序允许用户通过改变 QPainter 坐标系的平移、旋转和缩放来操作形状的渲染。
该示例由两个类和一个全局枚举组成
RenderArea
类控制给定形状的渲染。Window
类是应用程序的主窗口。Operation
枚举描述了应用程序中可用的各种变换操作。
首先,我们将快速查看 Operation
枚举,然后我们将回顾 RenderArea
类,以了解形状是如何渲染的。最后,我们将查看在 Window
类中实现的变换应用程序的功能。
变换操作
通常,QPainter 在关联设备的坐标系上操作,但它也很好地支持坐标变换。
绘图设备的默认坐标系原点在左上角。x 值向右增加,y 值向下增加。您可以使用 QPainter::scale() 函数通过给定偏移量缩放坐标系,使用 QPainter::rotate() 函数顺时针旋转它,并使用 QPainter::translate() 函数将其平移(即添加给定点)。您还可以使用 QPainter::shear() 函数围绕原点(称为剪切)扭曲坐标系。
所有变换操作都在 QPainter 的变换矩阵上操作,您可以使用 QPainter::worldTransform() 函数检索该矩阵。矩阵将平面上的点转换到另一个点。有关变换矩阵的更多信息,请参阅 Coordinate System 和 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的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()函数中,我们通过存储新值或值并调用QWidget::update()槽(它安排在Qt回到主事件循环时处理绘制事件)来更新RenderArea小部件。
QSize RenderArea::minimumSizeHint() const { return QSize(182, 182); } QSize RenderArea::sizeHint() const { return QSize(232, 232); }
我们重新实现了QWidget的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()函数清除需要重绘的区域。
我们还将坐标系与一个常量偏移量一起转换,以确保原始形状以合适的边距渲染。
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。
我们还连接了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()
槽在用户更改选择的操作时调用。
我们通过查询相关的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在绘图操作中执行变换的能力。该示例还允许用户尝试各种变换操作。
© 2024 Qt公司有限公司。此处包含的文档贡献均为其各自所有者的版权。所提供的文档根据Free Software Foundation发布的