问:如何为所有小部件和小部件类型(通过虚拟基类插槽)实现通用的基类信号/插槽功能? [英] Qt: How to implement common base-class signal/slot functionality for all widgets and widget types (via a virtual base class slot)?

查看:85
本文介绍了问:如何为所有小部件和小部件类型(通过虚拟基类插槽)实现通用的基类信号/插槽功能?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I would like to derive all of my widgets from a base class widget that automatically establishes a signal/slot connection between a slot for the class and a (rarely called) signal.

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

The slot is a virtual function, so that any widgets for which I wish to implement custom functionality can derive from the virtual slot function. In the desired scenario, all my widgets would derive from this base class with the virtual slot, so that by default all of my widget instances would be connected to the desired signal with a slot defined for the object (with default behavior from the base class).

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

I know that virtual slots are allowed in Qt. However, deriving from two QObject classes is not supported, so that, for example, the following code is disallowed:

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的好处,也无法获得从所需信号功能到虚拟插槽功能的自动连接。

As the sample code demonstrates, it seems impossible to gain both the benefits of (in this example) the QTabWidget, and also the automatic connection from the desired signal function to the virtual slot function.

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

Is there some way, in Qt, to have all my application's widget classes share common base-class slot and connect() functionality while allowing my widgets to nonetheless derive from Qt widget classes such as QTabWidget, QMainWindow, etc.?

推荐答案

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

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

这是Qt 4中需要的方法:与其从 QObject 派生,还不如从一个非 QObject 类( MyObjectShared )包含一个辅助对象 QObject 用作将信号连接到其插槽的代理;帮助程序会将调用转发给非 QObject 类。

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.

在Qt 5中,不必派生完全来自 QObject :信号可以连接到任意函子。 MyObjectShared 类保持不变。

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.

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

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"

到在两个 QObject 之间共享一些代码,您可以让 QObject 作为该类的成员,并且插入非使用通用类型的对象类,该通用类在基本类型上被参数化为 only 。通用类可以具有插槽和信号。必须仅在立即派生的类中使它们对 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.

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

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.

编译时检查的 connect 甚至都不会编译,因为它使用的 this 指针具有错误的编译时类型,您对此一无所知

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.

仅适用于Qt-4样式连接的一种解决方法是使用 doConnections 方法

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.

因此,让我们在 base 派生的类-后者又称为好奇地重复的模板模式 ,或简称CRTP。

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.

现在,您可以访问派生类的类型,并且可以使用帮助函数来转换 this 指向派生类的指针,并在Qt 5风格的compile-time-checke中使用它d connect s。

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.

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

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.

插槽要求略有不同

如果插槽是虚拟并且在立即派生的类,则应以常规方式将其暴露于moc-使用 slots 部分或 Q_SLOT 宏。

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.

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

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

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

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

还请记住适用于所有Qt代码的一般规则:如果有插槽,它将应该只向 moc 声明一次。因此,如果您有一个类进一步从 MyTreeWidget MyTableWidget 派生,则不需要 Q_SLOT slots 宏在任何必需的虚拟插槽替代之前。如果存在,它将巧妙地破坏事物。但是您绝对希望 Q_DECL_OVERRIDE

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.

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

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

QTreeWidget QTableWidget 完全是任意的,毫无意义的,不应认为这种用法有任何意义(可能没有意义)。

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

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

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.

这篇关于问:如何为所有小部件和小部件类型(通过虚拟基类插槽)实现通用的基类信号/插槽功能?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