警告

本节包含从 C++ 自动翻译到 Python 的代码片段,可能存在错误。

转换示例#

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

转换示例展示了转换如何影响 QPainter 渲染图形原语的方式。特别是它展示了转换顺序如何影响结果。

../_images/transformations-example.png

该应用程序允许用户通过更改 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

我们还连接了QComboBoxactivated()信号到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()函数将给定的形状插入到列表的末尾。

我们还将与其相关的QComboBoxactivated()信号连接到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在绘画操作上执行变换的能力。该示例还允许用户尝试各种变换操作。

示例项目 @ code.qt.io