Как отказаться от QEvent вместо того, чтобы просто игнорировать его

У меня есть два виджета ParentWidget и ChildWidget, оба производные от QWidget и оба переопределяющие void dragEnterEvent(QDragEnterEvent *event).

Теперь ChildWidget содержится в ParentWidget. Теперь предположим, что некий QDragEvent*, называемый event, может быть действительным для ParentWidget, но не для ChildWidget, и предположим, что dragEnterEvent вызывается для ChildWidget.

Теперь я могу просто вызвать event->ignore(), чтобы проигнорировать событие для ChildWidget, но тогда вызывается dragEnterEvent для ParentWidget.

И это моя проблема. Я не хочу, чтобы вызывалось dragEnterEvent для ParentWidget, если событие уже было отброшено в ChildWidget.

Проще говоря, я просто не хочу, чтобы событие просто игнорировалось, но, кроме того, событие должно быть полностью отброшено внутри dragEnterEvent из ChildWidget.

Как можно добиться такого поведения, если предположить, что ParentWidget и ChildWidget являются слабо связанными компонентами?

Минимальный пример

Следующий пример показывает, чего я пытаюсь достичь, а также в некотором смысле является работоспособным подходом. В случае более сложных сценариев это приведет к чрезмерно сложному коду.

ChildWidget допускает отбрасывание имен файлов, оканчивающихся на txt, тогда как ParentWidget принимает все отбрасывания, кроме тех, которые уже игнорируются 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();
        }
    }
};

person Aleph0    schedule 10.05.2017    source источник
comment
Можете показать свою реализацию? Вы можете переопределить dragEnterEvent для ChildWidget, потому что это виртуальный   -  person thibsc    schedule 10.05.2017
comment
@ThibautB., я реализовал dragEnterEvent для обоих виджетов, ChildWidget и ParentWidget. Просто dragEnterEvent для ParentWidget должно вести себя иначе, если событие было проигнорировано в dragEnterEvent из ChildWidget.   -  person Aleph0    schedule 10.05.2017
comment
@ТибоБ. Может ли QDragEnterEvent транспортировать это логическое значение?   -  person Aleph0    schedule 10.05.2017
comment
Если вы используете переменную класса да, я думаю   -  person thibsc    schedule 10.05.2017
comment
@ThibautB., я нашел обходной путь для своей проблемы. Если в позиции событий есть дочерний виджет, я знал, что событие уже ранее игнорировалось в дочернем виджете. Следовательно, я также могу игнорировать его в родительском виджете. Следующий код входит в dragEnterEvent файла ParentWidget. if (auto childWidget = childAt(event->pos())) { event->ignore(); }   -  person Aleph0    schedule 10.05.2017
comment
странно, сигнатура вашего метода такая же? Но если у вас есть решение... ;) однако dragEnterEvent из ChildWidget следует вызывать без вызова dragEnterEvent из ParentWidget   -  person thibsc    schedule 10.05.2017
comment
Мне действительно нужно раздеть мой код, чтобы создать минимальный пример. Это действительно сбивает с толку. Наверное. :-)   -  person Aleph0    schedule 10.05.2017
comment
Давайте продолжим обсуждение в чате.   -  person thibsc    schedule 10.05.2017


Ответы (3)


Если вы хотите, чтобы событие было отклонено, вам нужно принять его:

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

Вот как Qt отправляет события виджетам: событие распространяется от дочернего к родительскому до тех пор, пока виджет его не примет.

Из кода Qt:

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();
}
person Benjamin T    schedule 10.05.2017
comment
Большое спасибо за это классное решение. Я также нашел очень похожее решение вчера. Я опубликую это здесь. - person Aleph0; 11.05.2017

Ваше решение - достойный обходной путь.

Кроме того, вы можете изменить тип события на событие без перетаскивания. Поскольку событие перестает быть QDragEnterEvent, оно не будет отправлено родителю. Есть два способа реализовать это: один — изменить член t (тип) QEvent. Другой способ — уничтожить событие на месте и воссоздать там простое нулевое событие.

// https://github.com/KubaO/stackoverflown/tree/master/questions/event-discard-43885834
#include <QtWidgets>

void wipeEvent(QEvent * event) {
   struct Helper : QEvent {
      static void wipe(QEvent * e) {
         static_cast<Helper*>(e)->t = QEvent::None;
      }
   };
   Helper::wipe(event);
}

void wipeEvent2(QEvent *event) {
   event->~QEvent(); // OK since the destructor is virtual.
   new (event) QEvent(QEvent::None);
}

class ChildWidget : public QWidget {
   Q_OBJECT
   QHBoxLayout m_layout{this};
   QLabel m_label{"ChildLabel"};
public:
   ChildWidget() {
      setAcceptDrops(true);
      m_layout.addWidget(&m_label);
   }
   void dragEnterEvent(QDragEnterEvent *event) override {
      qDebug() << "Child";
      while (auto mimeData=event->mimeData()) {
         auto url = QUrl(mimeData->text());
         if (!url.isValid()) break;
         if (!url.isLocalFile()) break;
         auto filename = url.fileName();
         if (!filename.endsWith(".txt")) break;
         // ChildWidget can only process txt files.
         qDebug() << url.fileName();
         return event->acceptProposedAction();
      }
      wipeEvent(event);
   }
};

class ParentWidget : public QWidget {
   Q_OBJECT
   QHBoxLayout m_layout{this};
   QLabel m_label{"ParentLabel"};
   ChildWidget m_child;
public:
   ParentWidget() {
      setAcceptDrops(true);
      m_layout.addWidget(&m_label);
      m_layout.addWidget(&m_child);
   }
   void dragEnterEvent(QDragEnterEvent *event) override {
      qDebug() << "Parent";
      event->acceptProposedAction();
   }
};

int main(int argc, char** args) {
   QApplication app{argc, args};
   ParentWidget widget;
   widget.show();
   app.exec();
}
#include "main.moc"
person Kuba hasn't forgotten Monica    schedule 10.05.2017

После долгого разговора вчера я нашел следующее лучшее решение моей проблемы. Решение похоже на решение Бенджамина Т. Еще раз большое спасибо ThibautB. за плодотворное обсуждение.

Вот мой рабочий код.

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();
}

ChildWidget.h

#pragma once

#include <QWidget>
#include <QUrl>
#include <QMimeData>
#include <QDragEnterEvent>
#include <QLabel>
#include <QDebug>
#include <QByteArray>
#include <QHBoxLayout>
//#include "MyDragEnterEvent.h"

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) {
        qDebug() << "Child";
        if (auto mimeData=event->mimeData()) {
            auto url = QUrl(mimeData->text());
            if (!url.isValid()) { event->setDropAction(Qt::DropAction::IgnoreAction); event->ignore(); return; }
            if (!url.isLocalFile()) { event->setDropAction(Qt::DropAction::IgnoreAction); event->ignore(); return; }
            auto filename = url.fileName();
            if (!filename.endsWith(".txt")) { event->setDropAction(Qt::DropAction::IgnoreAction); event->ignore(); return; }
            // ChildWidget can only process txt files.
            qDebug() << url.fileName();     
            event->acceptProposedAction();
        }
        else {
            qDebug() << "Ignored in Child";
            event->setDropAction(Qt::DropAction::IgnoreAction);
            event->ignore();
        }
    }
};

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 {
        if (event->dropAction() == Qt::IgnoreAction) {
            qDebug() << "Ignored in Parent";
            event->ignore();
        }
        else {
            qDebug() << "Accepted in Parent";
            event->acceptProposedAction();
        }
    }
};
person Aleph0    schedule 11.05.2017