PDA

View Full Version : Rubberband relative to QGraphicsScene



pherthyl
11th December 2007, 06:57
Hi All,

So I've got a standard "boxes and lines" type application. Now I recently implemented autoscroll, so if you're dragging an item near the edge of the QGraphicsView, the view scrolls in that direction slowly. That works quite well.

However if you're dragging a rubberband box and drag your mouse near the edge of the graphics view, the scene moves while the rubberband doesn't. In other words, the rubber band in QGraphicsView (as in setDragMode(QGraphicsView::RubberBandDrag)) is relative to the view, and not the scene. This doesn't work with autoscroll, since I really want the rubberband to be relative to the scene, and thus expand when I'm dragging near the view edge.

I've tried using QRubberBand, but that won't work since it will be on top of everything. I suppose I could fake a rubber band by creating a QGraphicsItem to look like one, but that doesn't seem to be a very elegant solution, and it won't look like a native rubber band either. So is there any way to make the rubber band in graphics view relative to the scene instead of the view?

Thanks,
Leo

wysota
11th December 2007, 08:12
I don't think you should go in that direction - you are likely to encounter problems for example while zooming. Maybe you should be auto scrolling the view, not the scene? You can do it in a similar fashion to autoscrolling implemented in view classes.

Gopala Krishna
11th December 2007, 08:45
Hi All,
However if you're dragging a rubberband box and drag your mouse near the edge of the graphics view, ....


I just tried the chip demo and i think the rubberbanding implementation is buggy in graphicsview framework. Just start rubberbanding and use mouse wheel to scroll further down while still rubberbanding is being done. You can see strange articrafts!
That is , your auto scroll feature is not interfering with rubberbanding. It is just that rubberbanding is buggy.

