Qt元对象系统

元对象系统

一 元对象系统的基本概念

  1. qt 元对象系统主要提供功能:对象间通信的信号和槽机制,运行时类型信息和动态属性系统等。
  2. 元对象系统是Qt对原有C++进行的一些扩展,主要是为实现信号和槽机制引入的,信号和槽机制是Qt的核心特征
  3. 使用元对象必须满足以下条件:
    (1)该类 必须 继承自QObject类
    (2)必须在类声明的私有区域添加Q_OBJECT宏,该宏用于启动元对象特性。然后便可以使用动态特性,信号和槽等功能
    (3)元对象编译器(moc)为每个QObject的子类,提供实现了元对象特性所必须的代码。
  4. 元对象系统具体运行原则
    (1)由于元对象系统是对C++的扩展,因此使用传统的编译器是不能直接编译Qt程序的,因此编译Qt程序之前需要把Qt扩展的语法去掉。这就是moc要做的事
    (2)moc全称 Meta-Object Compiler(元对象编译器),它是一个工具(类似于qmake),该工具读取并分析C++源文件,若发现一个或多个包含了Q_OBJECT 宏的类的声明,则会生成另外一个包含了Q_OBJECT 宏实现代码的 C++源文件(该源文件通常名称为 moc_*.cpp) ,这个新的源文件要么被#include 包含到类的源文件中,要么被编译键接到类的实现中(通常是使用的此种方法)。 注意: 新文件不会“替换”掉旧的文件,而是与原文件一起编译。
    5.其他概念
    (1) 元对象代码: 指的是 moc 工具生成的源文件的代码,其中包含有 Q_OBJECT 宏的实现代码
    (2) moc 工具的路径为: F:\app\Qt5.8.0MinGw\5.8\mingw53_32\bin

二 Q_OBJECT宏

列出该宏代码的目的主要是为了讲解使用 Qt Creator 时易犯的错误。 选中Q_OBJECT 按F2 即可跳转到宏的定义。
该文件在 C:\Qt\Qt5.6.1\5.6\msvc2013_64\include\QtCore\qobjectdefs.h

