QT 元对象系统(上)

发布于 2021-09-20  177 次阅读


综述

QT是一个非常强大的C++库,相比于标准C++,QT提供了以下额外的特性:

  • 信号与槽
  • 属性系统(支持反射)
  • 父子对象系统
  • 事件系统

而这些特性的基础,就是QT的元对象系统(QMetaObject)。下文将由表及里,通过例子逐步探究其实现。

从一个例子开始

我们在使用QT的时候,经常使用到的就是信号与槽。例如一个窗口中有一个按钮控件和一个文本框控件。当按钮被按下的时候,文本框会把内容清空。此时我们会写如下的代码:

connect(ui->pushBtn,SIGNAL(clicked()),ui->lineEdit,SLOT(clear()));

clicked()是QPushButton的信号,clear()是QLineEdit的方法。

QT规定signal和slot的参数必须互相匹配(signial的参数可以比slot多,会忽略多余参数),并且signal的返回值必须是void。

调用完connect之后,就相当于把这个按钮对象的click信号与文本框对象的clear槽绑定了,此时只要在某处emit了这个按钮的 click信号,就会自动去调用 clear 清空文本框(当然这个调用还是有很多细节的,同步or异步,跨线程调用等等,这里先不谈)。

那么这个connect到底做了什么事呢?首先来看这两个宏:SIGNAL()和 SLOT()

# define SLOT(a)      ”1″#a  
# define SIGNAL(a)   ”2″#a  

这里的#a是把a替换之后加上双引号,即把传入的a转为字符串。

所以上面的connect调用在宏展开之后会变成这样:

connect(ui->pushBtn,"2clicked()",ui->lineEdit,"1clear()");

再来看connect的参数:

ui->pushBtn(对象),"2clicked()"(宏展开的字符串),ui->lineEdit(对象),"1clear()" (宏展开的字符串)

居然是传了两个对象和两个字符串?通过字符串咋能把两个函数连接起来啊?到底有啥黑魔法?

这里就能通过这个字符串参数大概猜到一些。我们看到这个传入的信号和槽的名字,并没有带上对应类的域限定符,即

QPushButton::clicked()
QLineEdit::clear()

所以大概率是通过这两个对象的内部方法,能够通过一个代表函数的字符串来找到这个函数。这™不就是反射么??

啊确实,QT就是通过某些奇技淫巧,实现了反射的功能,这就是要说的【元对象系统】。

先想想要咋用

QT这些机制这么叼,我们要怎么做才能用上呢?

首先是需要继承QObject这个类,其次是在类中加上Q_OBJECT这个宏,就这么简单。

之后就可以使用signal:来声明信号,slot:来声明槽啦。但要注意,slot:之前可以加上public,private。但signal不行。

此外这里的信号尽可能声明成无返回值的,即使有返回值也经常不能得到想要的结果。

class Test : public QObject {
    Q_OBJECT
public:
    ... //othercode
signals:
    void signal_1(QString);
public slots:
    int slot_1(QString);
}

signalslot是啥呢?事实上这两个也都是宏:

#define signals public
#define slots

所以实际上还是正常的声明了函数。

此外QT中,不管slot是public还是private的,只要是通过信号与槽机制,都可以调用slot。因此在这里的访问限定只对你直接调用槽函数有关。很神奇吧?

那么现在就要想了,这些宏展开之后啥也没干啊看上去,怎么就能实现反射了呢?这就要提到QT的一个辅助机制:--元对象编译器MOC(Meta Object Compiler)

元对象编译器(Meta Object Compiler)

如果我们去看看一个qt工程的编译输出,会发现里头出现了一些以moc开头的cpp文件,这就是元对象编译器干完活的成品。这时候我们大概就能猜到这个MOC做了些啥。

由于C++不在语言层面提供反射机制,即无法直接获得对象or类的信息。既然它不体面,MOC就帮它体面。

MOC会直接读取源代码,遇到与qt相关的宏会对其进行处理,直接添加上一些其他代码,使其能够做到保存对象or类的信息。大概能猜到的实现就是去扫描类里头的函数,创建一个表,把函数的名称字符串与函数的指针关联起来。之后添加对这个表的读取or写入方法。

事实上MOC也就是这么干的。

首先来看Q_OBJECT这个宏里头是啥:

#define Q_OBJECT \  
public: \  
Q_OBJECT_CHECK \  
static const QMetaObject staticMetaObject; \  
virtual const QMetaObject *metaObject() const; \  
virtual void *qt_metacast(const char *); \  
QT_TR_FUNCTIONS \  
virtual int qt_metacall(QMetaObject::Call, int, void **); \  

我们看到了一个激动人心的QMetaObject类,总的来说就是这个宏会向这个类里头添加一个QMetaObject成员,并添加一些有关 QMetaObject 的操作方法。

当我们提到元对象操作的时候,实际上就是对每个对象里头所包含的这个元对象执行操作。这个元对象里头就包含着有关这个类的静态信息和有关这个对象的运行时信息了!

所以实际上就是QT实现了一个反射库,利用MOC自动生成一个支持反射的对象到每一个需要使用元对象系统的类里头。并不是什么黑魔法。

具体MOC如何实现反射,且听下回分解。


当其他人都认为你要鸽的时候,你鸽了,亦是一种不鸽