Some extra information:
Actually as i have said this previously (http://www.qtcentre.org/forum/f-qt-programming-2/t-qgraphicsitem-leaves-junk-on-screen-4854.html) , i have implemented wiring between two components (ele cad s/w) to use rubberband to represent wires till mouse is released as a part of optimization. I tried rubberbanding and scrolling simultaneously, and it worked best to my knowledge. I however control the rubberband through the scene.

Conclusion:
I think it is better to implement custom rubberbanding controlled through scene which is not the case in QGraphicsView.

P.S: I am using kde4's unupdated qtcopy (qt-4.3). So this bug could have already been fixed.

pherthyl
11th December 2007, 15:55
I don't think you should go in that direction - you are likely to encounter problems for example while zooming. Maybe you should be auto scrolling the view, not the scene? You can do it in a similar fashion to autoscrolling implemented in view classes.

Sorry I don't think I explained it very well. I am scrolling the view. As in, when the mouse pointer is dragged near the edge of the view, I periodically call
verticalScrollBar()->setValue(verticalScrollBar()->value()-scrollMagnitude); in the view. The problem is that the Rubberband's coordinates are relative to the view, so the rubber band does not change size when you're scrolling.

pherthyl
11th December 2007, 16:06
I just tried the chip demo and i think the rubberbanding implementation is buggy in graphicsview framework. Just start rubberbanding and use mouse wheel to scroll further down while still rubberbanding is being done. You can see strange articrafts!
That is , your auto scroll feature is not interfering with rubberbanding. It is just that rubberbanding is buggy.

Hmm, I didn't check on the chip demo, but my application does not show these artifacts. (4.3.3)



Conclusion:
I think it is better to implement custom rubberbanding controlled through scene which is not the case in QGraphicsView.

Yes, the problem is that QRubberBand is a widget, so it won't actually be on the canvas. This presents a problem when you want your rubberband to be larger than the view (ie, any image editor can do this). I think I will have to implement my own rubber band qgraphicsitem and do it all myself.

Gopala Krishna
11th December 2007, 18:25
From my point of view you shouldn't implement rubberband as a graphics item , because rubberband keeps changing its geometry throughout which results in unnecessary overhead.

What i mean to say is you just need to initialize your own QRubberBand in the scene's mouse press event. You can usually get the active QGraphicsView from QGraphicsSceneMouseEvent::widget(). Also QGraphicsScene::views() may be of use. Set the view's viewport as parent of rubberband.

Then on its just a matter of setting geometry of rubberband(with some coordinate transformations) in mouse move event and correspondingly calling QGraphicsScene::setSelectionArea(). May be you can use most of the code for this from qt itself.

Regarding the articrafts, i didn't get the same on windows(qt-4.3.0) but now i can see what you meant by "the rubberband's size remains same".

pherthyl
11th December 2007, 22:25
From my point of view you shouldn't implement rubberband as a graphics item , because rubberband keeps changing its geometry throughout which results in unnecessary overhead.

What i mean to say is you just need to initialize your own QRubberBand in the scene's mouse press event. You can usually get the active QGraphicsView from QGraphicsSceneMouseEvent::widget(). Also QGraphicsScene::views() may be of use. Set the view's viewport as parent of rubberband.

Hmm, yes I thought about this at first, but won't the rubberband be over my application? Since it is not actually sitting on the scene, it will be drawn by Qt on top of the application, not inside the view. For example, think about my graphics view visualizing the rectangle x:100, y:100, w:200, h:200 and my rubberband is x:50,x:50,w:200,h:200 in scene co-ordinates.
The rubberband is partially outside the view, so it will be drawn on top of my view and the rest of the application window, instead of being clipped by the view, which is what I want. Is there a solution to this problem?

wysota
11th December 2007, 22:43
It will be clipped by the view if you make the view its parent.

pherthyl
12th December 2007, 03:51
It will be clipped by the view if you make the view its parent.

Ah, indeed. It gets drawn over the view's scrollbars, but I guess I can live with that.

pherthyl
12th December 2007, 07:13
Ah, indeed. It gets drawn over the view's scrollbars, but I guess I can live with that.

Yuk. This has kinda opened a can of worms. I can't do the rubber band detection in the scene's mouse events, because when the mouse is over one of my items, I don't get mouse move events in the scene ( I guess I need to differentiate between mouse drags and mouse moves).

But anyway, I moved the code to the view, and now I get the mouse move events, but sometimes randomly I will not receive a mouse release event in the QGraphicsView. So if I drag the rubberband at a certain speed and then release the mouse button the view doesn't get the event and never hides the rubber band.

Also drawing of the rubberband is different than using the built in dragMode. Somehow it seems to draw a white rectangle first, and then repaint it with the dashed line a moment later. This might be a linux issue. Performance of this has always been terrible under linux, but I don't understand why the drawing would be different. Looks like the built in rubber band has some other hacks that I'm not doing... Maybe I'll just stick with that for now.. But this should really be easier than it is.

wysota
12th December 2007, 10:31
Ah, indeed. It gets drawn over the view's scrollbars, but I guess I can live with that.
If you can't then make the view's viewport the parent of the band.

Gopala Krishna
12th December 2007, 10:56
Yuk. This has kinda opened a can of worms. I can't do the rubber band detection in the scene's mouse events, because when the mouse is over one of my items, I don't get mouse move events in the scene ( I guess I need to differentiate between mouse drags and mouse moves).

But anyway, I moved the code to the view, and now I get the mouse move events, but sometimes randomly I will not receive a mouse release event in the QGraphicsView. So if I drag the rubberband at a certain speed and then release the mouse button the view doesn't get the event and never hides the rubber band.

Also drawing of the rubberband is different than using the built in dragMode. Somehow it seems to draw a white rectangle first, and then repaint it with the dashed line a moment later. This might be a linux issue. Performance of this has always been terrible under linux, but I don't understand why the drawing would be different. Looks like the built in rubber band has some other hacks that I'm not doing... Maybe I'll just stick with that for now.. But this should really be easier than it is.

I just wrote a small test example and it seems to work for most of cases. I indeed use viewport as its parent and rubberband is controlled from scene.

And note to see the scrolling effect try to use wheel while rubberbanding. For me the following test worked well on windows.


#include <QtGui>

class Scene : public QGraphicsScene
{
public:
Scene(QRectF rect) : QGraphicsScene(rect)
{
rubberband = 0;
}

protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event)
{
//allow scene to process its events first.
QGraphicsScene::mousePressEvent(event);
//if no selection open up a rubberband.
if(selectedItems().isEmpty()) {

QGraphicsView *view = qobject_cast<QGraphicsView*>(event->widget()->parent());
Q_ASSERT(view);
rubberband = new QRubberBand(QRubberBand::Rectangle, view->viewport());
QPoint p(view->mapFromScene(event->scenePos()));
rubberband->setGeometry(QRect(p, p));
rubberband->show();
origin = view->mapFromScene(event->scenePos());
}
}

void mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if(rubberband) {
QGraphicsView *view = qobject_cast<QGraphicsView*>(rubberband->parent()->parent());
Q_ASSERT(view);
origin = view->mapFromScene(event->buttonDownScenePos(Qt::LeftButton));
QRectF rect(origin, view->mapFromScene(event->scenePos()));
rubberband->setGeometry(rect.toRect().normalized());
QPolygonF p = view->mapToScene(rect.toRect().normalized());
QPainterPath path;
path.addPolygon(p);
setSelectionArea(path);
}
else {
QGraphicsScene::mouseMoveEvent(event);
}
}

void mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if(rubberband) {
delete rubberband;
rubberband = 0;
}
else {
QGraphicsScene::mouseReleaseEvent(event);
}
}
private:
QPointF origin;
QRubberBand *rubberband;
};

void addItems(Scene *s)
{
QGraphicsEllipseItem *item = s->addEllipse(10,10, 150, 150);
item->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemIsFocusable);

item = s->addEllipse(100,210, 50, 150);
item->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemIsFocusable);

}

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QRectF rect = QRectF(0,0, 1024,768);
Scene *s = new Scene(rect);
addItems(s);
QGraphicsView view;
view.setScene(s);

view.show();
return app.exec();
}

pherthyl
12th December 2007, 17:29
I just wrote a small test example and it seems to work for most of cases. I indeed use viewport as its parent and rubberband is controlled from scene.

Thanks. That's pretty much what I had as well. Unfortunately I can't do it from the scene, because I have some items with setAcceptsHoverEvents(true), which means that when my mouse is over those items, the scene does not get mouse move events. As in:


void mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
qDebug() << "scene mouse move";
QGraphicsScene::mouseMoveEvent(event);
}

never prints the output if my mouse is over an item that accepts hover events. I think this behaviour is really weird. I thought the scene dispatched events to its items, but somehow the items are intercepting the events before they even hit the scene.

Also, if I start a drag on the scene, it is really counterintuitive that the item will still pick up the mouse hover events mid-drag.

Anyway it doesn't really matter. I can implement a similar technique to what you did in the QGraphicsView's mousePress/Release/Move events. It works for the most part. I've just got two bugs left. One is that sometimes I don't get the mouse release events when dragging a rubber band, and the other is that setSelectionArea behaves quite oddly when the item is not contained in the rubberband rectangle, but is bisected by it. Hard to explain, but here's some images to illustrate.

selectionbugs.png shows what happens when I drag a rubber band from above the box item to below and to the right. As you can see, the box is not selected (it turns red when selected), even though it intersects the rubber band rectangle. There is actually another bug visible here. The right and bottom edges of the rubberband are not drawn at the instant the screenshot was taken, which is strange, but not as important.

noselectionbugs.png shows the behaviour with the QGraphicsView built in rubber band, (by setting the dragMode). Here everything works as expected. The item is selected properly, and the rubber band is drawn correctly.

For completeness, here're the relevant functions in my QGraphicsView deriving class:


void GraphWidget::mouseMoveEvent(QMouseEvent* evt) {
if(evt->buttons().testFlag(Qt::LeftButton)) {
// handle the rubberband
if(rubberBand->isVisible()) {
QPoint mouseDownView = mapFromScene(mouseDownPos);
lastMouseViewPos = evt->pos();
QRect rubberRect(mouseDownView, lastMouseViewPos);
rubberRect = rubberRect.normalized();
rubberBand->setGeometry(rubberRect);
QPolygonF p = mapToScene(rubberRect);
QPainterPath path;
path.addPolygon(p);
scene()->setSelectionArea(path, Qt::IntersectsItemShape);
}
}
QGraphicsView::mouseMoveEvent(evt);
}

void GraphWidget::mouseReleaseEvent(QMouseEvent* evt) {
qDebug() << "release";
rubberBand->hide();
QGraphicsView::mouseReleaseEvent(evt);
}

