Custom QGraphicsItem - weird event handling in parent and child



  • Hi all :)

    This is a re-post of my question on Stackoverflow. It is also a follow-up of an answer I gave a few days ago here (I didn't include it here because the post will become a novel :D). The Qt version that I'm using Qt 5.5 on a 64bit Debian Jessie.

    Recently I've started working with QGraphicsViewer, QGraphicsScene and QGraphicsItem just out of curiosity. I decided to implement my own version of QGraphicsItem but also take what's already there whenever I don't want to customize a specific feature of that class.

    From the link above you can see my original code without the usage of a custom QGraphicsItem. The purpose is to create an UI that uses a composite node view allowing an easy and fast creation of image processing pipelines. Each node will be represented by a QGraphicsProxyWidget as well as 3 custom QGraphicsItem instances each serving a specific purpose. The example I've given in the linked answer of mine I am using an item positioned at the top of the proxy widget, which will be responsible for selecting, moving or deleting the entire node (proxy widgets and all 3 QGraphicsItems).

    Below you can see the additional code. I'm at a very early stage of experimenting so things like memory leaks etc. are not of the essence here.

    proxytransformitem.hpp

    #ifndef PROXYTRANSFORMITEM_HPP
    #define PROXYTRANSFORMITEM_HPP
    #include <QGraphicsRectItem>
    #include <QPainter>
    #include <QStyleOptionGraphicsItem>
    #include <QPen>
    #include <QBrush>
    #include <QGraphicsSceneHoverEvent>
    //#include <QRectF>
    
    class ProxyTransformItem : public QGraphicsRectItem
    {
        qreal w;
        qreal h;
        bool hovered;
    
        QPen *defaultPen;
        QBrush *defaultInlineBrush;
        QBrush *inlineBrushHover;
        QBrush *inlineBrushSelected;
    
      public:
        explicit ProxyTransformItem(qreal x, qreal y, qreal width, qreal height, QGraphicsItem *parent = 0);
        ~ProxyTransformItem();
    
        void setPaintTools(QPen *defaultPen, QBrush *defaultInlineBrush, QBrush *inlineBrushHover, QBrush *inlineBrushSelected);
    
        float width();
        float height();
    
      protected:
        virtual QRectF boundingRect() const;
        virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0);
        virtual QPainterPath shape();
    
        virtual void hoverEnterEvent(QGraphicsSceneHoverEvent *event);
        //virtual void hoverMoveEvent(QGraphicsSceneHoverEvent *event);
        virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent *event);
    
        virtual void mousePressEvent(QGraphicsSceneMouseEvent *event);
        virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
        virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event);
        virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
    };
    
    #endif // PROXYTRANSFORMITEM_HPP
    

    proxytransformitem.cpp

    #include "proxytransformitem.hpp"
    #include <iostream>
    
    ProxyTransformItem::ProxyTransformItem(qreal x, qreal y, qreal _width, qreal _height, QGraphicsItem *parent) :
      QGraphicsRectItem(parent),
      w(_width),
      h(_height),
      hovered(false)
    {
      setAcceptHoverEvents(true);
      setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
      setFlag(QGraphicsItem::ItemIsMovable);
      setFlag(QGraphicsItem::ItemIsSelectable);
      setFlag(QGraphicsItem::ItemSendsGeometryChanges);
      setPos(x, y);
    }
    
    ProxyTransformItem::~ProxyTransformItem()
    {
    
    }
    
    void ProxyTransformItem::setPaintTools(QPen *_defaultPen, QBrush *_defaultInlineBrush, QBrush *_inlineBrushHover, QBrush *_inlineBrushSelected)
    {
      defaultPen = _defaultPen;
      defaultInlineBrush = _defaultInlineBrush;
      inlineBrushHover = _inlineBrushHover;
      inlineBrushSelected = _inlineBrushSelected;
    }
    
    float ProxyTransformItem::width()
    {
      return w;
    }
    
    float ProxyTransformItem::height()
    {
      return h;
    }
    
    QRectF ProxyTransformItem::boundingRect() const
    {
     return QRectF(0, 0, w, h);
    }
    
    void ProxyTransformItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
    {
      QRectF rect = boundingRect();
      painter->setPen(*defaultPen);
      if (hovered) painter->setBrush(*inlineBrushHover);
      else
      {
        if(!isSelected()) painter->setBrush(*defaultInlineBrush);
        else painter->setBrush(*inlineBrushSelected);
      }
    
    //  painter->drawRect(rect.x() - defaultPen->width(), rect.y() - defaultPen->width(), w + defaultPen->width(), h + defaultPen->width());
      painter->drawRect(rect);
    }
    
    QPainterPath ProxyTransformItem::shape()
    {
      QPainterPath path;
      QRectF rect = boundingRect();
    //  path.addRect(rect.x() - defaultPen->width(), rect.y() - defaultPen->width(), w + defaultPen->width(), h + defaultPen->width());
      path.addRect(rect);
      return path;
    }
    
    void ProxyTransformItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
    {
      hovered = true;
      prepareGeometryChange();
      QGraphicsRectItem::hoverEnterEvent(event);
    }
    
    void ProxyTransformItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
    {
      hovered = false;
      prepareGeometryChange();
      QGraphicsRectItem::hoverLeaveEvent(event);
    }
    
    void ProxyTransformItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
    {
      // TODO
      QGraphicsRectItem::mousePressEvent(event);
    }
    
    void ProxyTransformItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
    {
      // TODO
      QGraphicsRectItem::mouseReleaseEvent(event);
    }
    
    void ProxyTransformItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
    {
      // TODO
      QGraphicsRectItem::mouseDoubleClickEvent(event);
    }
    
    void ProxyTransformItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
    {
      // TODO
      QGraphicsRectItem::mouseMoveEvent(event);
    }
    

    scenewithmovableproxies.hpp

    #ifndef SCENEWITHMOVABLEPROXIES_HPP
    #define SCENEWITHMOVABLEPROXIES_HPP
    
    #include <QWidget>
    #include <QPoint>
    
    namespace Ui {
      class SceneWithMovableProxies;
    }
    
    class SceneWithMovableProxies : public QWidget
    {
        Q_OBJECT
    
      public:
        explicit SceneWithMovableProxies(QWidget *parent = 0);
        ~SceneWithMovableProxies();
    
      private:
        Ui::SceneWithMovableProxies *ui;
        QGraphicsProxyWidget* addWidgetToScene(QPointF initPos);
    
        QPen *defaultPen;
        QBrush *defaultInlineBrush;
        QBrush *inlineBrushHover;
        QBrush *inlineBrushSelected;
    };
    
    #endif // SCENEWITHMOVABLEPROXIES_HPP
    

    scenewithmovableproxies.cpp

    #include "scenewithmovableproxies.hpp"
    #include "ui_scenewithmovableproxies.h"
    #include "scenewidgetitem.hpp"
    #include "proxytransformitem.hpp"
    #include <QGraphicsProxyWidget>
    #include <iostream>
    
    SceneWithMovableProxies::SceneWithMovableProxies(QWidget *parent) :
      QWidget(parent),
      ui(new Ui::SceneWithMovableProxies)
    {
      ui->setupUi(this);
      ui->graphicsView->setRenderHint(QPainter::Antialiasing);
      ui->graphicsView->setScene(new QGraphicsScene(this));
    
      defaultPen = new QPen(Qt::black);
      defaultPen->setWidth(.2);
      defaultInlineBrush = new QBrush(Qt::darkGreen);
      inlineBrushHover = new QBrush(Qt::red);
      inlineBrushSelected = new QBrush(Qt::yellow);
    
      QGraphicsProxyWidget *proxy1 = addWidgetToScene(QPoint(10, 10));
      QGraphicsProxyWidget *proxy2 = addWidgetToScene(QPoint(300, 100));
      QGraphicsProxyWidget *proxy3 = addWidgetToScene(QPoint(200, 200));
    
      std::cout << proxy2->collidesWithItem(proxy3) << std::endl;
    }
    
    SceneWithMovableProxies::~SceneWithMovableProxies()
    {
      delete defaultPen;
      delete defaultInlineBrush;
      delete inlineBrushHover;
      delete inlineBrushSelected;
      delete ui;
    }
    
    QGraphicsProxyWidget* SceneWithMovableProxies::addWidgetToScene(QPointF initPos)
    {
      SceneWidgetItem *widget = new SceneWidgetItem();
    
      ProxyTransformItem *proxyControl = new ProxyTransformItem(initPos.x(), initPos.y(), widget->width(), 20);
      ui->graphicsView->scene()->addItem(proxyControl);
      proxyControl->setPaintTools(defaultPen, defaultInlineBrush, inlineBrushHover, inlineBrushSelected);
    
    //  QGraphicsRectItem *proxyControl = ui->graphicsView->scene()->addRect(initPos.x(), initPos.y(), widget->width(), 20, QPen(Qt::black), QBrush(Qt::darkGreen));
    //  proxyControl->setFlag(QGraphicsItem::ItemIsMovable, true);
    //  proxyControl->setFlag(QGraphicsItem::ItemIsSelectable, true);
    
      QGraphicsProxyWidget * const proxy = ui->graphicsView->scene()->addWidget(widget);
      proxy->setPos(0, proxyControl->height());
    //  proxy->setPos(initPos.x(), initPos.y()+proxyControl->rect().height());
      proxy->setParentItem(proxyControl);
    
    //  proxyControl->setRotation(45);
    
      return proxy;
    }
    

    Now to my problem - both my proxy widget (child) and my custom QGraphicsRectItem (parent) are doing some weird handling of events:

    • In my original code if I use the standard QGraphicsItem (and its flavours) things work as expected - I click on the QGraphicsItem (not the QGraphicsProxyWidget!), it gets selected. I click somewhere on the scene or another item - it gets deselected. I press the item and move around my cursor - both the item and the proxy attached to it as a child move around. Everything works as intended.

    • With my custom QGraphicsRectItem whenever I hover my proxy, the parent (the custom item) changes its colour however if I hover the custom item nothing happening. Clicking onto the item doesn't work either. Clicking on the proxy works as expected - UI components (checkbox, combobox, button) can be interacted with.

    I would very much like to have the same behaviour as in the linked answer of mine however with my custom QGraphicsRectItem (btw initially it was inheriting from QGraphicsItem but since a rectangle is what I need I decided to go for the QGraphicsRectItem. Alas! neither worked as intended.

    After looking up online all I was able to find was the exact opposite - child item not handling events and not the parent. I have the feeling that for some reason my child (the proxy widget) is eating up my events that are supposed to be processed by the parent (the custom item). Then again this doesn't quite explain why an event on top of my proxy reflects in my custom item...

    I am pretty sure that I haven't messed up the relation because

    proxy->setParentItem(proxyControl); // proxy : QGraphicsProxyWidget | proxyControl : custom QGraphicsRectItem
    

    and also calling childItem() on my custom item after adding the proxy widget as its child

    QList<QGraphicsItem *> childrenOfProxyControl = proxyControl->childItems();
      for(int i = 0; i < childrenOfProxyControl.length(); i++)
        std::cout << "Child " << i << std::endl;
    

    does return a single child for each node I add. If I call the code above on my proxy widget I don't get any children.


  • Qt Champions 2016

    @Red-Baron
    Hello,
    I'm not completely sure I understand the problem, but it sounds like you want your parent's events to be processed in the child, which at least to my knowledge is quite the opposite of what Qt will do. If a child doesn't process (and accept) the event it'll be passed to the parent and so on, so it "bubbles up" the object hierarchy. I'm not aware, however, of any standard method to make the parent process its childrens' events besides intercepting them with an event filter. If you wish to experiment with that you could use QGraphicsItem::installSceneEventFilter.

    Kind regards.



  • @kshegunov Thanks for the reply.

    No, I actually don't want parent to be processed in child. It's the current situation - child takes over the parent - that I have no idea how to fix. Since it's a proxy widget I don't want any QGraphicsItem specific mouse handlers so that I don't screw up the interaction with the QWdiget that the proxy represents and its UI components (checkboxes, buttons etc.). As mentioned this unexpected behaviour started happening once I inherited QGraphicsItem (or its other flavours). It was not happening before when I was using a normal QGraphicsItem. You can check what behaviour I really want to have by following the second link to SO - the answer I gave to a question there. The code can be compiled and executed.


  • Qt Champions 2016

    @Red-Baron
    Do you mind putting a MWE in a repo somewhere, so I could just download the code an play with it a bit? It's quite hard to sift through all the fragments (and screenshots from the SO post).



  • @kshegunov Sure thing. Thanks for taking the time. :) Here is the link to a GitLab repo with the code. It represents the very basics of what I'm going to do. I want to replace the default QGraphicsItem with my own adding hover event handling and other features. And here is the one with the custom QGraphicsRectItem that doesn't work as intended.


  • Qt Champions 2016

    @Red-Baron
    Hello,
    Change this:

    ProxyTransformItem::ProxyTransformItem(qreal x, qreal y, qreal _width, qreal _height, QGraphicsItem *parent) :
      QGraphicsRectItem(parent),
      w(_width),
      h(_height),
      hovered(false)
    {
        // ...
    }
    

    To this:

    ProxyTransformItem::ProxyTransformItem(qreal x, qreal y, qreal _width, qreal _height, QGraphicsItem *parent) :
      QGraphicsRectItem(QRectF(x, y, _width, _height), parent),
      w(_width),
      h(_height),
      hovered(false)
    {
         // ...
    }
    

    And you'll see that you're going to correctly receive the events. If you need to know why, my guess is that the scene optimizes the items based on their geometry so you have a nice BSP retrieval (instead a full fledged searching) for an item (or a shape item that is) when the mouse moves. So making your rectangle item have invalid geometry in the constructor makes this impossible and consequently you're getting weird behavior. Additionally, if you're not doing anything special, you should use what the base class provides for the geometry instead of adding your own members (I'm talking about
    qreal ProxyTransformItem::w and qreal ProxyTransformItem::h). Use QGraphicsRectItem::rect for that purpose, since you're already deriving from the rect item and probably something in the graphics scene engine already depends on its rect property. Furthermore, your shape() implementation seems to do what the default does - just returning the rectangle, so you should consider leaving the base class to handle that instead.

    PS.
    Your problem didn't have to do with the proxy widget actually. While testing I removed it to verify that the rectangle item was receiving the events and it didn't. That's what made my investigations easier so I knew I had to focus on the rect item itself and not on the widget proxy.

    Kind regards.



  • @kshegunov Thanks for the corrections. The shape is something that I will be changing. As for the width and height - I changed that according to your suggestion.

    Still things are not working properly. Only the first "node" (QGraphicsRectItem + QGraphicsProxyWidget) can be moved, selected etc. The other two have the same behaviour as before. Also (and this applies for all) hovering the proxy widget still affects the parent instead of being ignored and only the parent having this ability.

    As for the problem being in the custom QGraphicsRectItem - yeap, I knew that since with the default implementation my proxy widget was working perfectly fine. :P


  • Qt Champions 2016

    @Red-Baron
    It works for me (I'm testing on Qt 5.6) provided I fix up the geometry issue. Here's what I have as code (bear in mind I've moved everything to the stack where possible, not that it should matter in this case):

    QGraphicsProxyWidget * SceneWithMovableProxies::addWidgetToScene(QPointF initPos)
    {
    	SceneWidgetItem * widget = new SceneWidgetItem();
    
    	qint32 width = widget->width();
    
    	ProxyTransformItem * proxyControl = new ProxyTransformItem(QRectF(initPos.x(), initPos.y(), width, 20));
    	proxyControl->setPaintTools(defaultPen, defaultInlineBrush, inlineBrushHover, inlineBrushSelected);
    
    	QGraphicsProxyWidget * proxy = new QGraphicsProxyWidget(proxyControl);
    	proxy->setWidget(widget);
    	proxy->setPos(initPos.x(), initPos.y() + proxyControl->height());
    	proxy->setAcceptHoverEvents(false);
    
    	ui.graphicsView->scene()->addItem(proxyControl);
    
    	return proxy;
    }
    

    And in the proxytransformitem.cpp I have:

    ProxyTransformItem::ProxyTransformItem(const QRectF & rect, QGraphicsItem *parent)
    	: QGraphicsRectItem(rect, parent), hovered(false)
    {
    	setAcceptHoverEvents(true);
    	setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
    	setFlag(QGraphicsItem::ItemIsMovable);
    	setFlag(QGraphicsItem::ItemIsSelectable);
    	setFlag(QGraphicsItem::ItemSendsGeometryChanges);
    }
    
    float ProxyTransformItem::width()
    {
    	return rect().width();
    }
    
    float ProxyTransformItem::height()
    {
    	return rect().height();
    }
    
    QRectF ProxyTransformItem::boundingRect() const
    {
    	return rect();
    }
    
    QPainterPath ProxyTransformItem::shape()
    {
      QPainterPath path;
      path.addRect(rect());
      return path;
    }
    

    I believe that's all the changes I made. I hope it helps.

    Kind regards.



  • @kshegunov Okay, I have forgotten to change my constructor facepalm. However it is still not working properly. :-/ I see you have set the setAcceptHoverEvents() of the proxy widget which seems to fix the problem and the proxy widget doesn't do anything whenever I hover it. However proxy->setPos(initPos.x(), initPos.y() + proxyControl->height()); seems to be incorrect. If I add this the proxies are not glued to their parent QGraphicsRectItem counterparts. Correct me if I'm wrong but I do believe that adding an item as a child to another item automatically "transfers" that child item to the local coordinate system of its parent. So if I use initPos.x() or initPos.y() + proxyControl->height() with the x and y different from 0 I will actually be translating my child item with x and y units inside the parent's coordinate system. At least that's how it appears on the screen. :D So I did

    // Just setting the rectangle for some reason doesn't seem to
    // do the trick; that's why previously I was manually setting
    // the position in my constructor by passing the x and y coordinates
    proxyControl->setPos(initPos);
    ...
    // 0 along the x and height() along the y position the proxy
    // widget right beneath my proxyControl as it's meant to be
    proxy->setPos(0, proxyControl->height());
    

    In addition to that only the first node works properly. So not much has changed here. I even copy-pasted your code (of course ui in my case is not on the stack but as you've mentioned this shouldn't be the source of the problem at hand) but it's still broken.

    Even though Qt doesn't have backwards compatibility per se I doubt that things have changed that much between 5.5 and 5.6 in terms how the QGraphicsRectItem as well as the QGraphicsProxyWidget behave.


  • Qt Champions 2016

    @Red-Baron

    However it is still not working properly.

    It's working on my machine (for 5.5 as well, I didn't have the Qt sources, that's why I was testing on 5.6). I've uploaded the whole project if you wish to diff and find out what are the differences exactly.

    Correct me if I'm wrong but I do believe that adding an item as a child to another item automatically "transfers" that child item to the local coordinate system of its parent. So if I use initPos.x() or initPos.y() + proxyControl->height() with the x and y different from 0 I will actually be translating my child item with x and y units inside the parent's coordinate system.

    You're not wrong, however proxyControl->pos() returns (0, 0) for each instance for me. What I assume is happening is that the items are not immediately "moved" to their positions, so if I don't specify the global coordinates the proxies are not positioned properly.

    Even though Qt doesn't have backwards compatibility per se

    Actually it does, for the interfaces at least. All minor versions are binary compatible, so the interfaces don't change (this includes 5.5 and 5.6).



  • @kshegunov Your code is working so I can do some comparison and dig into it to see where the differences are. Thank you so much for helping me with this! Much appreciated. ^_^


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.