PDA

View Full Version : QGraphicsItem coordinates, again



d_stranz
18th June 2011, 02:26
Still struggling to understand coordinate mappings.

I have a QGraphicsItem-derived object that uses a logical X coordinate range of 0.0 - 1000.0 arbitrary units. Inside it, it creates a QGraphicsSimpleTextItem, which of course uses pixels as its logical coordinate system. I've also set ItemIgnoresTransformations on the text so it doesn't become invisibly small in the view.

I want to put the center().x() of the text item at the center().x() of my graphics item, but my graphics item doesn't know anything about pixels.

So, how to do this? The scene that contains this might be displayed in multiple views of different sizes, so I can't just position the label with respect to one view.

Thanks for any help.

wysota
18th June 2011, 02:31
boundingRect of the text item should be defined relative to the center of the item and not its top-left corner as it is by default. Then use setPos(), passing boundingRect().center() of the parent item. Alternatively use mapFrom* and mapTo* to map the coordinates between items.

d_stranz
18th June 2011, 02:59
If I call boundingRect() on the QGraphicsSimpleTextItem, I get a rect of (x = 0, y = 0, w = 86, h = 19) for the font and text string I am using. That tells me that the text object's origin is at the top left.

If the origin was at the center, I should get a rect of (x = -43, y = -9.5, w = 86, h = 19), right?

If I call textItem->setPos( bounds.center() ) (where "bounds" is the bounding rect of my object that contains the text), then the top left corner of the simple text item is at the center of my object's rect, not the text's center.

So, back to the beginning: it seems like I have an impossible situation. I have a graphics object that can be scaled by the scene, but it contains a text object that is set to ignore the scaling transformation. So how can I determine where the center of the text object is with respect to the center of my graphics object?

Is there a different way to do this? Would it be better to make my graphics object and the text object siblings instead of parent and child?

wysota
18th June 2011, 03:23
If I call boundingRect() on the QGraphicsSimpleTextItem, I get a rect of (x = 0, y = 0, w = 86, h = 19) for the font and text string I am using. That tells me that the text object's origin is at the top left.

If the origin was at the center, I should get a rect of (x = -43, y = -9.5, w = 86, h = 19), right?
Yes.


If I call textItem->setPos( bounds.center() ) (where "bounds" is the bounding rect of my object that contains the text), then the top left corner of the simple text item is at the center of my object's rect, not the text's center.
It's not what I told you to do. You need to redefine boundingRect() for the text item by subclassing it or wrapping it into another item with origin in the centre of the item.

d_stranz
18th June 2011, 04:12
It's not what I told you to do. You need to redefine boundingRect() for the text item by subclassing it or wrapping it into another item with origin in the centre of the item.

I am not trying to argue, just understand. Wrapping or subclassing the text item doesn't help, because it still has an internal coordinate system in pixels.

So, I am almost at a solution. I turned off ItemIgnoresTransformations for the QGraphicsSimpleTextItem instance. So, now when I obtain the text width and call textItem->mapRectToParent( textItem->boundingRect() ), I get a rect in centimeters. I can center the text because I have apples + apples.

To prevent it from scaling, I set a scale transform on the text item that is the inverse of the centimeter item's scale (i.e. 1 / sx, 1 / sy). So now the text remains the same size regardless of the scale of the centimeter item.

A few more tweaks, and I think I will have something that behaves the way I want it to.

Thank you for the help.

wysota
18th June 2011, 10:25
Wrapping or subclassing the text item doesn't help, because it still has an internal coordinate system in pixels.
I don't see how this matters.

Rachol
18th June 2011, 11:10
lets assume your derived graphicsItem draws a circle, so that's what you have to do:

*boundingRect() must return QRectF(-width/2, -height/2, width, height);
*in paint(..) you draw you circle, so it's center is at 0,0. painter->drawArc(-width/2, -height/2, width, height, 0, 5760);

then your derived textitem, must be drawn also in the middle:

*in boundingRect() use QFontMetrics do determine the bounding rectangle of the text. Then adjust that rect like this: rect.adjust(-rect.width()/2,-rect.height()/2,-rect.width()/2,-rect.height()/2); and then return it
*in paint(..) you draw your text in the rectangle calculated the same way as you have done it in boundingRect() method. Of course you can do the calculation elsewhere, so you don't do it over and over again.

