Gaspard Bucher
Gaspard Bucher

Reputation: 6137

How to pass click event to sibling underneath in Qt?

I have two overlapping widgets in a window child A and child B. Child A is above B and gets mouse events but sometimes the click should pass through and eventually hit child B.

The ideal solution would have been to use event->ignore() but this passes the event to the parent widget, not siblings.

The "pass all" solution setAttribute(Qt::WA_TransparentForMouseEvents); does not work either because child A needs to capture some events.

How do I tell Qt "I do not want to handle this event, act like I am not there" ?

Upvotes: 20

Views: 9750

Answers (6)

dougg3
dougg3

Reputation: 1359

I ran into this problem recently, and was able to implement a solution similar to Stefan's, but (in my opinion) a little bit cleaner because it doesn't depend on a 1 millisecond timeout. It does require implementing more event forwarding code, but I'm pretty happy with it.

#include <QMouseEvent>

MyWidget::MyWidget(QWidget *parent) :
    QWidget(parent),
    _clickedWidget(nullptr) // This is a QWidget *
{

}

bool MyWidget::event(QEvent *event)
{
    bool forwardMouseEvent = false;

    if (event->type() == QEvent::MouseButtonPress ||
        event->type() == QEvent::MouseButtonDblClick) {
        // Determine which sibling widget is underneath this widget.
        // This approach might also detect the parent which is okay.
        QMouseEvent * const mouseEvent = static_cast<QMouseEvent *>(event);
        setAttribute(Qt::WA_TransparentForMouseEvents, true);
        _clickedWidget = QApplication::widgetAt(mouseEvent->globalPos());
        setAttribute(Qt::WA_TransparentForMouseEvents, false);

        // Remember the widget so we can forward additional events
        // until the button is released
        if (_clickedWidget == this)
            _clickedWidget = nullptr;
        else if (_clickedWidget)
            forwardMouseEvent = true;
    } else if (event->type() == QEvent::MouseButtonRelease ||
               event->type() == QEvent::MouseMove) {
        // Forward release/move events if have a widget
        forwardMouseEvent = _clickedWidget != nullptr;
    }

    if (forwardMouseEvent) {
        QMouseEvent * const mouseEvent = static_cast<QMouseEvent *>(event);
        QMouseEvent forwardedEvent(mouseEvent->type(),
                                   _clickedWidget->mapFromGlobal(mouseEvent->screenPos().toPoint()),
                                   window()->mapFromGlobal(mouseEvent->screenPos().toPoint()),
                                   mouseEvent->screenPos(),
                                   mouseEvent->button(), mouseEvent->buttons(),
                                   mouseEvent->modifiers(), mouseEvent->source());
        qApp->sendEvent(_clickedWidget, &forwardedEvent);

        // Clean up when we're done
        if (event->type() == QEvent::MouseButtonRelease)
            _clickedWidget = nullptr;

        event->accept();
        return true;
    }

    return QWidget::event(event);
}

Upvotes: 1

Patrick
Patrick

Reputation: 29

With current Qt5 (surely this works with Qt6, too; I don't know if it works with Qt4) I found the following works very well if you want to stack several semi-opaque widgets on top of each other (such as custom drawing canvas or QMdiArea above a media viewer, etc).

General assumption is to view QObject parenting in the sense of event handling (if the child can't handle an event, the parent will deal with it).

In my case this looks similar to the following:

class MyCentralWidget(QWidget):

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)

    # helper class - initializes a layout suitable for widget stacking
    class _Layout(QGridLayout):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setContentsMargins(0, 0, 0, 0)

    # set layout for this widget
    layout = _Layout(self)

    # initialize layer widgets
    layer0 = ...  # bottom most layer
    layer1 = ...
    layer2 = ...
    layer3 = ...  # top layer

    # Stack entities on top of another
    layout.addWidget(layer0, 0, 0)  # this is shielded by layer1 and will never receive mouse events
    layout.addWidget(layer1, 0, 0)  # this will receive all mouse events not handled by layers above

    # Parenting-based stacking using QGridLayout
    # to enable proper event propagation (events flow in
    # child->parent direction if unused by child)
    _Layout(layer1).addWidget(layer2, 0, 0)
    _Layout(layer2).addWidget(layer3, 0, 0)

    # Resulting mouse event propagation: layer3 -> layer2 -> layer1

    # possibly add some other floating widget for floating menus...
    toolbox = MyToolbox(parent=self)

Now in order to propagate an event to a lower widget (e.g. from layer3 to layer2) you have to make sure the widget does not consume the events. Usually when a widget is not interested in an event it simply passes it to its parent by calling event() on its base (e.g. QMdiArea does it when no QMdiSubWindow or button is hit):

# example implementation of event() method
def event(self, e) -> bool:
    if self.consumeEvent(e):
        # return if event was consumed
        return True
    # event was not consumed - send to parent via super call
    return super().event(e)

Upvotes: 1

Andr&#233;
Andr&#233;

Reputation: 590

You may also approach the issue from the other side. Instead of forwarding events to the "other" widget, you could have your other widget listen in to events for the first widget using an event filter. I am not sure if that fits your use case, it depends on what/who determines what events are to be handled by what object.

Upvotes: 4

Guillaume Charbonnier
Guillaume Charbonnier

Reputation: 41

Here is a possible option :

  1. give the child B pointer to the child A objet.
  2. redefine bool QObjet::event(QEvent *event) to reforward the event to child B whenever needed.

For example :

bool WidgetA::event(QEvent *event)
 {
    QWidget::event( event);
    QEvent::Type type = event->type();
    if ( (type != QEvent::KeyPress) &&
         (type != QEvent::Wheel) &&
         (type != QEvent::MouseButtonDblClick) &&
         (type != QEvent::MouseMove) &&
         (type != QEvent::MouseButtonPress) )
        return true;
    //forward the event
    if ( m_pChildB )
         m_pChildB->event( event);
    return true;

 }

Hope this helps.

Upvotes: 1

Stefan Majewsky
Stefan Majewsky

Reputation: 5555

The easier solution (if applicable) is to set WA_TransparentForMouseEvents depending on your click-through condition.

For example, if you want to click through certain regions of Child-A, you can use its mouseMoveEvent() to check whether WA_TransparentForMouseEvents needs to be set or unset. Then click events pass through automatically.

If you cannot determine whether a click event should be accepted before it has actually been triggered, you can do something like this:

void ChildA::mousePressEvent(QMouseEvent* event) {
    const bool accepted = ...; //insert custom logic here
    if (accepted) {
        ... //handle event here
    } else {
        //not accepting event -> resend a copy of this event...
        QMouseEvent* eventCopy = new QMouseEvent(*event);
        QApplication::instance()->postEvent(eventCopy);
        //...but this time, ignore it
        setAttribute(Qt::WA_TransparentForMouseEvents, true);
        QTimer::singleSlot(1, this, SLOT(resetClickTransparency()));
        //don't propagate original event any further
        event->accept();
    }
}

void ChildA::resetClickTransparency() { //slot
    setAttribute(Qt::WA_TransparentForMouseEvents, false);
}

Disclaimer: All of this written down by heart after a year of not doing Qt, so please correct me on name or type errors.

Upvotes: 6

Manuel
Manuel

Reputation: 2296

If the event have to be ignored by Child-A, emit a signal and capture it with its parent, then the parent emits a new signal to be captured by Child-B

Child-A -> Parent -> Child-B
(Signal) -> (Slot)(Signal) -> (Slot)

Upvotes: 2

Related Questions