如何优化QGraphicsView的性能?

17

我正在使用 Qt 5.6.2 开发 CAD 应用程序,需要在廉价计算机上运行,同时需要处理同一场景中的数千个项目。因此,我不得不进行大量实验以获得最佳性能。

我决定创建这篇文章来帮助其他人和自己,同时也希望其他人能贡献更多的优化技巧。

我的文章仍然在进展中,如果我发现更好的技术(或者说了些愚蠢的话),我会更新它。

2个回答

47
禁用场景交互
事件处理是QGraphicsView引擎CPU使用的一部分。在每次鼠标移动时,视图会询问场景下鼠标所在位置的物品,这会调用QGraphicsItem::shape()方法来检测交叉。即使项目被禁用,这种情况仍然会发生。因此,如果您不需要场景与鼠标事件交互,可以设置QGraphicsView::setInteractive(false)。在我的工具中,我有两种模式(测量和移动/旋转),其中场景基本上是静态的,所有编辑操作都由QGraphicsView执行。通过这样做,我能够将帧率提高30%,不幸的是ViewportAnchor::AnchorUnderMouse停止工作。
重复使用您的QPainterPaths
在您的QGraphicsItem对象内缓存您的QPainterPaths。构建和填充它可能非常慢。在我的情况下,由于我将一个包含6000个点的点云转换为具有多个矩形的QPainterPath,读取文件需要6秒钟。您不希望重复执行此操作。 此外,在Qt 5.13中,现在可以预留QPainterPaths的内部向量大小,以避免随着其增长而进行多次复制。
简化您的QGraphicsItem::shape()方法
这个方法在鼠标事件期间被多次调用,即使该项未启用。尽量使其尽可能高效。 有时,即使缓存QPainterPath也不足以应对复杂形状的场景执行的路径交叉算法可能非常慢。在我的情况下,我返回了一个包含大约6000个矩形的形状,速度相当慢。在对点云进行降采样后,我能够将矩形数量减少到约1000个,这显著提高了性能,但仍然不理想,因为即使在禁用项时仍会调用shape()。因此,我决定保留原始的QGraphicsItem:shape()(返回边界框矩形),并在启用项时返回更复杂的缓存形状。这样在移动鼠标时帧率提高了近40%,但我仍认为这是一种权宜之计,如果我找到更好的解决方案,我会更新这篇文章。尽管如此,在我的测试中,只要保持其边界框不变,就没有任何问题。如果不是这种情况,您将不得不调用prepareGeometryChange(),然后在其他地方更新边界框和形状缓存。
测试两种引擎:光栅和OpenGL引擎。
我本以为OpenGL总是比光栅化更好,如果你只是想减少CPU使用率的话,这可能是真的。然而,如果你只是想增加每秒帧数,尤其是在廉价/旧电脑上,也值得尝试测试光栅化(默认的QGraphicsView视口)。在我的测试中,新的QOpenGLWidget比旧的QGLWidget稍微快一些,但FPS的数量几乎比使用光栅化慢了20%。当然,这可能是应用程序特定的,结果可能因渲染内容而异。
在OpenGL中使用FullViewportUpdate,并倾向于在光栅化中使用其他部分视口更新方法(尽管需要更严格地维护项的边界矩形)。
尝试禁用/启用垂直同步,看哪个对你更有效:QSurfaceFormat::defaultFormat().setSwapInterval(0或1)。启用可以降低帧率,禁用可能会导致“撕裂”。 https://www.khronos.org/opengl/wiki/Swap_Interval 缓存复杂的QGraphicsItems 如果您的QGraphicsItem::paint操作过于复杂,并且大部分时间都是静态的,尝试启用缓存。如果您没有对项目应用变换(如旋转),请使用DeviceCoordinateCache;否则,请使用ItemCoordinateCache。避免频繁调用QGraphicsItem::update(),否则缓存可能会比不使用缓存还要慢。如果您需要更改项目中的某些内容,有两个选项:在子项中绘制它,或者使用QGraphicsView::drawForeground()。 将类似的QPainter绘图操作分组 优先使用drawLines而不是多次调用drawLine;优先使用drawPoints而不是drawPoint。使用QVarLengthArray(使用堆栈,因此可能更快)或QVector(使用堆)作为容器。避免频繁更改画刷(我怀疑在使用OpenGL时更重要)。此外,QPoint比QPointF更快且更小。 优先使用化妆线进行绘图,避免透明度和抗锯齿 可以禁用抗锯齿,特别是如果您绘制的只是水平、垂直或45度的线条(这样看起来实际上更好),或者您正在使用“视网膜”显示器。 寻找热点 瓶颈可能出现在意想不到的地方。使用性能分析器(在macOS中我使用Instruments/Time Profiler)或其他方法,如经过的时间计时器、qDebug或FPS计数器(我将其放在我的QGraphicsView::drawForeground中),以帮助定位它们。不要让你的代码变得丑陋,试图优化那些你不确定是否是热点的东西。以下是一个FPS计数器的示例(尽量保持在25以上):
MyGraphicsView:: MyGraphicsView(){
    ...
    timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(oneSecTimeout()));
    timer->setInterval(1000);
    timer->start();
}

