PDA

View Full Version : QGraphicsView slow when adding new items



Il Luppoooo
21st February 2016, 18:02
Hi everyone, new Qt user here.

I am currently dealing with a QGraphicsScene / QGraphicsView animation with a small (<100) number of QGraphicsItems moving around. I am experiencing problems when trying to display their trajectories on the screen. As a first approach, for every new frame I'm creating some QGraphicsEllipseItem(s) with size 1 and adding them to the scene at the previous item positions.

In the first steps of the animation, when the trajectories are short, the animation runs smooth. However, after some time (depending on how many items are present) the animation starts to get jerky. I disabled the scene indexing, no improvement. By hiding the trajectories (all trajectory points are grouped in a QGraphicsItemGroup) the animation becomes smooth again, so i think the issue is not with the point creation but with their rendering.



//somewhere before the animation starts
trajectory_points = new QGraphicsItemGroup;

scene->addItem(trajectory_points);

//...

//at each new frame

for(int i = 0; i < number_of_new_points; i++)
{

//...

QGraphicsEllipseItem* point = new QGraphicsEllipseItem( temp_x, temp_y, 1, 1);

trajectory_points->addToGroup(point);
}




By searching in the doc and some other questions on the internet, I thought the problem could be caused by the high number of calls to trajectory_points->addToGroup(point), one for each new point. So i tried to create a temporary QGraphicsItemGroup each frame, add the points to the temporary and then call scene->addItem(tempGroup) just once per frame, but it didn't solve the problem. I also tried setting the trajectory_points to not visible when adding the points and set it to visible again, but still no improvement.

