本文介绍了如何优化 QGraphicsView 的性能?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm developing a CAD application using Qt 5.6.2 that is required to run in cheap computers at the same time that it needs to handle thousands of items in the same scene. Because of that, I had to make a lot of experimentations in order to get the best performance.

我决定创建此帖子以帮助他人和我自己,只要其他人也提供更多优化技巧.

I decided to create this post to help others and also myself, as long as other people also contribute with more optimization tips.

我的文字仍在创作中,如果我发现更好的技巧(或者我说了一些非常愚蠢的话),我可能会更新它.

My text is still a work in progress and I may update it if I discover better techniques (or that I said something really stupid).

推荐答案

禁用场景交互

事件处理由 QGraphicsView 引擎的 CPU 使用率的很大一部分负责.每次鼠标移动时,视图都会向场景询问鼠标下的项目,它调用 QGraphicsItem::shape() 方法来检测交叉点.即使禁用项目也会发生这种情况.所以,如果你不需要你的场景与鼠标事件交互,你可以设置 QGraphicsView::setIntenteractive(false).就我而言,我的工具中有两种模式(测量和移动/旋转),其中场景基本上是静态的,所有编辑操作都由 QGraphicsView 执行.通过这样做,我能够将帧速率提高 30%,不幸的是 ViewportAnchor::AnchorUnderMouse 停止工作(修复它的一个技巧是重新启用交互并覆盖 QGraphicsView::mouseMoveEvent(QMouseEvent e) 以人为地按下其中之一鼠标按钮使用基于 e 的新 QMouseEvent).

Event handling is responsible by a good part of the CPU usage by the QGraphicsView engine. At each mouse movement the view asks the scene for the items under the mouse, which calls the QGraphicsItem::shape() method to detect intersections. It happens even with disabled items. So, if you don't need your scene to interact with mouse events, you can set QGraphicsView::setIntenteractive(false). In my case, I had two modes in my tool (measurement and move/rotate) where the scene was basically static and all edit operations were executed by QGraphicsView instead. By doing that, I was able to increase the frame rate in 30%, unfortunately ViewportAnchor::AnchorUnderMouse stopped working (one hack to fix it was to re-enable interaction and override QGraphicsView::mouseMoveEvent(QMouseEvent e) to artificially press one of the mouse buttons using a new QMouseEvent based on e).

重用你的 QPainterPaths

在 QGraphicsItem 对象中缓存 QPainterPaths.构建和填充它可能非常缓慢.在我的情况下,读取文件需要 6 秒,因为我将具有 6000 个点的点云转换为具有多个矩形的 QPainterPath.你不会想要做不止一次.此外,在 Qt 5.13 中,现在可以保留 QPainterPaths 的内部向量大小,避免随着其增长而出现多个副本.

Cache your QPainterPaths inside your QGraphicsItem object. Constructing and populating it can be very slow. In my case it was taking 6 seconds to read a file just because I was converting a point-cloud with 6000 points into a QPainterPath with multiple rectangles. You won't want to do it more than once.Also, in Qt 5.13 it is now possible to reserve the QPainterPaths 's internal vector size, avoiding multiple copies as it grows.

简化您的 QGraphicsItem::shape()

在鼠标事件期间多次调用此方法,即使该项目未启用.尝试使其尽可能高效.有时,即使缓存 QPainterPath 也不够,因为场景执行的路径交叉算法对于复杂形状可能非常慢.在我的例子中,我返回了一个大约有 6000 个矩形的形状,而且速度很慢.在对点云进行下采样后,我能够将矩形的数量减少到 1000 左右,这显着提高了性能,但仍然不理想,因为即使项目被禁用,shape() 仍然被调用.因此,我决定保留原始的 QGraphicsItem:shape()(它返回边界框矩形),并在启用该项目时返回更复杂的缓存形状.它在将鼠标移动近 40% 时提高了帧速率,但我仍然认为这是一个黑客行为,如果我想出更好的解决方案,我会更新这篇文章.尽管如此,在我的测试中,只要我保持其边界框不变,我就没有任何问题.如果不是这种情况,则必须调用 prepareGeometryChange(),然后更新边界框和其他地方的形状缓存.

