c++qtdrag-and-dropdragqevent

How to discard an QEvent instead of just ignoring it


I have two widgets ParentWidget and ChildWidget both being derived from QWidget and both overriding void dragEnterEvent(QDragEnterEvent *event).

Now ChildWidget is contained in the ParentWidget. Now assume that a certain QDragEvent* called event might be valid for ParentWidget, but not for ChildWidget and assume that dragEnterEvent for ChildWidget is called.

Now I can just call event->ignore() in order to ignore the event for ChildWidget, but then dragEnterEvent for ParentWidget is called.

And this is my problem. I don't want, that dragEnterEvent for ParentWidget is getting called, if the event was already discarded in ChildWidget.

Simply speaking I just don't want the event just to be ignored, but moreover the event needs to be completely discarded inside the dragEnterEvent of ChildWidget.

How can achieve such a behavior under the assumption that ParentWidget and ChildWidget are loosely coupled components?

Minimal Example

The following example shows what I'm trying to achieve and also is a workable approach in some sense. In case of more complicated scenarios it would result in overly complicated code.

The ChildWidget accepts drop of filenames ending with txt, whereas the ParentWidget accepts all drops, except the ones already ignored by ChildWidget.

main.cpp

#include <QApplication>
#include "ParentWidget.h"

int main(int argc, char** args) {
    QApplication app(argc, args);
    auto widget=new ParentWidget;
    widget->show();
    app.exec();
}

ParentWidget.h

#pragma once

#include <QWidget>
#include <QDebug>
#include <QDragEnterEvent>
#include <QHBoxLayout>
#include <QLabel>

#include "ChildWidget.h"

class ParentWidget : public QWidget {
    Q_OBJECT
public:
    ParentWidget(QWidget* parent = nullptr) : QWidget(parent) {
        setLayout(new QHBoxLayout);
        setAcceptDrops(true);
        layout()->addWidget(new QLabel("ParentLabel"));
        layout()->addWidget(new ChildWidget);
    }

    void dragEnterEvent(QDragEnterEvent *event) override {
        qDebug() << "Parent";
        // Check if event was already ignored in ChildWidget? 
        if (auto childWidget = qobject_cast<ChildWidget*>(childAt(event->pos()))) {
            event->ignore();
        }
        else {
            event->acceptProposedAction();
        }
    }
};

ChildWidget.h

#pragma once

#include <QWidget>
#include <QUrl>
#include <QMimeData>
#include <QDragEnterEvent>
#include <QLabel>
#include <QDebug>
#include <QByteArray>
#include <QHBoxLayout>

class ChildWidget : public QWidget {
    Q_OBJECT
public:
    ChildWidget(QWidget* parent = nullptr) : QWidget(parent) {
        setAcceptDrops(true);
        setLayout(new QHBoxLayout);
        layout()->addWidget(new QLabel("ChildLabel"));
    }

    void dragEnterEvent(QDragEnterEvent *event) override {
        qDebug() << "Child";
        if (auto mimeData=event->mimeData()) {
            auto url = QUrl(mimeData->text());
            if (!url.isValid()) { event->ignore(); return; }
            if (!url.isLocalFile()) { event->ignore(); return; }
            auto filename = url.fileName();
            if (!filename.endsWith(".txt")) { event->ignore(); return; }
            // ChildWidget can only process txt files.
            qDebug() << url.fileName();         
            event->acceptProposedAction();
        }
        else {
            event->ignore();
        }
    }
};

Solution

  • If you want the event to be discarded, you need to accept it:

    void dragEnterEvent(QDragEnterEvent *event) override {
        qDebug() << "Child";
        if (auto mimeData=event->mimeData()) {
            [...]         
            event->acceptProposedAction();
        }
        else {
            event->setAction(Qt::IgnoreAction);
            event->accept();
        }
    }
    

    This is how Qt dispatch events to widgets: the event is propagated from child to parent until a widget accepts it.

    From Qt code:

    while (w) {
        if (w->isEnabled() && w->acceptDrops()) {
            res = d->notify_helper(w, dragEvent); // calls dragEnterEvent() on w
            if (res && dragEvent->isAccepted()) {
                QDragManager::self()->setCurrentTarget(w);
                 break; // The event was accepted, we break, the event will not propagate to the parent 
            }
        }
        if (w->isWindow())
            break;
        dragEvent->p = w->mapToParent(dragEvent->p.toPoint());
        w = w->parentWidget();
    }