#define Q_OBJECT \
public: \
    Q_OBJECT_CHECK \
    QT_WARNING_PUSH \
    Q_OBJECT_NO_OVERRIDE_WARNING \
    static const QMetaObject staticMetaObject; \                 (1
    virtual const QMetaObject *metaObject() const; \              (2virtual void *qt_metacast(const char *); \                    (3virtual int qt_metacall(QMetaObject::Call, int, void **); \     (4)
    QT_TR_FUNCTIONS \     (5private: \
    Q_OBJECT_NO_ATTRIBUTES_WARNING \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    QT_WARNING_POP \
    struct QPrivateSignal {}; \
    QT_ANNOTATE_CLASS(qt_qobject, "")

Q_OBJECT 宏为声明的类之中增加了一些成员,而且2,3,4是虚函数成员(注意,这些虚函数没有定义),按 C++语法,虚函数必须定义或者被声明为纯虚函数, moc工具的工作之一就是生成以上成员的定义,并且还会生成一些其他必要的代码。 此处讲解
Q_OBJECT 宏代码的目的主要是为了讲解使用 Qt Creator 时易犯的错误及怎样正确使用Q_OBJECT 宏,读者可查看由 moc 生成的 moc_*.cpp 的内容以了解更详细的信息。

三 使用Qt Creator 启动元对象系统

  1. 此时 moc 工具是通过 Qt Creator 来使用的,因此必须保证 moc 能发现并处理项目中包含有 Q_OBJECT 宏的类,为此,需要遵守以下规则
    (1)从 QObject 派生的含有 Q_OBJECT 宏的类的定义必须在头文件中。
    (2)确保 pro 文件中,是否列举了项目中的所有源文件(SOURCES 变量)和头文件(HEADERS 变量)
    (3)应在头文件中使用逻辑指令(比如#ifndef)防止头文件被包含多次。
    (4)QObject 类应是基类列表中的第一个类。
    (5)由以上规则可见,使用 Qt Creator 编写代码时,类应定义在头文件中,成员函数的定义应位于源文件中(这样可避免头文件被包含多次产生的重定义错误),虽然这样编写程序比较麻烦,但这是一种良好的代码组织方式。
    2、 不按规则 1 的方法编写程序,则 moc 工具就不能正确生成代码, 这时的错误原因通常是未定义由 Q_OBJECT 展开后在类中声明的虚函数引起的,其错误信息如下:
    (1)若使用 MinGw 编译,产生的错误信息类似如下:
    undefined reference to `vtable for A’表示类 A 的虚函数表(vtable)不能正常生成,通常是有虚函数未定义。
    (2)若使用 VC++2015 编译,产生的错误信息类似如下:
    LNK2001: 无法解析的外部符号 "public: virtual struct QMetaObject const *__thiscall A::metaObject(void)const " (……)
    表示虚函数 A::metaObject 未定义。
    3、以下错误不易被发现
    若定义了 QObject 类的派生类,并进行了构建,在这之后再添加 Q_OBJECT 宏,则此时
    必须执行一次 qmake 命令(“构建” >“执行 qmake” ),否则 moc 不能生成代码。

示例: 使用 Qt Creator 启动元对象系统


新建空项目、添加一个 m.h 和 m.cpp 文件。
/*m.h 文件内容。要在 Qt Creator 中启动元对象系统,包含 Q_OBJECT 宏的类的定义必须位于头文件中,否
则 moc 工具不能生成元对象代码*/#ifndef M_H //用于防止头文件被多次包含的逻辑指令
#define M_H#include<QObject> //因为要使用 QObject 类,为此需要包含此头文件
class B{};
//错误, moc 不能启动。因为多重继承时 QObject 必须位于基类列表中的第一位。
//class A:public B,public QObject{ Q_OBJECT };
class A:public QObject,public B{ /*要使用元对象系统必须继承自 QObject 类,且 QObject 应位于基类
继承列表中的第一位。 */
Q_OBJECT //启动元对象系统。 Q_OBJECT 必须位于私有区域。
public: A(){qDebug("DDDD\n");} }; //qDebug 函数用于在控制台输出一些信息
#endif // M_H
//m.cpp 文件内容。
#include "m.h"
int main(int argc, char *argv[]){ A ma; return 0; }

四 在命令行启动元对象系统

1.在命令行需使用 moc 工具,并在源文件中包含 moc 工具生成的 cpp 文件。
2、 此时包含 Q_OBJECT 的类不需要位于头文件中,假设位于 m.cpp 文件内,内容为:
#include
class A:public QObject{ Q_OBJECT
public: A(){} };
int main(int argc, char *argv[]){ A ma; return 0; }
3、 打开 Qt 5.8 for Desktop (MinGW 5.3.0 32 bit)命令行工具,输入如下命令
moc d:\qt\m.cpp -o d:\qt\mm.cpp
以上命令会根据 m.cpp 生成 mm.cpp 文件, mm.cpp 文件中的代码就是元对象代码, 此处
m.cpp 和 mm.cpp 都位于 d:\qt 文件夹下。
4、 然后再次打开 m.cpp,在其中使用#include 把 mm.cpp 包含进去,如下
#include
//#include “mm.cpp” //不能把 mm.cpp 包含在类 A 的定义之前,原因见下面的注释
class A:public QObject{ Q_OBJECT
public: A(){} };
#include “mm.cpp” /*必须把 mm.cpp 包含在类 A 的定义之后,因为 mm.cpp 源文件中有对类 A 的
成员的定义,此时必须见到类 A 的完整定义。 */
int main(int argc, char *argv[]){ A ma; ;return 0; }
5、 然后再使用 qmake 生成项目文件和 makefile 文件,再使用 mingw32-make 命令即可。

元对象

一 QByteArray类简介

  1. QByteArray类简介
    (1)该类是一个用于处理字符串的类似于 C++的 string 类型的类,在 Qt 中,对字符串的处理,经常使用的是 QString 类,
    (2)该类保证字符串以’\0’结尾,并使用隐式共享(copy-on-write)来减少内存用量和不必要的数据复制。
    (3)QByteArray 适合用于存储纯二进制数据和内存资源比较短缺的情况下。
    (4)下面是对 QByteArray 类的简单使用方法
#include<QByteArray>
#include<iostream>
using namespace std;
int main(int argc, char *argv[]){
QByteArray by("AAA"); //创建 QByteArray 的方法之一。
const char * pc="ABC";
QByteArray by1(pc); //创建 QByteArray 的方法之一。
const char *pc1=by.data(); //返回指向该字符串的 char*类型的指针
cout<<pc1<<endl; //输出 AAA
by1.append("DDD"); //在末尾追加字符串
cout<<by1.data()<<endl; //输出 by1 中的字符串 ABCDDD
cout<<by1.size()<<endl; //输出 6, 返回字符串的字符数(不含'\0')
cout<<by1[2]<<endl; //使用下标访问单个字符,输出 C
cout<<by1.at(1)<<endl; //at 函数类似于下标算符。 输出 B
return 0;

二 元对象系统与反射机制

  1. reflection 模式(反射模式或反射机制):是指在运行时,能获取任意一个类对象的所有类型信息、属性、成员函数等信息的一种机制。
  2. 元对象系统与反射机制
    ①、 元对象系统提供的功能之一是为 QObject 派生类对象提供运行时的类型信息及数据成员的当前值等信息,也就是说,在运行阶段,程序可以获取 QObject 派生类对象所属类的名称、父类名称、该对象的成员函数、枚举类型、数据成员等信息,其实这就是反射机制。
    ②、因为 Qt 的元对象系统必须从 QObject 继承,又从反射机制的主要作用可看到, Qt 的元对象系统主要是为程序提供了 QObject 类对象及其派生类对象的信息,也就是说不是从 QObject 派生的类对象,则无法使用 Qt 的元对象系统来获取这些信息。
  3. 元对象:是指用于描述另一个对象结构的对象。使用编程语言具体实现时,其实就是一个类的对象,只不过这个对象专门用于描述另一个对象而已,比如 class B{…}; class A{…B mb;…};假设 mb 是用来描述类 A 创建的对象的,则 mb 就是元对象。
  4. Qt 具体实现反射机制的方法
    (1) Qt 使用了一系列的类来实现反射机制,这些类对对象的各个方面进行了描述,其中QMetaObject 类描述了 QObject 及其派生类对象的所有元信息,该类是 Qt 元对象系统的核心类,通过该类的成员函数可以获取 QObject 及其派生类对象的所有元信息,因此可以说QMetaObject 类的对象是 Qt 中的元对象。注意:要调用 QMetaObject 类中的成员函数需要使用 QMetaObject 类型的对象。
    (2) 对对象的成员进行描述:一个对象包含数据成员、函数成员、构造函数、枚举成员等成员,在 Qt 中,这些成员分别使用了不同的类对其进行描述,比如函数成员使用类QMetaMethod 进行描述,属性使用 QMetaProperty 类进行描述等,然后使用QMetaObject 类对整个类对象进行描述,比如要获取成员函数的函数名,其代码如下:

QMetaMethod qm = metaObject->method(1); //获取索引为 1 的成员函数
qDebug()<<qm.name()<<“\n”; //输出该成员函数的名称。
5. 使用qt反射机制的条件
(1)需要继承自 QObject 类,并需要在类之中加入 Q_OBJECT 宏。
(2)注册成员函数:若希望普通成员函数能够被反射,需要在函数声明之前加入
QObject::Q_INVOKABLE 宏。
(3)注册成员变量:若希望成员变量能被反射,需要使用 Q_PROPERTY 宏。
6. Qt 反射机制实现原理简述
(1)Q_OBJECT 宏展开之后有一个虚拟成员函数 meteObject(),该函数会返回一个指向QMetaObject 类型的指针,其原型为
virtual const QMetaObject *metaObject() const;
因为启动了元对象系统的类都包含 Q_OBJECT 宏,所以这些类都有含有 metaObject()虚拟成员函数,通过该函数返回的指针调用 QMetaObject 类中的成员函数,便可查询到 QObject 及其派生类对象的各种信息。
(2)Qt 的 moc 会完成以下工作
a. 为 Q_OBJECT 宏展开后所声明的成员函数的成生实现代码
b. 识别 Qt 中特殊的关键字及宏,比如识别出 Q_PROPERTY 宏、 Q_INVOKABLE宏、 slot、 signals 等

// slot signals 宏

#     define slots Q_SLOTS
#     define signals Q_SIGNALS

# define Q_SLOTS QT_ANNOTATE_ACCESS_SPECIFIER(qt_slot)
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)

#ifndef QT_ANNOTATE_ACCESS_SPECIFIER
# define QT_ANNOTATE_ACCESS_SPECIFIER(x)
#endif

三 使用反射机制获取类对象的成员函数的信息

1. QMetaMethod 类

①、 作用:用于描述对象的成员函数,可使用该类的成员函数获取对象成员函数的信息。
②、 该类拥有如下成员:
 enum MethodType{Method, Signal, Slot, Constructor}
此枚举用于描述函数的类型,即:普通成员函数(Method)、信号(Signal)、槽(Slot)、构造函数(Constructor)。
 enum Access{Private, Protected, Public}
此枚举主要用于描述函数的访问级别(私有的、受保护的、公有的)
 QByteArray methodSignature() const; //返回函数的签名(qt5.0)
 MethodType methodType() const; //返回函数的类型(信号、槽、成员函数、构造函数)
 QByteArray name() const; //返回函数的名称(qt5.0)
 int parameterCount() const ; //返回函数的参数数量(qt5.0)
 QList parameterNames() const; 返回函数参数名称的列表。
 int parameterType(int index) const;
返回指定索引处的参数类型。返回值是使用 QMetaType 注册的类型,若类型未注册,则返回值为 QMetaType::UnknownType。
 QList parameterTypes() const; 返回函数参数类型的列表。
 int returnType() const;
返回函数的返回类型。返回值是使用 QMetaType 注册的类型,若类型未注册,则返回值为 QMetaType::UnknownType。
 const char * typeName() const; 返回函数的返回类型的名称。
 Access access() const; 返回函数的访问级别(私有的、受保护的、公有的)

2. QMetaObject 类中有关获取类对象成员函数的函数有:

①、 int indexOfMethod(const char* f) const;
返回名为 f 的函数的索引号,否则返回-1。 此处应输入正确的函数签名,比如函数形
式为 void f(int i,int j);则正确的形式为 xx.indexOfMethod(“f(int,int”); 以下形式都不是
正确的形式, “f(int i, int j)”、 “void f(int, int)”、 “f”、 "void f"等。
②、 int indexOfSignal(const char * s) const;
返回信号 s 的索引号,否则返回-1,若指定的函数存在,但不是信号,仍返回-1。
③、 int indexOfConstructor(const char *c) const; //返回构造函数 c 的索引号,否则返回-1
④、 int constructorCount() const ; //返回构造函数的数量。
⑤、 QMetaMethod constructor(int i)const; 返回指定索引 i 处的构造函数的元数据。
⑥、 int methodCount() const;
返回函数的数量,包括基类中的函数、信号、槽和普通成员函数。 (不包括构造)
⑦、 int methodOffset() const;
返回父类中的所有函数的总和,也就是说返回的值是该类中的第一个成员函数的索
引位置。
⑧、 QMetaMethod method(int i) const; 返回指定索引 i 处的函数的元数据。

示例

#ifndef TEST_H
#define TEST_H
#include<QObject> //因为要使用 QObject 类,为此需要包含此头文件
class A:public QObject
{
    Q_OBJECT //启动元对象系统,必须声明此宏
public:
    //定义 2 个构造函数、 1 个信号、 3 个函数。
    Q_INVOKABLE A(){} //要想函数被反射,需要指定 Q_INVOKABLE 宏。
    Q_INVOKABLE A(int){}
    Q_INVOKABLE void f(){}
    Q_INVOKABLE void g(int i,float j){}
    void g1(){} //注意: 此函数不会被反射。
signals:
    void gb3();
};
class B:public A{
    Q_OBJECT //要使用元对象系统,应在每个类之中都声明此宏
public:
    //定义 1 个函数、 2 个信号
    Q_INVOKABLE void f(){}
signals:
    void gb4();
    void gb5();
};
#endif // TEST_H


#include "mainwindow.h"
#include <QApplication>
#include"test.h"
#include<QMetaMethod>
#include<QByteArray>
#include<QDebug>

int main(int argc, char *argv[])
{
    A a;
    B b;
    const QMetaObject *pa = a.metaObject();
    const QMetaObject *pb = b.metaObject();
    //以下为通过 QMetaObject 的成员函数获取的信息。
   ///返回对象 a 中的成员函数数量,包括从父类 QObject 继承而来的 5 个
   //成员函数及本对象中的 2 个成员函数(注意,不包括 g1)、 1 个信号,所以
   //总数为 8。
    qDebug()<<pa->methodCount();   //8
    //获取对象 a 中的成员函数 g 的索引号
    qDebug()<<pa->indexOfMethod("g(int,float)");     // 7
    qDebug()<<pa->indexOfMethod("f()");     // 6
    qDebug()<<pa->indexOfMethod("A(int )");     //-1
    qDebug()<<pa->indexOfMethod("A()");     // -1     不包括构造
    qDebug()<<"~~~~~~~~~~~~~~~~~~~~~~~~";
    //a中构造函数的数量
    qDebug()<<pa->constructorCount();   //2
    //b 中构造函数的数量,B没有构造为0,不包含父类的构造
    qDebug()<<pb->constructorCount();   //0
    qDebug()<<"~~~~~~~~~~~~~~~~~~~~~~~~";
    qDebug()<<pa->indexOfConstructor("A()");    //0
    qDebug()<<pa->indexOfConstructor("A(int)");   // 1
    qDebug()<<pa->indexOfSignal("gb3()");      //5
    qDebug()<<pa->indexOfMethod("gb3()");  // 5
    qDebug()<<pa->indexOfSignal("f()");      //-1    f为函数 不是信号 返回-1
    qDebug()<<pa->indexOfMethod("f()");  //  6
    qDebug()<<"__________________________________";
    //获取父类的成员函数数量, 包括父类A及QObject中的成员函数,总共为8
    qDebug()<<pb->methodOffset();   //8    此处相当于是对象 b 自身成员函数开始处的索引号。
    qDebug()<<pb->methodCount();   // 11  父类8 + f() + 2个信号
    qDebug()<<pb->indexOfMethod("f()");   // 10
    qDebug()<<pb->indexOfMethod("gb4()");   //8
    qDebug()<<pb->indexOfMethod("gb5()");   // 9
    qDebug()<<pb->indexOfSignal("gb4()");   //8
    qDebug()<<pb->indexOfSignal("gb5()");   // 9
    qDebug()<<"~~~~~~~~~~~~~~~~~~~~~~________________";
    // 通过QMetaMethod 的成员函数获取信息
    // 获取对象a的成员函数
    QMetaMethod m = pa->method(pa->indexOfMethod("g(int,float)"));
    QByteArray s = m.name();
    qDebug()<<s;   // 获取成员函数 g的函数名      g
    s = m.methodSignature();   //g的签名
    qDebug()<<s.data();    //    g(int,float)
    //获取函数 g 的类型,此处返回的是 QMetaMethod::MethodType 中定义的枚举值,
    //其中 Method=0,表示类型为成员函数*/
    qDebug()<<m.methodType();  //获取g的类型
    //以下信息与函数的返回类型有关
    s = m.typeName();      // 获取函数g的返回值的类型名
    qDebug()<<s.data();   //void
     获取函数g返回值的类型。 此处的类型是 QMetaType 中定义的枚举值,其中枚举
    //值 QMetaType::void=43*/
    qDebug()<<m.returnType();
    //以下信息与函数的参数有关
    ///*获取函数 g 中索引号为 1 的参数类型,此处的类型是 QMetaType 中定义的
    //枚举值,其中枚举值 QMetaType::float=38*/
    qDebug()<<m.parameterType(1);
    //获取函数g的参数名列表
    QList<QByteArray> q = m.parameterNames();
    qDebug()<<q;   // i ,j
    //获取函数g的参数类型列表
    q = m.parameterTypes();
    qDebug()<<q;   // int  float
    qDebug()<<q[0].data()<<" "<<q[1].data();
    return 0;
}


四 使用反射机制获取与类相关的信息

1. QMetaObject 类中获取与类相关的信息的成员函数有

(1)const char* className() const;
获取类的名称,注意,若某个 QObject 的子类未启动元对象系统(即未使用 Q_OBJECT
宏),则该函数将获取与该类最接近的启动了元对象系统的父类的名称,而不再返回
该类的名称,因此建议所有的 QObject 子类都使用 Q_OBJECT 宏。
(2)const QMetaObject* superClass() const;
//返回父类的元对象,若没有这样的对象则返回 0。
(3)bool inherits(const QMetaObject* mo) const; (Qt5.7)
//若该类继承自 mo 描述的类型,则返回 true,否则返回 false。类被认为继承自身。

2. QObject 类中获取与类相关的信息的成员函数有

bool inherits(const char* className) const;
//若该类是className 指定的类的子类则返回true,否则返回false。类被认为继承自身。

继承关系判断

示例:

#ifndef TEST_H
#define TEST_H
#include<QObject> //因为要使用 QObject 类,为此需要包含此头文件
class A: public QObject{Q_OBJECT};
class B: public A{Q_OBJECT};
class C: public QObject{Q_OBJECT};
class D:public C {};
#endif // TEST_H

//main.cpp
#include "mainwindow.h"
#include <QApplication>
#include"test.h"
#include<QMetaMethod>
#include<QByteArray>
#include<QDebug>

int main(int argc, char *argv[])
{
    A ma;
    B mb;
    C mc;
    D md;
    const QMetaObject *pa = ma.metaObject();
    const QMetaObject *pb = mb.metaObject();
    //输出类名 A
    qDebug()<<pa->className();
    //使用 QMetaObject::inherits()函数判断继承关系
    qDebug()<<pa->inherits(pa); //输出 1,类被认为是自身的子类
    qDebug()<<pa->inherits(pb); //输出 0,由 pb 所描述的类 B 不是类 A 的子类。
    qDebug()<<pb->inherits(pa); //输出 1,由 pa 所描述的类 A 是类 B 的子类。
    //使用 QObject::inherits()函数判断继承关系。
    qDebug()<<ma.inherits("B"); //输出 0,类 A 不是类 B 的子类。
    qDebug()<<ma.inherits("A"); //输出 1,类被认为是自身的子类
    qDebug()<<md.inherits("D"); //输出 0,因为类 D 未启动元对象系统。
    qDebug()<<md.inherits("C"); /*输出 1,虽然类 D 未启动元对象系统,但类 C 已启动,此种情形下
    能正确判断继承关系。 */

    输出 C,此处未输出 D,因为类 D 未启动元对象系统,与类D
    /// 最接近的启动了元对象系统的父类是C,因此返回C
    qDebug()<<md.metaObject()->className();

    return 0;
}




3. qobject_cast 函数

 使用语法如下
			DestType* qobject_cast<DestType*>(QObject *p);

(1)该函数类似于 C++中的 dynamic_cast,但执行速度比 dynamic_cast 更快,且不需要C++的 RTTI 的支持,但 qobject_cast 仅适用于 QObject 及其派生类。
(2)主要作用是把源类型 QObject 转换为尖括号中的目标类型 DesType(或者子类型),并返回指向目标类型的指针,若转换失败,则返回 0。或者说源类型 QObject 属于目标类型 DestType(或其子类型),则返回指向目标类型的指针,否则返回 0。
(3)使用 qobject_cast 的条件:目标类型 DestType 必须继承(直接或间接)自 QObject,并使用 Q_OBJECT 宏

qobject_cast 及其应用
#ifndef TEST_H
#define TEST_H
#include<QObject> //因为要使用 QObject 类,为此需要包含此头文件
#include<QDebug>
using namespace std;
class A:public QObject
{
    Q_OBJECT
public:
    void fa()
    {
        qDebug()<<"FA";
    }
};
class B:public A
{
    Q_OBJECT
public:
    void fb()
    {
        qDebug()<<"FB";
    }
};
class C:public QObject
{
    Q_OBJECT
public:
    void fc()
    {
        qDebug()<<"FC";
    }
};
class D:public C
{
public:
    void fd()
    {
        qDebug()<<"FD";
    }
};
#endif // TEST_H


#include "mainwindow.h"
#include <QApplication>
#include"test.h"
#include<QMetaMethod>
#include<QByteArray>
#include<QDebug>
//qobject_cast 的简单应用(类型判断)
void g(QObject *p)
{
    if(qobject_cast<A*>(p)) //若 p 是类 A 及其派生类类型
    {
        qDebug()<<"GA";
    }
    if(qobject_cast<B*>(p))//若 p 是类 B 及其派生类类型
    {
        qDebug()<<"GB";
    }
    else //若 p 不是类 B 及其派生类类型
    qDebug()<<"XX";
}
int main(int argc, char *argv[])
{
    A *pa=new A;
    B *pb=new B;
    C *pc=new C;
    D *pd=new D;
   qobject_cast<B*>(pa)->fb(); //输出 FB,转换成功后可调用子类中的函数。
   //qobject_cast<D*>(pc); //错误,因为类 D 未使用 Q_OBJECT 宏。
   g(pa); //输出 GA、 XX。因为 pa 不是 B 及其派生类类型所以会输出 XX。
   g(pb); //输出 GA、 GB。因为 pb 是 A 的派生类类型,所以首先输出 GA,然后输出 GB。
   g(pc); //输出 XX,因为 pc 即不是 A 也不是 B 及其派生类的类型,所以输出 XX。
   g(pd); //输出 XX,原因同上。

    return 0;
}

属性系统

属性基础

  1. 属性与数据成员相似,但是属性可使用 Qt 元对象系统的功能。 他们的主要差别在于存取方式不相同,比如属性值通常使用读取函数(即函数名通常以 get 开始的函数)和设置函数(即函数名通常以 set 开始的函数)来存取其值,除此种方法外, Qt 还有其他方式存取属性值。
  2. 在 Qt 中属性和数据成员是两个不同的概念,他们可以相关联也可以没有联系,比如名为a 的属性,与数据成员 a,虽然他们名称相同,若他们之间没有产生关联,则数据成员 a与属性 a 是完全不相关的,通常,一个属性都有与之相关联的数据成员,而采用的命名规则通常是加上 m_前缀,比如属性名为 a,则与之相关联的数据成员名称通常为 m_a。
  3. 属性值可使用以下方式进行存取
    (1) 可使用 QObject::property 和 QObject::setProperty 函数进行存取
    (2) 若属性有相关联的存取函数,则可使用存取函数进行存取
    (3) 属性还可通过元对象系统的 QMetaObject 类进行存取。
    (4) 若属性与某个数据成员相关联,则可通过存取普通数据成员的值来间接存取属性的
    值。
    (5) 注意: Qt 中的类,只有属性没有数据成员,因此只能通过前面三种方式对属性值进
    行修改。
  4. 要在类之中声明属性,该类必须继承自 QObject 类,还应使用 Q_OBJECT 宏,然后使用
    QObject::Q_PROPERTY 宏声明属性,该宏语法如下:
Q_PROPERTY(type name
(READ getFunction [WRITE setFunction] |MEMBER memberName [(READ getFunction | WRITE setFunction)])
[RESET resetFunction]
[NOTIFY notifySignal]
[REVISION int]
[DESIGNABLE bool]
[SCRIPTABLE bool]
[STORED bool]
[USER bool]
[CONSTANT]
[FINAL])

(1) 方括号中的是可选项,各选项之间使用空格隔开。
(2) type:指定属性的类型,可以是 QVariant 支持的类型或用户自定义类型,若是枚举类型,还需使用 Q_ENUMS 宏对枚举进行注册,若是自定义类型,需要使用Q_DECLARE_METATYPE( Type )宏进行注册(详见后文)。
(3) name:指定属性的名称。
(4) READ getFunction:
用于指定读取函数(读取属性的值),其中 READ 表示读取,不可更改, getFunction用于指定函数名称。
若没有指定 MEMBER 变量,则必须指定 READ 函数。
通常, READ 函数是 const 的, READ 函数必须返回属性的类型或对该类型的引用。
(5) WRITE setFunction:
 用于指定设置函数(设置属性的值),其中 WRITE 表示写入,不可更改, setFunction用于指定函数名称。
 此函数必须只有一个参数,且返回值类型必须为 void。
 若为只读属性,则不需要指定 WRITE 函数。
 MEMBER memberName
 用于把指定的成员变量 memberName 设置为具有可读和可写性质, 而无需创建READ 和 WRITE 函数。 其中 MEMBER 表示成员,不可更改, memberName 表示类中的成员变量。
 若没有指定 READ 函数,则必须指定 MEMBER 变量。
 RESET resetFunction: 用于把属性重置为默认值,该函数不能有参数,且返回值必
须为 void。其中 RESET 表示重置,不可更改, resetFunction 用于指定函数名称。
 NOTIFY notifySignal: 表示给属性关联一个信号。 如果设置该项,则应指定该类中
已经存在的信号,每当属性值发生变化时就会发出该信号。若使用 MEMBER 变量,
则 NOTIFY 信号必须为零或一个参数, 且参数必须与属性的类型相同, 该参数将接
受该属性的新值。
 REVISION:设置版本号,默认为 0。
 DESIGNABLE: 用于设置属性在 GUI 设计工具的属性编辑器(例如 Qt 设计师)中是
否可见,多数属性是可见的,默认值为 ture,即可见。 该变量也可以指定一个返回布尔值的成员函数替代 true 或 false。
 SCRIPTABLE:设置属性是否可被脚本引擎访问,默认为 true
 STORED:设置在保存对象的状态时是否必须保存属性的值。大多数属性此值为 true
 USER:设置属性是否为可编辑的属性, 每一个类只有一个 USER 属性(默认值为
false)。 比如 QAbstractButton::checked 是用户可编辑的属性
 CONSTANT:表明属性的值是常量,常量属性不能有WRITE函数和NOTIFY信号。
对于同一个对象实例,每一次使用常量属性的 READ 函数都必须得到相同的值,但
对于类的不同实例,这个值可以不同。
 FINAL:表示属性不能被派生类重写。

声明以及使用属性
#ifndef TEST_H
#define TEST_H
#include<QObject> //因为要使用 QObject 类,为此需要包含此头文件
#include<QDebug>
using namespace std;
class A:public QObject
{
    Q_OBJECT
public:
    /*通常,若属性名为 a,则相应的读取函数通常命名为 geta,设置函数命名为 seta,本例并未使用这种命名
    规则*/
    //声明一个类型为 int,名称为 a 的属性,并使用函数 f 读取属性值,使用函数 g 设置属性值。
    Q_PROPERTY(int a READ f WRITE g)
    //声明一个类型为 int,名称为 b 的属性,并把该属性与成员变量 m_b 相关联,该属性未设置存取函数。
    Q_PROPERTY(int b MEMBER m_b)
    //声明一个只读属性 c,本例没有设置该属性值的函数,因此该属性是只读的。
    Q_PROPERTY(int c READ getc)
    /*在存取函数中可把属性与成员变量相关联,方法如下所示。对存取函数的返回类型和参数类型及数量在本
    例影响不大,在后文会讲解其影响。 */
    int f(){return m_a;} //属性 a 的读取函数。
    void g(int i){m_a=i;} //属性 a 的设置函数。
    /*成员变量也可以不与属性相关联,本函数也可直接返回数值 3。
        从此处可看到,属性可以不与数据成员相关联。 */
    int getc(){m_c=3; return m_c;}
    int m_a,m_b; //属性若命名为 a,则与其相对应的成员变量习惯上应命名为 m_a。
private:
    成员变量通常都应声明为私有的,这样可提高程序的封装性。
    int m_c;
};
#endif // TEST_H





#include "mainwindow.h"
#include <QApplication>
#include"test.h"
#include<QMetaMethod>
#include<QByteArray>
#include<QDebug>
int main(int argc, char *argv[])
{
    A ma;
    //像普通成员变量一样存取属性值
    ma.m_a=1;
    qDebug()<<ma.m_a<<endl; //输出 1
    //因为属性 b 没有存取函数,本例暂时只使用普通成员变量的方式存取该属性值。ma.m_b=2; cout<<ma.m_b<<endl; //输出 2
    ma.g(4); //使用属性 a 的设置函数修改属性值。
    qDebug()<<ma.f()<<endl; // 输出 4,使用属性 a 的读取函数读取属性值。
    qDebug()<<ma.getc()<<endl; /*输出 3,属性 c 是只读的,只能通过他的读取函数访问其值,因为没有设置
    函数,因此无法改变属性 c 的值。 */

    return 0;
}

QVariant类

  1. 使用 QObject::property 函数可读取属性的值,使用 QObject::setProperty 函数可以设置属性的值,但是属性有很多种类型,怎样使用 property 函数返回的属性值具有正确的类型呢?为解决这个问题,使用了一个 QVariant 来描述类型。
  2. QVariant 类用于封装数据成员的类型及取值等信息, 该类类似于 C++共用体 union, 一个QVariant 对象,一次只能保存一个单一类型的值。 该类封装了 Qt 中常用的类型,对于QVariant 不 支 持 的 类 型 ( 比 如 用 户 自 定 义 类 型 ) , 则 需 要 使 用Q_DECLARE_METATYPE( Type )宏进行注册(详见后文)。
  3. QVariant 拥有常用类型的单形参构造函数,因此可把这些常用类型转换为 QVariant 类型,同时 QVariant 还重载了赋值运算符,因此可把常用类型的值直接赋给 QVariant 对象。注:由 C++语法可知,单形参构造函数可进行类型转换。
  4. 使用 QVariant 构造函数和赋值运算符,见下面示例
    注意: QVariant 没有 char 类型的构造函数,若使用 char 值会被转换为对应的 int 型。
    QVariant v(1); //调用 QVariant(int )构造函数创建一个 QVariant 类型的对象,并把数值 1 保存到 v 中。
    v=2.2; //调用 QVariant 的赋值运算符,把值 2 保存在 v 之中,因为 QVariant 是类似于共用体的类,
    因此同一时间只会保存一个值。
  5. 获取 QVariant 对象存储的值的类型,可使用如下函数
     Type type() const
    获取 QVariant 对象当前存储值的类型,类型以枚举 QMetaType::Type 的形式表示
     const char * typeName() const;
    以字符串的形式返回 QVariant 对象存储的值的类型。若是无效类型则返回 0。
     const char* typeToName(int t);
    把以枚举类型 QMetaType::Type 表示的类型以字符串的形式返回。若枚举值为
    QMetaType::UnknownType 或不存在,则返回一个空指针。
     示例
    QVariant v(1);
    cout<<v.typeName()<<endl; //输出 int
    cout<<v.typeToName(v.type())<<endl; //输出 int
  6. 获取和设置 QVariant 对象存储的值,可使用如下函数
     void setValue(const T& v);
    把一个值的副本存储到 QVariant 对象中,若类型 T 是 QVariant 不支持的类型,则使用QMetaType 来存储该值,若 QMetaType 也不能处理,则发生编译错误。注:若是用户自定义类型则需要使用宏 Q_DECLARE_METATYPE(…)进行注册(详见后文)
     T value() const;
    把存储的值转换为类型 T 并返回转换后的值,存储值本身不会被改变。若 T 是QVariant 支持的类型,则该函数与 toInt、 toString 等函数功能完全相同。注:使用该函数时需要使用尖括号指定 T 的类型,比如 xx.value();
     T toT()
     其中 T 是某一类型,比如若 T 是 int,则该函数形式就为 int toInt()。
     该函数用于把存储的值转换为类型T并返回转换后的值,存储值本身不会被改变。
    其中比较常用的是 toString 函数,该函数可把存储的值转换为 QString 形式,这样
    便可以字符串的形式输出存储的值。
     注意:没有与自定义类型相对应的 toT 函数,比如 class C{};则没有 toC 函数,要
    把存储的值转换为自定义类型,需要使用 value 函数,且还需对自定义类型注册。
  7. 注意: QVariant 中的枚举类型 Type 已被废弃。
  8. 使用 QVariant 的默认构造函数,将创建一个无效的 QVariant 对象(或空的 QVariant 对象),
    可通过 isNull()成员函数进行判断。

QVariant类的使用

#include "mainwindow.h"
#include <QApplication>
#include"test.h"
#include<QMetaMethod>
#include<QByteArray>
#include<QDebug>
class C{}; //自定义类型
int main(int argc, char *argv[])
{
    QVariant v('a'); /*QVariant 没有专门的 char 构造函数,此处的字符 a 会被转换为 int 型,因此 v
    中存储的是数值 97,而不是字符 a 。 */
    qDebug()<<v.value<int>()<<endl; //输出 97
    qDebug()<<v.value<char>()<<endl; //输出 a,将 97 转换为 char 型,并输出转换后的值。
    qDebug()<<v.toChar().toLatin1()<<endl; /*输出 a,原因同上,注意 toChar 返回的类型是 QChar 而不是 char。 */
    qDebug()<<v.toString().toStdString()<<endl; /*输出 97,把存储在 v 中的值转换为 QString,然后以字符串形式输出。 */
    qDebug()<<v.typeName()<<endl; //输出 int, 可见存储在 v 中的值的类型为 int
    qDebug()<<v.typeToName(v.type())<<endl; /*输出 int,其中 type 返回存储值的枚举形式表示的类型,而
    typeToName 则以字符串形式显示该枚举值所表示的类型。 */
    char c='b';
    v.setValue(c);
    qDebug()<<v.toString().toStdString()<<endl; //输出 b
    qDebug()<<v.typeName()<<endl; /*输出 char,若是使用 QVariant 构造函数和直接赋值 char 型字符,此
    处会输出 int,这是 setValue 与他们的区别。 */
    C mc; //自定义类型 C 的对象 mc
    //QVariant v1(mc); //错误,没有相应的构造函数。
    QVariant v2;
    //v2=mc; //错误,没有与类型 C 匹配的赋值运算符函数。
    //v2.setValue(mc); //错误,自定义类型 C 未使用宏 Q_DECLARE_METATYPE 声明。

    return 0;
}

使用QObject类中的成员函数存取属性值与动态属性

  1. 注册自定义类型与 QMetaType 类
    ①、 QMetaType 类用于管理元对象系统中命名的类型, 该类用于帮助 QVariant 中的类型以及队列中信号和槽的连接。它将类型名称与类型关联,以便在运行时动态创建和销毁该名称。
    ②、 QMetaType::Type 枚举类型定义了 QMetaType 支持的类型。 其原型为
    enum Type{void, Bool,Int……UnknowType} //全部类型详见帮助文档。
    ③、 对于 QVariant 类和属性中使用的自定义类型,都需要进行注册,然后才能使用。 使用宏 Q_DECLARE_METATYPE()声明新类型,使它们可供 QVariant 和其他基于模板的函数使用。调用 qRegisterMetaType()将类型提供给非模板函数。
    ④、使用 Q_DECLARE_METATYPE( Type )宏声明类型
     使用该宏声明类型之后,会使所有基于模板的函数都知道该类型。
     使用该宏的类需要具有 public 默认构造函数、 public 析构函数和 public 复制构造函数。
     使用该宏时,被声明的类型 Type 需要是完全定义的类型,因此,该宏通常位于类或结构的声明之后。
     对于指针类型,需要使用 Q_DECLARE_OPAQUE_POINTER(T)宏进行声明。
     对于 QVariant 类,只需使用该宏声明类型之后便可使用该类型了。
     若需要在队列中的信号和槽连接中,或 QObject 的属性系统中使用该类型,则还必须调用 qRegsiterMetaType 函数注册该类型,因为这些情况是动态运行的。
     以下类型会自动注册,不需使用此宏(全部内容详见帮助文档)
     指向从 QObject 派生的类的指针类型。
     使用 Q_ENUM 或 Q_FLAG 注册的枚举
     具有 Q_GADGET 宏的类。
     示例:
    class A{}; Q_DECLARE_METATYPE(A) //声明位于类定义之后
    namespace N{ class B{}; } Q_DECLARE_METATYPE(N::B) //类型位于名称空间中的情形
    A ma; QVariant v; v.setVaule(ma); ma.value(); //声明后 QVariant 类便可直接使用
    ④、使用 int qRegisterMetaType()函数注册类型
     使用该函数时需要使用尖括号指定 T 的类型,比如 qRegisterMetaType()
     该函数返回 QMetaType 使用的内部 ID。
     类型 T 必须使用 Q_DECLARE_METATYPE( Type )宏声明
     类型注册后,就可以在运行时运态创建和销毁该类型的对象了。
     被注册的类或结构需要具有 public 默认构造函数、 public 析构函数和 public 复制构造函数。

示例:声明与注册类型

#include<QVariant>
#include <iostream>
using namespace std;
class A{public: int i;};class B{public:int i;};
class D{public:D(int){}};//该类无 public 默认构造函数
class E{ };
//声明类型
Q_DECLARE_METATYPE(A)
Q_DECLARE_METATYPE(B)
//Q_DECLARE_METATYPE(D) //错误,类 D 没有公有的默认构造函数
//Q_DECLARE_METATYPE(F) //错误,因为父类 QObject 的复制构造函数、赋值运算符等是私有的。
int main(int argc, char *argv[])
{
	//注册类型
	qRegisterMetaType<B>();
	//qRegisterMetaType<E>(); //错误,类型 E 未使用宏 Q_DECLARE_METATYPE(T)声明
	// qRegisterMetaType<F>();
	A ma; ma.i=1;
	B mb; mb.i=2;
	//QVariant v1(ma); //错误,没有相应的构造函数。
	QVariant v;
	v.setValue(ma); //将对象 ma 存储在 v 之中
	cout<<v.value<A>().i<<endl; //输出 1。
	cout<<v.typeName()<<endl; //输出 A
	cout<<v.toString().toStdString()<<endl; //输出一个空字符,因为 ma 是一个对象,不是一个值。
	//自定义类型需要使用 userType 才能返回正确的类型 ID。
	cout<<v.typeToName(v.userType())<<endl; //输出 A
	cout<<v.typeToName(v.type())<<endl; //不一定输出 A。
	A ma1;
	ma1=v.value<A>(); //把存储在 v 之中的对象 ma 赋值给 ma1
	cout<<ma1.i<<endl; //输出 1,可见赋值成功。
	B mb1;
	//mb1=v.value<A>(); //错误,类型不相同。
	mb1=v.value<B>(); //正确, 由类型 A 转换到类型 B 失败,此时 value 会返回一个默认构造的值。
	cout<<mb1.i<<endl; //输出 0。
	return 0; 
}
  1. QVariant QObject::property(const char* name) const;
    作用:获取属性名称为 name 的值,该值以 QVariant 对象的形式返回。若属性 name 不存在,则返回的 QVariant 对象是无效的。
  2. setProperty 函数及动态属性
    bool QObject::setProperty(const char* name, const QVariant & v);
     作用:把属性 name 设置为值 v。
     若属性使用 Q_PROPERTY 进行了声明, 且值 v 与属性 name 的类型兼容, 则把值 v
    存储在属性 name 中,并返回 true,
     若值与属性的类型不兼容则属性不会更改,并返回 false。

动态属性(针对对象而不是类)

 若属性 name 未使用 Q_PROPERTY 进行声明,则把该属性和值作为新属性添加到对
象中,并返回 false,这就是动态属性。
 动态属性仍可使用 property 进行查询,还可设置一个无效的 QVariant 对象来删除动
态属性。
 动态属性是基于某个类的实例的,也就是说动态属性是添加到某个类的对象中的,
而不是添加到 QMetaObject 中的,这意味着,无法使用 QMetaObject 的成员函数获取
动态属性的信息。
 更改动态属性的值,会发送 QDynamicPropertyChangeEvent 到该对象。

示例:动态属性及使用 property 和 setProperty 存取属性值。

#ifndef M_H //要使用元对象系统,需在头文件中定义类。
#define M_H
#include<QObject>
class B{public:int i;};
class C{public:int i;};
class D{public:int i;};
Q_DECLARE_METATYPE(B)
Q_DECLARE_METATYPE(C)
class Z:public QObject{ Q_OBJECT
public: Z(){}
Q_PROPERTY(B b READ fb WRITE gb)
Q_PROPERTY(C c READ fc WRITE gc)
Q_PROPERTY(D d READ fd WRITE gd)
B fb(){return m_mb;} void gb(B x){m_mb=x;}
C fc(){return m_mc;} void gc(C x){m_mc=x;}
D fd(){return m_md;} void gd(D x){m_md=x;}
B m_mb; C m_mc; D m_md; };
#endif // M_H
//源文件的内容
#include "m.h"
#include<QVariant>
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
	//注册类型
	qRegisterMetaType<B>();
	qRegisterMetaType<C>();
	B mb; C mc; D md; Z mz;
	mb.i=2; mc.i=3; md.i=4;
	mz.gb(mb); mz.gc(mc); mz.gd(md);
	//使用 porperty 和 setProperty 存取属性值。
	//mz.property("d"); //错误,不能使用 property 函数访问属性 d,因为属性 d 的类型 D 未注册。
	mz.property("MMM"); /*这是正确的,因为属性 MMM 不存在,所以,返回的是一个空的 QVariant 对象,
	可见,属性不存在与属性的类型未注册是不同的。 */cout<<mz.fd().i<<endl; /*输出 4。虽然不能使用 property 函数访问属性 d,但仍可使用存取函数
	访问该属性的值。 */
	QVariant v; B mb1;
	mb1.i=6; v.setValue(mb1);
	//mz.setProperty("b",mb1); //错误,第二个参数的类型不匹配。
	mz.setProperty("b",v); //正确设置属性 b 的值的方法,把属性 b 的值设置为 v 中存储的值 mb1。
	mz.setProperty("c",v); /*正确,但是属性 c 的类型与 v 中存储的值的类型不兼容,因此属性 c 不
	会被更改*/
	mz.setProperty("c",7); //原因同上。
	cout<<mz.property("b").typeName()<<endl; //输出 B,输出属性 b 的类型
	cout<<mz.property("c").typeName()<<endl; //输出 C,输出属性 c 的类型
	cout<<mz.property("b").value<B>().i<<endl; //输出 6。输出的是 mb1.i 的值。
	cout<<mz.property("c").value<C>().i<<endl; //输出 3,属性 c 的值并未被更改。
	//动态属性
	mc.i=7; v.setValue(mc);
	// mz.setProperty("w",mc); //错误,第二个参数的类型不匹配。
	mz.setProperty("x",v); //动态属性,新增加属性 x,并设置其值为 v 中存储的值(即 mc)
	cout<<mz.property("x").typeName()<<endl; //输出 C,即动态属性 x 的类型。
	cout<<mz.property("x").value<C>().i<<endl; //输出 7。
	Z mz1;
	//cout<<mz1.property("x").typeName()<<endl; //错误,动态属性 x 是基于对象 mz 的。
	cout<<mz1.property("b").typeName()<<endl; //输出 B,属性 b 不是动态属性。
	return 0; 
}

使用反射机制获取属性的信息

1.QMetaProperty 类

①、作用:用于描述对象的属性,可使用该类的成员函数获取对象属性的信息。
②、 该类拥有一系列的返回 bool 值的成员函数,用于判断属性的行为(见下表)
以下函数的返回类型都为 bool
isReadable() 可读返回 true
isEnumType() 若属性的类型是枚举,则返回 true
isWritable() 可写返回 true
isFinal() 声明属性时 FINAL 是否为 true
isValid() 属性有效则返回 true。
isFlagType() 若属性的类型是标志枚举,则返回 true
isConstant() 声 明 属 性 时CONSTANT 是否为 true
isResettable() 若属性可被重置为默认值同返回 true,即声明属性时指定了 RESET 函数
bool isUser(const QObject* object=Q_NULLPTR) const 声明属性时 USER 是否为 true
bool isStored(const QObject* object=Q_NULLPTR) const 声明属性时 STORED 是否为 true
bool isScriptable(const QObject* object=Q_NULLPTR) 声明属性时 SCRIPTABLE 是否为 true
bool isDesignable(const QObject* object=Q_NULLPTR) 声明属性时 DESIGNABLE 是否为 true
③、其他成员函数如下
 const char* name() const; 获取属性的名称
 const char* typeName() const; 返回此属性类型的名称。
 QVariant::Type type() const; 返回属性的类型,其值为 QVariant::Type 的枚举值之一。
 int userType() const;返回此属性的用户类型,返回值是使用 QMetaType 注册的值之一(是 QMetaType
类中的一个枚举值),若类型未注册,则返回 QMetaType::UnknownType
 int propertyIndex() const; 返回此属性的索引。
 QMetaEnum enumerator() const
若属性的类型是枚举类型则返回该枚举,否则返回的值是未定义的。
 QVariant read(const QObject* object) const;
从给定的对象 object 读取属性的值,若能读取值则返回该值,否则返回一个无效的 QVariant 对象。
 bool write(QObject* object, const QVariant & v) const;
把值 v 作为属性值写入给定的对象 object 中,若写入成功,则返回 true,否则返回 false。若值的类型与属性的类型不相同,则尝试进行转换,若属性是可重置的,则空的 QVariant 对象(即无效的 QVariant 对象)等价于调用 reset()函数,或者以其他方式设置一个默认构造的对象。
 bool reset(QObject* object) const;
使用 RESET 函数重置给定的对象 object 的属性,若重置成功,则返回 true,否则返回 false
 bool hasNotifySignal() const;
若属性有通知信号,则返回 true,否则返回 false。
 QMetaMethod notifySignal() const;
若属性指定了通知信号,则返回该信号的 QMetaMethod 实例,否则返回无效的
QMetaMethod
 int notifySignalIndex() const; 返回属性通知信号的索引,否则返回-1。

2.QMetaObject 类中与属性有关的成员函数有

 int indexOfProperty(const char* name) const; 返回属性 name 的索引,否则返回-1。
 int propertyCount() const; 返回属性的数量(包括从父类继承的属性)
 int propertyOffset() const;
返回父类中的属性数量,也就是说此返回值是此类第一个属性的索引位置。
 QMetaProperty property(int index) const;
返回索引号为 index 的属性的元数据,若不存在这样的属性,则返回空的QMetaProperty
 QMetaProperty userProperty() const; 返回 USER 标志设置为 true 的属性的元数据。

使用 QMetaObject 成员函数存取属性值
//源文件 m.h 的内容
#ifndef M_H //要使用元对象系统,需在头文件中定义类。
#define M_H
#include<QObject>
#include <iostream>
using namespace std;
class Z:public QObject{ Q_OBJECT
public:
Q_PROPERTY(int b READ gb WRITE sb)int gb(){return m_mb;} void sb(int x){m_mb=x;}
int m_mb; };
#endif // M_H
//源文件内容
#include "m.h"
#include<QMetaProperty>
int main(int argc, char *argv[])
{
	Z mz;
	const QMetaObject *p=mz.metaObject();
	QMetaProperty pe=p->property(p->indexOfProperty("b")); //获取属性 b 的元数据。
	cout<<pe.name()<<endl; //输出属性的名称 b
	cout<<pe.typeName()<<endl; //输出属性 b 的类型 int
	pe.write(&mz,47); //把值 47 写入属性 b
	cout<<pe.read(&mz).value<int>()<<endl; //输出属性 b 的值 47。
	return 0; 
}

信号和槽

1.信号和槽是用于对象之间的通信的,这是 Qt 的核心。 为此 Qt 引入了一些关键字,他们是slots、 signals、 emit,这些都不是 C++关键字,是 Qt 特有的,这些关键字会被 Qt 的 moc转换为标准的 C++语句。
2. Qt 的部件类中有一些已经定义好了的信号和槽, 通常的作法是子类化部件类,然后添加自已的信号和槽。

信号和槽原理

  1. C++虽然是面象对象的语言,但程序的具体实现代码仍然是由函数来实现的,因此所谓的对象之间的通信, 从程序设计语言语法角度来看就是函数调用的问题,只不过是某个对象的成员函数调用另一个对象的成员函数而已,
  2. 函数调用的几种形式
    在这里插入图片描述

见上图,假设函数 f 需要 g 的处理结果,有以下几种处理方式
①、 最简单的方式就是直接调用函数 g,但这种方式有一个明显的缺点,必须知道函数 g
的名称“g”以及函数 g 的参数类型。但是若 f 只需要 g 的处理结果就可以了, 而 g
f(…){ ……
此处需要函数 g 的处理结果
…… }
g(…){……}的处理结果不一定需要函数 g 来完成,它也可以是 x、 y 或其他函数来完成, 那么这
种直接调用函数的方式就无法胜任了,因为系统不知道用户会使用哪个函数来完成这
种处理结果,也就是系统不知道调用的函数名究竟是 g、 x 或其他名称。
②、 另一种方式就是回调函数,即在函数 f 中使用一个指向函数的指针去调用需要的函数,
这样就可以调用任意名称的函数(只要函数类型与指针相同即可),此时只要是完成了
函数 g 功能的函数都可以作为函数 f 的结果被调用,这样就不会被函数名称所限制。
比如
void (*p)(int i,int j); //假设这是系统内部代码
f(…){……; p(2,3); ……} //假设 f 也是系统内部源代码函数
void g(int,int){……} //假设 g 是由程序员实现的代码
void h(int,int){……} //原理同 g
p=h; //只需对指向函数的指针 p 赋予不同的函数地址,便能实现由系统回调不同的由程序
员实现的代码,此处在函数 f 内部将调用函数 h。
③、 Qt 使用的信号和槽机制:
注意:信号和槽不是 C++标准代码,因此这些代码需要使用 Qt 的 moc 进行重新编译。
其基本思想如下:
 创建一个信号,其中创建信号需要一些规则。
 当需要调用外部函数时,发送一个信号,
 此时与该信号相关联的槽便会被调用,槽其实就是一个函数,当然要使函数成
为槽是有一定规则的。槽与信号的关联需要由程序员来完成。
 在 Qt 中,信号和槽都需要位于一个类之中。
 示例(具体实现时 Qt 引入了一些新的关键字):

信号: x //假设创建一个名为 x 的信号
槽: void g(……){...} //假设 g 和 h 都是槽,槽是由程序员编写的代码。
槽: void h(……){……}
关联: (x, g); //假设把信号 x 和槽 g 相关联,此步骤是需要由程序员来指定的。
f(){……; 发送: x;..} //信号既可由程序员发送,也可由系统发送,发送信号就相当于
是一个函数调用(此处相当于调用函数 g),当接收信号的槽(即 g)执行完其自
身的代码后,会继续沿着 x 之后的代码执行

信号和槽使用事项

1.只有 QObject 及其派生类才能使用信号和槽机制,且在类之中还需要使用 Q_OBJECT 宏。
2.信号需符合以下规则
(1) 信号使用 signals 关键字声明,在其后面有一个冒号“:”,在其前面不能有 public、private、protected 访问控制符,信号默认是 public
(2) 信号只需像函数那样声明即可, 其中可以有参数,参数的主要作用是用于和槽的通信,这就像普通函数的参数传递规则一样。信号虽然像函数, 但是对他的调用方式不一样,信号需要使用 emit 关键字发射。
(3) 信号只需声明,不能对其进行定义, 信号是由 moc 自动生成的。
(4) 信号的返回值只能是 void 类型的。3
3. 槽需符合以下规则
(1)声明槽需要使用 slots 关键字, 在其后面有一个冒号“:”,且槽需使用 public、 private、
protected 访问控制符之一。
(2)槽就是一个普通的函数,可以像使用普通函数一样进行使用,槽与普通函数的主要
区别是,槽可以与信号关联。
4. 发射信号需符合以下规则:
(1)发射信号需要使用 emit 关键字,注意,在 emit 后面不需要冒号。
(2) emit 发射的信号使用的语法与调用普通函数相同,比如有一个信号为 void f(int),则发送的语法为: emit f(3);
(3) 当信号被发射时,与其相关联的槽函数会被调用(注意:信号和槽需要使用QObject::connect 函数进行关联之后,发射信号后才会调用相关联的槽函数)。
(4) 注意:因为信号位于类之中,因此发射信号的位置需要位于该类的成员函数中或该类能见到信号的标识符的位置。
5.信号和槽的关系
(1) 槽的参数的类型需要与信号参数的类型相对应,
(2) 槽的参数不能多余信号的参数,因为若槽的参数更多,则多余的参数不能接收到信号传递过来的值,若在槽中使用了这些多余的无值的参数,就会产生错误。
(3) 若信号的参数多余槽的参数,则多余的参数将被忽略。
(4) 一个信号可以与多个槽关联,多个信号也可以与同一个槽关联,信号也可以关联到另一个信号上。
(5) 若一个信号关联到多个槽时,则发射信号时,槽函数按照关联的顺序依次执行。
(6) 若信号连接到另一个信号,则当第一个信号发射时,会立即发射第二个信号。
6. 因 Qt 在其类库中预定义了很多信号和槽,因此在 Qt 中可以 仅使用 Qt 类库中预定义的信号和槽,也可以只使 Qt 类库中预定义的信号而使用自已的槽,也可以使用 Qt 类库中预定义的槽来响应自已定义的信号,当然,槽和信号也都可以使用自定义的。
示例:创建信号和槽

//头文件 m.h 的内容
#ifndef M_H
#define M_H
#include<QObject>
#include <iostream>
using namespace std;
class A:public QObject{ //信号和槽必须继承自 QObject 类
Q_OBJECT //必须添加该宏
//public signals:void s1(int); //错误 signals 前不能有访问控制符。
signals:void s(); //使用 signals 关键字声明信号,信号的语法与声明函数相同。
signals:void s(int,int); //正确,信号可以有参数,也可以重载。
//void s2(){} //错误,信号只需声明,不能定义。
void s3(); //注意:这仍是声明的一个信号
public: //信号声明结束后,重新使用访问控制符,表示以下声明的是成员函数。
void g(){emit s3(); /*发射信号,其语法与调用普通函数相同,在信号与槽关联之前,发射的信
号不会调用相应的槽函数。 */
// emit: s3(); //错误, emit 后不能有冒号。
}};
class B:public QObject{ Q_OBJECT
public slots: //使用 slots 关键字声明槽
void x(){cout<<"X"<<endl;} /*正确,槽就是一个普通函数,只是需要使用 slots 关键字,且
能和信号相关联。 */
//slots: void x(){} //错误,声明槽时需要指定访问控制符。
public:
void g(){ // emit s3(); //错误,在类 B 中对于标识符 s3 是不可见的
} };
#endif // M_H
//源文件的内容
#include "m.h"
int main(int argc, char *argv[]){ A ma; B mb;
QObject::connect(&ma,&A::s3,&mb,&B::x); //关联信号和槽,详见后文
ma.g(); //调用对象 mb 的成员函数 x 输出 X,可见对象 ma 和 mb 之间实现了通信。
return 0; }

信号和槽的connect重载

信号和槽使用QObject类中的成员函数connect进行关联,该函数有多个重载版本

  1. 重载1:
static QMetaObject::Connection connect(
const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type = Qt::AutoConnection)
//示例:
class A:public QObject {Q_OBJECT singals: void s(int i);};
class B:public QObject{Q_OBJECT public slots: void x(int i){}};
A ma; B mb;
QObject::connect (&ma, SIGNAL( s(int) ), &mb, SLOT(x(int) );
信号的指定必须使用宏 SIGNAL()和槽必须使用宏 SLOT(),这两个宏能把括号中的
内容转换为与形参相对应的 const char*形式。在指定函数时,只能指定函数参数的类
型,不能有参数名,也不能指定函数的返回类型。比如 SLOT( x(int i)),是错误的,
因为指定了参数名 i,正确形式为 SLOT(x(int) )

各参数意义:
(1)sender:表示需要发射信号的对象。
(2)signal:表示需要发射的信号,该参数必须使用 SIGNAL()宏。
(3)receiver:表示接收信号的对象。
(4) method:表示与信号相关联的槽函数,这个参数也可以是信号,从而实现信号与信号的关联。该参数若是槽,需使用 SLOT()宏,若是信号需使用 SIGNAL 宏。 返回值的类型为 QMetaObject::Connection, 如果成功将信号连接到槽,则返回连接的句柄,否则, 连接句柄无效,可通过将句柄转换为 bool 来检查该句柄是否有效。 该返回值可用于 QObject::disconnect()函数的参数,以断开该信号和槽的关联。
(5) type:用于指明信号和槽的关联方式, 它决定了信号是立即传送到一个槽还是在稍后时间排队等待传送。 关联方式使用枚举 Qt::ConnectionType 进行描述,下表为其取值及意义
在这里插入图片描述

  1. 重载2
QMetaObject::Connection connect(const QObject *sender,
const char *signal, const char *method,
Qt::ConnectionType type = Qt::AutoConnection) const
 各参数的意义见重载 1
 注意:此函数是非静态的, 它是 QObject 的成员函数。
 此函数是重载1  的简化版本,相当于是 connect(sender, signal, this, method, type)
 示例:假设 void s(int i)是类 A 中定义的信号, void x(int i)是类 B 中定义的槽,则
A ma; B mb; mb.connect(&ma, SIGNAL(s(int)), SLOT(x(int));
以上注意调用 connect 的方式。
  1. 重载3 :
static QMetaObject::Connection connect(
const QObject *sender, PointerToMemberFunction signal,
const QObject *receiver, PointerToMemberFunction method,
Qt::ConnectionType type = Qt::AutoConnection)
 各参数的意义见重载 1
 这是 Qt5 中加入的新函数
示例:假设 void s(int i)是类 A 中定义的信号, void x(int i)是类 B 中定义的槽,则
A ma; B mb; QObject::connect(&ma, &A::s, &mb, &B::x );
注意:该函数对信号和槽函数的指定方式不是使用的宏。
 该形式的函数其实是一个模板函数,其完整原型类似如下:
template<typename PointerToMemberFunction>
static QMetaObject::Connection connect(……)
  1. 重载4:
static QMetaObject::Connection connect(const QObject *sender,
PointerToMemberFunction signal, Functor functor)
 各参数的意义见形式 1
 该函数的第三个参数支持仿函数、全局函数、静态函数、 Lambda 表达式,但是不能
是类的非静态成员函数
 示例:假设 void s(int i)是类 A 中定义的信号, void x(int i)是类 B 中定义的静态槽,
则 A ma; QObject::connect(&ma, &A::s, &B::x );
 该形式的函数其实是一个模板函数,其完整原型类似如下:
template<typename PointerToMemberFunction , typename Functor>
static QMetaObject::Connection connect(……)
  1. 重载5:
static QMetaObject :: Connection QObject :: connect(
const QObject * sender, const QMetaMethod&signal,
const QObject * receiver, const QMetaMethod& method,
Qt :: ConnectionType type = Qt :: AutoConnection)
 此函数的工作方式与形式 1 相同, 只是它使用 QMetaMethod 指定信号和槽

重载区别

(1)重载1 的 SINGAL 和 SLOT 宏实际是把该宏的参数转换为字符串,当信号和槽相关联时,使用的是字符串进行匹配,因此,信号和槽的参数类型的名字必须在字符串意义上相同,所以信号和槽无法使用兼容类型的参数,也因此也不能使用 typedef 或namespace 的类型,虽然他们的实际类型相同,但由于字符串名字不同,从而无法使用重载1。
(2)重载 3 的信号和槽函数的参数类型不需完全一致,可以进行隐式转换。重载 3 还支持typedef 和命名空间。
(3)重载3 以指针的形式指定信号和槽函数,不需再使用 SIGNAL()和 SLOT 宏。
(4)重载 3 的槽函数可以不使用 slots 关键字声明,任意的成员函数都可以是槽函数。重载1 的槽函数必须使用 slots 修饰。
(5)重载 1 的槽函数不受 private 的限制,也就是说即使槽是 private 的,仍可通过信号调用该槽函数,而重载 3 则在使用 connect 时就会发生错误。
(6)当信号或槽函数有重载的重载时,使用重载 3 可能会产生二义性错误,此时可使用函数指针的形式指定信号或槽函数,或使用重载1,比如
class A:public QObject {Q_OBJECT singals: void s(int i); };
class B:public QObject{Q_OBJECT public slots: void x(){} void x(int i){}};
A ma; B mb;QObject::connect(&ma, &A:😒, &mb, &B::x ); //二义性错误。
可使用如下方式解决(对于信号类似)
QObject::connect(&ma, &A:😒, &mb, static_cast<void (B:😗)(int)> (&B::x) );

示例:信号和槽的关联
//头文件 m.h 的内容
#ifndef M_H
#define M_H
#include<QObject>
#include <iostream>
using namespace std;
class A:public QObject{ Q_OBJECT
signals:void s(); void s(int,int); void s1(); void s2(int); };
class B:public QObject{ Q_OBJECT
public slots:
void x(){cout<<"x"<<endl;} void y(int i,int j){cout<<"y="<<i<<j<<endl;}
void z(int){cout<<"zi"<<endl;} void z1(float){cout<<"z1f"<<endl;}
public:
void z2(){ //注意,该函数未使用 slots 声明。
cout<<"z2"<<endl; }
private slots: //私有槽
void z3(){ cout<<"z3"<<endl; } };
#endif // M_H
//源文件的内容
#include "m.h"
int main(int argc, char *argv[]){ A ma; B mb;
QObject::connect(&ma,SIGNAL(s()),&mb,SLOT(x())); //形式 1
emit ma.s(); //输出 x
QObject::connect(&ma,&A::s1,&mb,&B::x); //形式 3
emit ma.s1(); //输出 x
// QObject::connect(&ma,&A::s1,&mb,&B::y); //错误,槽参数的数量多余信号参数的数量
//类型转换
//关联失败,形式 1 不支持类型转换
//QObject::connect(&ma,SIGNAL(s2(int)),&mb,SLOT(z1(float)));
typedef int T;
//关联失败,对于形式 1,类型 T 和 int 在字符串形式上并不相同。
//QObject::connect(&ma,SIGNAL(s2(T)),&mb,SLOT(z(int)));
QObject::connect(&ma,&A::s2,&mb,&B::z1); //正确,形式 3 支持隐式类型转换
emit ma.s2(2); //输出 z1f
//槽函数与 slots
//关联失败,形式 1 的槽必须使用 slots 声明。
//QObject::connect(&ma,SIGNAL(s1()),&mb,SLOT(z2()));
QObject::connect(&ma,&A::s1,&mb,&B::z2); //正确,形式 3 的槽不需使用 slots 声明。
emit ma.s1(); /*输出 x, z2,注意:在之前 s1 和 x 的关联并未断开,此时信号 s1 同时与槽 x 和
z2 关联。 */
//访问控制符
QObject::connect(&ma,SIGNAL(s1()),&mb,SLOT(z3())); //正确,形式 1 的槽不受访问控制符限制。
emit ma.s1(); //输出 x,z2,z3,因为此时 s1 与多个槽相关联。
//QObject::connect(&ma,&A::s1,&mb,&B::z3);//错误, z3 是私有的,形式 3 会受访问控制符的限制。//函数重载
return 0; }

断开信号和槽的关联

信号和槽使用 QObject 类中的成员函数 disconnect 函数断开其关联,该函数有多个重载版
本,如下所示。
2、形式 1: static bool QObject::disconnect(
const QObject *sender, const char *signal,
const QObject *receiver, const char *method)
 断开 sender 对象中的信号 singal 与 receiver 对象中槽函数 method 的关联。
 注意:此函数指定 signal 和 method 时需要使用 SIGNAL 和 SOLT 宏。
 如果连接成功断开,则返回 true;否则返回 false。 .
 当所涉及的任何一个对象被销毁时,信号槽连接被移除。
 0 可以用作通配符,分别表示“任意信号”、 “任何接收对象”或“接收对象中的任何插
槽”。
 sender 永远不会是 0。
 如果 signal 为 0,则将 receiver 对象中的槽函数 method 与所有信号断开。 否则,则只
与指定的信号断开, 此方法可断开槽与所有信号的关联,比如
类 A 中有信号 void s()和 void s1(); 类 B 中有槽 void x();
A ma; B mb;
然后把 ma 中的信号 s 和 s1 与 mb 中的槽 x 相关联,
若 signal 为 0,则 mb 中的 x 会断开与 s 和 s1 的关联。
 如果 receiver 为 0, 此时 method 也必须为 0
 如果 method 为 0,则断开与连接到 receiver 的任何连接。否则,只有命名 method 的
槽将被断开连接,而所有其他槽都将被单独保留。如果没有 receiver,则 method 必
须为 0,因此不能断开所有对象上指定的槽。 比如
类 A 中有信号 void s(); 类 B 中有槽 void x(); 类 C 中有槽 void y();
A ma; B mb, mb1; C mc;
然后把信号 s 与对象 mb 中的槽 x、对象 mb1 中的槽 x、对象 mc 中的槽 y 相关联,
若 receiver 被指定为 mb, method 为 0,则 mb 中的 x、 mb1 中的 x 会与信号 s 断开,但 mc 中的 y
不会与信号 s 断开,
若 receiver 被指定为 mb, method 为 x, 则 mb 中的 x 会与信号 s 断开, mb1 中的 x、 mc 中的 y 不会
与 s 断开。
 除此之外,还有以下几种常用的用法
disconnect(&ma, 0, 0, 0); 断开与对象 ma 中的所有信号相关联的所有槽。
disconnect(&ma , SIGNAL(s()), 0, 0);断开与对象 ma 中的信号 s 相关联的所有槽。
disconnect(&ma, 0, &mb, 0) ; 断开 ma 中的所有信号与 mb 中的所有槽的关联
3、 形式 2: static bool QObject::disconnect ( const QMetaObject::Connection &connection)该函数断开使用 connect 函数返回的信号和槽的关联,若操作失败则反回 false。
4、形式 3: static bool QObject::disconnect(
const QObject sender, PointerToMemberFunction signal,
const QObject
receiver, PointerToMemberFunction method)
 此方法与形式 1 相同,只是指定函数的方式是使用函数指针。
 注意:该函数不能断开信号连接到一般函数或 Lambda 表达式之间的关联,此时需要
使用形式 2 来断开这种关联。
5、形式 4: static bool QObject::disconnect(
const QObject sender, const QMetaMethod &signal,
const QObject
receiver, const QMetaMethod &method)
该函数与形式 1 相同,只是指定函数的方式是使用 QMetaMethod 类。
6、形式 5: bool QObject::disconnect( const char *signal = Q_NULLPTR,
const QObject *receiver = Q_NULLPTR,
const char *method = Q_NULLPTR) const
注意:该函数是非静态的,该函数是形式 1 的重载形式。
7、形式 6: bool QObject::disconnect( const QObject *receiver,
const char *method = Q_NULLPTR) const
注意:该函数是非静态的,该函数是形式 1 的重载形式。
8、注意:若关联信号和槽时使用的是函数指针形式,则在断开信号和槽时,最好使用相对应
的函数指针形式的 disconnect 版本,以避免产生无法断开关联的情形

signals、 slots、 emit 关键字原型

  1. 在以上关键字上右击然后按下 F2,在 qobjectdefs.h 文件中可以看到这些关键字的原型
  2. signals 关键字:最终被#define 置换为一个访问控制符,其简化后的语法为
    #define signals public
  3. slots 关键字:最终被#define 置换为一个空宏,即简化后的语法为: #define slots
  4. emit 关键字:同样被#define 置换为一个空宏,即简化后为: #define emit
  5. 由以上各关键字原型可见,使用 emit 发射信号,其实就是一个简单的函数调用
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值