This method is called multiple times during mouse events, even if the item is not enabled. Try to make it as efficient as possible.Sometimes, even caching the QPainterPath is not enough since the path intersection algorithm, executed by the scene, can be very slow for complex shapes. In my case I was returning a shape with around 6000 rectangles and it was pretty slow. After downsampling the point-cloud I was able to reduce the number of rectangles to around 1000, that improved the performance significantly but still wasn't ideal since shape() was still being called even when the item was disabled. Because of that I decided to maintain the original QGraphicsItem:shape() (which returns the bounding box rectangle) and just return the more complex cached shape when the item is enabled. It improved the frame-rate when moving the mouse in almost 40% but I still think it is a hack and will update this post if I come up with a better solution. Despite that, in my tests I didn't have any issue as long as I keep its bounding box untouched. If it is not the case, you'll have to call prepareGeometryChange() and then update the bounding box and the shape caches elsewhere.

同时测试:光栅和 OpenGL 引擎

我原以为 OpenGL 总是比光栅更好,如果您只是出于显而易见的原因想要减少 CPU 使用率,这可能是真的.但是,如果您只想增加每秒的帧数,尤其是在廉价/旧计算机中,那么也值得尝试测试光栅(默认的 QGraphicsView 视口).在我的测试中,新的 QOpenGLWidget 比旧的 QGLWidget 略快,但 FPS 的数量比使用 Raster 慢了近 20%.当然,它可以是特定于应用程序的,并且结果可能会因您呈现的内容而异.

I was expecting that OpenGL would always be better than raster, and it is probably true if all you want is to reduce CPU usage for obvious reasons. However, if all you want is to increase the number of frames per second, especially in cheap/old computers, it worth trying to test raster as well (the default QGraphicsView viewport). In my tests, the new QOpenGLWidget was slightly faster than the old QGLWidget, but the number of FPS was almost 20% slower than using Raster. Of course, it can be application specific and the result can be different depending on what you are rendering.

将 FullViewportUpdate 与 OpenGL 一起使用,并更喜欢其他带有光栅的部分视口更新方法(尽管需要对项目进行更严格的边界矩形维护).

Use FullViewportUpdate with OpenGL and prefer other partial viewport update method with raster (requires a more strict bounding rectangle maintaining of the items though).

尝试禁用/启用 VSync 以查看哪个更适合您:QSurfaceFormat::defaultFormat().setSwapInterval(0 或 1).启用会降低帧率,禁用会导致撕裂".https://www.khronos.org/opengl/wiki/Swap_Interval

Try to disable/enable VSync to see which one works better for you: QSurfaceFormat::defaultFormat().setSwapInterval(0 or 1). Enabling can reduce the frame rate and disabling can cause "tearing".https://www.khronos.org/opengl/wiki/Swap_Interval

缓存复杂的 QGraphicsItems

如果你的 QGraphicsItem::paint 操作过于复杂,并且大部分是静态的,那么尝试启用缓存.如果您没有对项目或 ItemCoordinateCache 应用转换(如旋转),请使用 DeviceCoordinateCache.避免经常调用 QGraphicsItem::update() ,否则它会比没有缓存更慢.如果您需要更改项目中的某些内容,两个选项是:在子项中绘制它,或使用 QGraphicsView::drawForeground().

If your QGraphicsItem::paint operation is too complex and at the same type mostly static, try to enable caching. Use DeviceCoordinateCache if you are not applying transformation (like rotation) to the items or ItemCoordinateCache otherwise. Avoid call QGraphicsItem::update() very often, or it can be even slower than without caching. If you need to change something in your item, two options are: to draw it in a child, or use QGraphicsView::drawForeground().

将类似的 QPainter 绘图操作分组

更喜欢 drawLines 而不是多次调用 drawLine;比 drawPoint 更喜欢 drawPoints.使用 QVarLengthArray(使用堆栈,因此可以更快)或 QVector(使用堆)作为容器.避免经常更换画笔(我怀疑在使用 OpenGL 时更重要).此外,QPoint 可以更快并且比 QPointF 小.

Prefer drawLines over calling drawLine multiple times; favor drawPoints over drawPoint. Use QVarLengthArray (uses stack, so can be faster) or QVector (uses heap) as container. Avoid change the brush very often (I suspect it is more important when using OpenGL). Also QPoint can be faster and is smaller than QPointF.

更喜欢使用装饰线绘图,避免透明和抗锯齿

