Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QGraphicsItem receive mouse events based on bounding rect, not item shape



  • I'm in a situation where I need to be able to select an item based on its geometry, not its visible shape. I have my items laid out in a grid, in a fashion where they would never overlap.
    I am using mouse press events to determine single-item selection, but it's as if my items aren't receiving mouse events that happen in their bounding rectangle, only their visible shape.
    I need it to function the same way the containing view's rubber band does when its selection mode is set to Qt::IntersectsItemBoundingRect.



  • @Ewan-Green
    Unless someone answers better. These are only suggestions from me, I have no evidence for their efficacy!

    The only reference I could find to same/similar to yours is https://stackoverflow.com/questions/44621746/qt-check-if-mouse-in-bounding-rect-of-qgraphicsitem. That went without any answers, which is often indicative that nobody knows!

    Did you look at what each of boundingRegion(), boundingRect() & shape() return for your QGraphicsItems? Although documentation talks about repaint & collision detection for these, worth testing whether they have any effect on mouse events? For example, maybe shape() would be better returning a rectangle than your actual shape?

    Otherwise maybe you can implement what you need by intercepting QGraphicsView/Scene mouse events and doing your own "intersects" instead of those on the QGraphiceItems themsleves?

    Finally, if you say view's rubber band does the right thing, you could always look at its code for some ideas....!



  • Thank you! I've devised this based on what you've said.

    void AbstractTileView::mousePressEvent(QMouseEvent *evt) {
        for (int i = 0; i < items().size(); i++) {
          QGraphicsItem *item = items().at(i);
          if (item->boundingRegion(item->sceneTransform()).contains(evt->pos())) {
            item->setSelected(!item->isSelected());
            return;  // This can happen twice probably cuz of float rounding nonsense
          }
        }
    }
    

    It does exactly what I want, but I'm concerned that iterating over every single item in the scene every mouse press event is expensive & unnecessary. How can I make this better?



  • @Ewan-Green
    Can you use one of the QGraphicsScene::items() overloads to map the mouse position to your QGraphicsItem(s) there? I think the point is that is fast.

    QGraphicsScene uses an indexing algorithm to manage the location of items efficiently.

    There are Qt::IntersectsItemBoundingRect or Qt::IntersectsItemShape variants. If it's not accurate for you, you could use the const QRectF &rect overload to match against an area around the mouse point, at least to narrow down which QGraphicsItems before applying your test above.



  • Thank you. I've improved the function using the items() indexing. It looks like this:

      if (evt->button() == Qt::LeftButton) {
        QList<QGraphicsItem *> items =
            this->items(QRect(evt->pos().x() - 10, evt->pos().y() - 10,
                              evt->pos().x() + 10, evt->pos().y() + 10),
                        Qt::IntersectsItemBoundingRect);
        QListIterator<QGraphicsItem *> it(items);
        while (it.hasNext()) {
          QGraphicsItem *item = it.next();
          if (item->boundingRegion(item->sceneTransform()).contains(evt->pos())) {
            item->setSelected(!item->isSelected());
            return;  // This can happen twice cuz of float rounding nonsense
          }
        }
    

    Now my question is, how can I have my overridden mousePressEvent() and still have the rubber band? I don't know of a way to do this without overriding the drag events as well. In this situation I would usually call the method of the parent class, but it's a protected member...



  • @Ewan-Green said in QGraphicsItem receive mouse events based on bounding rect, not item shape:

    In this situation I would usually call the method of the parent class, but it's a protected member...

    I don't know about whatever the rubber band does. But what method of what parent's class are you talking about?



  • My class inherits QGraphicsScene. The QGraphicsScene mousePressEvent() surely has something to allow for the generation of the rubber band, which is why I want to call it.



  • @Ewan-Green said in QGraphicsItem receive mouse events based on bounding rect, not item shape:

    My class inherits QGraphicsScene

    So whatever it is you are looking for, if it's a protected QGrapicsScene::... method then you can access/call it....?



  • My bad, I was actually using QGraphicsView as a base. I've instead made a custom QGraphicsScene and I've overridden the mouse press event, like so:

    void CustomGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *evt) {
      switch (evt->button()) {
        case Qt::LeftButton: {
          QList<QGraphicsItem *> items =
              this->items(evt->pos(), Qt::IntersectsItemBoundingRect);
          QListIterator<QGraphicsItem *> it(items);
          while (it.hasNext()) {
            QGraphicsItem *item = it.next();
            if (item->boundingRegion(item->sceneTransform())
                    .contains(evt->pos().toPoint())) {
              item->setSelected(!item->isSelected());
              break;  // This can happen twice cuz of float rounding nonsense
            }
          }
          break;
        }
        case Qt::RightButton: {
          clearSelection();
          break;
        }
        default: {
          break;
        }
      }
      QGraphicsScene::mousePressEvent(evt);
    }
    

    The reason I don't just use the graphics view mouse event is because, even though it was working before to an extent, it broke when I scrolled because the mouse event's position doesn't scroll with it.
    This doesn't work the same as that did, though, when it comes to selection of the whole item. Now I'm back where I started and am considering creating a rectangle around the point & getting the containing items of that & doing the boundingRect() logic myself. The code that somewhat worked before:

    void CustomGraphicsView::mousePressEvent(QMouseEvent *evt) {
      switch (evt->button()) {
        case Qt::LeftButton: {
          QList<QGraphicsItem *> items =
              this->scene()->items(evt->pos(), Qt::IntersectsItemBoundingRect);
          QListIterator<QGraphicsItem *> it(items);
          while (it.hasNext()) {
            QGraphicsItem *item = it.next();
            if (item->boundingRegion(item->sceneTransform()).contains(evt->pos())) {
              item->setSelected(!item->isSelected());
              break;  // This can happen twice cuz of float rounding nonsense
            }
          }
          break;
        }
        case Qt::RightButton: {
          scene()->clearSelection();
          break;
        }
        default: {
          break;
        }
      }
      QGraphicsView::mousePressEvent(evt);
    }
    

Log in to reply