在接口类中声明抽象信号

44
如何在抽象类/接口中声明Qt信号,当实现类已经从QObject/QWidget派生?
class IEmitSomething
{
   public:
     // this should be the signal known to others
     virtual void someThingHappened() = 0;
}

class ImplementEmitterOfSomething : public QWidget, public IEmitSomething
{
     // signal implementation should be generated here
     signals: void someThingHappended();
}

你可以只写 signals: void someThingHappened();。不需要实现信号的功能。 - Ruu
我知道信号是在实现中生成的,但如果只有接口,观察者如何知道这是一个(qt)信号? - Beachwalker
@Beachwalker,请查看我的回答更新。 - Dmitry Sazonov
2
@Saz 这只适用于Qt 4。在Qt 5中,信号是公开的 - 否则新的connect语法将无法工作。 - Kuba hasn't forgotten Monica
@KubaOber 您是正确的。但是方法的可见性不会影响对原始问题的解决方案。 - Dmitry Sazonov
显示剩余4条评论
4个回答

66

最近几天我发现... Qt 的处理方式如下:

class IEmitSomething
{
   public:
     virtual ~IEmitSomething(){} // do not forget this

   signals: // <- ignored by moc and only serves as documentation aid
            // The code will work exactly the same if signals: is absent.
     virtual void someThingHappened() = 0;
}

Q_DECLARE_INTERFACE(IEmitSomething, "IEmitSomething") // define this out of namespace scope

class ImplementEmitterOfSomething : public QWidget, public IEmitSomething
{
   Q_OBJECT
   Q_INTERFACES(IEmitSomething)

   signals:
      void someThingHappended();
}

现在,您可以连接到这些接口信号。

如果在连接信号时没有访问实现,则连接语句需要对 QObject 进行动态转换:

IEmitSomething* es = ... // your implementation class

connect(dynamic_cast<QObject*>(es), SIGNAL(someThingHappended()), ...);

这样做的好处是您不必将实现类暴露给订阅者和客户端。是的!!!


1
时间过去了...我不记得参考资料在哪里,但如果你有关键字Q_INTERFACES和Q_DECLARE_INTERFACE,那么你可以在谷歌上搜索。我发帖时的问题是我不知道它们。你可以在这里阅读一些相关信息http://doc.qt.io/qt-5/qtplugin.html#Q_DECLARE_INTERFACE,并浏览有关该接口的信息。 - Beachwalker
根据这里的说法,信号不应该是虚拟的。 - Super-intelligent Shade
4
我很惊讶这个实际上起作用了,但我很失望它需要使用旧式的SIGNAL()/SLOT()语法... - Sty
问题在于,如果您尝试使用常规语法连接到ImplementEmitterOfSomething:connect(obj,&ImplementEmitterOfSomething :: somethingHappened,...),则连接将失败。 - AlGrenadine
对于使用槽的接口,这种方法也适用。在接口中放置“public slots:”,它将构建良好,允许您在任何派生类中要求这些槽。 - Paul Masri-Stone
显示剩余4条评论

18
在Qt中,“signals”是“protected”的同义词。但它有助于MOC生成必要的代码。因此,如果您需要使用某些信号的接口-应将它们声明为虚拟抽象受保护方法。 MOC将生成所有必要的代码-您可以看到细节,即“emit somesignal”将被替换为具有相同名称的受保护方法的虚拟调用。请注意,该方法的主体也由Qt生成。
更新: 示例代码:
MyInterfaces.h
#pragma once

struct MyInterface1
{
signals:
    virtual void event1() = 0;
};

struct MyInterface2
{
signals:
    virtual void event2() = 0;
};

MyImpl.h

#ifndef MYIMPL_H
#define MYIMPL_H

#include <QObject>
#include "MyInterfaces.h"

class MyImpl
    : public QObject
    , public MyInterface1
    , public MyInterface2
{
    Q_OBJECT

public:
    MyImpl( QObject *parent );
    ~MyImpl();

    void doWork();

signals:
    void event1();
    void event2();
};

class MyListner
    : public QObject
{
    Q_OBJECT

public:
    MyListner( QObject *parent );
    ~MyListner();

public slots:
    void on1();
    void on2();
};

#endif // MYIMPL_H

MyImpl.cpp

#include "MyImpl.h"
#include <QDebug>

MyImpl::MyImpl(QObject *parent)
    : QObject(parent)
{}

MyImpl::~MyImpl()
{}

