Need a QGraphicsScene signal or event for _after_ change

I am using QGraphicsScene Qt framework. Inside the scene, I have a QGraphicsItem that the user can select and move. I would like to have an information label where the current x and y coordinates of the current moved selection are displayed (can consist of many elements).

I tried with the changed signal from QGraphicsScene . But it starts before the properties x () and y () of the elements are set to new values. Thus, labels always display the second-last coordinates. If you move your mouse slowly, the display is not very wrong. But with fast movements and sudden stops, the marks are wrong. I need a signal that fires after a scene changes.

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

I also tried to override the mouseMove events of QGraphicsScene and QGraphicsView , but they are also in front of setting new coordinates.

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

What can I do? Make all items immovable and process everything yourself?

+4
source share
2 answers

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

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

Pay attention to the use of QGraphicsItem::ItemPositionHasChanged , not QGraphicsItem::ItemPositionChange , the first is called after changing the position, and not earlier.

+4
source

The solution is to combine the various things that you are already doing. Tool itemChange , search and count items with updated geometry. When you have counted as many elements as there are in the current selection, trigger a signal that will be ready to update your status. Make sure you set the QGraphicsItem::ItemSendsGeometryChanges on all of your items!

This code has been edited to eliminate the lag inherent in using a zero-timer approach. Below is an sscce that demonstrates it.

You create circles of random radius by clicking in the window. The selection is switched using Ctrl-click or ⌘-click. When you move objects, the centroid diamond follows the centroid of the selected group. This gives a clear confirmation that the code really works. When the selection is empty, the centroid is not displayed.

I added code for free to show how to use the Qt property system so that elements can be universal and use the scene's notifier property, if any. In his absence, the elements simply do not notify about it.

example screenshot

 // 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" 
+2
source

All Articles