4

I use QGraphicsScene of the Qt framework. Inside the scene I have some QGraphicsItems which the user can select and move. I would like to have an info label where the current x and y coordinate of the currently moved selection (can consist of many items) is displayed.

I have tried with the signal changed of QGraphicsScene. But it is fired before the x() and y() property of the items is set to the new values. So the labels always show the second-to-last coordinates. If one moves the mouse slowly, the display is not very wrong. But with fast moves and sudden stops, the labels are wrong. I need a signal that is fired after the scene hast changed.

I have also tried to override the itemChange method of QGraphicsItem. But it is the same. It is fired before the change. (The new coordinates are inside the parameters of this method, but I need the new coordinates of all selected items at once)

I have also tried to override the mouseMove events of QGraphicsScene and of QGraphicsView but they, too, are before the new coordinates are set.

I did a test: I used a oneshot timer so that the labels are updated 100 ms after the signals. Then everything works fine. But a timer is no solution for me.

What can I do? Make all items un-moveable and handle everything by my own?

eikuh
  • 673
  • 1
  • 9
  • 18
  • A 0ms timer has a special meaning: wait until all events have been processed and then timeout. That's still not ideal, but it's probably exactly equivalent to the behaviour that you want (unlike a 100ms timer). I'd consider it as a fallback. – cgmb Jun 27 '12 at 18:15
  • Also, have to tried calling QApplication.processEvents() first before checking the values? The docs for `itemChange` indicate that you should be able to see the new value just fine. – jdi Jun 27 '12 at 18:17
  • @Slavik81: Thank you very, very much! Your solution with the 0ms timer did work and solve the problem! It is still not a very beautiful way, but probably the best of all possible ways. How can I mark your answer as solution? – eikuh Jun 28 '12 at 07:22
  • @jdi: Thank you for your answer, but the processEvents() method did not solve the problem. You are right that the itemChange methods of all items have the new values as parameter but if I move 10 items simultaneously, then I would get 10 signals... – eikuh Jun 28 '12 at 07:23

2 Answers2

5

QGraphicsItem::itemChange() is the correct approach, you were probably just checking the wrong flag. Something like this should work fine:

QVariant::myGraphicsItem( GraphicsItemChange change, const QVariant &value )
{
  if( change == QGraphicsItem::ItemPositionHasChanged )
  {
     // ...
  }
}

Note the use of QGraphicsItem::ItemPositionHasChanged rather than QGraphicsItem::ItemPositionChange, the former is called after the position changes rather than before.

Chris
  • 17,119
  • 5
  • 57
  • 60
  • Thank you for your answer! But when I move 100 items, then I would get 100 separate itemChange signals... I need the information in one place at one time. – eikuh Jun 28 '12 at 14:59
  • 1
    The "getting all information in one place at one time" is apart from the question you asked, and I think we'd need more information as to what you're trying to accomplish exactly in order to offer an elegant solution. – Chris Jun 28 '12 at 20:03
3

The solution is to combine various things that you're already doing. Instrument itemChange, looking for and count the items with updated geometry. Once you've counted as many items as there are in the current selection, fire off a signal that will have everything ready for updating your status. Make sure you've set the QGraphicsItem::ItemSendsGeometryChanges flag on all your items!

This code was edited to remove the lag inherent in using a zero-timer approach. Below is a sscce that demonstrates it.

You create circles of random radius by clicking in the window. The selection is toggled with Ctrl-click or ⌘-click. When you move the items, a centroid diamond follows the centroid of the selected group. This gives a visual confirmation that the code does indeed work. When the selection is empty, the centroid is not displayed.

I've gratuitously added code to show how to leverage Qt's property system so that the items can be generic and leverage the notifier property of a scene if it has one. In its absence, the items simply don't notify, and that's it.

screenshot of the example

// https://github.com/KubaO/stackoverflown/tree/master/questions/scenemod-11232425
#include <QtWidgets>

const char kNotifier[] = "notifier";

class Notifier : public QObject
{
   Q_OBJECT
   int m_count = {};
public:
   int count() const { return m_count; }
   void inc() { m_count ++; }
   void notify() { m_count = {}; emit notification(); }
   Q_SIGNAL void notification();
};

typedef QPointer<Notifier> NotifierPointer;
Q_DECLARE_METATYPE(NotifierPointer)

