【问题标题】:Qt: How to implement common base-class signal/slot functionality for all widgets and widget types (via a virtual base class slot)?Qt:如何为所有小部件和小部件类型实现通用基类信号/插槽功能(通过虚拟基类插槽)?
【发布时间】:2013-06-12 03:26:26
【问题描述】:

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

插槽是一个虚拟函数,因此我希望实现自定义功能的任何小部件都可以从虚拟插槽函数派生。在所需的场景中,我的所有小部件都将从具有虚拟插槽的基类派生,因此默认情况下,我的所有小部件实例都将使用为对象定义的插槽连接到所需的信号(具有来自基类的默认行为)。

我知道 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
        }
}

正如示例代码所示,似乎不可能同时获得(在此示例中)QTabWidget 的好处,以及从所需信号函数到虚拟槽函数的自动连接。

在 Qt 中,有什么方法可以让我的应用程序的所有小部件类共享通用的基类插槽和 connect() 功能,同时允许我的小部件仍然从 Qt 小部件类(如 QTabWidget、QMainWindow 等)派生?

【问题讨论】:

    标签: c++ qt signals-slots


    【解决方案1】:

    有时当继承存在问题时,可以用组合替换它或其中的一部分。

    这就是 Qt 4 中需要的方法:不是从 QObject 派生,而是从一个非 QObject 类 (MyObjectShared) 派生,该类带有一个帮助器 QObject,用作连接向其插槽发出信号;助手将该调用转发给非QObject 类。

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

    如果 Qt 4 兼容性通常在代码的其他区域有用,可以使用通用的 connect 函数,将信号连接到 Qt 4 和 Qt 5 中的函子(在 Qt 4 中,它将使用隐式帮助器 @ 987654330@)。

    // 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"
    

    要在两个QObjects 之间共享一些代码,您可以将QObject 作为类的成员,这是一个插入的非对象类,它使用在上参数化的泛型类基础类型。泛型类可以有槽和信号。它们必须仅在直接派生类中对moc 可见 - 而不是在任何进一步派生类中。

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

    经过编译时检查的connect 甚至无法编译,因为它所作用的this 指针的编译时类型不正确,而您对派生类的类型一无所知。

    仅适用于 Qt-4 样式连接的解决方法是使用派生构造函数必须调用的 doConnections 方法,在此建立连接。

    因此,让我们在 basederived 类上使泛型类参数化 - 后者被称为 Curiously Recurring Template Pattern,或简称 CRTP。

    现在您可以访问派生类的类型,并且可以使用辅助函数将this 转换为指向派生类的指针,并在Qt 5 风格的编译时检查connects 中使用它.

    检查connect 的Qt 4 风格运行时仍需要从doConnections 调用。所以,如果你使用 Qt 5,那不是问题。无论如何,您不应该在 Qt 5 代码中使用 Qt 4 风格的 connect

    根据从泛型类直接派生的类是否覆盖它们,插槽需要稍微不同的处理。

    如果一个槽是虚拟的并且在直接派生的类中有一个实现,你应该以正常的方式将它暴露给 moc - 使用slots 部分或Q_SLOT 宏。

    如果一个槽没有在直接派生类中有实现(无论是否虚拟),它在泛型类中的实现应该对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();
       }
    };
    

    泛型类使用起来非常简单。请记住将任何非虚拟覆盖的插槽包装在 moc-only 防护中!

    还记得适用于所有 Qt 代码的一般规则:如果你有一个插槽,它应该只声明一次 moc。因此,如果您有一个进一步派生自MyTreeWidgetMyTableWidget 的类,则您不希望在任何必要的虚拟插槽覆盖之前使用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
    };
    

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

    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"
    

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

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

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

    【讨论】:

      【解决方案2】:

      在这种情况下,您可以使用组合而不是多重继承。像这样的:

      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
              }
      }
      

      【讨论】:

      • 不幸的是,组合似乎破坏了 Qt Creator 的设计器。在设计器中,您为每个小部件指定您希望实例化的类(它必须派生自适当的 Qt 小部件基类),然后 Qt 在 UI 构建期间自动实例化该类的对象。最终用户不会像这个组合示例那样直接实例化小部件类。你能想出一种方法来结合 Qt 的自动化 UI 小部件实例化系统使用组合(或任何其他方法)来达到预期的效果吗?
      • @DanNissenbaum,我已根据您的要求更改了答案。设计器中有机会使用从 QWidget 派生的自定义类。因此,您可以将MyBaseWidgetContainer 父类更改为 QWidget 并在设计器中使用它的子类。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多