PDA

View Full Version : Implementing scale-invariant QGraphicsItems



spud
25th October 2006, 15:43
I want to dispay objects in a QGraphicsView and let the user resize the objects using "grab handles". The handles should be squares of a fixed size i.e. 3x3 pixels.

I know how to draw the handles scale- and rotation-invariant, but how do i perform the hit test? shape() returns scene coordinates and I don't know where else to implement this functionality.

This seems like a very general problem which I'd think many users would stumble upon, but so far I have no concept of how to solve it.

Thanks

wysota
25th October 2006, 20:30
I guess you should reimplement QGraphicsItem::mousePressEvent() and QGraphicsItem::mouseMoveEvent() for your items and implement the functionality there.

spud
25th October 2006, 21:25
Thanks for the answer! I am sorry if my last post wasn't very clear.

The problem is by the time I receive a mouse event, the hit test has already been performed.

To clarify:
I want to receive a mousePressEvent if the user clicks within a 3x3 pixel area. Not considering the current rotation or scaling.

I'd appreciate any suggestions!

wysota
25th October 2006, 23:08
I think you can do it in two ways:
1. compose your object from a rectangle "parent" object and "child" proper object and only rotate the child, so that the parent always keeps it alignment. You'll probably avoid the need to reimplement the shape() method this way as the rectangle will provide a correct shape automatically
2. reimplement shape() for your object and make sure it always returns an aligned rectangle including the "handle" of your object which you want to use for dragging. You can intersect or sum two shapes (the outer rectangle and the inner proper shape) to have a shape consisting of the proper shape and the handle leaving the remaining space empty. I hope I'm explaining this clear enough... I have to get some sleep, maybe people will understand me better then :)

spud
26th October 2006, 16:05
Now we're getting somewhere.
The problem with those two solutions is that they are scale sensitive. I've come up with a solution overriding QGraphicView's drawForeground() and mouseEvents.

It's not perfect but it's a start.

As I said, I think this should be a common problem. Quite often you want to render a scene where most objects should be completely scalable, whereas others(text, lines, points) should be rendered with a fixed pixel size.



#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsEllipseItem>
#include <QWheelEvent>

static const QRect selectRect(-1,-1,3,3);

class GraphicsView : public QGraphicsView
{
public:
GraphicsView(QGraphicsScene *scene, QWidget *parent = 0)
: QGraphicsView(scene, parent)
, grabItem(0)
{}
void drawForeground(QPainter* painter, const QRectF& rect)
{
painter->setRenderHint(QPainter::Antialiasing, false);
foreach(QGraphicsItem*item, scene()->items()){
QGraphicsEllipseItem* ellipse = qgraphicsitem_cast<QGraphicsEllipseItem*>(item);
if(ellipse && ellipse->isSelected())
{
QPoint point1 = mapFromScene(ellipse->scenePos()+ellipse->rect().topLeft() );
QPoint point2 = mapFromScene(ellipse->scenePos()+ellipse->rect().bottomRight() );
painter->setMatrixEnabled(false);
painter->setBrush(Qt::black);
painter->drawRect(selectRect.translated(point1));
painter->drawRect(selectRect.translated(point2));
}
}
}
QGraphicsEllipseItem* hittest(QPoint point, bool& topleft)
{
foreach(QGraphicsItem*item, scene()->items()){
QGraphicsEllipseItem* ellipse = qgraphicsitem_cast<QGraphicsEllipseItem*>(item);
if(ellipse && ellipse->isSelected())
{
QPoint diff = mapFromScene(ellipse->scenePos()+ellipse->rect().topLeft())-point;
if(selectRect.adjusted(-1,-1,1,1).contains(diff)){
topleft=true;
return ellipse;
}

diff = mapFromScene(ellipse->scenePos()+ellipse->rect().bottomRight())-point;
if(selectRect.adjusted(-1,-1,1,1).contains(diff)){
topleft=false;
return ellipse;
}
}
}
return 0;
}
void mousePressEvent(QMouseEvent*event)
{
mousePos = event->pos();
grabItem = hittest(mousePos, topleft);
if(grabItem==0)
QGraphicsView::mousePressEvent(event);
}
void mouseMoveEvent(QMouseEvent*event)
{
if(grabItem){
QPoint newPos = event->pos();
QPointF diff = mapToScene(newPos)-mapToScene(mousePos);
if(topleft)
grabItem->setRect(grabItem->rect().adjusted(diff.x(), diff.y(),0,0));
else
grabItem->setRect(grabItem->rect().adjusted(0,0,diff.x(), diff.y()));
mousePos=newPos;
}
else
QGraphicsView::mouseMoveEvent(event);
}
void mouseReleaseEvent(QMouseEvent*event)
{
if(grabItem)
grabItem=0;
else
QGraphicsView::mouseReleaseEvent(event);
}
void wheelEvent(QWheelEvent* event)
{
if(event->delta()<0)
scale(0.8, 0.8);
else
scale(1.25, 1.25);
}
private:
QPoint mousePos;
QGraphicsEllipseItem* grabItem;
bool topleft;
};