template <typename T> class NotifyingItem : public T
{
protected:
   QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override {
      QVariant v;
      if (change == T::ItemPositionHasChanged &&
          this->scene() &&
          (v=this->scene()->property(kNotifier)).isValid())
      {
         auto notifier = v.value<NotifierPointer>();
         notifier->inc();
         if (notifier->count() >= this->scene()->selectedItems().count()) {
            notifier->notify();
         }
      }
      return T::itemChange(change, value);
   }
};

// Note that all you need to make Circle a notifying item is to derive from
// NotifyingItem<basetype>.

class Circle : public NotifyingItem<QGraphicsEllipseItem>
{
   QBrush m_brush;
public:
   Circle(const QPointF & c) : m_brush(Qt::lightGray) {
      const qreal r = 10.0 + (50.0*qrand())/RAND_MAX;
      setRect({-r, -r, 2.0*r, 2.0*r});
      setPos(c);
      setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable |
               QGraphicsItem::ItemSendsGeometryChanges);
      setPen({Qt::red});
      setBrush(m_brush);
   }
};

class View : public QGraphicsView
{
   Q_OBJECT
   QGraphicsScene scene;
   QGraphicsSimpleTextItem text;
   QGraphicsRectItem centroid{-5, -5, 10, 10};
   Notifier notifier;
   int deltaCounter = {};
public:
   explicit View(QWidget *parent = {});
protected:
   Q_SLOT void gotUpdates();
   void mousePressEvent(QMouseEvent *event) override;
};

View::View(QWidget *parent) : QGraphicsView(parent)
{
   centroid.hide();
   centroid.setRotation(45.0);
   centroid.setPen({Qt::blue});
   centroid.setZValue(2);
   scene.addItem(&centroid);
   text.setPos(5, 470);
   text.setZValue(1);
   scene.addItem(&text);
   setRenderHint(QPainter::Antialiasing);
   setScene(&scene);
   setSceneRect(0,0,500,500);
   scene.setProperty(kNotifier, QVariant::fromValue(NotifierPointer(&notifier)));
   connect(&notifier, &Notifier::notification, this, &View::gotUpdates);
   connect(&scene, &QGraphicsScene::selectionChanged, &notifier, &Notifier::notification);
}

void View::gotUpdates()
{
   if (scene.selectedItems().isEmpty()) {
      centroid.hide();
      return;
   }
   centroid.show();
   QPointF centroid;
   qreal area = {};
   for (auto item : scene.selectedItems()) {
      const QRectF r = item->boundingRect();
      const qreal a = r.width() * r.height();
      centroid += item->pos() * a;
      area += a;
   }
   if (area > 0) centroid /= area;
   auto st = QStringLiteral("delta #%1 with %2 items, centroid at %3, %4")
         .arg(deltaCounter++).arg(scene.selectedItems().count())
         .arg(centroid.x(), 0, 'f', 1).arg(centroid.y(), 0, 'f', 1);
   this->centroid.setPos(centroid);
   text.setText(st);
}

void View::mousePressEvent(QMouseEvent *event)
{
   const auto center = mapToScene(event->pos());
   if (! scene.itemAt(center, {})) scene.addItem(new Circle{center});
   QGraphicsView::mousePressEvent(event);
}

int main(int argc, char *argv[])
{
   QApplication app{argc, argv};
   View v;
   v.show();
   return app.exec();
}
#include "main.moc"
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Thank you for your answer! It seems to work but would be very cumbersome. I like Slavik81's idea better. He uses a 0ms timer. – eikuh Jun 28 '12 at 07:25
  • Run the code and see how it performs. A previous version (check the edits) used a 0ms timer and lagged -- as one would expect. Run that one too and see for yourself. To see the effect you must have graphical feedback -- your vision will notice the lag. I don't see how it's cumbersome -- you can factor out the `itemChange` into a separate template class in the [CRTP](http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern) fashion. I'm editing the code to show how. – Kuba hasn't forgotten Monica Jun 28 '12 at 12:53
  • IOW: If you think this is "cumbersome", you've seen nothin'. Is adding `NotifyngItem` "cumbersome"? I'd say it's the cleanest way to do it given existing Qt implementation, and given that you don't want any lag. Try it, and also try the code with 0ms timer :) – Kuba hasn't forgotten Monica Jun 28 '12 at 13:01