void MyGraphicsView::oneSecTimeout()
{
    frameRate=(frameRate+numFrames)/2;
    qInfo() << frameRate;
    numFrames=0;
}

void MyGraphicsView::drawForeground(QPainter * painter, const QRectF & rect)
{
    numFrames++;
    //...
}

http://doc.qt.io/qt-4.8/qelapsedtimer.html

避免深拷贝
在迭代QT容器时,使用foreach(const auto& item, items)、const_iterator或items.at(i),而不是items[i],以避免分离。尽量使用const运算符和调用const方法。始终尝试使用良好估计的实际大小初始化(reserve())您的向量/数组。 https://www.slideshare.net/qtbynokia/optimizing-performance-in-qtbased-applications/37-Implicit_data_sharing_in_Qt 场景索引
对于物品较少和/或动态场景(带有动画),优先选择NoIndex;对于物品较多(主要静态)的场景,选择BspTreeIndex。BspTreeIndex可在使用QGraphicsScene::itemAt()方法进行快速搜索。
不同缩放级别使用不同的绘制算法
如在Qt 40000芯片的例子中,您不需要使用相同的详细绘图算法来绘制在屏幕上看起来非常小的东西。您可以为此任务使用2个不同的QPainterPath缓存对象,或者像我一样,拥有2个不同的点云向量(一个是原始向量的简化子集,另一个是补集)。因此,根据缩放级别,我绘制其中一个或两个都绘制。另一种选择是打乱您的点云,并根据缩放级别仅绘制向量的前n个元素。仅仅使用这种最后一种技术就将我的帧率从5提高到了15fps(在我最初有100万个点的场景中)。在您的QGraphicsItem::painter()中使用类似以下的代码:
const qreal lod = option->levelOfDetailFromTransform(painter->worldTransform());
const int n = qMin(pointCloud.size(), pointCloud.size() * lod/0.08);
painter->drawPoints(pointCloud.constData(), n);

扩大您的QGraphicsScene::sceneRect()

如果您不断增加场景矩形的大小,重新索引可能会导致应用程序在短时间内冻结。为了避免这种情况,您可以设置一个固定的大小或添加和删除一个临时矩形来强制场景增加到更大的初始大小:

auto marginRect = addRect(sceneRect().adjusted(-25000, -25000, 25000, 25000));
sceneRect(); // hack to force update of scene bounding box
delete marginRect;

禁用滚动条
如果在滚动场景时视图闪烁,禁用滚动条可以解决这个问题。
setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );

使用分组将鼠标控制的变换应用于多个项目

使用QGraphicsScene::createItemGroup()进行分组可以避免在变换过程中多次调用QGraphicsItem::itemChange。只有在创建和销毁分组时才会调用两次。