void MyImpl::doWork()
{
    emit event1();
    emit event2();
}

MyListner::MyListner( QObject *parent )
{}

MyListner::~MyListner()
{}

void MyListner::on1()
{
    qDebug() << "on1";
}

void MyListner::on2()
{
    qDebug() << "on2";
}

主程序.cpp

#include <QCoreApplication>
#include "MyImpl.h"

int main( int argc, char *argv[] )
{
    QCoreApplication a( argc, argv );

    MyImpl *invoker = new MyImpl( NULL );
    MyListner *listner = new MyListner( NULL );

    MyInterface1 *i1 = invoker;
    MyInterface2 *i2 = invoker;

    // i1, i2 - not QObjects, but we are sure, that they will be.
    QObject::connect( dynamic_cast< QObject * >( i1 ), SIGNAL( event1() ), listner, SLOT( on1() ) );
    QObject::connect( dynamic_cast< QObject * >( i2 ), SIGNAL( event2() ), listner, SLOT( on2() ) );

    invoker->doWork();

    return a.exec();
}

如果您确定i1是QObject,则不需要dynamic_cast。在这种情况下,static_cast可以正常工作。 - Fernando Pelliccioni
2
@FernandoPelliccioni 不,你完全错了。因为接口不继承 QObject。只能使用 reinterpret_cast,但它不起作用——因为存在多重继承。 - Dmitry Sazonov
1
@SaZ 哦!我误读了代码。我没有看到交叉转换。 在我的误解中,我想象了一个向下转换。 对于造成的干扰,我感到抱歉。 - Fernando Pelliccioni

6
在接口中声明信号为抽象方法存在两个问题:
  1. 只有在特定的实现方式下,即由moc生成的实现并包含在对象的元数据中时,信号才从Qt的角度被认为是信号。
  2. 直接从对象外部发出信号通常是不好的设计。
因此,由于接口是抽象的,你实际上不需要声明其信号 - 它除了用于记录意图之外没有任何作用,因为:
  1. 如果一个信号在派生自该接口的类中实现,你可以使用元对象系统来验证其存在。
  2. 你不应该直接调用这些信号方法。
  3. 一旦将非对象接口动态转换为QObject,那么实现是从接口派生的这一事实就不再重要。
进行这种花式操作的唯一有效原因是:
  1. 诱导Doxygen或另一个文档生成器为您的代码提供文档。
  2. 强制具体类具有相同名称的方法实现。当然,这并不能保证它实际上是一个信号。

我认为在抽象接口中声明信号的另一个原因是:假设您有一个小部件,它通过指向该接口的指针接受对象。让对象具有抽象方法QString getName()setName(QString)和信号nameModified。那么您的小部件可以在构造函数中订阅此信号,以便在具体实现通知名称更改时得到通知。 - user8044236
1
只要使用名称建立连接(QT4语法),这将完美地工作,而且基类甚至不需要有信号,只要派生类有即可。相反,基类需要的是一个virtual QObject *object() = 0;方法(所有这样的“从接口派生QObject”的类都需要它!)-否则,您将无法首先获取对象指针 :) - Kuba hasn't forgotten Monica
还有另一个原因:如果您从抽象接口派生,这意味着您正在使用多重继承(因为您必须从QObject派生以使用信号,而抽象接口不从QObject派生)。多重继承会防止您使用Qt5的connect()形式。 - Hedede
由于在 C++ 中将指针与虚成员函数进行比较是未指定的行为,并且 QMetaObject 无法找到信号,因此出现了这种情况。 - Hedede

3
我们都希望彻底摆脱MOC,但在此之前,我想添加一个可行的替代方案,该方案不需要包括QObject.h,并且在接口类中不使用Q_OBJECT和Q_INTERFACE。
首先,在接口中定义一个抽象的connect函数:
class I_Foo
{
public:
    virtual void connectToSignalA(const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection) = 0;
};

现在在派生类中,覆盖函数。同时声明信号,添加Q_OBJECT等。

class Bar : public QObject, public I_Foo
{
    Q_OBJECT

public:
    void connectToSignalA(const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);

signals:
    void A();
};

然后在该类的 .cpp 文件中进行连接:

Bar::connectToSignalA(const QObject * receiver, const char *method, Qt::ConnectionType void type)
{
    connect(this, SIGNAL(A()), receiver, method, type);
}

需要注意的是,你必须在每个派生类中编写连接函数,并且必须使用旧式连接(或者可以使用模板函数),但大致如此。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接