Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Maintaining relative child position after applying QGraphicsItem::ItemIgnoresTransformations

I have a QGraphicsTextItem parented to a QGraphicsItem. I want the QGraphicsTextItem to always reside directly above the QGraphicsItem, but I also want the text to remain the same size when the scale factor goes below 1, i.e. the text remains the size it is at a scale factor of 1 even when the parent graphics item is scaled smaller. I have found that setting the QGraphicsItem::ItemIgnoresTransformations flag to true when the scale factor is below 1 does the trick for retaining the size.

But I can’t seem to find a way to get the position of the text to always remain above the QGraphicsItem. Is there a way to do this? I tried using deviceTransform () function, but the text still moved off of the QGraphicsItem as I scrolled out. What was worse is that some of the text items started “jiggling”, i.e. they started continuously changing their position ever so slightly, so that it looked like they were shaking. If this is the function I need to use, I guess I don’t know how to use it properly.

In the constructor of my QGraphicsItem I’ve added a QGraphicsTextItem:

fTextItem = new QGraphicsTextItem(getName(), this);
fTextItem->setFlag(QGraphicsItem::ItemIgnoresTransformations);

Here is code snippet from paint function of QGraphicsItem

qreal lod = painter->worldTransform().m22();
if(lod <= 1.0) {
     fTextItem-setFlag(QGraphicsItem::ItemIgnoresTransformations);
     fTextItem->setPos(fTextItem->deviceTransform(view-viewportTransform()).inverted().map(view->mapFromScene(mapToScene(0,0))));
} else {
     fTextItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, false);
     fTextItem->setPos(0, 0);
}
like image 512
KD07 Avatar asked Jul 12 '12 19:07

KD07


3 Answers

My suggestion is to subclass QGraphicsSimpleTextItem in this manner:

class TextItem
    : public QGraphicsSimpleTextItem
{
public:
    TextItem(const QString &text)
        : QGraphicsSimpleTextItem(text)
    {

    }
    void paint(QPainter *painter, 
        const QStyleOptionGraphicsItem *option, QWidget *widget)
    {
        painter->translate(boundingRect().topLeft());
        QGraphicsSimpleTextItem::paint(painter, option, widget);
        painter->translate(-boundingRect().topLeft());
    }
    QRectF boundingRect() const
    {
        QRectF b = QGraphicsSimpleTextItem::boundingRect();
        return QRectF(b.x()-b.width()/2.0, b.y()-b.height()/2.0, 
            b.width(), b.height());
    }
};
QGraphicsSimpleTextItem *mText = new TextItem("Item");
scene()->addItem(mText);
mText->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
mText->setPos(itemToFollow->pos());
like image 179
Pat Avatar answered Nov 14 '22 13:11

Pat


Disclaimer: this may be overkill for what you are trying to do. We had some additional restrictions in our project that made this solution the easiest for us.

We had to do something similar in a project, and it ended up being easiest for us to not use ItemIgnoresTransformations and instead roll our own transform. Here is the main function we use to create a translation-only (no scaling) transform for drawing an item at a specific location. You might be able to modify it for your usage.

static QTransform GenerateTranslationOnlyTransform(
    const QTransform &original_transform,
    const QPointF &target_point) {
  // To draw the unscaled icons, we desire a transform with scaling factors
  // of 1 and shearing factors of 0 and the appropriate translation such that
  // our icon center ends up at the same point. According to the
  // documentation, QTransform transforms a point in the plane to another
  // point using the following formulas:
  // x' = m11*x + m21*y + dx
  // y' = m22*y + m12*x + dy
  //
  // For our new transform, m11 and m22 (scaling) are 1, and m21 and m12
  // (shearing) are 0. Since we want x' and y' to be the same, we have the
  // following equations:
  // m11*x + m21*y + dx = x + dx[new]
  // m22*y + m12*x + dy = y + dy[new]
  //
  // Thus,
  // dx[new] = m11*x - x + m21*y + dx
  // dy[new] = m22*y - y + m12*x + dy
  qreal dx = original_transform.m11() * target_point.x()
             - target_point.x()
             + original_transform.m21() * target_point.y()
             + original_transform.m31();
  qreal dy = original_transform.m22() * target_point.y()
             - target_point.y()
             + original_transform.m12() * target_point.x()
             + original_transform.m32();

  return QTransform::fromTranslate(dx, dy);
}

To use, take the QPainter transform that is passed to the paint method and do something like:

painter->save();
painter->setTransform(GenerateTranslationOnlyTransform(painter->transform(),
                                                       some_point));
// Draw your item.
painter->restore();
like image 4
Dave Mateer Avatar answered Nov 14 '22 14:11

Dave Mateer


Great answer by Dave Mateer! I had the problem that I wanted to define a different scale factor at different zoom levels. This is how I did it:

void MyGraphicsItem::paint(QPainter * painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
    //save painter for later operations
    painter->save();
    QTransform originalTransform = painter->transform();
    QPointF originalCenter = rect().center();
    qreal dx = originalTransform.m11() * originalCenter.x() + originalTransform.m21() * originalCenter.y() + originalTransform.m31();
    qreal dy = originalTransform.m22() * originalCenter.y() + originalTransform.m12() * originalCenter.x() + originalTransform.m32();
    //normally our target scale factor is 1, meaning the item has keeps its size, regardless of zoom
    //we adjust the scale factor though when the item is smaller than one pixel in comparison to the background image
    qreal factor = 1.0;
    //check if scale factor if bigger that the item size, and thus it occupies less that a pixel in comparision to the background image
    if (rect().width() < originalTransform.m11()) {
        //calculate adjusted scale factor
        factor = originalTransform.m11() / rect().width();
    }
    //adjust position according to scale factor
    dx -= factor * originalCenter.x();
    dy -= factor * originalCenter.y();
    //set the new transform for painting
    painter->setTransform(QTransform::fromScale(factor, factor) * QTransform::fromTranslate(dx, dy));
    //now paint...
    QGraphicsXYZItem::paint(painter, option, widget);
    //restore original painter
    painter->restore();
}

You do need to adjust the bounding rectangle too in that case:

QRectF MyGraphicsItem::boundingRect() const
{
    QRectF rect = QGraphicsEllipseItem::boundingRect();
    //this is a bit hackish, let me know if you know another way...
    if (scene() != NULL && scene()->views().at(0) != NULL)
    {
        //get viewport transform
        QTransform itemTransform = scene()->views().at(0)->transform();
        QPointF originalCenter = rect.center();
        //calculate back-projected original size of item
        qreal realSizeX = rect.width() / itemTransform.m11();
        qreal realSizeY = rect.height() / itemTransform.m11();
        //check if scale factor is bigger that the item size, and thus it occupies less that a pixel in comparison 
        //to the background image and adjust size back to equivalent of 1 pixel
        realSizeX = realSizeX < 1.0 ? 1.0 : realSizeX;
        realSizeY = realSizeY < 1.0 ? 1.0 : realSizeY;
        //set adjusted position and size according to scale factor
        rect = QRectF(rect.center().x() - realSizeX / 2.0, rect.center().y() - realSizeY / 2.0, realSizeX, realSizeY);
    }
    return rect;
}

With this solution the item work very well in my case.

like image 1
Bim Avatar answered Nov 14 '22 12:11

Bim