SixDegrees
18th June 2011, 12:27
1) Determine the center of the text item in local coordinates.

2) setTransform() the item to shift its origin to the center.

3) Do the same with your parent graphicsItem.

Now, everyone is working with a center-based coordinate system.

You can do the same with scenes.

d_stranz
18th June 2011, 17:03
1) Determine the center of the text item in local coordinates.

2) setTransform() the item to shift its origin to the center.

3) Do the same with your parent graphicsItem.

Now, everyone is working with a center-based coordinate system.

You can do the same with scenes.

OK, got it. As long as everyone is in a center-based coordinate system, then I can position things with respect to their centers without worrying about transformations between pixels and a non-pixel logical coordinate system.

Why is it that QGraphicsSimpleTextItem is different from all other QGraphicsItem classes? If it also had its origin at the center, then none of this discussion would have been necessary.

Thanks to everyone for the help.


I don't see how this matters.

You're right, it doesn't. Being able to align centers is all that matters.

wysota
18th June 2011, 17:55
Why is it that QGraphicsSimpleTextItem is different from all other QGraphicsItem classes? If it also had its origin at the center, then none of this discussion would have been necessary.
None of the standard item classes have the origin at the centre.

And while we're at it, it is not required that all items are to be center-aligned. It is only required that the text item is to have its origin at its centre if you want to anchor the centre of the item to something else. If you wanted to align the bottom right corner, then the bottom right corner should be in the item origin. This is all only to make calculations easier since then the offset between the anchor and the origin is always 0, regardless of the scale or unit interpretation (0*anything = 0).

d_stranz
18th June 2011, 20:40
And while we're at it, it is not required that all items are to be center-aligned. It is only required that the text item is to have its origin at its centre if you want to anchor the centre of the item to something else. If you wanted to align the bottom right corner, then the bottom right corner should be in the item origin. This is all only to make calculations easier since then the offset between the anchor and the origin is always 0, regardless of the scale or unit interpretation (0*anything = 0).

In that case, we're back to the original question again. If all of the objects have their origins at the center, then it doesn't matter what logical coordinates they use. One could be in pixels, another in cm, and a third in cubits. If you want to put a child item at the center of its parent, then you just call child->setPos() with the parent's center point.

This is not true if the child item does not have its origin at the center, and uses a different basis unit from its parent. If the origin is at the top left or bottom left (or anywhere except the center), you then need to know how to convert pixels into cubits so you can center the pixel-based child in the cubit-based rect.

This conversion is different for every view, because the scaling from view coordinates (pixels) to scene coordinates (cubits) is different for every view, but the conversion from view pixels to text pixels is fixed and constant. So if I want my text at some fixed location in the scene, I do not see any way to assign a position to the text so that it remains at the same relative location in every view of the scene.

What am I missing here that all of you seem to understand but I don't?

wysota
18th June 2011, 21:38
I don't know why are you treating the centre of an object as some magical spot. Let's look at the definition of QGraphicsItem::setPos(). What it does is that it positions the origin of the object at a given position of the parent expressed in paren't coordinate space. It is totally irrelevant where the centre of each item is or what is the logical unit of each of the items. The only reason why we're talking about the centre of the child item is that if you make the centre the origin of the item, it is trivial to calculate the item coordinate that needs to be set in the parent's centre since this is the origin itself so it is enough to call setPos() passing the parent's centre as the position.

Now if you wanted to do the same but having the origin of the child in its top-left corner, you would have to answer the following question: What is the position of the child item's top-left corner (origin) in its parent coordinate space that makes the centres of both items overlap? The answer is calculated based on the relative scale between the items which immediately implies that if the parent item grows and the child stays the same size, the result of the calculation will change --- the top-left corner of the child item will still be in the same position of the parent (as imposed by the setPos() call) but since the parent got bigger, its centre will move (when seen from the child's coordinate space), displacing the child item.

Now if you answer the same question when the child's origin is in the centre -- What is the position of the child's centre point (origin) in its parent coordinate space that makes the centres of both items overlap? -- then the answer is obvious, it is the centre point of the parent. Now if the parent grows and the child does not, the centre point of the parent doesn't move (as seen from the child's perspective) since the result of the question is independent of the relative scale of both items because setPos() will always position the centre of the child in the centre of the parent.

