PDA

View Full Version : More on selective QGraphicsItem visibility



d_stranz
12th February 2013, 22:15
OK, in follow-up to my previous post, I have refactored my data plotting widget so that it uses a QGraphicsView instance only for the part of the plot that displays the data itself. The axes, titles, footers, etc. are displayed using QwtScaleWidget, QLabel, etc.

One of the characteristics of this plot is that labels for the data points are drawn so that they don't collide or overlap, and that the size of the label does not scale (remains a fixed size) when the plot is zoomed in or out. This is accomplished with the ItemIgnoresTransformations flag.

I have also implemented view-based collision detection: I create the candidate set of labels based on the zoom region in the view, then map the label positions and sizes from scene to view. I then go through this list and check for collisions; for any colliding pair of labels, the one with the lower z-value is made invisible. All of this works fine.

The way I am trying to realize this on screen is to overlay two QGraphicsView windows. One window contains the data and its plot, the other contains only the labels. There are two QGraphicsScene instances, one for the data and plot, the other for the labels. The data scene can be shared among multiple plots, which allows me to display one in 1:1 format, the other zoomed in or out, etc. without having to duplicate the underlying data or scene objects. The label scene is dedicated to the QGraphicsView that displays the labels. In this way, two views of the same scene could have a different set of non-colliding labels, as a sort of "level of detail" effect.

The problem I am having is that I cannot seem to make both graphics views simultaneously visible. I have tried various versions of this code in my plot widget constructor:



mpCanvas = new QGraphicsView( this );
mpCanvas->setViewportUpdateMode( QGraphicsView::FullViewportUpdate );
mpCanvas->setEnabled( true );
mpCanvas->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
mpCanvas->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
mpCanvas->setWindowOpacity( 0.0 );
mpCanvas->setBackgroundBrush( Qt::NoBrush );

mpLabelCanvas = new QGraphicsView( &mLabelScene, this );
mpLabelCanvas->setViewportUpdateMode( QGraphicsView::FullViewportUpdate );
mpLabelCanvas->setEnabled( false );
mpLabelCanvas->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
mpLabelCanvas->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );

mpLabelCanvas->stackUnder( mpCanvas );


where "mpCanvas" is the QGraphicsView that uses the shared QGraphicsScene containing the data, and "mpLabelCanvas" is the QGraphicsView with the dedicated scene holding the labels. (Scenes are assigned later in the constructor, and labels are calculated on-the-fly during resize and zoom operations).

In the version posted above, the "mpCanvas" is visible, the "mpLabelCanvas" is not. (I see the data but no labels). If I stack them the other way, the opposite occurs, so I know that I am positioning, sizing, and setting transforms for the two views correctly.

What I want to happen is that the upper (data) view is transparent so that the lower (label) view shows through it. This is my last hurdle in getting this widget to a state where I can actually write the app that uses it.

Is there some combination of window flags, opacity, background brush, etc. that will create this effect?

norobro
13th February 2013, 00:19
Will setStyleSheet work:
mpCanvas->setStyleSheet("background : transparent");

Santosh Reddy
13th February 2013, 09:34
mpCanvas->setWindowOpacity( 0.0 );
This will not help you to get to what you wanted in the first place. Making a widget transparent will make all the contents loose their opacity too, i.e. the items on the QGrpahicsView will blend into the back ground (below) QGraphicsView, but where as you need the top QGraphicsView items to be overwritten (visible solid) on the bottom QGraphcisView.

So the only way I see out is to se the view's backgroud to transparent

Just a side note, you could also use QStackedLayout in StackAll mode, and set transparent backgroud. Here you can use the features which layouts give, if wanted to.

wysota
13th February 2013, 12:56
Instead of composing two views together, draw content of one scene in the other view's drawForeground() implementation. Or just have one scene only and draw the labels in the drawForeground() call without storing them in a separate scene.

d_stranz
13th February 2013, 15:59
Instead of composing two views together, draw content of one scene in the other view's drawForeground() implementation. Or just have one scene only and draw the labels in the drawForeground() call without storing them in a separate scene.

Oh, I like this idea. Much simpler than my semi-solution. I had considered doing something similar using the paint event in the main widget, but that would have been complicated. Very nice that the foreground drawing is in scene coordinates so I don't have to worry about transformations at that point.

Thanks to the others for their suggestions as well.

d_stranz
21st February 2013, 03:25
OK, this turned out to be trickier than anticipated, but it is working.


Very nice that the foreground drawing is in scene coordinates so I don't have to worry about transformations at that point.