int main(int argc, char *argv[])
{
QApplication app(argc, argv);

QGraphicsScene scene;
GraphicsView* view = new GraphicsView(&scene);
view->setRenderHint(QPainter::Antialiasing);
QGraphicsEllipseItem* ellipse;

ellipse = new QGraphicsEllipseItem(QRect(0,0,50,50),0, &scene);
ellipse->setFlags(QGraphicsItem::ItemIsMovable|QGraphicsIte m::ItemIsSelectable);

ellipse = new QGraphicsEllipseItem(QRect(0,0,50,50),0, &scene);
ellipse->setFlags(QGraphicsItem::ItemIsMovable|QGraphicsIte m::ItemIsSelectable);

ellipse->setPos(100,50);

view->show();

return app.exec();
}

wysota
26th October 2006, 18:20
If you only make the child object scalable, the result should be scale insensitive.

spud
26th October 2006, 18:46
It would be scale insensitive to object and scene transformations perhaps. But not to the transformation of the view(s).

wysota
26th October 2006, 19:46
It would be scale insensitive to object and scene transformations perhaps. But not to the transformation of the view(s).
Yes, this is true. But it will be if you don't paint the handles as an object but paint directly on the canvas instead. drawForeground() is something to start with, as you already mentioned.

You could also try something else. It should be possible to retrieve the transformation matrix for the view (QGraphicsView::matrix()). Then, if you inverted the matrix and applied it to the painter, you could paint scale-insensitive things in every item you want - you'd just "revert" the scaling for a little time.

Bitto
7th December 2006, 19:59
In general, though, scale-invariant items break the update contracts between the scene and the view, so if you try to implement them using a reverse-mapped transformation matrix, you're likely to see rendering bugs.

The view and scene really need to cooperate to make something like this work. We're researching it, and maybe we can come up with a good solution.

hjm
6th October 2007, 20:36
I had the same problem: multiple QGraphicsViews on the same QGraphicScene containing pointlike objects. Those objects should be represented as circles with a fixed radius (e.g. 4 pixels) and attached with a Label (short Text).

Painting them is not a problem: Having a QPainter, you can always use QPainter::resetTransform() to get back to the Widget coordinates an draw the circle and the label "upright" and untransformed on the screen.

And here is the trick I use to grab the mouse: I define a boundingRect for the pointlike items which is so large that it includes the Text for every reasonable scale ans orientation used in the views. This is necessary for correct painting anyway.

As a result, the item receives every mouse event which hits into the large boundingRect. To check whether the event actually hit the small circle, one has to check the distance between the position of the item and the position of the mouse click in viewport coordinates of the view in which the mouse was pressed. The view is available from the QGraphicsSceneMouseEvent by qobject_cast<QGraphicsView*>( event->widget()->parent() ).

Using the various map-functions, both the item coordinates and the mouse click coordinates can be transformed to view coordinates, and then the distance in pixes can be computed. If the mouse click was outside the circle, simply ignore() the event and it will be passed to the next candidate.



void PointItem::mousePressEvent( QGraphicsSceneMouseEvent * ev ) {
QGraphicsView * view = qobject_cast<QGraphicsView*>( ev->widget()->parent() );
/* check here if view is valid -- */
QPointF item_pos = view->mapFromScene( scenePos() );
QPointF mouse_pos = view->mapFromScene( ev->scenePos() );
bool ok = /* check if mouse_pos lies within 4 pixes around item_pos */
if ( ok ) {
/* grab the mouse and do something */
} else {
ev->ignore();
}
};

Bitto
7th October 2007, 17:43
Have you looked at QGraphicsItem::ItemIgnoresTransformations?

hb
15th April 2008, 19:19
I know how to draw the handles scale- and rotation-invariant

How do you do that? I am looking for a solution in this thread (http://www.qtcentre.org/forum/f-qt-programming-2/t-qgraphicsitem-with-a-cosmetic-pen-of-a-fixed-width-in-pixels-bigger-1-13057-post68419.html), but have not yet found a satisfactory result.

trilec
13th January 2012, 06:18
class SimpleItem : public QGraphicsItem
{
public:
...
SimpleItem (int dx, int dy){
setFlags(ItemIsSelectable | ItemIsMovable | ItemIgnoresTransformations);
setAcceptsHoverEvents(true);
nodeRectangle.setRect(0,0,100,30);

...
}