QGraphicsSimpleTextItem() 的 setRotation 方法慢 zommig 和 pannig 交互

setRotation method for QGraphicsSimpleTextItem() slow zommig and pannig interaction

我有一个 GUI,显示行和文本与每一行对齐,我使用 QGraphicsSimpleTextItem() 作为文本。

问题:

如果没有在 QGraphicsSimpleTextItem() 上使用旋转方法缩放和平移(subclass QGraphcisView)交互 运行s 快速,但如果将旋转分配给这些项目文本缩放和平移交互变得很慢

问题:

我使用 Line Profiler 来查找 class 中消耗更多时间的行,但没有什么特别突出的,如下所示。有什么理由会发生这种情况吗?我该如何改进?

设置文字旋转(角度)

注释掉文字旋转线(ang)

每次点击时间没有显示出很大的增加或减少,但在注释行 # dict_Text[str(i)].setRotation(ang[i]) 时关于缩放和平移交互的用户体验非常好不同。

重现问题:

下面是重现我遇到的问题的代码,首先 运行 代码原样,您将有一个非常慢的缩放和平移交互,然后注释掉行 dict_Text[ str(i)].setRotation(ang[i]) 和缩放和平移交互会非常快。

代码:

from PyQt5 import QtWidgets, QtCore, QtGui
import numpy as np
import sys
print(QtCore.PYQT_VERSION_STR)

class GraphicsView(QtWidgets.QGraphicsView):
    @profile
    def __init__(self, scene, parent):
        super(GraphicsView, self).__init__(scene, parent)

        #Mouse Tracking
        self.setMouseTracking(True)
        #Zoom Anchor
        self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
        #Antialiasing and indexing
        self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.HighQualityAntialiasing | QtGui.QPainter.TextAntialiasing)
        self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
        self.resetCachedContent()
        scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)

        #Pan variable
        self.pos_init_class = None

    @profile
    def mousePressEvent(self, event):
        pos = self.mapToScene(event.pos())
        #Mouse Pan
        if event.button() == QtCore.Qt.MiddleButton:
            self.pos_init_class = pos
        super(GraphicsView, self).mousePressEvent(event)

    @profile
    def mouseReleaseEvent(self, event):
        if self.pos_init_class and event.button() == QtCore.Qt.MiddleButton:
            #Mouse Pan
            self.pos_init_class = None
        super(GraphicsView, self).mouseReleaseEvent(event)

    @profile
    def mouseMoveEvent(self, event):
        if self.pos_init_class:
            #Mouse Pan
            delta = self.pos_init_class - self.mapToScene(event.pos())
            r = self.mapToScene(self.viewport().rect()).boundingRect()
            self.setSceneRect(r.translated(delta))
        super(GraphicsView, self).mouseMoveEvent(event)

    @profile
    def wheelEvent(self, event):
        #Mouse Zoom
        if event.angleDelta().y() > 0:
            self.scale(1.5, 1.5)
        else:
            self.scale(1 / 1.5, 1 / 1.5)


class Ui_MainWindow(object):
    def __init__(self):
        super(Ui_MainWindow, self).__init__()


    def plt_plot(self):
        #Create data set
        size = 200
        x = np.random.randint(0, high=1000, size=size, dtype=int)
        y = np.random.randint(0, high=1000, size=size, dtype=int)
        ang = np.random.randint(1, high=360, size=size, dtype=int)

        #Store Text in Dict
        dict_Text = {}

        for i in range(len(x)):
            #Create Text Item
            dict_Text[str(i)] = QtWidgets.QGraphicsSimpleTextItem()

            #Set text
            dict_Text[str(i)].setText('nn-mm \nL: 50.6 m \nD: 1500 mm')

            #Set Pos
            dict_Text[str(i)].setPos(x[i], y[i])

            #Set rotation angle
            dict_Text[str(i)].setRotation(ang[i])

            #Add to Scene
            self.graphicsView.scene().addItem(dict_Text[str(i)])


    def setupUi(self, MainWindow):
        #Central Widget
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        MainWindow.setCentralWidget(self.centralwidget)
        main_width, main_heigth = 1200, 800
        MainWindow.resize(main_width, main_heigth)

        #Create GraphicsView and Scene
        self.scene = QtWidgets.QGraphicsScene()
        self.graphicsView = GraphicsView(scene=self.scene, parent=self.centralwidget)

        #Set Geometry
        self.graphicsView.setGeometry(QtCore.QRect(0, 0, main_width, main_heigth))

        #plot dummy data set
        self.plt_plot()

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

