Qt: How to implement common base-class signal/slot functionality for all widgets and widget types (via a virtual base class slot)?

故事扮演 提交于 2019-11-30 15:38:18

Sometimes when inheritance is problematic, one can replace it, or a part of it, with composition.

That's the approach needed in Qt 4: instead of deriving from a QObject, derive from a non-QObject class (MyObjectShared) that carries a helper QObject that is used as a proxy to connect the signal to its slot; the helper forwards that call to the non-QObject class.

In Qt 5, it is not necessary to derive from a QObject at all: signals can be connected to arbitrary functors. The MyObjectShared class remains the same.

Should Qt 4 compatibility be generally useful in other areas of the code, one can use a generic connect function that connects signals to functors in both Qt 4 and Qt 5 (in Qt 4, it would use an implicit helper QObject).

// https://github.com/KubaO/stackoverflown/tree/master/questions/main.cpp
#include <QtCore>
#include <functional>
#include <type_traits>

class MySignaler : public QObject {
   Q_OBJECT
  public:
   Q_SIGNAL void mySignal();
} signaler;

#if QT_VERSION < 0x050000
class MyObjectShared;
class MyObjectHelper : public QObject {
   Q_OBJECT
   MyObjectShared *m_object;
   void (MyObjectShared::*m_slot)();

  public:
   MyObjectHelper(MyObjectShared *object, void (MyObjectShared::*slot)())
       : m_object(object), m_slot(slot) {
      QObject::connect(&signaler, SIGNAL(mySignal()), this, SLOT(slot()));
   }
   Q_SLOT void slot() { (m_object->*m_slot)(); }
};
#endif

class MyObjectShared {
   Q_DISABLE_COPY(MyObjectShared)
#if QT_VERSION < 0x050000
   MyObjectHelper helper;

  public:
   template <typename Derived>
   MyObjectShared(Derived *derived) : helper(derived, &MyObjectShared::mySlot) {}
#else
  public:
   template <typename Derived, typename = typename std::enable_if<
                                   std::is_base_of<MyObjectShared, Derived>::value>::type>
   MyObjectShared(Derived *derived) {
      QObject::connect(&signaler, &MySignaler::mySignal,
                       std::bind(&MyObjectShared::mySlot, derived));
   }
#endif

   bool baseSlotCalled = false;
   virtual void mySlot() { baseSlotCalled = true; }
};

class MyObject : public QObject, public MyObjectShared {
   Q_OBJECT
  public:
   MyObject(QObject *parent = nullptr) : QObject(parent), MyObjectShared(this) {}
   // optional, needed only in this immediately derived class if you want the slot to be a
   // real slot instrumented by Qt
#ifdef Q_MOC_RUN
   void mySlot();
#endif
};

class MyDerived : public MyObject {
  public:
   bool derivedSlotCalled = false;
   void mySlot() override { derivedSlotCalled = true; }
};

void test1() {
   MyObject base;
   MyDerived derived;
   Q_ASSERT(!base.baseSlotCalled);
   Q_ASSERT(!derived.baseSlotCalled && !derived.derivedSlotCalled);
   signaler.mySignal();
   Q_ASSERT(base.baseSlotCalled);
   Q_ASSERT(!derived.baseSlotCalled && derived.derivedSlotCalled);
}

int main(int argc, char *argv[]) {
   test1();
   QCoreApplication app(argc, argv);
   test1();
   return 0;
}

#include "main.moc"

To share some code between two QObjects, you could have the QObject as a member of the class,an interposing non-object class that uses generic class that's parametrized only on the base type. The generic class can have slots and signals. They must be made visible to moc only in the immediately derived class - and not in any further derived ones.

Alas, you generally cannot connect any of the generic class's signals or slots in the constructor of the class, since at that point the derived class isn't constructed yet, and its metadata isn't available - from Qt's perspective, the signals and slots don't exist as such. So the Qt 4-style runtime-checked connect will fail.

The compile-time-checked connect will not even compile, because the this pointer it works on has an incorrect compile-time type, and you know nothing about the type of the derived class.

A workaround for Qt-4 style connect only is to have a doConnections method that the derived constructor has to call, where the connections are made.

Thus, let's make the generic class parametric on the base and the derived class as well - the latter is known as the Curiously Recurring Template Pattern, or CRTP for short.

Now you have access to the derived class's type, and can use a helper function to convert this to a pointer to the derived class, and use it in the Qt 5-style compile-time-checked connects.

The Qt 4-style runtime checked connect still needs to be invoked from doConnections. So,if you use Qt 5, that's not an issue. You shouldn't be using Qt 4-style connect in Qt 5 code anyway.

The slots require slightly different treatment depending on whether the class immediately derived from the generic class overrides them or not.

If a slot is virtual and has an implementation in the immediately derived class, you should expose it to moc in the normal fashion - using a slots section or the Q_SLOT macro.

If a slot doesn't have an implementation in the immediately derived class (whether virtual or not), its implementation in the generic class should be made visible to moc only, but not to the compiler - you don't wish to override it, after all. Thus the slot declarations are wrapped in #ifdef Q_MOC_RUN block that is only active when moc is reading the code. The generated code will refer to the generic implementations of the slots.