d_stranz
21st February 2016, 21:18
Do you need interaction with the trajectory ellipses? If not, why not just create a custom QGraphicsItem to hold the ellipses, and draw them on demand using QPainter calls? (That is, don't make them proper QGraphicsItems, just make a custom QGraphicsItem and paint them on demand). Since you know their positions when you create them, you can simply store this information in your QGraphicsItem for drawing instead of creating child ellipses. Set the z-level of the ellipse class lower than the others so it appears "beneath" them.

Il Luppoooo
22nd February 2016, 09:02
Thanks for your answer.
By "draw them using QPainter calls" you mean to override its paint() method, right? I'll give it a try.
By the way, with this method there will always be one item around, but it will have to redraw itself fully at each new frame only to add a few points every time, is it corect? Maybe this could be a problem with long trajectories. Is it possible to enable some sort of caching to avoid this?

d_stranz
22nd February 2016, 17:35
I mean, derive a class from QGraphicsItem. In its paint method, you use QPainter calls to draw ellipses in the right places. By doing it this way, you have only one QGraphicsItem for all the ellipses, not dozens of QGraphicsEllipseItems. Of course it will redraw itself completely with each paint event, but I don't think your problem is with the drawing, it is the QGraphicsScene / QGraphicsView having to update so many items in the scene with each animation step. By eliminating all of the ones for the ellipses by pushing all of them into a single item, you minimize the number of things that must be updated.

I doubt that the ellipse drawing using QPainter calls will be a bottleneck, but if it does turn out to be slower than you wish, then you could experiment with using a QPainterPath and adding successive ellipses to it. Then your paint method consists of a single call to QPainter::drawPath(). This would also mean that your custom graphics item doesn't need to keep track of the ellipses - your animation code simply adds a new ellipse to the path, and the path will "remember" it.

Il Luppoooo
23rd February 2016, 13:00
I mean, derive a class from QGraphicsItem. In its paint method, you use QPainter calls to draw ellipses in the right places. By doing it this way, you have only one QGraphicsItem for all the ellipses, not dozens of QGraphicsEllipseItems. Of course it will redraw itself completely with each paint event, but I don't think your problem is with the drawing, it is the QGraphicsScene / QGraphicsView having to update so many items in the scene with each animation step. By eliminating all of the ones for the ellipses by pushing all of them into a single item, you minimize the number of things that must be updated.

I doubt that the ellipse drawing using QPainter calls will be a bottleneck, but if it does turn out to be slower than you wish, then you could experiment with using a QPainterPath and adding successive ellipses to it. Then your paint method consists of a single call to QPainter::drawPath(). This would also mean that your custom graphics item doesn't need to keep track of the ellipses - your animation code simply adds a new ellipse to the path, and the path will "remember" it.

I followed your advice and noticed a major improvement.
By the way, after defining the "global" item to hold the trajectory points, i had an issue with its paint method:



void TrajectoryItem::paint(QPainter *painter,
const QStyleOptionGraphicsItem *,
QWidget *)
{
painter->setPen( QPen (QColor(235,145,0)));
painter->setBrush(QBrush(QColor(235,145,0)));

painter->drawPath(path);
}


I was suprised to realize that the setBrush line caused a bottleneck for long trajectories. I also realized it was useless, as the QPainterPath only stored 1-sized ellipses, so i just removed it.

Aside from this case, I was wondering if there exists some way to specify default pen and brush options for an item without having to set expicitly set them at each paint() call.

d_stranz
23rd February 2016, 22:03
By the way, after defining the "global" item to hold the trajectory points

I hope you mean you defined a QPainterPath member variable of the class TrajectoryItem. If not, that is what you should be doing.


I was wondering if there exists some way to specify default pen and brush options for an item

Instead of deriving a custom class from QGraphicsItem, you could simply use a QGraphicsPathItem and set your QPainterPath on it. (QGraphicsPathItem::setPath()). Since this class is derived from QAbstractGraphicsShapeItem, you can set the pen and brush when you create the item and the QGraphicsPathItem::paint() method will use them.

Sorry I didn't think of this earlier. What you have done is equivalent to QPainterPathItem, just that QPainterPathItem creates and stores the path for you.

Il Luppoooo
24th February 2016, 11:56
I hope you mean you defined a QPainterPath member variable of the class TrajectoryItem. If not, that is what you should be doing.

Yes, that's what i did :)


Instead of deriving a custom class from QGraphicsItem, you could simply use a QGraphicsPathItem and set your QPainterPath on it. (QGraphicsPathItem::setPath()). Since this class is derived from QAbstractGraphicsShapeItem, you can set the pen and brush when you create the item and the QGraphicsPathItem::paint() method will use them.

Sorry I didn't think of this earlier. What you have done is equivalent to QPainterPathItem, just that QPainterPathItem creates and stores the path for you.

This looks nice, i'll give it a try.

I was also thinking about letting the user decide the length of the trajectories, so that for smaller settings the first points are not repainted. After a brief search it seems to me that this is not possible with QPainterPath, as there is no way to remove subpaths once their are added. So in this case I think I must stick to your first suggestion, that is drawing each ellipse separately in the paint() method. With this method i'll be able to choose which ellipse should drawn, depending of the user's setting. Is this the only way, or it could also be done with the QPainterPathItem?

d_stranz
24th February 2016, 16:05
Yes, one of the disadvantages of QPainterPath is that it is "write-only". However, you can still use the QGraphicsPathItem if it is convenient for the painting.

If all of your ellipses are the same size, then you can use QList to keep track of their QPoint centers. At each animation step, you can use QList::push_front() to put the latest ellipse location on the top of the list, and if the list is larger than the maximum number of ellipses, you use QList::pop_back() to remove the last item on the list. With this convention, the most recent trajectory ellipse is always first on the list and the oldest one is last.

However, if you want to make your animation a little bit more slick, go back to drawing each ellipse individually in the TrajectoryItem:: paint() method. Derive your class from QAbstractGraphicsShapeItem so you can take advantage of its built-in pen() and brush() methods.



// Pseudocode, but you know what to do to make it work

void TrajectoryItem::paint( QPainter * painter, ... )
{
QPen pen = pen(); // use a new pen copied from the stored one
QColor color( pen.color() );

int nMaxItems = maximum trajectory length;
int nItems = ellipseList.size();
for ( int nItem = 0; nItem < nItems; ++nItem )
{
const QPoint & point = ellipseList.at( nItem );

color.setAlphaF( float( nMaxItems - nItem ) / float( nMaxItems ) );
pen.setColor( color );
painter->setPen( pen );
painter->drawEllipse( point, radius1, radius2 )
}
}


With this code, once the list has reached maximum length the alpha value of the color will decrease with each ellipse until the last one is barely visible. So if you keep a finite-length trajectory, then the trails will seem to fade out over each step of the animation. If the length of the list hasn't yet reached the maximum, the last ellipse will still have good visibility. Eventually nItems will equal nMaxItems.

Il Luppoooo
25th February 2016, 11:58
Yes, one of the disadvantages of QPainterPath is that it is "write-only". However, you can still use the QGraphicsPathItem if it is convenient for the painting.

If all of your ellipses are the same size, then you can use QList to keep track of their QPoint centers. At each animation step, you can use QList::push_front() to put the latest ellipse location on the top of the list, and if the list is larger than the maximum number of ellipses, you use QList::pop_back() to remove the last item on the list. With this convention, the most recent trajectory ellipse is always first on the list and the oldest one is last.

I'm not sure I understand this. I basically used this strategy in the implementation without QPainterPath, I don't see how this could apply to the QPainterPath approach. In the QGraphicsPathItem case the ellipses are added to the path and there is no way to have the path reading what to draw from a list, isn't that true?



However, if you want to make your animation a little bit more slick, go back to drawing each ellipse individually in the TrajectoryItem:: paint() method. Derive your class from QAbstractGraphicsShapeItem so you can take advantage of its built-in pen() and brush() methods.



// Pseudocode, but you know what to do to make it work

void TrajectoryItem::paint( QPainter * painter, ... )
{
QPen pen = pen(); // use a new pen copied from the stored one
QColor color( pen.color() );

int nMaxItems = maximum trajectory length;
int nItems = ellipseList.size();
for ( int nItem = 0; nItem < nItems; ++nItem )
{
const QPoint & point = ellipseList.at( nItem );

color.setAlphaF( float( nMaxItems - nItem ) / float( nMaxItems ) );
pen.setColor( color );
painter->setPen( pen );
painter->drawEllipse( point, radius1, radius2 )
}
}


With this code, once the list has reached maximum length the alpha value of the color will decrease with each ellipse until the last one is barely visible. So if you keep a finite-length trajectory, then the trails will seem to fade out over each step of the animation. If the length of the list hasn't yet reached the maximum, the last ellipse will still have good visibility. Eventually nItems will equal nMaxItems.

This is a nice hint, thanks.

d_stranz
25th February 2016, 16:54
I'm not sure I understand this. I basically used this strategy in the implementation without QPainterPath, I don't see how this could apply to the QPainterPath approach. In the QGraphicsPathItem case the ellipses are added to the path and there is no way to have the path reading what to draw from a list, isn't that true?

If you use it with QPainterPath, then you need to re-create the path each time the list changes. And if you are going to do that, then using a QPainterPath may be less efficient than simply drawing the ellipses manually as you did in your first implementation. The only advantage using the QPainterPath buys you is that it is probably faster at painting between changes than the manual approach. The downside is that it doesn't let you implement the fade-out algorithm - everything in the path is painted using the same pen. So if you like the fade-out idea, then stick to keeping track of the ellipses yourself and painting them manually in the paint() method.

Sorry for throwing so many different solutions at you. The more I think about it, the more ideas come to mind.

Il Luppoooo
25th February 2016, 19:46
Ok now I understand. But couldn't this continous redefinition of the path make the overall method slower than the manual painting? Anyway, I decided to stick to the manual painting, after some other optimization here and there I'm satisfied of the result.

No problem for the different solutions, it's great to have more than one hint, makes me dig it deeper :)