虽然 QGraphicsView 框架文档指出它非常快,但这并不意味着它总是 很快。

渲染速度取决于很多因素,单个项目转换会大大降低整体性能。

认为所有项的绘制是作为个别光栅绘制(后端几乎都是Qt自己的渲染:其优化,虽然通常很好,但并不完美)。
对于每个具有单独变换的项目,画家将需要根据 那个 变换进行绘画。
如果您有 200 个项目,每个项目都有自己的转换,这意味着需要 很多 的计算。

注意:变换是可以变换绘画的matrix(意味着所有东西都需要特殊和额外的计算)。
Qt 转换非常标准:

  • 翻译
  • 规模
  • shear
  • [投影]
  • 旋转(通过结合剪切和缩放来完成,因此很复杂)
  • 透视(结合投影和缩放来完成)

然后您必须添加一个事实,即您绘制的不是简单的项目,而是基于文本的项目。文本绘制需要大量计算,尽管 Qt 和底层系统提供了所有优化。

我不会深入探讨文字绘画是如何完成的,但是您必须考虑很多方面;让我们只考虑其中 极少数 个:

  • 每个字母由许多复杂的多边形组成(其中许多使用贝塞尔曲线)
  • 每个字母都有不同的大小和间距,包括 per-letter(和每个 letter-pair)间距,也称为字距调整
  • 一些字体具有更高级的功能,例如 ligature
  • 甚至必须考虑简单对齐,可能根据 system, widget or even text option 布局方向
  • 很多其他的东西...

考虑一下(它并不完全像这样工作,但这只是为了示例):在您的文本中,您有大约 20 个可绘制字符。
将每个角色想象成一个 new 创建的 QPainterPath 实例,包含 lots 的直线和贝塞尔曲线(几乎所有角色都是如此)。大约有 4000 条带有自己曲线的单独路径,每条路径在每次绘制 时创建。
然后你还需要应用一个转换矩阵,由于旋转(如前所述,both剪切and 翻译).

我需要说明的是,以上是对如何完成文本绘制的over的简化(因为 Qt 也部分依赖于底层系统字体渲染)。

那么,有解决办法吗?
好吧,“不是真的”和“并非总是”。

首先,不使用setSceneRect(),您可以通过滚动场景的内容来获得一些轻微的改进。这是通过设置一个(非常)大的 sceneRect 并使用 set<Orientation>ScrollBarPolicyScrollBarAlwaysOff 隐藏滚动条,然后通过设置滚动条值上的增量位置来移动可见区域来完成的。移动滚动条只会导致视口重绘,而 setSceneRect() 还需要(递归)根据滚动条大小的变换 计算可见区域。

然后是 OpenGL 替代方案,它可能会改进 performance:

In order to accurately and quickly apply transformations and effects to items, Graphics View is built with the assumption that the user's hardware is able to provide reasonable performance for floating point instructions. [...] As a result, certain kinds of effects may be slower than expected on certain devices. It may be possible to compensate for this performance hit by making optimizations in other areas; for example, by using OpenGL to render a scene.

参见OpenGL Rendering,但考虑到它并不总能保证更好的性能。

最后,如果您需要显示许多单独的文本项,每个文本项都有自己的旋转,您必须预计性能会急剧下降。唯一可能的选择是尝试将这些文本项呈现为(更大的)图像,然后使用 QGraphicsPixmapItem,但为了获得可靠的结果(因为基于位图的对象在转换时容易出现锯齿),您需要使用更大的尺寸对于每个项目。