QT元对象系统
-
-
- 01、属性系统
-
- [1.1、 属性基础](#1.1、 属性基础)
- [1.2、 QVariant类](#1.2、 QVariant类)
- [1.3、 使用QObject类存取属性值与动态属性](#1.3、 使用QObject类存取属性值与动态属性)
- [1.4、 使用反射机制获取属性信息](#1.4、 使用反射机制获取属性信息)
- 02、信号与槽
-
- [2.1、 信号与槽原理](#2.1、 信号与槽原理)
- [2.2、 创建信号与槽](#2.2、 创建信号与槽)
- [2.3、 信号与槽的连接](#2.3、 信号与槽的连接)
- [2.4、 断开信号与槽](#2.4、 断开信号与槽)
- [2.5、 关键字原型](#2.5、 关键字原型)
- 03、对象树与生命期
-
- 3.1、组合模式与对象树
- [3.2、 QObject、对象树、生命期](#3.2、 QObject、对象树、生命期)
-
本文主要对元对象系统的功能划分对应的一个表现展开描述,如有理解错误,欢迎指正!
01、属性系统
1.1、 属性基础
- 属性与数据成员相似,但是属性可使用Qt元对象系统的功能。他们的主要差别在于存取方式不相同,比如属性值通常使用读取函数(即函数名通常以get开始的函数)和设置函数(即函数名通常以set开始的函数)来存取其值,除此种方式外,Qt还有其他方式存取属性。
- 在 Qt 中属性和数据成员是两个不同的概念,他们可以相关联也可以没有联系,比如名为a 的属性,与数据成员 a,虽然他们名称相同,若他们之间没有产生关联,则数据成员 a 与属性 a 是完全不相关的,通常,一个属性都有与之相关联的数据成员,而采用的命名规则通常是加上 m_前缀,比如属性名为 a,则与之相关联的数据成员名称通常为 m_a。
- 属性值的存取方式如下:
1、可使用 QObject::property 和 QObject::setProperty 函数进行存取
2、若属性有相关联的存取函数,则可使用存取函数进行存取
3、属性还可通过元对象系统的 QMetaObject 类进行存取。
4、若属性与某个数据成员相关联,则可通过存取普通数据成员的值来间接存取属性的
值。
ps: 注意:Qt 中的类,只有属性没有数据成员,因此只能通过前面三种方式对属性值进
行修改。
- 要在类之中声明属性,该类必须继承自 QObject 类,还应使用 Q_OBJECT 宏,然后使用
QObject::Q_PROPERTY
宏声明属性,该宏语法如下:
cpp
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])
参数解释:
- 方括号中的是可选项,各选项之间使用空格隔开。
type
:指定属性的类型,可以是 QVariant 支持的类型或用户自定义类型,若是枚举类型,还需使用 Q_ENUMS 宏对枚举进行注册,若是自定义类型,需要使用Q_DECLARE_METATYPE( Type )宏进行注册name
:指定属性的名称。READ getFunction
: 1)用于指定读取函数(读取属性的值),其中READ表示读取,不可更改,getFunction用于指定函数名称。2)若没有指定MEMBER变量,则必须指定READ函数。3)通常,READ函数是const的,READ函数必须返回属性的类型或对该类型的引用。WRITE setFunction
:1)用于指定设置函数(设置属性的值),其中WRITE表示写入,不可更改,setFunction用于指定函数名称。2)此函数必须只有一个参数,且返回值类型必须为void。3)若为只读属性,则不需要指定WRITE函数。MEMBER memberName
:1)用于把指定的成员变量memberName设置为具有可读和可写性质,而无需创建READ和WRITE函数,其中MEMBER表示成员,不可更改,memberName表示类中的成员变量。2)若没有指定READ函数,则必须指定MEMBER变量。RESET resetFunction
:用于把属性重置为默认值,该函数不能有参数,且返回值必须为 void。其中 RESET 表示重置,不可更改,resetFunction 用于指定函数名称。NOTIFY notifySignal
:表示给属性关联一个信号。如果设置该项,则应指定该类中已经存在的信号,每当属性值发生变化时就会发出该信号。若使用 MEMBER 变量,则 NOTIFY 信号必须为零或一个参数,且参数必须与属性的类型相同,该参数将接受该属性的新值。REVISION
:设置版本号,默认为 0。DESIGNABLE
:用于设置属性在 GUI 设计工具的属性编辑器(例如 Qt 设计师)中是否可见,多数属性是可见的,默认值为 ture,即可见。该变量也可以指定一个返回布尔值的成员函数替代 true 或 false。SCRIPTABLE
:设置属性是否可被脚本引擎访问,默认为 trueSTORED
:设置在保存对象的状态时是否必须保存属性的值。大多数属性此值为 trueUSER
:设置属性是否为可编辑的属性,每一个类只有一个 USER 属性(默认值为false)。比如 QAbstractButton::checked 是用户可编辑的属性。CONSTANT
:表明属性的值是常量,常量属性不能有WRITE函数和NOTIFY信号。对于同一个对象实例,每一次使用常量属性的 READ 函数都必须得到相同的值,但对于类的不同实例,这个值可以不同。FINAL
:表示属性不能被派生类重写。
cpp
/*声明及使用属性示例程序:*/
// .h文件内容如下:
#ifndef M_H
#define M_H
#include <QObject>
class A : public QObject
{
Q_OBJECT
public:
// 通常Qt自带的类,属性名如果为File,那么对应的读取函数和设置函数一般都是
// getFile 、 setFile
// 声明一个类型为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)
/*
* 在存取函数中可把属性与成员变量相关联,放入如下:
* ps: 对存取函数的返回类型和参数类型及数量在本例中影响不大,后面会说
*/
int f() { return m_a; } // 属性a的读取函数
void g(int i) { m_a = i; } // 属性a的设置函数
int getc() { m_c = 3; return m_c; } // 属性也可以不与数据成员相关联,直接返回常量3
int m_a,m_b; // 属性若命名为a,则与其对应的成员变量习惯上应命名为m_a
private:
int m_c; // 成员变量通常都应声明为私有的,这样可提高程序的封装性
};
#endif
// .cpp文件如下:
#include "m.h"
#include <iostream>
#include <QDebug>
using namespace std;
int main(int argc, char* argv[])
{
A ma;
// 像普通成员变量一样存取属性值
ma.m_a = 1;
qDebug()<< "m_a = " << ma.m_a; // 输出1
//因为属性 b 没有存取函数,本例暂时只使用普通成员变量的方式存取该属性值
ma.m_b = 2;
qDebug()<< "m_b = " << ma.m_b;
ma.g(4); // 使用属性a的设置函数修改属性值
qDebug()<< "modify later m_a = " << ma.f(); // 输出4,使用读取函数输出属性a,也就是m_a的值
qDebug()<< "m_c = " << ma.getc(); // 输出 3,属性 c 是只读的,只能通过他的读取函数访问其值,因为没有设置函数,因此无法改变属性 c 的值
return 0;
}
1.2、 QVariant类
- 使用
QObject::property
函数可读取属性的值,使用QObject::setProperty
函数可以设置属性的值,但是属性有很多种类型,怎样使用property函数返回的属性值具有正确的类型?为了解决这个问题,使用了一个QVariant来描述这个类型。 - QVariant类用于封装数据成员的类型及取值等信息,该类类似于C++共用体
union
,一个QVariant对象,一次只能保存一个单一类型的值。 该类封装了Qt中常用的类型,对于QVariant不支持的类型(比如用户自定义类型),则需要使用Q_DECLARE_METATYPE(Type)
宏进行注册。 - QVariant拥有常用类型的单形参构造函数,因此可以把这些常用类型转换为QVariant类型,同事QVariant还重载了赋值运算符,因此可把常用类型的值直接赋给QVariant对象。注:由C++语法我们可以知道,单形参构造函数可以直接进行类型转换。
- 使用QVariant构造函数和赋值运算符,可见下面示例:
注意:QVariant没有char类型的构造函数,若使用char值会被转换为对应的int类型。
cpp
QVariant v(1); // 调用QVariant(int)构造函数创建一个QVariant类型的对象,并把数值1保存到v中
v = 2.2; // 调用QVariant的赋值运算符,把值为2保存在v之中,因为上面说了一个QVariant只保存一个单一类型的值
- 获取QVariant对象存储的值的类型,可使用以下参数:
Function | Descrition |
---|---|
Type type() const | 获取QVariant对象当前存储值的类型,类似以枚举QMetaType::Type的形式表示 |
const char* typeName() const | 以字符串的形式返回 QVariant 对象存储的值的类型。若是无效类型则返回 0 |
const char* typeToName(int t) | 把以枚举类型 QMetaType::Type 表示的类型以字符串的形式返回。若枚举值为QMetaType::UnknownType 或不存在,则返回一个空指针 |
一个示例:
cpp
QVariant v(1);
qDebug()<< v.typeName(); // 输出int
qDebug()<< v.typeToName(v.type()); // 输出int
-
获取和设置 QVariant 对象存储的值,可使用如下函数:
6.1、
void setValue(const T& v)
:把一个值的副本存储到 QVariant 对象中,若类型 T 是 QVariant 不支持的类型,则使用QMetaType 来存储该值,若 QMetaType 也不能处理,则发生编译错误。注:若是用户自定义类型则需要使用宏Q_DECLARE_METATYPE(...)
进行注册。6.2、
T value() const
:把存储的值转换为类型 T 并返回转换后的值,存储值本身不会被改变。若 T 是QVariant 支持的类型,则该函数与 toInt、toString 等函数功能完全相同。注:使用该函数时需要使用尖括号指定 T 的类型,比如 xx.value();6.3、
T toT()
:1)其中T是某一类型,若T是int,则该函数形式就为int toInt(),详见C++泛型编程中函数模版。2)该函数用于把存储的值转换为类型T并返回转换后的值,存储值本身不会被改变。其中比较常用的是 toString 函数,该函数可把存储的值转换为 QString 形式,这样便可以字符串的形式输出存储的值。3)注意:没有与自定义类型相对应的 toT 函数,比如 class C{};则没有 toC 函数,要把存储的值转换为自定义类型,需要使用 value 函数,且还需对自定义类型注册。 -
注意:QVariant 中的枚举类型 Type 已被废弃。
-
使用 QVariant 的默认构造函数,将创建一个无效的 QVariant 对象(或空的 QVariant 对象),可通过 isNull()成员函数进行判断。
一个示例:
cpp
#include<QVariant>
#include <iostream>
using namespace std;
class C
{
}; //自定义类型
int main(int argc, char *argv[])
{
/*QVariant 没有专门的 char 构造函数,此处的字符 a 会被转换为 int 型,因此 v中存储的是数值 97,而不是字符 a */
QVariant v('a');
cout<<v.value<int>()<<endl; // 输出 97
cout<<v.value<char>()<<endl; // 输出 a,将 97 转换为 char 型,并输出转换后的值。
cout<<v.toChar().toLatin1()<<endl; /*输出 a,原因同上,注意 toChar 返回的类型是 QChar 而不是 char */
cout<<v.toString().toStdString()<<endl; /* 输出 97,把存储在 v 中的值转换为 QString,然后以字符串形式输出 */
cout<<v.typeName()<<endl; // 输出 int,可见存储在 v 中的值的类型为 int
cout<<v.typeToName(v.type())<<endl; /*输出 int,其中 type 返回存储值的枚举形式表示的类型,而typeToName 则以字符串形式 显示该枚举值所表示的类型 */
char c='b';
v.setValue(c);
cout<<v.toString().toStdString()<<endl; // 输出 b
cout<<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;
}
1.3、 使用QObject类存取属性值与动态属性
上面一直提到一个宏:Q_DECLARE_METATYPE(),这里细说一下
-
注册自定义类型与QMetaType类
1.1、QMetaType类用于管理元对象系统中命名的类型,该类用于帮助QVariant中的类型以及队列中信号和槽的连接。它将类型名称与类型关联,以便在运行时动态创建和销毁该名称。
1.2、QMetaType::Type枚举类型定义了QMetaType支持的类型。其原型为:
enum Type { void,Bool,Int......UnknowType}
1.3、对于QVariant类和属性中使用的自定义类型,都需要进行注册,然后才能使用。使用宏
Q_DECLARE_METATYPE()
声明新类型,使它们可供QVariant和其他基于模版的函数使用。调用qRegisterMetaType()
将类型提供给非模版函数。1.4、使用
Q_DECLARE_METATYPE(Type)
宏声明类型注意事项:Index Descrition ① 使用该宏声明类型之后,会使所有基于模板的函数都知道该类型 ② 使用该宏的类需要具有 public 默认构造函数、public 析构函数和 public 复制构造函数 ③ 使用该宏时,被声明的类型 Type 需要是完全定义的类型,因此,该宏通常位于类或结构的声明之后 ④ 对于指针类型,需要使用 Q_DECLARE_OPAQUE_POINTER(T)
宏进行声明⑤ 对于 QVariant 类,只需使用该宏声明类型之后便可使用该类型了 ⑥ 若需要在队列中的信号和槽连接中,或 QObject 的属性系统中使用该类型,则还必须调用 qRegsiterMetaType
函数注册该类型,因为这些情况是动态运行的⑦ 以下类型会自动注册,不需使用此宏(全部内容详见帮助文档) 1、指向从 QObject 派生的类的指针类型 2、使用 Q_ENUM 或 Q_FLAG 注册的枚举 3、具有 Q_GADGET 宏的类 cpp// 一个简单的示例: class A {}; Q_DECLARE_METATYPE(A) // 声明位于类定义之后 namespace N { class B {}; } Q_DECLARE_METATYPE(N::B) // 类型位于命名空间的情况 A ma; QVariant v; v.setValue(ma); ma.value<A>(); // 声明后QVariant类便可直接使用
多说几句,关于
int qRegisterMetaType<T>()
函数注册的类型。1、使用该函数时需要使用尖括号指定 T 的类型,比如 qRegisterMetaType()
2、该函数返回 QMetaType 使用的内部 ID
3、类型 T 必须使用 Q_DECLARE_METATYPE( Type )宏声明
4、类型注册后,就可以在运行时运态创建和销毁该类型的对象了
5、被注册的类或结构需要具有 public 默认构造函数、public 析构函数和 public 拷贝构造函数
具体用一个例子说明:
cpp#include<QVariant> #include <iostream> using namespace std; class A { public: int i; }; class B { public: int i; }; class D { public: // 该类无默认构造函数 D(int) {} }; class E {}; // 声明类型 Q_DECLARE_METATYPE(A) Q_DECLARE_METATYPE(B) //Q_DECLARE_METATYPE(D) // 错误,类 D 没有公有的默认构造函数 //Q_DECLARE_METATYPE(E) // 错误,因为父类 QObject 的拷贝构造函数、赋值运算符等是私有的 int main(int argc, char* argv[]) { // 注册类型 qRegisterMetaType<B>(); //qRegisterMetaType<D>(); // 错误,类型D未使用宏Q_DECLARE_METATYPE(T)声明 //qRegisterMetaType<E>(); // 同上 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>(); // OK,但是由类型 A 转换到类型 B 失败,此时 value 会返回一个默认构造的值 cout << mb1.i << endl; // 输出0 return 0; }
-
QVariant QObject::property(const char* name) const
作用:获取属性名称为 name 的值,该值以 QVariant 对象的形式返回。若属性 name 不存在,则返回的 QVariant 对象是无效的
-
setProperty 函数及动态属性
cppbool QObject::setProperty(const char* name, const QVariant & v);
3.1、作用:把属性name设置为值v。
3.2、若属性使用Q_PROPERTY进行了声明,且值v与属性name的类型兼容,则把值v存储在属性name中,并返回true。
3.3、若值与属性的类型不兼容则属性不会更改,并返回false。
动态属性
- 若属性 name 未使用 Q_PROPERTY 进行声明,则把该属性和值作为新属性添加到对象中,并返回 false,这就是动态属性。
- 动态属性仍可使用 property 进行查询,还可设置一个无效的 QVariant 对象来删除动态属性。
- 动态属性是基于某个类的实例的,也就是说动态属性是添加到某个类的对象中的,而不是添加到 QMetaObject 中的,这意味着,无法使用 QMetaObject 的成员函数获取动态属性的信息。
- 更改动态属性的值,会发送 QDynamicPropertyChangeEvent 到该对象。
一个动态属性及使用property和setProperty存取属性值的示例:
cpp
// m.h:
#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
// .cpp:
#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"); // OK,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中存储的值
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.4、 使用反射机制获取属性信息
-
QMetaProperty类
①、作用: 用于描述对象的属性,可使用该类的成员函数获取对象属性的信息。
②、判断属性的行为(bool类型返回值):
Function Descrition isReadable() 可读返回true isWritable() 可写返回true isValid() 属性有效则返回true isConstant() 声明属性时CONSTANT是否为true isEnumType() 若属性的类型是枚举,则返回 true isFinal() 声明属性时 FINAL 是否为 true isFlagType() 若属性的类型是标志枚举,则返回 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 ③、其他成员函数:
Function Descrition 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 -
QMetaObject 类中与属性有关的成员函数
如下:
int indexOfProperty(const char* name) const
:返回属性 name 的索引,否则返回-1int propertyCount() const
:返回属性的数量(包括从父类继承的属性)int propertyOffset() const
:返回父类中的属性数量,也就是说此返回值是此类第一个属性的索引位置QMetaProperty property(int index) const
:返回索引号为 index 的属性的元数据,若不存在这样的属性,则返回空的QMetaPropertyQMetaProperty userProperty() const
:返回 USER 标志设置为 true 的属性的元数据
一个示例:
cpp
// .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
// .cpp:
#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;
}
02、信号与槽
- 信号和槽是用于对象之间的通信的,这是Qt的核心。 为此Qt引入了一些关键字,他们是slots、signals、emit,这些都不是C++关键字,是Qt特有的,这些关键字会被Qt的moc转换为标准的C++语句。
- Qt的部件类中有一些已经定义好了的信号和槽,通常的做法是子类化部件类,然后添加自己的信号和槽。
- 因为信号和槽与函数相似,所以通常把信号称为信号函数,槽称为槽函数。
2.1、 信号与槽原理
1、C++虽然是面向对象的语言,但程序的具体实现代码仍然是由函数来实现的,因此所谓的对象之间的通信,从程序设计语言语法角度来看
就是函数调用的问题,只不过是某个对象的成员函数调用另一个对象的成员函数而已。 信号和槽其实也就是设计模式中的观察者模式的一种
实现。
2、函数调用的几种形式,这里看图:
见上图,假设函数 f 需要 g 的处理结果,有以下几种处理方式:
①:最简单直接的方式就是直接调用函数g,但这种方式有一个缺点,那就是调用者必须知道g的名称以及函数g的参数类型,但是如果f只需要g的处理结果就可以了,而g的处理结果不一定需要函数g来完成,它也可以是x、y或其他名称的函数来完成,那么这种直接调用函数的方式就没办法达到我们想要的需求,因为系统不知道用户会调用哪个函数来完成这个处理结果,也就是系统不知道调用的函数名究竟是g、x、y还是其他名称的函数。
②:回调函数,即在函数f中使用一个指向函数的指针去调用需要的函数,这样就可以调用任意名称的函数(只要函数类型与指针相同即可),此时只要是完成了函数g功能的函数都可以作为函数f的结果被调用,这样就不会被函数名称所限制。比如:
cpp
void (*p)(int i, int j); // 假设这是系统内部代码
f(...) { // 假设f也是系统内部源代码函数
p(2,3);
....
}
// 假设g和h都是由我们程序员实现的代码
void g(int,int) { .... };
void h(int,int) { .... };
p = h; // 只需要对指向函数的指针赋予不同的函数地址,便能实现由系统回调不同的函数
③:Qt使用的信号和槽机制:
注意:信号和槽不是C++标准代码,因此这些代码需要使用Qt的moc进行重新编译。
基本思想如下:
- 创建一个信号,其中创建信号需要一些规则。
- 当需要调用外部函数时,发送一个信号。
- 此时与该信号相关联的槽便会被调用,槽其实就是一个函数,当然要使函数称为槽也是有一定规则的,槽与信号的关联需要我们程序员自己去完成。
- 在Qt中,信号和槽都需要位于一个类之中,不能独立于类外实现。
如下图所示:
2.2、 创建信号与槽
信号和槽的创建,等下会有一个例子。
- 只有QObject及其派生类才能使用信号和槽机制,且在类之中还需要使用Q_OBJECT宏。
- 信号需符合以下规则 :
①:信号使用signals
关键字声明,在其后面有一个冒号 ":",在其前面不要加public、private、protected访问控制符,信号默认是public的。
②:信号只需像函数那样声明即可(无需实现),其中可以带有参数,参数的作用就是用于和槽的通信,这也跟普通函数的参数传递规则一样。信号虽然像函数,但是对它的调用方式不一样,信号需要使用emit
关键字发送。
③:信号只需声明,不需要对其定义,信号是由moc自动去生成的。
④:信号的返回值只能是void类型的。 - 声明槽需符合以下规则 :
①:声明槽需要使用slots
关键字,在其后面有一个冒号 ":",且槽需要使用public、private、protected访问控制符之一。
②:槽就是一个普通的函数,可以像使用普通函数一样进行使用,槽与普通函数的主要区别就是,槽可以与信号关联。 - 发射信号需符合以下规则 :
①:发射信号需要使用emit
关键字,注意,在emit
后面不需要冒号。
②:emit
发射的信号使用的语法与调用普通函数相同,比如有一个信号为void f(int)
,则发送的语法为:emit f(3);
③:当信号被发射时,与其相关联的槽函数会被调用(注意:信号和槽需要使用QObject::connect函数进行绑定之后,发射信号后才会调用相关联的槽函数)。
④:注意:因为信号位于类中,因此发射信号的位置需要位于该类的成员函数中或该类能见到信号的标识符的位置。 - 信号和槽的关系(重要) :
①:槽的参数的类型需要与信号参数的类型相对应。
②:槽的参数不能多于信号的参数,因为若槽的参数更多,则多余的参数不能接收到信号传递过来的值,若在槽中使用这些多余的无值参数,就会产生错误。
③:若信号的参数多余槽的参数,则多余的参数将被忽略。
④:一个信号可以与多个槽关联,多个信号也可以与同一个槽关联,信号也可以关联到另一个信号上。
⑤:若一个信号关联到多个槽时,则发射信号时,槽函数按照关联的顺序依次执行。
⑥:若信号连接到另一个信号,则当第一个信号发射时,会立即发射第二个信号。 - 因Qt在其类库中预定义了很多信号和槽,因此在Qt中可以仅使用Qt类库中预定义的信号和槽,也可以只使Qt类库中预定义的信号而使用自己的槽,也可以使用Qt类库中预定义的槽来相应自己定义的信号,当然,自己定义信号,自己定义槽必须会。
一个示例:
cpp
// Demo.h:
#ifndef DEMO_H
#define DEMO_H
#include <QObject>
#include <iostream>
using namespace std;
class A : public QObject
{
Q_OBJECT
signals:
void s(); // 定义一个无参的信号
void s(int, int); // 可以重载
//void s2() {} // error: 只能声明不能定义
void s3();
public:
void g() {
emit s3(); // 发射信号,在关联之前,发射信号不会有响应
}
};
class B : public QObject
{
Q_OBJECT
public slots:
void x() { // OK,槽就是一个普通函数,只需要使用slots关键字,且能和信号相关联
cout << "x: " << endl;
}
public:
void g() {
//emit s3(); // error,在类B中无法看见s3信号函数
}
};
/*
* 提示:一般类与类之间通信,我们采用的是如下形式:
* 比如:Cain.h中声明了信号 c 类QDemo.h中声明了槽函数load(),然后我们在QDemo.cpp中关联他们
* 首先,QDemo.h中包含Cain.h,在QDemo类的声明前前置标识 class Cain; 为的就是在后面不会报错
* Cain* cain = new Cain();
* QObject::connect(cain,&Cain::c,this, &QDemo::load); // 第五个参数默认就行
*/
#endif
// Demo.cpp:
#include "Demo.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之间通信建立OK
return 0;
}
2.3、 信号与槽的连接
-
信号和槽使用QObject类中的成员变量函数connect进行关联,该函数有多个重载版本,如下所示:
形式①:cppstatic 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) )。
各参数意义如下:
sender
:表示需要发射信号的对象。
signal
:表示需要发射的信号,该参数必须使用SIGNAL()宏。
receiver
:表示接收信号的对象。
method
:表示与信号相关联的槽函数,这个参数也可以是信号,从而实现信号与信号的关联。该参数若是槽,需使用 SLOT()宏,若是信号需使用 SIGNAL 宏。
type
:用于指明信号和槽的关联方式,它决定了信号是立即传送到一个槽还是在稍后时间排队等待传送。关联方式使用枚举 Qt::ConnectionType 进行描述,下表为其取值及意义:枚举 值 解释 Qt::AutoConnection 0 ( 自动关联,默认值)若接收者驻留在发射信号的线程中(即信号和槽在同一线程中),则使用 Qt :: DirectConnection,否则,使用 Qt :: QueuedConnection。当信号发射时确定使用哪种关联类型 Qt::DirectConnection 1 直接关联。当信号发射后,立即调用槽。在槽执行完之后,才会执行发射信号之后的代码(即 emit 关键字之后的代码)。该槽在信号线程中执行 Qt::QueuedConnection 2 队列关联。当控制权返回到接收者线程的事件循环后,槽才会被调用,也就是说 emit 关键字后面的代码将立即执行,槽将在稍后执行,该槽在接收者的线程中执行 Qt::BlockingQueuedConnection 3 阻塞队列关联。和 Qt :: QueuedConnection 一样,只是信号线程会一直阻塞,直到槽返回。如果接收者驻留在信号线程中,则不能使用此连接,否则应用程序将会死锁 Qt::UniqueConnection 0x80 唯一关联。这是一个标志,可使用按位或与上述任何连接类型组合。当设置 Qt :: UniqueConnection 时,则只有在不重复的情况下才会进行连接,如果已经存在重复连接(即,相同的信号指向同一对象上的完全相同的槽),则连接将失败,此时将返回无效的 QMetaObject::Connection 返回值的类型为QMetaObject::Connection,如果成功将信号连接到槽,则返回连接的句柄,否则,连接句柄无效,可通过将句柄转换为bool来检查该句柄是否有效。该返回值可用于
QObject::disconnect()
函数的参数,以断开该信号和槽的关联。至于该类型不必深入研究,了解即可,后面会说。形式②:
cppQMetaObject::Connection connect(const QObject* sender, const char* signal, const char* method, Qt::ConnectionType type = Qt::AutoConnection) const; // 示例: 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; mb.connect(&ma, SIGNAL(s(int)),SLOT(x(int))); // 调用方式是mb.connect 注意这里的不同(很少用,至少我用这么久Qt基本不使用这种方式,也许适用于特定的场合, // 比如A 中的对象B 在A中建立连接)
参数意义同形式1,这里不重复强调,需要注意的是:
1、此函数是非静态的,它是QObject的成员函数
2、此函数是形式①的简化版本,相当于是
connect(sender,signal,this,method,type)
。形式③:
cppstatic QMetaObject::Connection connect(const QObject* sender, PointerToMemberFunction signal, const QObject* receiver, PointerToMemberFunction 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, &A::s, &mb, &B::x); // 这是Qt5中新加入的函数,也是我现在用的最多的一种形式。
参数意义同前面两种形式,好处是这个函数在编写就会进行类型检查。
形式④:
cppstatic QMetaObject::Connection connect(const QObject* sender, PointerToMemberFuntion signal, Functor functor); // 示例: class A : public QObject { Q_OBJECT singals: void s(int i); }; class B : public QObject { Q_OBJECT public slots: static void x(int i) {}; }; A ma; /* * void s(int i)是类 A 中定义的信号 * void x(int i)是类 B 中定义的静态槽 */ QObject::connect(&ma, &A::s, &B::x);
该函数的第三个参数支持仿函数、全局函数、静态函数、Lambda 表达式,但是唯独不能是类的非静态成员函数。
该形式的函数其实是一个模板函数,其完整原型类似如下:
cpptemplate<typename PointerToMemberFunction, typename Functor> static QMetaObject::Connection connect(...)
形式⑤:
cppstatic QMetaObject::Connection QObject::connect(const QObject* sender, const QMetaMethod& signal, const QObject* receiver, const QMetaMethod& method, Qt::connectionType type = Qt::AutoConnection); // 此函数的工作方式与形式1相同,只是它使用QMetaMethod指定的信号和槽。
-
形式③与形式①的区别:
①:形式 1 的 SINGAL 和 SLOT 宏实际是把该宏的参数转换为字符串,当信号和槽相关联时,使用的是字符串进行匹配,因此,信号和槽的参数类型的名字必须在字符串意义上相同,所以信号和槽无法使用兼容类型的参数,也因此也不能使用 typedef 或namespace 的类型,虽然他们的实际类型相同,但由于字符串名字不同,从而无法使用形式 1。
②:形式 3 的信号和槽函数的参数类型不需完全一致,可以进行隐式转换。形式 3 还支持typedef 和命名空间。
③:形式 3 以指针的形式指定信号和槽函数,不需再使用 SIGNAL()和 SLOT 宏。
④:形式 3 的槽函数可以不使用 slots 关键字声明,任意的成员函数都可以是槽函数。形式 1 的槽函数必须使用 slots 修饰。
⑤:形式 1 的槽函数不受 private 的限制,也就是说即使槽是 private 的,仍可通过信号调用该槽函数,而形式 3 则在使用 connect 时就会发生错误。
⑥:当信号或槽函数有重载的形式时,使用形式 3 可能会产生二义性错误,此时可使用函数指针的形式指定信号或槽函数,或者使用形式 1,比如:
cppclass 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::s, &mb, &B::x); // error,二义性错误 //解决方式: QObject::connect(&ma, &A::s, &mb, static_cast<void (B::&)(int)>(&B::x)); // OK,
2.4、 断开信号与槽
老实说,我接触Qt以来,还从来没有使用过断开信号与槽,只是看源码的时候看见有这个函数,然后问了一些Qt开发丰富的老师和朋友,这里直接给你们说结论吧!
断开的意义:更加灵活地控制你好的触发和槽函数的执行,提高程序的效率和管理资源的安全性。
信号和槽使用QObject类中的成员函数disconnect函数断开其关联,该函数有多个重载版本,如下所示:
-
形式①:
cppstatic 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 相关联 QObject::connect(&ma, &A::s, &mb, B::x); QObject::connect(&ma, &A::s1, &mb, B::x); // 若 signal 为 0,则 mb 中的 x 会断开与 s 和 s1 的关联 QObject::disconnect(&ma, 0, &mb, SLOT(x())); /* * 如果 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 中的所有槽的关联
-
形式②:
static bool QObject::disconnect ( const QMetaObject::Connection &connection)
该函数断开使用 connect 函数返回的信号和槽的关联,若操作失败则反回 false。 -
形式③:
cppstatic bool QObject::disconnect(const QObject *sender, PointerToMemberFunction signal, const QObject*receiver, PointerToMemberFunction method);
此方法与形式 1 相同,只是指定函数的方式是使用函数指针。
注意:该函数不能断开信号连接到一般函数或 Lambda 表达式之间的关联,此时需要
使用形式 2 来断开这种关联。 -
形式④:
cppstatic bool QObject::disconnect(const QObject *sender, const QMetaMethod &signal, const QObject*receiver, const QMetaMethod &method);
该函数与形式 1 相同,只是指定函数的方式是使用 QMetaMethod 类。
-
形式⑤:
cppbool QObject::disconnect(const char *signal = Q_NULLPTR, const QObject *receiver = Q_NULLPTR, const char *method = Q_NULLPTR) const;
注意:该函数是非静态的,该函数是形式 1 的重载形式。
-
形式⑥:
cppbool QObject::disconnect(const QObject *receiver, const char *method = Q_NULLPTR) const;
注意:该函数是非静态的,该函数是形式 1 的重载形式。
注意:若关联信号和槽时使用的是函数指针形式,则在断开信号和槽时,最好使用相对应
的函数指针形式的 disconnect 版本,以避免产生无法断开关联的情形。
2.5、 关键字原型
Key | Descrition |
---|---|
signals | 最终被#define 置换为一个访问控制符,其简化后的语法为: #define signals public |
slots | 最终被#define 置换为一个空宏,即简化后的语法为:#define slots |
emit | 同样被#define 置换为一个空宏,即简化后为:#define emit |
以上关键字选中按F2就可以跳转到qobjectdefs.h中查看原型。
通过原型可以看出来,使用关键字,比如emit,其实就是一个简单的函数调用。
03、对象树与生命期
- 为什么要使用对象树:GUI 程序通常是存在父子关系的,比如一个对话框之中含有按钮、列表等部件,按钮、列表、对话框等部件其实就是一个类的对象(注意是类的对象,而非类),很明显这些对象之间是存在父子关系的,因此一个 GUI 程序通常会由一个父对象维护着一系列的子对象列表,这样更方便对部件的管理,比如当按下 tab 键时,父对象会依据子对象列表令各子对象依次获得焦点。当关闭对话框时,父对象依据子对象列表,找到每个子对象,然后删除它们。在 Qt 中,对对象的管理,使用的是树形结构,也就是对象树。
- 子对象和父对象:本小节的父/子对象是相对于由对象组成的树形结构而言了,父节点对象被称为父对象,子节点对象被称为子对象。注意:子对象并不是指类中的对象成员。
对象树和生命期,emmm,说实话,了解即可,知道怎么回事儿,也许以后面试会被问到,翻阅了一些资料,整理了一下,简单记录一下。
3.1、组合模式与对象树
- 组合模式指的是把类的对象组织成树形结构,这种树形结构也称为对象树,Qt 使用对象树来管理 QObject 及其子类的对象。注意:这里是指的类的对象而不是类。把类组织成树形结构只需使用简单的继承机制便可实现。
- 使用组合模式的主要作用是可以通过根节点对象间接调用子节点中的虚函数,从而可以间接的对子节点对象进行操作。
- 组合模式的基本思想是使用父类类型的指针指向子类对象,并把这个指针存储在一个数组中(使用容器更方便),然后每创建一个类对象就向这个容器中添加一个指向该对象的指针。
继承的思想,C++开发者应该不会陌生,下面的例子就是这个思想:
cpp
#include <iostream>
#include <vector> // 使用STL容器vector
using namespace std;
class A { //顶级父类
public:
vector<A*> v; //存储父类类型指针,也可以使用数组,但是数组没法自动扩容
public: // 养成成员变量和成员函数明确分开来写,即使是同权限
void add(A* ma) {
v.push_back(ma);
}
};
class B : public A { // 需要继承自A
public:
void add(A* ma) {
v.push_back(ma);
}
};
class C : public A {
// 该类继承自A,但是没有add函数,所以该类的对象不能有子对象
};
int main(int argc, char* argv[])
{
A ma, ma1, ma2, ma3;
B mb, mb1, mb2, mb3;
C mc, mc1, mc2, mc3;
// 创建对象树
ma.add(&mb);
ma.add(&mb1);
mb.add(&mb3);
mb.add(&mc);
mb.add(&mc1);
mb1.add(&mc2);
mb1.add(&ma1);
mb1.add(&ma2);
}
对象树结构图大致就是如下:
一个组合模式的小示例:
cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class A {
public:
string name; // 用于存储创建的对象的名称
vector<A*> v; // 创建一个存储父类类型指针的容器
public:
A() {} // 无参构造
A(string n) { name = n; } // 有参构造
void add(A* ma) {
v.push_back(ma);
}
virtual string g() { return name; } // 虚函数,用于获取对象名
void f() { // 用于显示当前对象的容器v中存储的内容
if(!v.empty()) { // 若v不为空
cout << "name: " <<; // 输出当前对象名称
for(vector<int>::size_type i = 0; i != v.size(); ++i)
{
cout << v[i]->g() << ","; // 输出容器v中存储的对象的名称,注意g是虚函数
cout << endl;
}
}
}
virtual void pt() { // 该函数会被递归调用,用以输出整个对象树中对象的名称。
f();
for(vector<int>::size_type i = 0; i != v.size(); i++)
{
v[i]->pt(); // 注意pt是虚函数,假如v[i]类型为其子类型B时,则会调用B::pt()
}
}
};
class B : public A { // 继承自类A,代码与A类似
public:
string name;
public:
B(string n){name=n;}
void add(A* ma) {
v.push_back(ma);
}
string g(){ return name;}
void f(){
if(!v.empty()){
cout<<name<<"=";
for (vector<int>::size_type i = 0; i!=v.size(); i++) {
cout<<v[i]->g()<<",";
}
cout<<endl;
}
} // f结束
void pt(){
f();
for (vector<int>::size_type i = 0; i!=v.size(); i++) {
v[i]->pt();
}
}
}; // 类B结束
class C : public A {
public: //需要继承自类A,该类无add函数,也就是说该类的对象不能有子对象。
string name;
public:
C(string n){ name=n;}
void f(){
if(!v.empty()){
cout<<name<<"=";
for (vector<int>::size_type i = 0; i!=v.size(); i++) {
cout<<v[i]->g()<<",";
}
cout<<endl;
}
} //f结束
string g(){return name;}
void pt(){
f();
for (vector<int>::size_type i = 0; i!=v.size(); i++) {
v[i]->pt();
}
}; //类C结束
int main() {
//创建对象时传递该对象的名称以便存储。
A ma("ma"),ma1("ma1"),ma2("ma2"),ma3("ma3"),ma4("ma4");
B mb("mb"),mb1("mb1"),mb2("mb2"),mb3("mb3"),mb4("mb4");
C mc("mc"),mc1("mc1"),mc2("mc2"),mc3("mc3"),mc4("mc4");
ma.add(&mb); //ma.v[0]=&mb;
mb.add(&mb1); //mb.v[0]=&mb1;
mb.add(&mb2); //mb.v[1]=&mb2;
ma.add(&mb1); //ma.v[1]=&mb1;
mb1.add(&mb3); //mb1.v[0]=&mb3;
mb1.add(&mb4); //mb1.v[1]=&mb4;
mb2.add(&mc1); //mb2.v[0]=&mc1;
mb2.add(&mc2); //mb2.v[1]=&mc2;
mb2.add(&ma1); //mb2.v[2]=&ma1;
cout<<"各对象拥有的子对象"<<endl;
ma.f(); mb.f(); mb1.f(); mb2.f();
cout<<endl<<"整个对象中结构"<<endl;
ma.pt();
}
运行结果大致如下:
3.2、 QObject、对象树、生命期
鉴于自己也没咋理解,也写不出来什么示例,这里只普及一下理论。
-
方便理解,把由 QObject 及其子类创建的对象称为 QObject、QObject 对象或 Qt 对象。在 Qt 中,QObject 对象通常就是指的 Qt 部件。
-
QObject类是所有 Qt 对象的基类,是 Qt 对象模型的核心,所有 Qt 部件都继承自 QObject。
-
QObject 及其派生类的单形参构造函数应声明为
explicit
,以避免发生隐式类型转换。 -
QObject 类既没有复制构造函数也没有赋值操作符函数(实际上它们被声明为私有的),因此无法通过值传递的方式向函数传递一个 QObject 对象。
-
Qt 库中的 QObject 对象是以树状结构组织自已的,当创建一个 QObject 对象时,可以为其设置父对象,新创建的对象会被加入到父对象的子对象列表中(可通过
QObject::children()
函数查询),因为 Qt 的部件类,都是以 QObject 为基类,因此,Qt 的所有部件类都具有对象树的特性。 -
对象树的组织规则:
①:每一个 QObject 对象只能有一个父 QObject 对象,但可以有任意数量的子 QObject 对象。比如:
cppA ma; B mb; C mc; ma.setParent(&mb); // 将对象ma添加到mb的子对象列表中 ma.setParent(&mc); // 该语句会把 ma 从 mb 的子对象列表中移出,并将其添加到mc 的子对象列表中
②:QObject 对象会把指向各个子对象地址的指针放在 QObjectList 之中。QObjectList 是QList<QObject*>的别名,QList 是 Qt 的一个列表容器。
-
对象删除规则(注意:以下规则并非 C++语法规则,而是 Qt 的对象树规则):
①:基本规则:父对象会自动删除子对象。父对象拥有对象的所有权,在父对象被删除时会在析构函数中自动删除其子对象。
②:手动删除子对象:当手动删除子对象时,会把该子对象从父对象的列表中移除,以避免父对象被删除时该子对象被再次删除。总之 QObject 对象不会被删除两次。
③:当一个对象被删除时,会发送一个 destroyed()信号,可以捕捉该信号以避免对 QObject对象的悬垂引用。
-
对象创建规则:
①:子对象通常应创建在堆中(使用 new 创建),此时就不再需要使用 delete 将其删除了,当父对象被删除时,会自动删除该子对象。
②:对于 Qt 程序,父对象通常创建在栈上,不应创建在堆上(使用 new 创建)
③:子对象不应创建在栈中,因为若父对象比子对象更早的结束生命期(即父对象创建于子对象之后),则子对象会被删除两次,第一次发生在父对象生命期结束时,由 Qt 对象树的规则,使用父对象删除子对象,第二次发生在子对象生命期结束时,由 C++规则删除子对象。这种错误可使用先创建父对象后创建子对象的方法解决,依据 C++规则,子对象会先被删除,由 Qt 对象树规则知,此时子对象会从父对象的列表中移除,当父对象结束生命期时,就不会再次删除子对象了。
-
其他规则:应确保每一个 QObject 对象在 QApplication 之后创建,在 QApplication 销毁之前销毁,因此 QObject 对象不能是 static 存储类型的,因为 static 对象将在 main()返回之后才被销毁,其销毁时间太迟了。
-
对象的名称:可以为每个对象设置一个对象名称,其主要作用是方便对对象树进行查询和管理。对象名称和对象是不同的,比如 A ma; 其中 ma 是对象,若为 ma 设置一个名称为"SSS",则对象 ma 的对象名称为"SSS"。
-
设置父对象的方法:
①:创建对象时,在构造函数中指定父对象,QObject 类及其子类都有一个形如 QObject*parent=0 的形参的构造函数,因此我们可以在创建对象时在构造函数中直接指定父对象。
②:使用
void QObject::setParent(QObject *parent)
函数为该对象设置父对象。 -
设置对象名称:对象名称由 QObject 的 objectName 属性指定(默认值为空字符串),该属性的读取函数分别如下所示(注:对象的类名可通过
QMetaObject::className()
查询。)
cpp
QString objectName() const // 读取该对象的名称
void setObjectName(const QString& name); // 设置该对象的名称为name
- 查询对象树的信息,可使用以下 QObject 类中的成员函数:
Function | Descrition |
---|---|
QObject* parent() const |
返回一个指向父对象的指针 |
const QObjectList& children() const |
返回指向父对象中全部子对象的指针列表,新添加的子对象位于列表的最后(某些特定操作可改变该顺序,例如提升或降低 QWidget 子对象)。其中QObjectList 类型如下:typedef QList<QObject*> QObjectList; |
QList<T> findChildren ( const QString& name=QString(),Qt::FindChildOptions options=Qt::FindChildrenRecursively) const |
返回能转换为类型 T,且名称为 name 的所有子对象,若没有此对象,则返回空列表,若 name 为默认值,则会匹配所有对象的名称。该函数是按递归方式执行的 。该函数与下面介绍的 findChild 的主要用途是可以通过父对象(或父部件)获取指向子对象(或子部件)的指针。name 参数:该参数是由 QObject::setObjectName 函数设置的名称。options 参数:该参数用于指定查询子对象的方式,该参数需要指定一个FindChildOption 类型的枚举值,FindChildOptions (注意,后面多了一个 s)是由QFlags<FindChildOption> 使用 typedef 重命名后的结果。该参数可取以下两个FindChildOption 类型的枚举值:①:Qt::FindDirectChlidrenOnly :表示查找该对象的直接子对象。②:Qt::FindChildrenRecursively :表示按递归方式查询该对象的所有子对象。 |
T findChild(const QString& name=QString(),Qt::FindChildOptions options=Qt::FindChildrenRecursively) const |
该函数的作用与 findChildren 相同,但是该函数只能返回单个的子对象。 |
void dumpObjectTree() |
该函数可在调试模式(debug)下输出某个对象的整个对象树结构。该函数在发布模式(release)下不会执行任何操作。 |
大致就是这些内容,这篇文章,前前后后,写了将近一周,查了很多资料,整理了一下,毕竟不是每次都这么认真的去做功课写文章,当然要记录一下啦。
元对象系统板块,还有最后一个事件没整理了!