void GraphWidget::mousePressEvent(QMouseEvent* evt) {
qDebug() << "press";
QGraphicsView::mousePressEvent(evt);
if(!scene()->itemAt(mapToScene(evt->pos()))) {
mouseDownPos = mapToScene(evt->pos()).toPoint();
lastMouseViewPos = evt->pos();
rubberBand->setGeometry(mapFromScene(mouseDownPos).x(), mapFromScene(mouseDownPos).y(), 0,0);
rubberBand->show();
}
}

Gopala Krishna
12th December 2007, 17:58
The mouseReleaseEvent bug is pretty strange and hard to find out the cause.
Regarding the selection, do you get the same bug with built in qt's graphics items ?
If you don't there should be some bug with your item's code.

Edit: Also make sure you have set the view's drag mode to none.

pherthyl
12th December 2007, 18:13
The mouseReleaseEvent bug is pretty strange and hard to find out the cause.

Yes, I'm hoping it won't happen on Windows, but it's a minor bug and doesn't happen too often.


Regarding the selection, do you get the same bug with built in qt's graphics items ?

Haven't tried, but I think it is not a bug with my items, because with the graphics view built in rubberband, selection works great.

UPDATE: I just tried it, created a QGraphicsRectItem like this:

QGraphicsRectItem* ri = new QGraphicsRectItem(QRectF(100,100,100,100));
ri->setFlag(QGraphicsItem::ItemIsSelectable);
addItem(ri);

And this item has the exact same selection bug with my rubberband as my items do.


Edit: Also make sure you have set the view's drag mode to none.

Yup, it's set to that.

I should add that when I'm using my implementation of the rubberband, every other type of selection works fine. For example in the images I've attached examples of a rubber band in different locations selecting an item correctly. The only time when an item is not selected but should be, is if only the left edge of the rubber band is over top of the item (as shown in my previous attachment). Every other case works correctly.

pherthyl
12th December 2007, 18:43
Another interesting difference between my implementation of a rubber band, and QGraphicsView's.
I turned on QT_FLUSH_PAINT and then took a snapshot of me resizing the two rubber bands fairly rapidly. Look how the Graphicsview version paints the previous location and the new location as expected. My version somehow only paints where the rectangle is at the current time. You never see that double rectangle effect like you would expect.

Also, performance on X is terrible. Both with my rubberband and graphicsview's, xorg uses 100% of the cpu when moving the rubber band at a reasonable pace (and the rubber band is noticeably laggy). Nothing else is being repainted (according to QT_FLUSH_PAINT), just a few pixels for the rubber band, so why is it so awfully slow? I know I have bad graphics drivers (ati) but rubber bands in other apps (Qt3/KDE) are plenty fast. This is not a problem on Windows though.

Gopala Krishna
12th December 2007, 19:05
I had a look at the sources of qt and was shocked to see their own rubberband implementation for the QGView! They are drawing the rubberband from a QPainter manually in paintEvent() and updating manually. They aren;t using QRubberBand widget at all now!

Anyways i really feel there is something related to hovering which is giving you pains. Have you tried my example and checked performance issue ?
It would be better if you could post a minimal working example. :)

pherthyl
12th December 2007, 20:57
I had a look at the sources of qt and was shocked to see their own rubberband implementation for the QGView! They are drawing the rubberband from a QPainter manually in paintEvent() and updating manually. They aren;t using QRubberBand widget at all now!

Ah yes, I suspected something like that.


Anyways i really feel there is something related to hovering which is giving you pains. Have you tried my example and checked performance issue ?
It would be better if you could post a minimal working example. :)

Yes, that's next on the list. I tried your example on my work machine and there are no problems with performance or weird selection behaviour. I'll modify that minimal example to handle the rubber band in the view and check if it still works.

pherthyl
12th December 2007, 21:27
I'll modify that minimal example to handle the rubber band in the view and check if it still works.

Ok, so with the following code it works fine on my work computer. I saw the "no mouse release event" bug once or twice but now I can't reproduce it anymore. I will try this same code at home and see what the situation is there.


#include <QtGui>

