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

时间:2013-06-12 03:26:26

标签: 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
        }
}

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

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

2 个答案:

答案 0 :(得分:5)

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

这是Qt 4中所需的方法:而不是从QObject派生,而是从带有帮助者的非QObject类(MyObjectShared)派生而来{{ 1}}用作代理将信号连接到其插槽;帮助者转发调用非QObject类。

在Qt 5中,没有必要从QObject派生:信号可以连接到任意仿函数。 QObject类保持不变。

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

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" 之间共享一些代码,您可以将QObject作为该类的成员,使用通用类进行参数化仅基础类型。泛型类可以有插槽和信号。它们必须仅在紧接派生的类中对QObject可见 - 而不是在任何进一步派生的类中。

唉,你通常不能在类的构造函数中连接任何泛型类的信号或槽,因为此时派生类还没有被构造,并且它的元数据不是&#39;可用 - 从Qt的角度来看,信号和插槽并不存在。因此,Qt 4样式的运行时检查moc将失败。

编译时检查的connect甚至不会编译,因为它所使用的connect指针具有不正确的编译时类型,并且您对派生类的类型一无所知。

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

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

现在您可以访问派生类的类型,并且可以使用辅助函数将doConnections转换为指向派生类的指针,并在Qt 5样式的编译时使用它 - 检查this s。

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

插槽需要稍微不同的处理,具体取决于直接派生自泛型类的类是否覆盖它们。

如果插槽是虚拟并且在直接派生类中有实现,则应以正常方式将其公开给moc - 使用connect部分或{{1} }宏。

如果一个插槽没有在直接派生的类中有一个实现(无论是否为虚拟),那么它在泛型类中的实现应该对 moc可见 only,但不是编译器 - 毕竟你不想覆盖它。因此,插槽声明包含在slots块中,该块仅在 moc 读取代码时才处于活动状态。生成的代码将引用插槽的通用实现。

由于我们希望确保这确实有效,我们会添加一些布尔来跟踪是否调用了插槽。

Q_SLOT

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

还要回想一下适用于所有Qt代码的一般规则:如果你有一个插槽,它应该只被声明为 moc 一次。因此,如果您有一个进一步派生自#ifdef Q_MOC_RUN// 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(); } }; 的课程,您就不希望在任何必要的虚拟广告位前面放置MyTreeWidgetMyTableWidget宏覆盖。如果存在,它会巧妙地破坏事物。但你绝对想要Q_SLOT

如果您使用的是Qt 4,请记得致电slots,否则该方法是不必要的。

Q_DECL_OVERRIDEdoConnections的特定选择完全是任意的,毫无意义,并且不应被视为意味着这种使用有意义(它可能不会)。

QTreeWidget

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

QTableWidget

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

  1. 公共代码类派生自基类,因此可以轻松调用或覆盖基类的行为。在此特定示例中,您可以重新实现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 }; 方法等。

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

答案 1 :(得分:1)

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

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