警告
本节包含从 C++ 自动翻译到 Python 的代码片段,可能存在错误。
转换示例#
转换示例展示转换如何影响 QPainter 渲染图形原语的方式。
转换示例展示了转换如何影响 QPainter 渲染图形原语的方式。特别是它展示了转换顺序如何影响结果。
该应用程序允许用户通过更改 QPainter 坐标系的平移、旋转和缩放来操作形状的渲染。
该示例由两个类和一个全局枚举组成
RenderArea
类控制特定形状的渲染。
Window
类是应用程序的主窗口。
Operation
枚举描述应用程序中可用的各种转换操作。
首先,我们将快速查看 Operation
枚举,然后我们将审查 RenderArea
类以了解形状的渲染方式。最后,我们将查看在 Window
类中实现的转换应用程序的功能。
转换操作#
通常,QPainter 在与其关联的设备自己的坐标系上操作,但它也很好地支持坐标转换。
绘制设备的默认坐标系原点位于左上角。x 值向右增加,y 值向下降。您可以使用 QPainter::scale() 函数使用一个给定的偏移量缩放坐标系,您可以使用 QPainter::rotate() 函数顺时针旋转它,您可以使用 QPainter::translate() 函数将其平移(即向点添加一个给定的偏移量)。您还可以使用 QPainter::shear() 函数围绕原点扭曲坐标系(称为剪切)。
所有转换操作都操作 QPainter 的转换矩阵,您可以使用 QPainter::worldTransform() 函数检索它。一个矩阵将平面上的一个点转换为另一个点。有关转换矩阵的更多信息,请参阅坐标系和 QTransform 文档。
Operation = { NoTransformation, Translate, Rotate, Scale }
全局 Operation
枚举在 renderarea.h
文件中声明,并描述了转换应用程序中可用的各种转换操作。
RenderArea
类定义#
RenderArea
类继承自 QWidget
,并控制特定形状的渲染。
class RenderArea(QWidget): Q_OBJECT # public RenderArea(QWidget parent = None) def setOperations(operations): def setShape(shape): QSize minimumSizeHint() override QSize sizeHint() override # protected def paintEvent(event):
我们声明了两个公共函数 setOperations()
和 setShape()
,以便能够指定 RenderArea
小部件的形状以及转换形状渲染在其内的坐标系。
我们重新实现了 QWidget 的 minimumSizeHint() 和 sizeHint() 函数,为 RenderArea 小部件在我们的应用程序中提供一个合理的尺寸,并重新实现了 paintEvent() 事件处理器来绘制渲染区域的形状并应用用户的变换选择。
# private def drawCoordinates(painter): def drawOutline(painter): def drawShape(painter): def transformPainter(painter): operations = QList() shape = QPainterPath() xBoundingRect = QRect() yBoundingRect = QRect()
我们还声明了几个方便的函数来绘制形状、坐标系轮廓和坐标,并根据选择的变换转换绘图器。
此外,RenderArea 小部件保留了一张当前应用变换操作的列表、一个对形状的引用以及我们将使用来渲染坐标的一组方便的变量。
RenderArea 类实现#
RenderArea 小部件通过重新实现 paintEvent() 事件处理器控制特定形状的渲染,包括坐标系的变换。但在谈论 paintEvent() 事件处理器之前,我们会简要看看构造函数以及提供给 RenderArea 小部件的访问函数。
def __init__(self, parent): super().__init__(parent) newFont = font() newFont.setPixelSize(12) setFont(newFont) fontMetrics = QFontMetrics(newFont) xBoundingRect = fontMetrics.boundingRect(tr("x")) yBoundingRect = fontMetrics.boundingRect(tr("y"))
在构造函数中,我们将父参数传递给基类,并定制了我们渲染坐标将要使用的字体。font() 函数返回当前设置给小部件的字体。只要没有设置特殊字体或者 call setFont()后,这将是小部件类的字体、父字体(如果这个小部件是顶级小部件,则为默认应用程序字体)。
在保证了字体大小为 12 点后,我们使用 QFontMetrics 类提取了包围坐标字母“x”和“y”的矩形。
QFontMetrics 提供了访问字体、其字符以及使用字体渲染的字符串的各个度量信息的功能。QFontMetrics::boundingRect() 函数返回给定字符相对于基线最左端的边界矩形。
def setOperations(self, operations): self.operations = operations update() def setShape(self, shape): self.shape = shape update()
在 setShape() 和 setOperations() 函数中,我们通过存储新值或值,然后调用 update() 插槽来更新 RenderArea 小部件,该插槽将安排一个绘制事件在 Qt 返回主事件循环时处理。
def minimumSizeHint(self): return QSize(182, 182) def sizeHint(self): return QSize(232, 232)
我们重新实现了 QWidget
中的 minimumSizeHint()
和 sizeHint()
函数,以便在我们的应用程序中为 RenderArea
小部件提供一个合理的大小。如果没有为这个小部件设置布局,这些函数的默认实现会返回一个无效的大小;否则,返回布局的最小尺寸或首选尺寸。
def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) painter.fillRect(event.rect(), QBrush(Qt.white)) painter.translate(66, 66)
paintEvent()
事件处理程序接收 RenderArea
小部件的绘制事件。绘制事件是指请求重新绘制小部件的所有或部分区域。它可能是由于 repaint()
或 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 状态,因为绘制坐标是最后的绘制操作。
def drawCoordinates(self, 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")) def drawOutline(self, painter): painter.setPen(Qt.darkGreen) painter.setPen(Qt.DashLine) painter.setBrush(Qt.NoBrush) painter.drawRect(0, 0, 100, 100) def drawShape(self, painter): painter.fillPath(shape, Qt.blue)
drawCoordinates()
、drawOutline()
和 drawShape()
是从 paintEvent()
事件处理程序调用的便利函数。有关 QPainter 的基本绘图操作和如何显示基本图形原语的信息,请参阅 Basic Drawing 示例。
def transformPainter(self, painter): for i in range(0, operations.size()): switch (operations[i]) { elif shape == Translate: painter.translate(50, 50) break elif shape == Scale: painter.scale(0.75, 0.75) break elif shape == Rotate: painter.rotate(60) break elif shape == NoTransformation: else:
方便函数 transformPainter()
还可以从事件处理器 paintEvent()
中调用来处理,它根据用户的转换选择,将给定的 QPainter 的坐标系进行转换。
窗口类定义#
Window
类是变换应用程序的主窗口。
应用程序显示四个 RenderArea
小部件。最左侧的小部件以 QPainter 的默认坐标系渲染形状,其他小部件除了向左侧小部件应用所有转换外,还使用所选择的转换渲染形状。
class Window(QWidget): Q_OBJECT # public Window() # public slots def operationChanged(): def shapeSelected(index):
我们声明了两个公共槽,使应用程序能够响应用户交互,根据用户的转换选择更新显示的 RenderArea
小部件。
当用户更改所选操作时,将调用 operationChanged()
槽来更新每个 RenderArea
小部件,并应用当前选择的转换操作。当用户更改首选形状时,将调用 shapeSelected()
槽来更新 RenderArea
小部件的形状。
# private def setupShapes(): enum { NumTransformedAreas = 3 } originalRenderArea = RenderArea() transformedRenderAreas[NumTransformedAreas] = RenderArea() shapeComboBox = QComboBox() operationComboBoxes[NumTransformedAreas] = QComboBox() shapes = QList()
我们还声明了一个私有方便函数,setupShapes()
,该函数在构建 Window
小部件时使用,并声明了对小部件各个组件的指针。我们选择将可用形状保存在 QPainterPaths 的 QList 中。此外,我们声明了一个私有的枚举,用于计算显示的 RenderArea
小部件数(不包括默认坐标系中渲染形状的小部件)。
窗口类实现#
在构造函数中,我们创建并初始化应用程序的组件
def __init__(self): originalRenderArea = RenderArea() shapeComboBox = QComboBox() shapeComboBox.addItem(tr("Clock")) shapeComboBox.addItem(tr("House")) shapeComboBox.addItem(tr("Text")) shapeComboBox.addItem(tr("Truck")) layout = QGridLayout() layout.addWidget(originalRenderArea, 0, 0) layout.addWidget(shapeComboBox, 1, 0)
首先创建将渲染形状在默认坐标系中小部件 RenderArea
。我们还创建了允许用户在四种不同形状中选择之一的关联 QComboBox
:时钟、房屋、文本和卡车。这些形状在构造函数的末尾通过 setupShapes()
方便函数创建。
for i in range(0, NumTransformedAreas): transformedRenderAreas[i] = RenderArea() operationComboBoxes[i] = 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, self.operationChanged) layout.addWidget(transformedRenderAreas[i], 0, i + 1) layout.addWidget(operationComboBoxes[i], 1, i + 1)
然后创建将根据坐标变换处理其形状的小部件 RenderArea
。默认情况下,应用的操作是无转换,即形状在默认坐标系中渲染。我们创建并初始化与相应转换操作对应的 QComboBox
。
我们还连接了QComboBox
的activated()
信号到operationChanged()
槽函数,以便在用户更改选定的转换操作时更新应用程序。
setLayout(layout) setupShapes() shapeSelected(0) setWindowTitle(tr("Transformations"))
最后,我们使用setLayout()
函数设置应用程序窗口的布局,使用私有的setupShapes()
便捷函数构建可用的形状,并在设置窗口标题之前,使用公共的shapeSelected()
槽函数使应用程序在启动时显示时钟形状。
def setupShapes(self): truck = QPainterPath() clock = QPainterPath() house = QPainterPath() text = QPainterPath() ... shapes.append(clock) shapes.append(house) shapes.append(text) shapes.append(truck) shapeComboBox.activated.connect( self.shapeSelected)
setupShapes()
函数在构造函数中被调用,创建表示应用程序中使用的形状的QPainterPath对象。有关构造细节,请参阅painting/transformations/window.cpp
示例文件。这些形状存储在QList中。QList::append()函数将给定的形状插入到列表的末尾。
我们还将与其相关的QComboBox
的activated()
信号连接到shapeSelected()
槽函数,以便在用户更改首选形状时更新应用程序。
def operationChanged(self): operationTable = { NoTransformation, Rotate, Scale, Translate operations = QList() for i in range(0, NumTransformedAreas): index = operationComboBoxes[i].currentIndex() operations.append(operationTable[index]) transformedRenderAreas[i].setOperations(operations)
当用户更改选定的操作时,会调用公共的operationChanged()
槽函数。
我们通过查询相关的QComboBoxes
来获取每个转换的RenderArea
小部件选定的转换操作。预计转换的小部件除了应用到其左侧的RenderArea
小部件的所有转换之外,还应该将形状与相关的组合框中指定的转换一起渲染。因此,对于每个我们查询的小部件,我们将相关操作追加到我们将应用到小部件的转换的QList中,然后再继续进行下一步。
def shapeSelected(self, index): shape = shapes[index] originalRenderArea.setShape(shape) for i in range(0, NumTransformedAreas): transformedRenderAreas[i].setShape(shape)
当用户更改首选形状时,会调用shapeSelected()
槽函数,使用它们公共的setShape()
函数更新RenderArea
小部件。
总结#
转换示例展示了转换如何影响QPainter绘制图形原语的方式。通常,QPainter在设备的坐标系统中操作,但它也很好地支持坐标转换。使用转换应用程序,您可以缩放、旋转和转换QPainter的坐标系统。应用这些转换的顺序对于结果至关重要。
所有转换操作都操作于QPainter的转换矩阵。有关转换矩阵的更多信息,请参阅坐标系统和QTransform文档。
Qt参考文档提供了多个绘画示例。其中之一是仿射变换示例,展示了Qt在绘画操作上执行变换的能力。该示例还允许用户尝试各种变换操作。