class View : public QGraphicsView
{
public:
View() : QGraphicsView()
{
rubberBand = new QRubberBand(QRubberBand::Rectangle, this);
}

protected:
void mouseMoveEvent(QMouseEvent* evt) {
if(evt->buttons().testFlag(Qt::LeftButton)) {
// handle the rubberband
if(rubberBand->isVisible()) {
QPoint mouseDownView = mapFromScene(mouseDownPos);
lastMouseViewPos = evt->pos();
QRect rubberRect(mouseDownView, lastMouseViewPos);
rubberRect = rubberRect.normalized();
rubberBand->setGeometry(rubberRect);
QPolygonF p = mapToScene(rubberRect);
QPainterPath path;
path.addPolygon(p);
scene()->setSelectionArea(path, Qt::IntersectsItemShape);
}
}
QGraphicsView::mouseMoveEvent(evt);
}

void mouseReleaseEvent(QMouseEvent* evt) {
qDebug() << "release";
rubberBand->hide();
QGraphicsView::mouseReleaseEvent(evt);
}

void mousePressEvent(QMouseEvent* evt) {
qDebug() << "press";
QGraphicsView::mousePressEvent(evt);
if(!scene()->itemAt(mapToScene(evt->pos()))) {
mouseDownPos = mapToScene(evt->pos()).toPoint();
lastMouseViewPos = evt->pos();
rubberBand->setGeometry(mapFromScene(mouseDownPos).x(), mapFromScene(mouseDownPos).y(), 0,0);
rubberBand->show();
}
}
private:
QPoint mouseDownPos;
QPoint lastMouseViewPos;
QRubberBand *rubberBand;
};

void addItems(View *s)
{
QGraphicsEllipseItem *item = s->scene()->addEllipse(10,10, 150, 150);
item->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);

item = s->scene()->addEllipse(100,210, 50, 150);
item->setFlags(QGraphicsItem::ItemIsSelectable);

}

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QRectF rect = QRectF(0,0, 1024,768);
QGraphicsScene* scene = new QGraphicsScene(rect);
View *s = new View();
s->setScene(scene);
addItems(s);

s->show();
return app.exec();
}

pherthyl
13th December 2007, 00:04
I tried your example on my work machine and there are no problems with performance or weird selection behaviour.

No performance problems on my home machine with that simple example either.

Turns out the problem in my app wasn't the rubberband but the drawing of the background. In my drawbackground I was drawing borders for pages, in a not-so-efficient way.


void ProjectScene::drawBackground(QPainter *painter, const QRectF &rect) {
if(renderBackground) {
painter->setBrush(Qt::white);
painter->setPen(Qt::black);

int hPages = getNumHorizontalPages();
int vPages = getNumVerticalPages();
QRectF currPage;
for(int h = 0; h < hPages; h++) {
for(int v = 0; v < vPages; v++) {
currPage.setRect(h*pageSize.width(), v*pageSize.height(), pageSize.width(), pageSize.height());
if(currPage.intersects(rect)) {
painter->drawRect(currPage);
}
}
}
}
}

I'll have to rethink that function.

So now I just have to figure out the strange selection bug...

pherthyl
13th December 2007, 00:27
So now I just have to figure out the strange selection bug...

Aha! Now I've isolated the problem.

Can someone please try out the attached code and select the items as shown in the screenshot? The ellipse is correctly selected, while the rectangle is not. Not sure where this is coming from yet. Something to do with that the rect item has a default shape implementation?

pherthyl
13th December 2007, 03:36
I'll have to rethink that function.


CacheBackground fixed this problem.

pherthyl
14th December 2007, 07:21
Well I fixed the selection bug by implementing my own version of setSelectionArea and reporting the bug to Trolltech. Now everything works.

Gopala Krishna
14th December 2007, 12:27
Nice, but what bug was there in qt's setSelectionArea function ?

pherthyl
14th December 2007, 15:48
Nice, but what bug was there in qt's setSelectionArea function ?

No idea. I just gave them the compileable example and let them figure it out :)
But I wrote my own that just iterates through all the items and uses either shape().intersects() (for line items) and sceneBoundingRect().intersects() for my boxes. Maybe not so great for large numbers of items (haven't checked how qt does it), but I don't anticipate having more than about 100 on the scene at once.

pherthyl
20th December 2007, 19:30
Got a response from Trolltech support:


This does indeed look like a bug in Qt. The problem is most likely the
same as the one reported here

http://trolltech.com/developer/task-tracker/index_html?method=entry&id=191706

as QGraphicsScene uses QPainterPath for calculating whether the
rectangle intersects with the selection rectangle.

We'll verify that fixing this also solves your problem.

Thanks for reporting.