As we wish to make sure this indeed works, we'll add some booleans to track whether the slots were invoked.

// main.cpp
#include <QtWidgets>

template <class Base, class Derived> class MyGenericView : public Base {
   inline Derived* dthis() { return static_cast<Derived*>(this); }
public:
   bool slot1Invoked, slot2Invoked, baseSlot3Invoked;
   MyGenericView(QWidget * parent = 0) : Base(parent),
      slot1Invoked(false), slot2Invoked(false), baseSlot3Invoked(false)
   {
      QObject::connect(dthis(), &Derived::mySignal, dthis(), &Derived::mySlot2); // Qt 5 style
      QObject::connect(dthis(), &Derived::mySignal, dthis(), &Derived::mySlot3);
   }
   void doConnections() {
      Q_ASSERT(qobject_cast<Derived*>(this)); // we must be of correct type at this point
      QObject::connect(this, SIGNAL(mySignal()), SLOT(mySlot1())); // Qt 4 style
   }
   void mySlot1() { slot1Invoked = true; }
   void mySlot2() { slot2Invoked = true; }
   virtual void mySlot3() { baseSlot3Invoked = true; }
   void emitMySignal() {
      emit dthis()->mySignal();
   }
};

The generic class is very simple to use. Remember to wrap any non-virtual overridden slots in a moc-only guard!

Also recall the general rule that applies to all Qt code: if you have a slot, it should be declared to moc only once. So, if you had a class that further derives from MyTreeWidget or MyTableWidget, you don't want a Q_SLOT or slots macro in front of any necessarily virtual slot overrides. If present, it'll subtly break things. But you definitely want Q_DECL_OVERRIDE.

If you're on Qt 4, remember to call doConnections, otherwise the method is unnecessary.

The particular choice of QTreeWidget and QTableWidget is completely arbitrary, meaningless, and shouldn't be taken to mean that such use makes any sense (it likely doesn't).

class MyTreeWidget : public MyGenericView<QTreeWidget, MyTreeWidget> {
   Q_OBJECT
public:
   bool slot3Invoked;
   MyTreeWidget(QWidget * parent = 0) : MyGenericView(parent), slot3Invoked(false) { doConnections(); }
   Q_SIGNAL void mySignal();
#ifdef Q_MOC_RUN // for slots not overridden here
   Q_SLOT void mySlot1();
   Q_SLOT void mySlot2();
#endif
   // visible to the C++ compiler since we override it
   Q_SLOT void mySlot3() Q_DECL_OVERRIDE { slot3Invoked = true; }
};

class LaterTreeWidget : public MyTreeWidget {
   Q_OBJECT
public:
   void mySlot3() Q_DECL_OVERRIDE { } // no Q_SLOT macro - it's already a slot!
};

class MyTableWidget : public MyGenericView<QTreeWidget, MyTableWidget> {
   Q_OBJECT
public:
   MyTableWidget(QWidget * parent = 0) : MyGenericView(parent) { doConnections(); }
   Q_SIGNAL void mySignal();
#ifdef Q_MOC_RUN
   Q_SLOT void mySlot1();
   Q_SLOT void mySlot2();
   Q_SLOT void mySlot3(); // for MOC only since we don't override it
#endif
};

Finally, this little test case shows that it indeed works as desired.

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   MyTreeWidget tree;
   MyTableWidget table;
   Q_ASSERT(!tree.slot1Invoked && !tree.slot2Invoked && !tree.slot3Invoked);
   emit tree.mySignal();
   Q_ASSERT(tree.slot1Invoked && tree.slot2Invoked && tree.slot3Invoked);
   Q_ASSERT(!table.slot1Invoked && !table.slot2Invoked && !table.baseSlot3Invoked);
   emit table.mySignal();
   Q_ASSERT(table.slot1Invoked && table.slot2Invoked && table.baseSlot3Invoked);
   return 0;
}

#include "main.moc"

This approach gives you the following:

  1. The common code class derives from the base class, and can thus easily invoke or override the behavior of the base class. In this particular example, you can reimplement the QAbstractItemView methods etc.

  2. There is full support for signals and slots. Even though the signals and slots are declared as such in the metadata of the derived class, you can still use them in the generic class.

In this situation you may make use of composition rather than multiple inheritance. Something like this:

class MySignaler : public QObject
{
    Q_OBJECT
    public:
        MySignaler : QObject(NULL) {}
    signals:
        void MySignal();
}

MySignaler signaler;

class MyBaseWidgetContainer: public QWidget
{
    Q_OBJECT
    public:
        MyBaseWidgetContainer() : QObject(NULL), widget(NULL)
        {
            connect(&signaler, SIGNAL(MySignal()), this, SLOT(MySlot()));
        }
    public slots:
        virtual void MySlot()
        {
            // Default behavior here
        }
    private:
        QWidget *widget;
}

class MyTabWidgetContainer : public MyBaseWidgetContainer
{
    Q_OBJECT
    public:
        MyTabWidgetContainer() {
            widget = new QTabWidget(this);
            QLayout *layout = new QBoxLayout(this);
            layout->addWidget(widget);
        }
    public slots:
        void MySlot()
        {
            // Decide to handle the signal for custom behavior
        }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!