本文主要对元对象系统的功能划分对应的一个表现展开描述,如有理解错误,欢迎指正!
1、可使用 QObject::property 和 QObject::setProperty 函数进行存取
2、若属性有相关联的存取函数,则可使用存取函数进行存取
3、属性还可通过元对象系统的 QMetaObject 类进行存取。
4、若属性与某个数据成员相关联,则可通过存取普通数据成员的值来间接存取属性的
值。
ps: 注意:Qt 中的类,只有属性没有数据成员,因此只能通过前面三种方式对属性值进
行修改。
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])
参数解释:
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
:表示属性不能被派生类重写。/*声明及使用属性示例程序:*/
// .h文件内容如下:
#ifndef M_H
#define M_H
#include
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
#include
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;
}
QObject::property
函数可读取属性的值,使用QObject::setProperty
函数可以设置属性的值,但是属性有很多种类型,怎样使用property函数返回的属性值具有正确的类型?为了解决这个问题,使用了一个QVariant来描述这个类型。union
,一个QVariant对象,一次只能保存一个单一类型的值。 该类封装了Qt中常用的类型,对于QVariant不支持的类型(比如用户自定义类型),则需要使用Q_DECLARE_METATYPE(Type)
宏进行注册。注意:QVariant没有char类型的构造函数,若使用char值会被转换为对应的int类型。
QVariant v(1); // 调用QVariant(int)构造函数创建一个QVariant类型的对象,并把数值1保存到v中
v = 2.2; // 调用QVariant的赋值运算符,把值为2保存在v之中,因为上面说了一个QVariant只保存一个单一类型的值
Function | Descrition |
---|---|
Type type() const | 获取QVariant对象当前存储值的类型,类似以枚举QMetaType::Type的形式表示 |
const char* typeName() const | 以字符串的形式返回 QVariant 对象存储的值的类型。若是无效类型则返回 0 |
const char* typeToName(int t) | 把以枚举类型 QMetaType::Type 表示的类型以字符串的形式返回。若枚举值为QMetaType::UnknownType 或不存在,则返回一个空指针 |
一个示例:
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()成员函数进行判断。
一个示例:
#include
#include
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;
}
上面一直提到一个宏: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 宏的类 |
// 一个简单的示例:
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
函数注册的类型。()
1、使用该函数时需要使用尖括号指定 T 的类型,比如 qRegisterMetaType()
2、该函数返回 QMetaType 使用的内部 ID
3、类型 T 必须使用 Q_DECLARE_METATYPE( Type )宏声明
4、类型注册后,就可以在运行时运态创建和销毁该类型的对象了
5、被注册的类或结构需要具有 public 默认构造函数、public 析构函数和 public 拷贝构造函数
具体用一个例子说明:
#include
#include
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未使用宏Q_DECLARE_METATYPE(T)声明
//qRegisterMetaType(); // 同上
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(); // 错误,类型不相同
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 函数及动态属性
bool QObject::setProperty(const char* name, const QVariant & v);
3.1、作用:把属性name设置为值v。
3.2、若属性使用Q_PROPERTY进行了声明,且值v与属性name的类型兼容,则把值v存储在属性name中,并返回true。
3.3、若值与属性的类型不兼容则属性不会更改,并返回false。
动态属性
一个动态属性及使用property和setProperty存取属性值的示例:
// m.h:
#ifndef M_H //要使用元对象系统,需在头文件中定义类。
#define M_H
#include
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
#include
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<
cout<<mz1.property("b").typeName()<<endl; // 输出 B,属性 b 不是动态属性
return 0;
}
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 的属性的元数据一个示例:
// .h:
#ifndef M_H //要使用元对象系统,需在头文件中定义类。
#define M_H
#include
#include
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
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、C++虽然是面向对象的语言,但程序的具体实现代码仍然是由函数来实现的,因此所谓的对象之间的通信,从程序设计语言语法角度来看
就是函数调用的问题,只不过是某个对象的成员函数调用另一个对象的成员函数而已。 信号和槽其实也就是设计模式中的观察者模式的一种
实现。
2、函数调用的几种形式,这里看图:
见上图,假设函数 f 需要 g 的处理结果,有以下几种处理方式:
①:最简单直接的方式就是直接调用函数g,但这种方式有一个缺点,那就是调用者必须知道g的名称以及函数g的参数类型,但是如果f只需要g的处理结果就可以了,而g的处理结果不一定需要函数g来完成,它也可以是x、y或其他名称的函数来完成,那么这种直接调用函数的方式就没办法达到我们想要的需求,因为系统不知道用户会调用哪个函数来完成这个处理结果,也就是系统不知道调用的函数名究竟是g、x、y还是其他名称的函数。
②:回调函数,即在函数f中使用一个指向函数的指针去调用需要的函数,这样就可以调用任意名称的函数(只要函数类型与指针相同即可),此时只要是完成了函数g功能的函数都可以作为函数f的结果被调用,这样就不会被函数名称所限制。比如:
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进行重新编译。
基本思想如下:
信号和槽的创建,等下会有一个例子。
signals
关键字声明,在其后面有一个冒号 “:”,在其前面不要加public、private、protected访问控制符,信号默认是public的。emit
关键字发送。slots
关键字,在其后面有一个冒号 “:”,且槽需要使用public、private、protected访问控制符之一。emit
关键字,注意,在emit
后面不需要冒号。emit
发射的信号使用的语法与调用普通函数相同,比如有一个信号为void f(int)
,则发送的语法为: emit f(3);
一个示例:
// Demo.h:
#ifndef DEMO_H
#define DEMO_H
#include
#include
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;
}
信号和槽使用QObject类中的成员变量函数connect进行关联,该函数有多个重载版本,如下所示:
形式①:
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) )。
各参数意义如下:
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()
函数的参数,以断开该信号和槽的关联。至于该类型不必深入研究,了解即可,后面会说。
形式②:
QMetaObject::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)
。
形式③:
static 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中新加入的函数,也是我现在用的最多的一种形式。
参数意义同前面两种形式,好处是这个函数在编写就会进行类型检查。
形式④:
static 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 表达式,但是唯独不能是类的非静态成员函数。
该形式的函数其实是一个模板函数,其完整原型类似如下:
template<typename PointerToMemberFunction, typename Functor>
static QMetaObject::Connection connect(...)
形式⑤:
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 的 SINGAL 和 SLOT 宏实际是把该宏的参数转换为字符串,当信号和槽相关联时,使用的是字符串进行匹配,因此,信号和槽的参数类型的名字必须在字符串意义上相同,所以信号和槽无法使用兼容类型的参数,也因此也不能使用 typedef 或namespace 的类型,虽然他们的实际类型相同,但由于字符串名字不同,从而无法使用形式 1。
②:形式 3 的信号和槽函数的参数类型不需完全一致,可以进行隐式转换。形式 3 还支持typedef 和命名空间。
③:形式 3 以指针的形式指定信号和槽函数,不需再使用 SIGNAL()和 SLOT 宏。
④:形式 3 的槽函数可以不使用 slots 关键字声明,任意的成员函数都可以是槽函数。形式 1 的槽函数必须使用 slots 修饰。
⑤:形式 1 的槽函数不受 private 的限制,也就是说即使槽是 private 的,仍可通过信号调用该槽函数,而形式 3 则在使用 connect 时就会发生错误。
⑥:当信号或槽函数有重载的形式时,使用形式 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::s, &mb, &B::x); // error,二义性错误
//解决方式:
QObject::connect(&ma, &A::s, &mb, static_cast<void (B::&)(int)>(&B::x)); // OK,
老实说,我接触Qt以来,还从来没有使用过断开信号与槽,只是看源码的时候看见有这个函数,然后问了一些Qt开发丰富的老师和朋友,这里直接给你们说结论吧!
断开的意义:更加灵活地控制你好的触发和槽函数的执行,提高程序的效率和管理资源的安全性。
信号和槽使用QObject类中的成员函数disconnect函数断开其关联,该函数有多个重载版本,如下所示:
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 相关联
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)
static bool QObject::disconnect(const QObject *sender,
PointerToMemberFunction signal,
const QObject*receiver,
PointerToMemberFunction method);
此方法与形式 1 相同,只是指定函数的方式是使用函数指针。static bool QObject::disconnect(const QObject *sender,
const QMetaMethod &signal,
const QObject*receiver,
const QMetaMethod &method);
该函数与形式 1 相同,只是指定函数的方式是使用 QMetaMethod 类。bool QObject::disconnect(const char *signal = Q_NULLPTR,
const QObject *receiver = Q_NULLPTR,
const char *method = Q_NULLPTR) const;
注意:该函数是非静态的,该函数是形式 1 的重载形式。bool QObject::disconnect(const QObject *receiver,
const char *method = Q_NULLPTR) const;
注意:该函数是非静态的,该函数是形式 1 的重载形式。注意:若关联信号和槽时使用的是函数指针形式,则在断开信号和槽时,最好使用相对应
的函数指针形式的 disconnect 版本,以避免产生无法断开关联的情形。
Key | Descrition |
---|---|
signals | 最终被#define 置换为一个访问控制符,其简化后的语法为: #define signals public |
slots | 最终被#define 置换为一个空宏,即简化后的语法为:#define slots |
emit | 同样被#define 置换为一个空宏,即简化后为:#define emit |
以上关键字选中按F2就可以跳转到qobjectdefs.h中查看原型。
通过原型可以看出来,使用关键字,比如emit,其实就是一个简单的函数调用。
对象树和生命期,emmm,说实话,了解即可,知道怎么回事儿,也许以后面试会被问到,翻阅了一些资料,整理了一下,简单记录一下。
继承的思想,C++开发者应该不会陌生,下面的例子就是这个思想:
#include
#include // 使用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);
}
一个组合模式的小示例:
#include
#include
#include
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();
}
鉴于自己也没咋理解,也写不出来什么示例,这里只普及一下理论。
方便理解,把由 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 对象。比如:
A ma; B mb; C mc;
ma.setParent(&mb); // 将对象ma添加到mb的子对象列表中
ma.setParent(&mc); // 该语句会把 ma 从 mb 的子对象列表中移出,并将其添加到mc 的子对象列表中
②:QObject 对象会把指向各个子对象地址的指针放在 QObjectList 之中。QObjectList 是QList
对象删除规则(注意:以下规则并非 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()
查询。)
QString objectName() const // 读取该对象的名称
void setObjectName(const QString& name); // 设置该对象的名称为name
Function | Descrition |
---|---|
QObject* parent() const |
返回一个指向父对象的指针 |
const QObjectList& children() const |
返回指向父对象中全部子对象的指针列表,新添加的子对象位于列表的最后(某些特定操作可改变该顺序,例如提升或降低 QWidget 子对象)。其中QObjectList 类型如下:typedef QList |
QList |
返回能转换为类型 T,且名称为 name 的所有子对象,若没有此对象,则返回空列表,若 name 为默认值,则会匹配所有对象的名称。该函数是按递归方式执行的 。该函数与下面介绍的 findChild 的主要用途是可以通过父对象(或父部件)获取指向子对象(或子部件)的指针。name 参数:该参数是由 QObject::setObjectName 函数设置的名称。options 参数:该参数用于指定查询子对象的方式,该参数需要指定一个FindChildOption 类型的枚举值,FindChildOptions (注意,后面多了一个 s)是由QFlags 使用 typedef 重命名后的结果。该参数可取以下两个FindChildOption 类型的枚举值:①:Qt::FindDirectChlidrenOnly :表示查找该对象的直接子对象。②:Qt::FindChildrenRecursively :表示按递归方式查询该对象的所有子对象。 |
T findChild(const QString& name=QString(),Qt::FindChildOptions options=Qt::FindChildrenRecursively) const |
该函数的作用与 findChildren 相同,但是该函数只能返回单个的子对象。 |
void dumpObjectTree() |
该函数可在调试模式(debug)下输出某个对象的整个对象树结构。该函数在发布模式(release)下不会执行任何操作。 |
大致就是这些内容,这篇文章,前前后后,写了将近一周,查了很多资料,整理了一下,毕竟不是每次都这么认真的去做功课写文章,当然要记录一下啦。
元对象系统板块,还有最后一个事件没整理了!