问:如何为所有小部件和小部件类型(通过虚拟基类插槽)实现通用的基类信号/插槽功能?

Dan*_*aum 4 c++ qt signals-slots

我想从基类窗口小部件派生我的所有窗口小部件,该基类窗口小部件会自动在该类的插槽和(很少调用的)信号之间建立信号/插槽连接。

插槽是一种虚拟功能,因此我希望为其实现自定义功能的任何小部件都可以从虚拟插槽功能中派生。在理想的情况下,我的所有小部件都将从带有虚拟插槽的该基类派生,因此默认情况下,我的所有小部件实例都将通过为该对象定义的插槽连接到所需的信号(基类具有默认行为) )。

我知道Qt中允许使用虚拟插槽。但是,不支持从两个QObject类派生,因此,例如,不允许以下代码:

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

MySignaler signaler;

class MyBaseWidget: public QObject
{
    Q_OBJECT
    public:
        MyBaseWidget() : QObject(null_ptr)
        {
            connect(&signaler, SIGNAL(MySignal()), this, SLOT(MySlot()));
        }
    public slots:
        virtual void MySlot()
        {
            // Default behavior here
        }
}

// Not allowed!
// Cannot derive from two different QObject-derived base classes.
// How to gain functionality of both QTabWidget and the MyBaseWidget base class?
class MyTabWidget : public QTabWidget, public MyBaseWidget
{
    Q_OBJECT
    public slots:
        void MySlot()
        {
            // Decide to handle the signal for custom behavior
        }
}
Run Code Online (Sandbox Code Playgroud)

如示例代码所示,似乎既无法获得QTabWidget的好处(在此示例中),也无法获得从所需信号功能到虚拟插槽功能的自动连接。

在Qt中,是否有某种方法可以让我的应用程序的所有窗口小部件类共享通用的基类插槽和connect()功能,同时允许我的窗口小部件从Qt窗口小部件类(例如QTabWidget,QMainWindow等)派生呢?

Rei*_*ica 5

有时,当继承出现问题时,可以用组合替换它或部分继承。

这就是Qt 4中需要的方法:与其从a QObject派生,不从一个非QObject类(MyObjectShared)派生,该类携带了一个QObject用作信号连接其插槽的代理的助手。助手将该呼叫转发给非QObject类。

在Qt 5中,根本不需要从a派生QObject:信号可以连接到任意函子。该MyObjectShared级保持不变。

如果Qt 4兼容性通常在代码的其他区域有用,则可以使用connect将信号连接到Qt 4和Qt 5中的函子的泛型函数(在Qt 4中,它将使用隐式帮助器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"
Run Code Online (Sandbox Code Playgroud)

要在两个QObjects 之间共享一些代码,您可以QObject作为类的成员,该类是插入的非对象类,该类使用在基本类型上参数化的泛型类。通用类可以具有插槽和信号。必须moc仅在立即派生的类中使它们可见,而不能在任何其他派生的类中使它们可见。

las,您通常无法在类的构造函数中连接任何泛型类的信号或插槽,因为此时尚未构造派生类,并且其元数据不可用-从Qt的角度来看,信号和插槽根本不存在。因此,Qt 4风格的运行时检查connect将失败。

经过编译时检查的connect甚至不会编译,因为this它所处理的指针具有错误的编译时类型,并且您对派生类的类型一无所知。

仅Qt-4样式连接的一种解决方法是具有一种doConnections派生的构造函数必须调用的方法,以在其中建立连接。

因此,让我们在类和派生类上也设置通用类的参数- 派生类被称为“ 好奇重复模板模式”,简称CRTP。

现在,您可以访问派生类的类型,并可以使用帮助器函数转换this为指向派生类的指针,并在Qt 5样式的编译时检查connects中使用它。

connect仍然需要从中调用已检查的Qt 4风格运行时doConnections。因此,如果您使用Qt 5,那不是问题。connect无论如何,您都不应该在Qt 5代码中使用Qt 4样式。

插槽需要略有不同的处理方式,具体取决于立即从泛型类派生的类是否覆盖它们。

如果插槽是虚拟的,并且在立即派生的类中具有实现,则应以常规方式-使用slots段或Q_SLOT宏将其公开给moc 。

如果插槽在立即派生的类中没有实现(无论是否为虚拟),则应该仅对moc可见该类在泛型类中的实现,而对编译器不可见-您不希望覆盖它,毕竟。因此,插槽声明被包装在#ifdef Q_MOC_RUN块中,该块仅在moc读取代码时才有效。生成的代码将引用插槽的通用实现。

由于我们希望确保确实如此,因此我们将添加一些布尔值以跟踪是否调用了插槽。

// 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();
   }
};
Run Code Online (Sandbox Code Playgroud)

泛型类非常易于使用。切记将所有非虚拟覆盖的插槽包装在仅Moc的防护装置中!

还要记住适用于所有Qt代码的一般规则:如果有插槽,则应仅将其声明为moc。因此,如果您有一个进一步从MyTreeWidget或派生的类MyTableWidget,则您不希望在任何必需的虚拟插槽替代之前添加Q_SLOTslots宏。如果存在,它将巧妙地破坏事物。但是你一定要Q_DECL_OVERRIDE

如果您使用的是Qt 4,请记住调用doConnections,否则该方法是不必要的。

的具体选择QTreeWidgetQTableWidget完全是任意的,毫无意义的,而不应被理解为这样的使用使任何意义(很可能没有)。

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
};
Run Code Online (Sandbox Code Playgroud)

最后,这个小的测试用例表明它确实可以按需工作。

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"
Run Code Online (Sandbox Code Playgroud)

此方法为您提供以下内容:

  1. 通用代码类派生自基类,因此可以轻松地调用或覆盖基类的行为。在此特定示例中,您可以重新实现QAbstractItemView方法等。

  2. 完全支持信号和插槽。即使在派生类的元数据中声明了信号和插槽,您仍然可以在泛型类中使用它们。