Similar -- if you wanted the child to always be in the parent's lower left corner, you would align lower-left corner of the child with the lower-left corner of the parent. If you wanted the child to always be centred horizontally and aligned vertically to the top of the parent, you would align middle points of top edges of the two items. Look that the origin of the parent doesn't require any modifications in each of the cases. It is the origin of the child that changes as it is the point serving as an anchor in the parent's coordinate space. The relative scale of the two items is not part of the calculations anywhere.

Here is a demo that might help you:


#include <QtGui>

class Canvas : public QGraphicsRectItem {
public:
Canvas(){
m_item = 0;
}
void setItem(QGraphicsItem *item) {
m_item = item;
m_item->setPos(itemPos);
}
void setItemPos(const QPointF &pt) {
itemPos = pt.toPoint();
if(m_item)
m_item->setPos(itemPos);
}
protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem * option, QWidget * widget = 0){
QGraphicsRectItem::paint(painter, option, widget);
if(m_item){
painter->save();
painter->setPen(QPen(Qt::blue));
painter->setBrush(Qt::blue);
painter->drawEllipse(m_item->pos(), 5, 5);
painter->restore();
}
}
void mousePressEvent(QGraphicsSceneMouseEvent *me) {
if(m_item && me->modifiers() & Qt::ShiftModifier){
m_item->setPos(me->pos());
}
}
void wheelEvent(QGraphicsSceneWheelEvent * event){
if(event->delta()>0) {
setScale(scale()*1.1);
} else if(event->delta()<0) {
setScale(scale()/1.1);
}
}
private:
QPoint itemPos;
QGraphicsItem *m_item;
};

class Item : public QGraphicsRectItem {
public:
Item(QGraphicsItem *parent = 0) : QGraphicsRectItem(parent){}
protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem * option, QWidget * widget = 0){
QGraphicsRectItem::paint(painter, option, widget);
painter->save();
painter->setPen(QPen(Qt::black));
painter->setBrush(Qt::black);
painter->drawEllipse(QPointF(0,0), 3, 3);
painter->restore();
}
void mousePressEvent(QGraphicsSceneMouseEvent *me) {
if(me->modifiers() & Qt::ControlModifier){
QRectF r = rect();
r.translate(-me->pos());
setRect(r);
} else me->ignore();
}
};

int main(int argc, char **argv) {
QApplication app(argc, argv);
QGraphicsView view;
QGraphicsScene scene;
view.setScene(&scene);
Canvas *canvas = new Canvas;
canvas->setRect(0,0,400,400);
scene.addItem(canvas);
Item *item = new Item(canvas);
canvas->setItem(item);
item->setRect(QRect(-100,-50,200,100));
item->setBrush(QColor(255,0,0,150));
item->setPen(QPen(Qt::black));
item->setFlag(QGraphicsItem::ItemIgnoresTransformations) ;

canvas->setItemPos(canvas->boundingRect().center());
view.show();
return app.exec();
}

Ctrl-clicking on the child item sets its origin to the point of click, shift-clicking on the parent item sets the position of the child item to the point of click. Scrolling the mouse wheel over the parent item changes the scale of the parent item. The child item is ignoring transformations. Have a look at what happens when you start changing those parameters. By default the items are positioned as you want it -- with centres aligned.

d_stranz
18th June 2011, 22:50
OK, thank you for the example. This works, because all of your coordinate systems are homogeneous; that is, the coordinate systems could have any units, but logically they are all in the same unit system. There is also an implicit assumption in the graphics system that the scene coordinate system has a 1:1 mapping to the view's pixel coordinates..

To illustrate the conceptual problem I am having:

1. Assume that all of the scene and item dimensions you have specified above are in cm logical coordinates, not pixels.

2. Add these two lines after line 80 in your example:



QGraphicsSimpleTextItem * text = new QGraphicsSimpleTextItem( item );
text->setText( "Some text in the rect" );


3. Now position the center of the text so that its center corresponds to the center of the ellipse without using the view dimensions, and with the scene and parent Item coordinates assumed to be in cm.

As far as I can tell, it is impossible to do this without assuming some scale conversion between pixels and cm, and this conversion does not exist independently of the view. You cannot simply do arithmetic on the coordinates of the text and the rect item, because the coordinate systems are not the same.