可以禁用抗锯齿,特别是如果您绘制的所有线条都是水平、垂直或 45 度线(它们实际上这样看起来更好),或者您使用的是视网膜"显示器.

Antialiasing can be disabled, especially if all you are drawing are horizontal, vertical or 45deg lines (they actually look better this way) or you are using a "retina" display.

搜索热点

瓶颈可能出现在令人惊讶的地方.使用探查器(在 macOS 中我使用仪器/时间探查器)或其他方法,如经过计时器、qDebug 或 FPS 计数器(我将它放在我的 QGraphicsView::drawForeground 中)来帮助定位它们.不要让你的代码变得丑陋,试图优化你不确定它们是否是热点的东西.FPS 计数器示例(尽量保持在 25 以上):

Bottlenecks may occur in surprising places. Use a profiler (in macOS I use Instruments/Time Profiler) or other methods like elapsed timer, qDebug or FPS counter (I put it in my QGraphicsView::drawForeground) to help locate them. Don't make your code ugly trying to optimize things you are not sure if they are hotspots or not. Example of a FPS counter (try to keep it above 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

Use foreach(const auto& item, items), const_iterator or items.at(i) instead of items[i], when iterating over QT containers, to avoid detachment. Use const operator and call const methods as much as possible. Always try to initialize (reserve() ) your vectors/arrays with a good estimation of its actual size.https://www.slideshare.net/qtbynokia/optimizing-performance-in-qtbased-applications/37-Implicit_data_sharing_in_Qt

场景索引

对于具有少量项目和/或动态场景(带有动画)的场景使用 NoIndex,对于具有很多(主要是静态)项目的场景使用 BspTreeIndex.BspTreeIndex 允许在使用 QGraphicsScene::itemAt() 方法时快速搜索.

Favor NoIndex for scenes with few items and/or dynamic scenes (with animations), and BspTreeIndex for scenes with many (mostly static) items. BspTreeIndex allows fast searching when using QGraphicsScene::itemAt() method.

不同缩放级别的不同绘制算法

在 Qt 40000 Chips 示例中,您不需要使用相同的详细绘图算法来绘制在屏幕上看起来很小的东西.您可以为此任务使用 2 个不同的 QPainterPath 缓存对象,或者在我的情况下,具有 2 个不同的点云向量(一个具有原始向量的简化子集,另一个具有补充).因此,根据缩放级别,我绘制一个或两个.另一种选择是根据缩放级别调整您的点云并仅绘制矢量的前 n 个元素.仅最后一项技术就将我的帧速率从 5fps 提高到 15fps(在我最初拥有 100 万个点的场景中).在您的 QGraphicsItem::painter() 中使用类似:

As in the Qt 40000 Chips example, you don't need to use the same detailed drawing algorithm to draw things that will look very small at the screen. You can use 2 different QPainterPath cached objects for this task, or as in my case, to have 2 different point-cloud vectors (one with a simplified subset of the original vector, and another with the complement). So, depending on the zoom level, I draw one or both. Another option is to shuffle your point-cloud and draw just the n first elements of the vector, according to the zoom level. This last technique alone increased my frame rate from 5 to 15fps (in a scene where I had originally 1 million points). Use in your QGraphicsItem::painter() something like:

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()

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

If you are constantly increasing your scene rectangle in size, reindexing can freeze your application for a short period of time. To avoid that, you can set a fixed size or add and remove a temporary rectangle to force the scene to increase to a bigger initial size:

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

禁用滚动条

如果滚动场景时视图闪烁,禁用滚动条可以解决此问题:

If the view is flickering when you scrool the scene, disabling the scroolbars can fix it:

setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );

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

使用 QGraphicsScene::createItemGroup() 分组避免在转换过程中多次调用 QGraphicsItem::itemChange.只在创建和销毁组时调用两次.

Grouping with QGraphicsScene::createItemGroup() avoids calling QGraphicsItem::itemChange multiple times during the transformation. It is only called twice when the group is created and destroyed.

比较多个 Qt 版本

我还没有足够的时间去研究它,但至少在我目前的项目中,Qt 5.6.2(在 Mac OS 上)比 Qt 5.8 快得多.

I didn't have enough time to investigate it yet, but in my current project at least, Qt 5.6.2 (on Mac OS) was much faster than Qt 5.8.

这篇关于如何优化 QGraphicsView 的性能?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-26 04:22