This bit turned out to be not as true as I would have liked. I could not get the QGraphicsSimpleTextItem instances representing the labels to draw by simply using the QGraphicsSimpleTextItem:: paint() method. For whatever reason, nothing appeared. I verified that other types of QGraphicsItem could be rendered this way (like a QGraphicsRectItem with position and size in scene coordinates). So what I ended up implementing was something like the following:



// MyGraphicsView inherits from QGraphicsView
// mForegroundItems is a vector of QGraphicsItem pointers passed into the view; in the case of text items (labels)
// these have been filtered to eliminate items that collide *in this view*. The text items have ItemIgnoresTransformations set.

void MyGraphicsView::drawForeground( QPainter * pPainter, const QRectF & rect )
{
QGraphicsView::drawForeground( pPainter, rect );

QStyleOptionGraphicsItem option;

std::vector< QGraphicsItem * >::iterator it = mForegroundItems.begin();
std::vector< QGraphicsItem * >::iterator eIt = mForegroundItems.end();
while ( it != eIt )
{
QGraphicsItem * pItem = *it++;
if ( pItem )
{
if ( pItem->type() == QGraphicsSimpleTextItem::Type )
{
QGraphicsSimpleTextItem * pText = qgraphicsitem_cast< QGraphicsSimpleTextItem *>( pItem );
QPointF pos = mapFromScene( pText->pos() );

pPainter->save();
pPainter->resetTransform();
pPainter->setFont( pText->font() );
if ( pText->pen() != Qt::NoPen )
pPainter->setPen( pText->pen() );
pPainter->setBrush( pText->brush() );
pPainter->drawText( pos, pText->text() );
pPainter->restore();
}
else
pItem->paint( pPainter, &option, this );
}
}
}


Basically, the text had to be painted as ordinary text at the correct pixel position as mapped from the scene. Prior to painting, it was necessary to reset the painter's transform (since it comes into this method with the scene transformation already applied), then restore it before exiting.

Note that this solution is specific to my needs: labels are a fixed pixel size and do not scale with the scene scaling, and labels are always drawn in horizontal orientation.

If there's a better way to do this, I'd be interested to know. In particular, I don't understand why I can't draw text items directly using the paint() call. Possibly it is because the items are not children of the scene (i.e. QGraphicsScene::addItem() is not called, and the items have no QGraphicsItem parent), but on the other hand, the QGraphicsRect item I drew in testing this method wasn't either, but did not have the ItemIgnoresTranformations flag set.

Whatever, this suggestion of using drawForeground() has turned out to be very useful. I have also implemented an override of drawBackground() for this view class, which will allow me to display Qwt "spectrogram" style images as a background pixmap, overlaid with QGraphicsItem things in the scene to allow for marking and selecting "features" in the image.

wysota
21st February 2013, 03:32
I don't understand why you are using items for your foreground. It does not make much sense to me. If you want to use items then have two scenes and render one in the other's drawForeground().

d_stranz
21st February 2013, 17:52
I don't understand why you are using items for your foreground.

For convenience mostly, and because I had already implemented an "aligned text item" that allows me to specify the text alignment relative to the pos() (using Qt::Alignment flags). There's a signal / slot pair that lets the view query the curves drawn in the plot for their set of proposed text labels. In the previous implementation, these labels were placed in the scene; in the current implementation, they are drawn in the foreground instead.


If you want to use items then have two scenes and render one in the other's drawForeground().

I don't understand this. Of the set of candidate labels returned by the curve, the labels that are actually displayed are view specific and depend on the screen size and section of the scene that is zoomed into in that view. The view takes the combined list of labels returned by each curve and ranks them by z-value, then eliminates any labels with lower z-value that collide with one of higher z. This list is then used in my view's drawForeground() as shown above.

Are you suggesting that these items should be added to a separate scene (maintained by each view), and that the "master" scene should render the appropriate additional scene in its drawForeground() method? How do I identify which view is the target in this method?

wysota
21st February 2013, 20:50
Are you suggesting that these items should be added to a separate scene (maintained by each view),
Yes, that's exactly what I mean.


and that the "master" scene should render the appropriate additional scene in its drawForeground() method? How do I identify which view is the target in this method?
No no. The view does that in QGraphicsView::drawForeground().

Hmm... maybe I should write a paper on this, I have played with such solution for some time and it works nice and people often ask for things like this.

d_stranz
21st February 2013, 22:20
No no. The view does that in QGraphicsView::drawForeground().


Ah, I see. I had tried using two scenes at one time, but I was also trying to overlay two QGraphicsView instances. I could not get that to work.

OK, I will give this a try. I admit that I felt that what I now am doing is somewhat kludgy. I'll have to pay close attention to what happens during zooming so I can keep the two scenes synchronized.

A paper would be a welcome addition.