wysota
19th June 2011, 00:27
OK, thank you for the example. This works, because all of your coordinate systems are homogeneous; that is, the coordinate systems could have any units, but logically they are all in the same unit system.
No. If you start scaling the parent then its logical unit changes compared to the child.


There is also an implicit assumption in the graphics system that the scene coordinate system has a 1:1 mapping to the view's pixel coordinates..[/qquote]
No. you can use QGraphicsView::scale() to change the zoom of the scene.

[quote]1. Assume that all of the scene and item dimensions you have specified above are in cm logical coordinates, not pixels.
No problem, a unit is a unit.


3. Now position the center of the text so that its center corresponds to the center of the ellipse without using the view dimensions, and with the scene and parent Item coordinates assumed to be in cm.
Look, in my code there is never any assumption about what the unit of anyone's parent is. Item X doesn't care what's its parent coordinates are, it is completely confined to its own coordinate space.

If you want to position the center of the text over whatever then define the origin of the text to be in its centre (which is easy) and use setPos() on the item, passing whatever (it's parent's boundingRect().center()). Whatever the unit of the parent is, the centre is always the centre. If the item is 10m wide, the centre will be exactly in the middle of those 10m -- at 5m, if it is 10inches wide, the centre again will be in the middle -- at 5 inches.

Here is what you wanted... Add the following to the beginning of the file:

class Text : public QGraphicsSimpleTextItem {
public:
Text(QGraphicsItem* parent = 0) : QGraphicsSimpleTextItem(parent){}

QRectF boundingRect() const {
QRectF br = QGraphicsSimpleTextItem::boundingRect();
return br.translated(-br.width()/2, -br.height()/2);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem * option, QWidget * widget = 0) {
painter->translate(-boundingRect().width()/2, -boundingRect().height()/2);
QGraphicsSimpleTextItem::paint(painter, option, widget);
}
};

and this after line #80 of the example:


Text *text = new Text(item);
text->setText("some text");
text->setPos(item->boundingRect().center());

Build, run and start clicking. You will notice the text is always center-aligned to its parent's origin (because the last line of the above snippet tells it to do so -- at the time of calling center of the boundingRect returns (0,0)). You can add some event handler that will call setPos() on the text passing it some arbitrary point and you'll see the text will be anchored to that point in the parent.

And feel free to add a call to view->scale(whatever, whatever) to break the 1:1 mapping between the scene's unit and the view's pixel. Of course ItemIgnoresTransformations will constrain the rectangle item, you can disable the flag there and e.g. set it on the text item.

d_stranz
20th June 2011, 17:37
Yes! Thank you for this example, the light bulb has finally come on.

It is the origin of the text object that counts, not its scale or location with respect to its parent.

So, I can generalize your Text class to an AlignedText class that has a Qt::Alignment property. Those flags will determine the origin of the text object in its own coordinate system, so whatever position the parent chooses, the text origin will be located at that point in the parent's frame of reference. Qt::AlignCenter will put the center of the text at the position, Qt::AlignLeft | Qt::AlignTop puts the top left corner there, etc.

Thank you all so much for your help and patience. As my head gets older, the wood gets harder and sometimes it takes a lot of hammering before the nail goes in.

wysota
20th June 2011, 22:10
It is the origin of the text object that counts, not its scale or location with respect to its parent.

That's what I was saying from the beginning:


boundingRect of the text item should be defined relative to the center of the item and not its top-left corner as it is by default. Then use setPos(), passing boundingRect().center() of the parent item.


You need to redefine boundingRect() for the text item by subclassing it or wrapping it into another item with origin in the centre of the item.


It is only required that the text item is to have its origin at its centre if you want to anchor the centre of the item to something else.

etc.



So, I can generalize your Text class to an AlignedText class that has a Qt::Alignment property. Those flags will determine the origin of the text object in its own coordinate system, so whatever position the parent chooses, the text origin will be located at that point in the parent's frame of reference. Qt::AlignCenter will put the center of the text at the position, Qt::AlignLeft | Qt::AlignTop puts the top left corner there, etc.
You can also do some code writing that will allow you to use QGraphicsLayout or you can teach the parent item to align its child in a way you find fit.