Qt真正的核心,元对象系统、属性系统、对象模型、对象树、信号槽
对象模型
1、一种非常强大的无缝对象通信机制,称为信号槽。
2、可查询和可设计的对象属性。
3、强大的事件Event和事件过滤器。
4、用于国际化的上下文字符串翻译。
5、高精度且先进的定时器QTimer,使得在事件驱动GUI中更优雅的集成更多任务成为可能。
6、以自然的方式组织对象所有权的分层和可查询的对象树,几乎所有的类都继承于QObject,且可通过QObject::inherits()查询某对象是否是一个类的实例,该类继承了QObject继承树中指定的类。
7、当被引用的对象被销毁时,保护指针(QPointer)会自动设置为0,不像普通的c++指针,当它们的对象被销毁时就会变成悬空指针。
8、提供qobject_cast实现跨库边界工作的动态类型转换。
9、提供Q_DECLARE_METATYPE实现自定义数据类型。
Qt的许多特性都是基于对QObject的继承,用标准的c++技术实现的。其他的,比如对象通信(信号槽)机制和动态属性系统,需要Qt的元对象编译器(moc)提供的元对象系统。
Qt元对象系统(QMetaObject)
Qt 的元对象系统叫 Mate-Object-System,提供了对象之间通信的信号与槽机制、运行时类型信息和动态属性系统。即使编译器不支持RTTI(RTTI的实现耗费了很大的时间和存储空间,这就会降低程序的性能),我们也能动态获取类型信息。
但是,元对象是基于三个条件的:
1、该类必须继承自QObject类
2、必须在类的私有声明区声明Q_OBJECT宏(在类定义的时候,如果没有指定public,
则默认为private,用来启用元对象功能,比如动态属性、信号和槽)。
3、 元对象编译器Meta-Object Compiler(moc)为 QObject的子类实现元对象
特性提供必要的代码。
有了元对象系统后,我们就可以使用Qt的信号和槽了。
moc(Meta-Object Compiler)元对象预编译器。
moc读取一个c++头文件。如果它找到包含Q_OBJECT宏的一个或多个类声明,它会生成一个包含这些类的元对象代码的c++源文件,并且以moc_作为前缀。
信号和槽机制、运行时类型信息和动态属性系统需要元对象代码。
由moc生成的c++源文件必须编译并与类的实现联系起来。通常,moc不是手工调用的,而是由构建系统自动调用的。
获取类关联的元对象的函数是:metaObject
QMetaObject *mtobj = QObject::metaObject()
QMetaObject 的className函数可在运行时返回类的名称字符串。
QObject *btn = new QPushButton();
QString str = btn->metaObject()->className();
qDebug() << str; // "QPushButton"
属性系统
QObject的 setProperty和property 用于通过属性名称动态设置和获取属性值。其实主要实现c++和qml交互。
由于元对象系统的特点,这就保证了Qt属性系统是独立于编译器和平台的。不仅如此,我们还可以使用Q_PROPERTY()宏来定义编译期的静态属性。
作用就是:当一个类的成员变量或者成员函数用属性系统处理一下,它们就从C++内部中暴露出来,而且大家都认得。
Q_PROPERTY宏用来定义可通过元对象系统访问的属性,通过它定义的属性,可以在QML中访问、修改,也可以在属性变化时发射特定的信号。
Q_PROPERTY()宏定义一个返回值类型为type,名称为name的属性,用READ、WRITE关键字定义属性的读取、写入函数,还有其他的一些关键字定义属性的一些操作特性。属性的类型可以是QVarient支持的任何类型,也可以用户自定义类型。
READ:用于读取属性值,如果未指定成员变量(通过MEMBER ),则需要读取访问器函数。
WRITE:写访问器函数是可选的。用于设置属性值。它必须返回void,并且必须只接受一个参数,要么是属性的类型,要么是指向该类型的指针或引用。
MEMBER:如果未指定读取访问器函数,则需要成员变量关联。这使得给定的成员变量可读写,而无需创建读写访问器函数。如果需要控制变量访问,除了成员变量关联(但不是两者)之外,还可以使用读或写访问器函数。
RESET:复位功能是可选的。它用于将属性设置回其特定于上下文的默认值。
NOTIFY:通知信号是可选的。如果已定义,它应该指定该类中的一个现有信号,该信号在属性值更改时发出。成员变量的通知信号必须采用零个或一个参数,这些参数必须与属性的类型相同。参数将采用属性的新值。仅当属性确实发生更改时才应发出NOTIFY信号,以避免绑定在QML中被不必要地重新计算。
头文件:
#ifndef COLORMAKER_H
#define COLORMAKER_H
#include <QObject>
class ColorMaker : public QObject
{
Q_OBJECT
public:
explicit ColorMaker(QObject *parent = nullptr);
~ColorMaker();
Q_PROPERTY(int value READ getvalue WRITE setvalue NOTIFY valueChanged);
int getvalue() const;
void setvalue(const int& value);
signals:
void valueChanged(int value);
private:
int m_value;
};
#endif // COLORMAKER_H
源文件
#include "ColorMaker.h"
ColorMaker::ColorMaker(QObject *parent)
{
}
ColorMaker::~ColorMaker()
{
}
int ColorMaker::getvalue() const
{
return m_value;
}
void ColorMaker::setvalue(const int &value)
{
m_value = value;
emit valueChanged(m_value);
}
#include "widget.h"
#include "ui_widget.h"
#include "ColorMaker.h"
#include <QDebug>
#include <QMetaProperty>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
ColorMaker *cm = new ColorMaker();
connect(cm,&ColorMaker::valueChanged,this,&Widget::recv);
cm->setvalue(1);
int value = cm->getvalue();
cm->setProperty("value", 2);
value = cm->property("value").toInt();
QObject *object = cm;
const QMetaObject *metaobject = object->metaObject();
int ncount = metaobject->propertyCount();
for (int i = 0; i < ncount; ++i)
{
QMetaProperty metaproperty = metaobject->property(i);
const char *name = metaproperty.name();
qDebug() << name;
QVariant value = object->property(name);
qDebug() << value;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::recv(int nval)
{
int a = nval;
qDebug() << a;
}
使用QObject的property()方法访问属性,如果该属性定义了WRITE方法,还可以使用setProperty()修改属性。
bool setProperty(const char *name, const QVariant &value);
QVariant property(const char *name) const;
QPushButton *button = new QPushButton;
QObject *object = button;
button->setDown(true);
object->setProperty("down", true);
QObject *object = ...
const QMetaObject *metaobject = object->metaObject();
int count = metaobject->propertyCount();
for (int i = 0; i < count; ++i)
{
QMetaProperty metaproperty = metaobject->property(i);
const char *name = metaproperty.name();
QVariant value = object->property(name);
...
}
Q_PROPERTY用于c++类注册到qml交互上。在c++的变化发送信号,而在qml上接收信号,实现处理槽函数。
这个使用于qml导出到c++类,c++类获取和设置qml的属性。
QObject* root = NULL;
QList<QObject*> rootObjects = engine.rootObjects();
int count = rootObjects.size();
for(int i=0; i<count; ++i) {
if(rootObjects.at(i)->objectName() == "rootObject") {
root = rootObjects.at(i);
break;
}
}
QObject* startButton = root->findChild<QObject*>("startButton");
startButton->setProperty("text","start");
关于通过属性实现c++和qml通讯关联的介绍参考:qt qml与c++_小飞侠hello的博客-CSDN博客
对象树
QObject类中存在一个私有变量QList<QObject *>用来存储这个类的子类们,当给一个对象指定一个父对象时,QList会将自己加入到父对象的children()列表中,也就是加入到QList<QObject *>变量中。
使用对象树的意义:
在父对象调用完毕被调用析构函数的时候,该父对象的子对象们也会被析构,析构顺序和构造顺序相反:
构造顺序:父对象->子对象
析构顺序:子对象->父对象
这样的机制在GUI编程中非常有用,可以减少代码冗余,而不用去一个一个从堆区把它们delete掉。
1.每个继承自QObject类的对象通过它的对象链表(QObjectList)来管理子类对象,当用户创建一个子对象时,其对象链表相应更新子类对象信息,对象链表可通过children()获取。
2.当父对象析构的时候,其对象链表中的所有(子)对象也会被析构,父对象会自动将其从父对象列表中删除。Qt 保证没有对象会被 delete 两次。开发中手动回收资源时建议使用deleteLater代替delete,因deleteLater多次是安全的,而delete多次是不安全的。
对象树所带来的问题:
构造函数在创建对象时被调用,也就是说如果子对象优先于父对象被创建(调用构造函数),在析构的时候,对象树会进行两次delete操作,这时候程序会报错。因为在栈里是先进后出,即先调用构造函数,会后调用析构函数。
延伸:如果在构造时设置父对象为 NULL,那么当前实例不会有父对象存在,Qt 也不会自动析构该实例,除非实例超出作用域导致析构函数被调用,或者用户在恰当时机使用 delete 操作符或者使用 deleteLater 方法。
qobject_cast
T qobject_cast ( QObject * object )
本方法返回object向下的转型T,如果转型不成功则返回0,如果传入的object本身就是0则返回0。
在使用时有两个限制:
1# T类型必须继承自QObject。
2# 在声明时必须有Q_OBJECT宏。
QMetaObject::invokeMethod()
可以使用QMetaObject::invokeMethod()调用QObject的某个注册到元对象系统中的方法。无论这个方法是公有的、保护的还是私有的
bool QMetaObjcet:invokeMethod(
QObject* obj,
const char* member,
Qt::ConnectionType type,
QGenericReturnArgument ret,
QGenericReturnArgument vla0 = QGenericReturnArgument(0),
QGenericReturnArgument vla1 = QGenericReturnArgument(),
QGenericReturnArgument vla2 = QGenericReturnArgument(),
QGenericReturnArgument vla3 = QGenericReturnArgument(),
QGenericReturnArgument vla4 = QGenericReturnArgument(),
QGenericReturnArgument vla5 = QGenericReturnArgument(),
QGenericReturnArgument vla6 = QGenericReturnArgument(),
QGenericReturnArgument vla7 = QGenericReturnArgument(),
QGenericReturnArgument vla8 = QGenericReturnArgument(),
QGenericReturnArgument vla9 = QGenericReturnArgument());
返回值:返回true说明调用成功;返回false,要么是因为没有你说的那个方法,要么是参数类型不匹配;
obj:被调用对象的指针;
member:方法名字 必须是信号、槽,以及Qt元对象系统能识别的类型, 如果不是信号和槽,可以使用qRegisterMetaType()来注册数据类型。此外,使用Q_INVOKABLE来声明函数,也可以正确调用。
type:连接类型;invokeMethod为信号槽而生,你可以指定连接类型,如果被调用的对象和发起调用的线程是同一线程,那么可以使用Qt::DirectConnection、Qt::AutoConnection、Qt::QueuedConnection,如果被调用对象在另一个线程,那么建议使用Qt::QueuedConnection;
ret:接收返回值;
然后就是多达10个可以传递给被调用方法的参数;(看来信号槽的参数个数是有限制的,最好不要超过10个)
QGenericArgument和QGenericReturnArgument是内部帮助程序类,由于可以动态调用信号和槽,因此必须使用Q_ARG()和Q_RETURN_ARG()宏来封装参数
注意:此功能是线程安全的。
#ifndef COLORMAKER_H
#define COLORMAKER_H
#include <QObject>
class CTest
{
public:
CTest(){}
int nAge;
QString strName;
};
Q_DECLARE_METATYPE(CTest)
class ColorMaker : public QObject
{
Q_OBJECT
public:
explicit ColorMaker(QObject *parent = nullptr);
~ColorMaker();
Q_PROPERTY(int value READ getvalue WRITE setvalue NOTIFY valueChanged);
Q_PROPERTY(CTest test READ gettest WRITE settest NOTIFY testChanged);//使用自定义的类
int getvalue() const;
void setvalue(const int& value);
CTest gettest() const;
void settest(const CTest& test);
signals:
void valueChanged(int value);
void testChanged(CTest test);
private slots:
QString testslot(int n);
private:
int m_value;
CTest m_test;
};
#endif // COLORMAKER_H
QString ColorMaker::testslot(int n)
{
return "q";
}
调用:
ColorMaker *cm = new ColorMaker();
QString retVal;
QMetaObject::invokeMethod(cm,"testslot", Qt::DirectConnection,
Q_RETURN_ARG(QString, retVal),
Q_ARG(int,1));
_ENUMS宏
要导出的类定义了想在QML使用的枚举类型,可以使用Q_ENUMS宏将该枚举类型注册到元对象系统中。
Q_INVOKABLE宏
定义一个类的成员函数时使用Q_INVOKABLE宏来修饰,就可以让该方法被元对象系统调用。
这个宏必须放在返回类型前面。
延伸:普通类成员函数是不能直接在qml使用。除非是声明为槽函数或者用Q_INVOKABLE声明函数。
Q_INVOKABLE void setAlgorithm(GenerateAlgorithm algorithm);
public slots:
void setcolor();
所以:Q_INVOKABLE的作用有:
1.c++和qml混用。在c++类中用Q_INVOKABLE声明函数。这样可以在qml直接调用。
2.在跨线程编程中的使用。需要QMetaObject::invokeMethod()结合。将方法声明为Q_INVOKABLE,并且在另一线程中用invokeMethod唤起。
延伸:只有信号、槽以及被标记成Q_INVOKABLE的方法才能够被其它线程所触发调用。如果你不想通过跨线程的信号、槽这一方法来实现调用驻足在其他线程里的QObject方法。另一选择就是将方法声明为Q_INVOKABLE,并且在另一线程中用invokeMethod唤起。
信号槽
前提:如果没有消息循环,那么Qt的信号和槽无法完全使用。
信号:
signals:
void startGetDataThread();
void sendPointer(MainWindow*);
1.声明一个信号要使用signals关键字。
2.在signals前面不能使用public、private和protected等限定符,因为只有定义该信号的类及其子类才可以发射该信号。(个人理解为使用emit函数)
3.信号只用声明,不需要也不能对它进行定义实现。
4.信号没有返回值,只能是void类型的。
5.只有QObject类及其子类派生的类才能使用信号和槽机制,使用信号和槽,还必须在类声明的最开始处添加Q_OBJECT宏。
6.使用emit 强行发射信号
emit sendPointer(this);
Qt之所以使用# define emit,是因为编译器并不认识emit啊,所以把它定义成一个空的宏就可以通过编译。
7. 信号是一个函数, 类的成员函数。所以可以是虚函数的重写。
即在基类定义一个纯虚函数,在子类的重写该虚函数,并且声明为信号。
8.信号可以支持重载
在同一个线程中,当一个信号被emit发出时,会立即执行其槽函数,等槽函数执行完毕后,才会执行emit后面的代码。如果一个信号链接了多个槽,那么会等所有的槽函数执行完毕后才执行后面的代码,槽函数的执行顺序是按照它们链接时的顺序执行的 。
如果信号和槽不是在同一线程,默认情况下,是异步执行,不会阻塞。
怎么获取信号发送者
当多个信号连接一个槽时,有时需要判断是哪个对象发来的,那么可以调用sender()函数获取对象指针,返回为QObject指针。
QObject* sender() ;
槽
1.声明一个槽需要使用slots关键字。
2.一个槽可以是private、public或者protected类型的,
3.槽也可以被声明为虚函数,静态函数、全局函数,这与普通的成员函数是一样的,也可以像调用一个普通函数一样来调用槽。本质就是回调函数。
4.发送者和接受者都需要是QObject的子类(当然,槽函数是全局函数,Lambda表达式等无需接收者的时候除外
public slots:
void handleTimeout();
void GetDataFromRTDB();
void getPointer(MainWindow *pMainWindow);
connect()函数
connect()函数原型如下:
bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection )
有两种方式表达:
1.
MyDialog *dlg = new MyDialog(this);
connect(dlg,SIGNAL(dlgReturn(int)),this,SLOT(showValue(int)));
connect(this,SIGNAL(sendPointer(MainWindow*)),m_pGetDataThreadObj,SLOT(getPointer(MainWindow*)));
2.
connect(this,&MainWindow::startGetDataThread,m_pGetDataThreadObj,&DataThreadObject::GetDataFromRTDB); connect(m_pGetDataThread,&QThread::finished,m_pGetDataThread,&QObject::deleteLater);
解绑定信号槽
不需要信号槽连接时,可使用disconnect()进行解绑定。其写法和connect一样,只需要将connect换成disconnect即可。
信号槽的特点
1、一个信号可以和多个槽相连
2、多个信号可以连接到一个槽
3、一个信号可以连接到另外的一个信号
4. 信号的参数类型可以与槽的参数类型对应,信号的参数可以比槽的参数多,但不可以少, 否则连接将失败
5. 槽可以被取消连接;这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。
6.connect的第5个参数
第5个参数一般不填,为默认值。
1、Qt::AutoConnection: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
2、Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数和信号发送者在同一线程。效果看上去就像是直接在信号发送位置调用了槽函数,效果上看起来像函数调用,同步执行。
emit语句后面的代码将在与信号关联的所有槽函数执行完毕后才被执行。
3、Qt::QueuedConnection:信号发出后,信号会暂时被放到一个消息队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,然后执行和信号关联的槽函数,这种方式既可以在同一线程内传递消息也可以跨线程操作。
emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕
4、Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。而且接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5、Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是为了避免重复连接。
6.信号和槽的参数只能有类型,不能有变量,例如写成SLOT(showValue(int value))是不对的。
7.对于信号和槽的参数问题,基本原则是信号中的参数类型要和槽中的参数类型相对应,而且信号中的参数可以多于槽中的参数,但是不能反过来,如果信号中有多余的参数,那么它们将被忽略。
8.在槽函数中获取发射信号的对象 函数为:
QObject *sender() const;
9.信号和槽也不能携带模板类参数
如果将信号、槽声明为模板类参数的话,即使moc工具不报告错误,也不可能得到预期的结果,也可以取巧,用typedef
typedef pair IntPair;
public slots:
void setLocation (IntPair location);
10.嵌套的类不能位于信号或槽区域内,也不能有信号或槽。即类b嵌套在类a内,想在类b中声明信号与槽是不行的。
11.友元声明不能位于信号或槽声明区内。相反,他们应该在普通C++的private、protected或public区内进行声明
信号槽机制的优缺点
优点:
Qt信号与槽机制降低了Qt对象的耦合度。
观察者模式,激发信号的Qt对象无须知道是哪个对象的哪个槽函数需要接收它发出的信号,它只需要做的是在适当的时间发送适当的信号就可以了,而不需要知道也不关心它的信号有没有被接收到,更不需要知道哪个对象的哪个槽接收了信号。
缺点:
信号槽机制,同回调函数相比,信号和槽机制运行速度有些慢。遍历,通过传递一个信号来调用槽函数将会比直接调用非虚函数运行速度慢10倍。原因如下:- 需要定位接收信号的对象;- 安全地遍历所有的关联(一个信号关联多个槽的情况);- 编组/解组传递的参数;- 多线程的时候,信号可能需要排队等待。
在没有信号槽机制的时代,C++对象间的交互一般使用回调函数来实现。使用某对象时,用指针指向另一个对象的函数,这个函数就称为回调函数。使用回调函数有个弊端,当某个对象被多个对象通信时,需要一个容器来存放多个对象的回调函数???。维护这个容器使得代码编写效率低、扩展性弱。
qt信号槽的本质就是回调函数。???
延伸:
1.信号槽类似观察者模式,延伸到模型视图也是观察者模式。
观察者模式是一种一对多,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者关联目标,目标聚合观察者。这样就会有双向关联了。
信号与槽的具体流程
moc查找头文件中的signals,slots,标记出信号和槽。
将信号槽信息存储到类静态变量staticMetaObject中,并且按声明顺序进行存放,建立索引。
当发现有connect连接时,将信号槽的索引信息放到一个map中,彼此配对。
当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数
通过active函数找到在map中找到所有与信号对应的槽索引
根据槽索引找到槽函数,执行槽函数。
以上,便是信号槽的整个流程,总的来说就是一个“注册-索引”机制。
其他宏介绍
Qt通过Q_DECLARE开头的几个宏及几个qRegister开头的函数向Qt元系统注册一些非基本类型。一旦注册后,在Qt元系统中就可以很方便的利用这些非基本类型
Q_DECLARE_METATYPE
Q_DECLARE_METATYPE(Type)
这个宏能使Qt元系统的QMetaType知道Type类型,但前提是Type类型必须提供一个公有的默认构造函数、公有的默认拷贝构造函数和公有的默认析构函数。当在QVariant中使用自定义类型时,利用该宏向Qt元系统声明自定义类型是必须的,否则编译会报错。
如果MyStruct在命名空间中,Q_DECLARE_METATYPE()必须在命名空间外部。
#ifndef PUBLICDATA_H
#define PUBLICDATA_H
#include <QString>
#include <QObject>
namespace Mydatapace
{
class person
{
public:
person() {}
int nage;
QString name;
};
}
Q_DECLARE_METATYPE(Mydatapace::person); // 在命名空间的作用域外部,且放在命名空间声明的最下面
#endif // PUBLICDATA_H
#include "publicdata.h"
using namespace Mydatapace;
void Dialog::on_pushButton_clicked()
{
person s;
s.nage = 10;
s.name = "qq";
QVariant varTest;
varTest.setValue(s);
person p = varTest.value<person>();
}