比较多个Qt版本

我还没有足够的时间来调查,但至少在我的当前项目中,Qt 5.6.2(在Mac OS上)比Qt 5.8要快得多。


5

我的应用程序虽然不是CAD程序,但类似于CAD,因为它允许用户在空间中构建各种项目的“蓝图”,用户可以添加尽可能多的项目,有些用户的设计可能非常拥挤和复杂,同时存在数百或数千个项目。

视图中的大部分项目将是静态的(即它们仅在用户单击/拖动时移动或更改外观,这种情况很少发生)。但通常还会有一些前景项目在场景中不断地动画化并以20fps的速度移动。

为避免需要定期重新渲染复杂的静态元素,我将所有静态元素预先渲染到QGraphicsView的背景缓存中,无论何时其中任何一个元素发生变化,或当QGraphicsView的缩放/滚动/大小设置发生变化时,都要排除它们作为正常前景视图重绘过程的一部分进行渲染。

这样,当QGraphicsView上运行着20fps的移动元素时,通过调用QGraphicsScene :: drawBackground()中的代码,所有非常繁琐而复杂的静态对象都将绘制为单个调用drawPixmap(),而不必算法地重新渲染每个项目。 然后,始终移动的元素可以以通常的方式在上面绘制。

实现这需要在QGraphicsView上调用setOptimizationFlag(IndirectPainting)setCacheMode(CacheBackground),并且还需要在任何静态项目的任何方面发生更改时(使缓存的背景图像尽快重新渲染)对它们调用resetCachedContent()

唯一棘手的部分是让所有“背景” QGraphicsItemsQGraphicsScene :: drawBackground()回调中进行渲染,并且不要在通常比QGraphicsScene :: drawBackground()更频繁调用的QGraphicsScene :: drawItems()回调中进行渲染。

在我的压力测试中,与“基本” QGraphicsScene / QGraphicsView 方法相比,这将使我的程序的稳态CPU使用率降低约50%(如果我通过在QGraphicsView 上调用setViewport(new QOpenGLWidget)来使用OpenGL,则降低约80%)。

唯一的缺点(除了增加的代码复杂性)是这种方法依赖于使用QGraphicsView :: setOptimizationFlag(IndirectPainting)QGraphicsView :: drawItems(),而这两者均已被Qt基本弃用,因此这种方法未来可能无法继续使用Qt版本。(它至少在Qt 5.10.1中有效;那是我尝试过的最新Qt版本)。

一些示例代码:

代码示例:

void MyGraphicsScene :: drawBackground(QPainter * p, const QRectF & r)
{
   if (_isInBackgroundUpdate == false)  // anti-infinite-recursion guard
   {
      QGraphicsScene::drawBackground(p, r);

      const QRectF rect = sceneRect();

      p->fillRect(rect, backgroundBrush().color());

      // Render the scene's static objects as pixels 
      // into the QGraphicsView's view-background-cache
      this->_isInBackgroundUpdate = true;  // anti-infinite-recursion guard
      render(p, sceneRect());
      this->_isInBackgroundUpdate = false;
   }
}

// overridden to draw only the items appropriate to our current
// mode (foreground items OR background items but not both!)
void MyGraphicsScene :: drawItems(QPainter *painter, int numItems, QGraphicsItem *items[], const QStyleOptionGraphicsItem options[], QWidget *widget)
{
   // Go through the items-list and only keep items that we are supposed to be
   // drawing in this pass (either foreground or background, depending)
   int count = 0;
   for (int i=0; i<numItems; i++)
   {
      const bool isItemBackgroundItem = (_backgroundItemsTable.find(items[i]) != _backgroundItemsTable.end());
      if (isItemBackgroundItem == this->_isInBackgroundUpdates) items[count++] = items[i];
   }

   QGraphicsScene::drawItems(painter, count, items, options, widget);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接