Qt 元对象系统
Qt 元对象系统
- Qt 元对象系统
- 1. 元对象的概念
- 2. 元对象系统的核心组件
- 2.1 QObject
- 2.2 Q_OBJECT 宏
- 2.3 Meta-Object Compiler (MOC)
- 3. 信号与槽
- 3.1 基本概念
- 信号与槽的本质
- 信号和槽的关键特征
- 3.2 绑定信号与槽
- 参数解析
- 断开连接
- 3.3 标准信号与槽
- 查找标准信号与槽
- 使用示例
- 规则与注意事项
- 3.4 自定义槽
- 3.5 自定义信号
- 3.6 信号和槽重载二义性问题
- 4. 内存管理
- 4.1 简介
- 4.2 关联图
- 4.3 详解
- 1. 对象分配在栈上
- 2. 对象分配在堆上
- 释放内存
- 4.4 对象名
- 4.5 智能指针
- QPointer
- QScopedPointer
- QSharedPointer
- QWeakPointer
- QScopedArrayPointer
- QSharedDataPointer
- 隐式与显式共享
- 优化Qt容器的使用性能
- QExplicitlySharedDataPointer
- 5. 属性系统
- 5.1 获取/设置属性值
- 5.2 声明自定义属性
- 属性声明参数详解
- 自定义属性示例
- 属性关联成员变量示例
- 5.3 绑定属性
- QProperty示例
- QObjectBindableProperty 示例
- 6. 实时类型信息
- 何为内省
- 枚举
- 命名空间中的枚举
- 类中的枚举
- QMetaEnum
- QMetaObject
- 附加信息
Qt 元对象系统
Qt元对象系统是对标准C++的扩展,为Qt提供了信号与槽机制、实时类型信息、动态属性系统等功能。
1. 元对象的概念
在计算机科学中,元对象是指能够操纵、创建、描述或执行其他对象的对象。被元对象描述的对象称为基对象。元对象可能包含的信息包括基础对象的类型、接口、类、方法、属性、变量和控制结构等。
2. 元对象系统的核心组件
2.1 QObject
QObject
是Qt对象模型的基础类,具有以下关键特性:
- 对象生命周期管理
- 对象树和父子关系管理
- 信号与槽通信机制
- 属性系统支持
- 运行时类型信息
class QObject
{
public:explicit QObject(QObject *parent = nullptr);virtual ~QObject();// 对象树管理void setParent(QObject *parent);QObject* parent() const;// 属性系统bool setProperty(const char *name, const QVariant &value);QVariant property(const char *name) const;
};
2.2 Q_OBJECT 宏
Q_OBJECT宏必须出现在类定义的私有部分,以启用信号与槽、动态属性系统以及Qt元对象系统提供的其他服务。该宏在编译时由MOC处理,以生成相应的元对象代码。
#define Q_OBJECT \
public: \// 编译器警告控制:压入当前警告状态QT_WARNING_PUSH \// 禁止重写警告Q_OBJECT_NO_OVERRIDE_WARNING \// 静态元对象,存储类的元信息static const QMetaObject staticMetaObject; \// 虚函数:返回对象的元对象指针virtual const QMetaObject *metaObject() const; \// 虚函数:运行时类型转换virtual void *qt_metacast(const char *); \// 虚函数:处理元对象调用(信号、槽、属性)virtual int qt_metacall(QMetaObject::Call, int, void **); \// 国际化相关的函数宏QT_TR_FUNCTIONS \private: \// 属性警告控制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, "")
2.3 Meta-Object Compiler (MOC)
MOC(Meta-Object Compiler)是Qt的预处理工具,它的主要职责包括:
- 为使用
Q_OBJECT
宏的类生成额外的元对象代码 - 实现信号与槽的底层机制
- 生成运行时类型信息
- 支持动态属性和方法调用
3. 信号与槽
3.1 基本概念
信号与槽是Qt框架的核心通信机制,本质上是一种解耦的观察者模式(发布-订阅模式)。在Qt中,当特定事件发生时(如按钮点击),对象会发出信号,这一过程类似于广播。感兴趣的对象可以使用connect()
函数将信号与特定的处理函数(槽)绑定,实现事件的自动响应。
信号与槽的本质
在Qt的元对象系统中,信号是一种特殊的函数,由框架自动生成,无需开发者手动实现。槽是普通函数,可以是成员函数、全局函数、静态函数或Lambda表达式。当信号被触发时,绑定的槽函数将自动调用,并传递相应的参数。
信号和槽的关键特征
- 解耦性:发送者和接收者相互独立,无需直接依赖。
- 灵活性:支持一对多、多对一的信号-槽关联。
- 类型安全:编译期进行严格的类型检查,确保参数匹配。
- 松散耦合:简化对象间通信。
3.2 绑定信号与槽
信号与槽绑定使用QObject::connent()
函数实现,其基本格式如下:
[static] QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method,, Qt::ConnectionType type = Qt::AutoConnection)[static] QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
参数解析
-
sender: 信号的发出者,必须是
QObject
类或其子类的对象。 -
signal: 发出的具体信号,需要传递一个函数指针。
-
receiver: 信号的接收者,必须是
QObject
类或其子类的对象。 -
method: 信号接收者处理信号的动作,需要传递一个函数指针(槽函数)。
-
**type:**第一个
connect
函数独有的参数,表示信号与槽的连接类型,通常使用默认值Qt::AutoConnection
。
在调用connect
函数连接信号与槽时,sender
对象的信号并不会立即产生,因此receiver
对象的method
也不会被调用。实际的调用时机是在信号被触发之后。调用槽函数的操作由Qt框架负责,connect
中的sender
和receiver
两个指针必须被实例化,否则连接将失败。
断开连接
信号与槽连接后,可以使用disconnect
函数断开连接,断开后信号触发时,槽函数将不再被调用。注意,断开连接时的参数必须与连接时完全一致。
bool disconnect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method)
当然,也可以使用connect
的返回值来断开连接:
bool disconnect(const QMetaObject::Connection &connection)
3.3 标准信号与槽
Qt提供了许多类来检测用户触发的特定事件。当这些事件被触发时,便会产生对应的信号,这些信号都是Qt类内部自带的,因此称之为标准信号。同样,Qt的许多类内部还提供了多个功能函数,这些函数可以作为信号触发后的处理动作,称为标准槽函数。
查找标准信号与槽
查找系统自带的信号和槽可以利用Qt的帮助文档。例如,对于按钮的点击信号,可以在帮助文档中输入QPushButton
。首先,在Contents
中寻找关键字signals
,如果没有找到,应该查看该类从父类继承下来的信号。因此,可以查看其父类QAbstractButton
,在该类中通常可以找到信号的相关信息。
QPushButton的信号
QPushButton
类是Qt中用于创建按钮的标准控件,它提供了一些标准信号,常用的包括:
clicked(bool checked = false)
:当按钮被点击时发出此信号。如果按钮是一个切换按钮(toggle button),则checked
参数指示按钮当前的状态(选中或未选中)。pressed()
:当按钮被按下时发出此信号。released()
:当按钮被释放时发出此信号。toggled(bool checked)
:当按钮的状态发生变化时(例如从未选中到选中)发出此信号。checked
参数指示按钮当前的状态。
这些信号允许开发者在用户与按钮交互时执行相应的操作。
QWidget的槽
QWidget
是Qt中所有用户界面对象的基类,许多其他控件(包括QPushButton
)都是从QWidget
派生的。QWidget
本身也提供了一些标准槽,常用的包括:
close()
:关闭窗口。当调用此槽时,窗口将关闭。show()
:显示窗口。当调用此槽时,窗口将被显示出来。可以在调用hide()
后再次使用此槽。hide()
:隐藏窗口。当调用此槽时,窗口将被隐藏,不再可见。setWindowTitle(const QString &title)
:设置窗口的标题。resize(int width, int height)
:调整窗口的大小。move(int x, int y)
:移动窗口到指定的位置。
这些槽函数可以直接用于响应信号,方便开发者实现自定义的窗口行为。
使用示例
以下是一个简单的示例,展示如何在窗口上放置一个按钮,并实现点击按钮关闭窗口的功能。
#include <QApplication>
#include <QPushButton>
#include <QMainWindow>class MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr) : QMainWindow(parent){QPushButton *btn = new QPushButton("Close Window", this);connect(btn, &QPushButton::clicked, this, &MainWindow::close);btn->setGeometry(50, 50, 150, 30);}
};int main(int argc, char *argv[]) {QApplication app(argc, argv);MainWindow window;window.resize(300, 200);window.show();return app.exec();
}#include "main.moc" // 如果在单文件中编写,需要包含此行
在上述代码中,信号与槽的连接可以分析如下:
- 信号发出者:
btn
- 具体信号:
clicked()
- 信号接收者:
this
- 处理动作:
close()
连接信号与槽的代码如下:
QObject::connect(btn,&QPushButton::clicked,this,&MainWindow::close);
规则与注意事项
在关联信号与槽时,需遵循一些规则,否则无法建立关联:
- 槽函数的参数应与信号的参数数量和类型一一对应。
- 信号的参数个数可以大于或等于槽函数的参数个数,未被槽函数接受的参数将被忽略。例如:
- 信号:
void QPushButton::clicked(bool checked = false)
- 槽:
bool QWidget::close()
- 在上述两个信号和槽中,槽函数没有接受信号传递的参数,因此这个
bool
类型的参数被忽略。
- 信号:
3.4 自定义槽
槽函数是信号的处理动作,自定义槽函数与普通函数的写法相同。自定义的槽函数一般放在public slots:
后面。在Qt5及以后的版本中,其实可以不再强制要求写slots
。
定义槽函数必须遵循以下规则
-
槽函数的返回类型必须是
void
,不能是其他类型。 -
槽函数的参数数量必须小于或等于信号的参数数量。
槽函数的类型:
- 成员函数
- 普通成员函数
- 静态成员函数
- 全局函数
- lambda表达式(匿名函数)
void global_func();
Widget::Widget(QWidget *parent): QWidget(parent)
{QPushButton *btn = new QPushButton(this);//连接标准槽函数connect(btn,&QPushButton::clicked,self,&Widget::close);//连接普通成员函数connect(btn,&QPushButton::clicked,this,&Widget::member_func);//连接静态成员函数connect(btn,&QPushButton::clicked,this,&Widget::static_func);//连接全局函数connect(btn,&QPushButton::clicked,this,&Widget::global_func); //连接lambda表达式connect(btn,&QPushButton::clicked,this,[=](){qInfo()<<"lambda"; this->close(); }); }
//普通成员函数
void Widget::member_func()
{this->close();
}
//静态成员函数
void Widget::static_func(bool checked)
{qInfo()<<"static_func"<<checked;
}
//全局函数
void global_func()
{qInfo()<<"global_func";
}
如果你想在槽中知道是哪个对象触发的信号,那么你可以使用 QObject *sender() const
函数获取,信号的发送者。
3.5 自定义信号
Qt框架提供的信号在某些特定场景下可能无法满足项目需求,因此可以设计自定义信号。同样地,使用connect()
函数可以连接自定义的信号和槽。
要使用自定义信号和槽,首先需要编写一个新的类,并让其继承Qt的某些标准类。如果您想在Qt中使用信号槽机制,必须满足以下条件:
- 该类必须从
QObject
类或其子类派生。 - 在定义类的第一行头文件中加入
Q_OBJECT
宏。
// 在头文件中派生类时,首先像下面这样引入Q_OBJECT宏:
class MyMainWindow : public QWidget
{Q_OBJECT
public:......
};
如果是单文件编写的,还需要在代码的最下面加上#include "name.moc"
,其中name
是指原文件的名称
自定义信号需要遵循以下规则:
-
信号是类的成员函数,且返回类型必须是
void
。 -
信号函数仅需声明,不需要定义(即没有函数体实现)。
-
参数可以随意指定,信号也支持重载。
-
信号需使用
signals
关键字进行声明,方法类似于public
等关键字。 -
在程序中发送自定义信号的本质是调用信号函数:
emit mysignals(); //发送信号
注意:emit是一个空宏,没有特殊含义,仅用来表示这个语句是发射一个信号,不写当然可以,但是不推荐。
// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class MyButton : public QPushButton
{Q_OBJECT
signals:void testsignal();void testsignal(int a);
};
信号参数的作用是数据传递,谁调用信号函数,谁就需要指定实参,实参最终会被传递给槽函数。
3.6 信号和槽重载二义性问题
在使用信号和槽时,如果信号和槽函数重载,可能会出现二义性问题。可以通过以下方法解决:
- 通过函数指针解决
// 定义无参信号的函数指针
void (Me::*signalWithoutArgs)() = &Me::hungury;
// 定义有参信号的函数指针
void (Me::*signalWithQString)(QString) = &Me::hungury;// 定义无参槽的函数指针
void (Me::*slotWithoutArgs)() = &Me::eat;
// 定义有参槽的函数指针
void (Me::*slotWithQString)(QString) = &Me::eat;// 连接有参信号和槽
connect(me, signalWithQString, me, slotWithQString);
// 连接无参信号和槽
connect(me, signalWithoutArgs, me, slotWithoutArgs);
- 通过Qt提供的重载类(QOverload)解决
// 连接有参信号和槽
connect(this, QOverload<QString>::of(&MyButton::hungury), this, QOverload<QString>::of(&MyButton::eat));
// 连接无参信号和槽
connect(this, QOverload<>::of(&MyButton::hungury), this, QOverload<>::of(&MyButton::eat));
- Qt4的连接方式
这种旧的信号槽连接方式在Qt6中仍支持,但不推荐使用,因为这种方式在进行信号槽连接时,信号和槽函数通过宏
SIGNAL
和SLOT
转换为字符串类型。由于信号槽函数的转换是通过宏进行的,因此传递到宏函数内部的数据不会被检查。如果使用者传错了数据,编译器不会报错,但实际上信号槽的连接已失败,只有在程序运行时才能发现问题,并且问题往往难以定位。
Me m;
// Qt4的连接方式 注意不要把信号和槽的名称写错,因为是转为字符串的,写错了不会报错,但连接会失败
connect(&m, SIGNAL(eat()), &m, SLOT(hungury()));
connect(&m, SIGNAL(eat(QString)), &m, SLOT(hungury(QString)));// Qt5的连接方式
connect(&m, &Me::eat, &m, &Me::hungury); // error: no matching member function for call to 'connect'
- 总结
- Qt4的信号槽连接方式由于使用了宏函数,宏函数对用户传递的信号槽不会做错误检测,容易产生bug。
- Qt5的信号槽连接方式传递的是信号和槽函数的地址,编译器会进行错误检测,从而减少bug的产生。
- 当信号槽函数被重载之后,Qt4的信号槽连接方式不受影响。
- 当信号槽函数被重载后,在Qt6中需要为被重载的信号或槽定义函数指针。
4. 内存管理
4.1 简介
在C++中,new
和delete
必须配对使用,否则可能会导致内存泄漏或其他问题。在Qt中,虽然使用了new
,但很少需要手动delete
,这是因为Qt实现了独特的内存管理机制。
QObject
以对象树的形式组织起来。当为一个对象创建子对象时,子对象会自动添加到父对象的children()
列表中。父对象拥有子对象的所有权,例如,父对象可以在自己的析构函数中删除其子对象。可以使用findChild()
或findChildren()
通过名称和类型查询子对象。
QObject(QObject *parent = nullptr)
- 如果
QObject
及其派生类的对象的parent
非nullptr
,那么在其父对象析构时,该对象也会被析构。 - 父子关系是Qt特有的,与类的继承关系无关。传递的参数与
parent
有关(基类、派生类或父类、子类),这是对于派生体系来说的,与parent
无关。
4.2 关联图
在Qt中,最基础和核心的类是QObject
。QObject
内部有一个名为children
的QObjectList
列表,用于保存所有子对象,还有一个指针parent
,用来指向父对象。当自身析构时,会先将自己从父对象的列表中删除,并析构所有的子对象。
4.3 详解
1. 对象分配在栈上
栈对象具有自动生命周期管理,推荐使用。
int main(int argc,char*argv[])
{QApplication a(argc,argv);QObject obj;qInfo()<<"hello Qt!";return a.exec();
}
2. 对象分配在堆上
当把对象分配到堆上时,如果忘记delete
,内存就不会释放,会发生内存泄漏
#include<QApplication>
#include<QDebug>
int main(int argc,char*argv[])
{QApplication a(argc,argv);QObject* obj = new QObject;qInfo()<<"hello Qt!";return a.exec();
}
释放内存
- 使用
delete
或者Qt提供的成员函数deleteLater()
释放内存,对象释放时会触发QObject::destroyed(QObject *obj = nullptr)信号
int main(int argc,char*argv[])
{QApplication a(argc,argv);QObject* obj = new QObject;//delete obj; //①//obj->deleteLater(); //②qInfo()<<"hello Qt!";return a.exec();
}
- 使用指定父对象的方式自动管理内存
#include<QApplication>
#include<QDebug>class MyObject:public QObject
{
public:MyObject(QObject* parent = nullptr):QObject(parent){qInfo()<<"MyObject created!";}~MyObject(){qInfo()<<"MyObject destory!";}
};int main(int argc,char*argv[])
{QApplication a(argc,argv);{MyObject parent;{MyObject* obj = new MyObject(&parent);//obj->deleteLater();//MyObject obj;}}qInfo()<<"hello Qt!";return a.exec();
}
4.4 对象名
在Qt中,可以为对象设置对象名,从而使用findChild()
通过名称(和类型)查找对象;还可以通过findChildren()
找到一组对象。
-
设置对象名
void QObject::setObjectName(const QString &name);
-
获取对象名
QString QObject::objectName() const;
-
通过对象名查找对象
template <typename T> T findChild(const QString &name = QString(), Qt::FindChildOptions options = Qt::FindChildrenRecursively) const
根据指定的名称
name
和指定的类型T
(T
可以是父类)查找子对象。如果没有这样的子对象,则返回nullptr
。-
示例:返回名为“button1”的
parentWidget
的子QPushButton
,即使该按钮不是父级的直接子级:QPushButton *button = parentWidget->findChild<QPushButton *>("button1");
-
示例:返回
parentWidget
的QListWidget
子组件:QListWidget *list = parentWidget->findChild<QListWidget *>();
-
示例:返回
parentWidget
(它的直接父元素)的一个名为"button1"的子QPushButton
:QPushButton *button = parentWidget->findChild<QPushButton *>("button1", Qt::FindDirectChildrenOnly);
-
示例:返回
parentWidget
的QListWidget
子组件,它的直接父组件:QListWidget *list = parentWidget->findChild<QListWidget *>(QString(), Qt::FindDirectChildrenOnly);
-
-
通过类型查找对象
QList<T> findChildren(const QString &name = QString(), Qt::FindChildOptions options = Qt::FindChildrenRecursively) constQList<T> findChildren(const QRegularExpression &re, Qt::FindChildOptions options = Qt::FindChildrenRecursively) const
根据指定的名称
name
和指定的类型T
(T
可以是父类)查找子对象。如果没有这样的子对象,则返回nullptr
。-
示例:查找名为
widgetname
的指定父Widget
的子Widget
列表:QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname");
-
示例:返回
parentWidget
的所有子QPushButton
:QList<QPushButton *> allPButtons = parentWidget.findChildren<QPushButton *>();
-
示例:返回所有与
parentWidget
直接关联的QPushButton
:QList<QPushButton *> childButtons = parentWidget.findChildren<QPushButton *>(QString(), Qt::FindDirectChildrenOnly);
-
4.5 智能指针
在C++中,为了有效管理内存和其他资源,程序员通常采用RAII(Resource Acquisition Is Initialization)机制:在类的构造函数中分配资源,在使用完成后通过析构函数释放资源。智能指针的引入,使得程序员不再需要手动管理new
对象的delete
操作,也无需编写复杂的异常捕获代码来释放资源,因为智能指针能够在作用域结束时(无论是正常退出还是异常退出)自动调用delete
来销毁在堆上动态分配的对象。
在Qt框架中,提供了多种类型的智能指针,以便于资源管理:
智能指针 | 描述 |
---|---|
QPointer | QObject 专享指针,当 QObject 或其子类对象被释放时,会自动将指针置为 nullptr。 |
QScopedPointer | 独享指针,当超出作用域时,自动释放所管理的对象。 |
QSharedPointer | 共享指针,支持多个指针共同拥有同一对象。 |
QWeakPointer | 监视指针,用于观察 QSharedPointer 所管理的对象是否仍然存在。 |
QScopedArrayPointer | 独享数组指针,当超出作用域时,自动释放所管理的对象数组。 |
QSharedDataPointer | 隐式共享指针,支持读时共享和写时拷贝。 |
QExplicitlySharedDataPointer | 显示共享指针,读时共享,写时需要手动拷贝(通过 detach() )。 |
QPointer
QPointer是一种受保护的指针,其行为类似于普通的C++指针T*,但当指向的对象被销毁时,QPointer会自动清空(与普通指针不同,后者会变成“悬空指针”)。T必须是QObject的子类。
QPointer在需要存储指向其他对象的QObject指针时非常有用,因为这些对象可能在你持有引用期间被销毁。使用QPointer,可以安全地检查指针的有效性。
需要注意的是,从Qt 5开始,QPointer的行为有所调整。在QWidget或其子类中,QPointer会在QObject的析构函数中清除,而非在QWidget的析构函数中。这意味着,在QWidget析构时,任何跟踪该小部件的QPointer不会被立即清除,直到QObject的析构过程进行时。
QPointer<QLabel> label = new QLabel;
label->setText("&Status:");
...
if (label)label->show();
在上述代码中,如果QLabel
被删除,label
变量将保存nullptr
,而不是无效地址,最后一行将不会执行。
请注意,类T必须继承QObject,否则将导致编译或链接错误。
QScopedPointer
手动管理堆分配对象往往复杂且容易出错,常见结果是内存泄漏且难以维护。QScopedPointer是一个轻量级的工具类,它通过将基于栈的内存所有权转移到堆分配的对象,从而极大简化了资源管理(这一概念被称为RAII)。
QScopedPointer确保当当前作用域结束时,所指向的对象将被删除。编译器为QScopedPointer生成的代码与手动编写的代码等效。由于QScopedPointer没有复制构造函数或赋值操作符,它明确传达了所有权和生命周期的信息。
QSharedPointer
QSharedPointer是一种自动共享指针,其行为与普通指针相似。如果没有其他QSharedPointer对象引用它,当其超出作用域时,将自动删除所持有的指针。QSharedPointer可以从普通指针、另一个QSharedPointer对象或通过提升QWeakPointer对象创建强引用。
QWeakPointer
QWeakPointer是一种自动弱引用指针,不能直接解引用,但可用于验证指针在其他上下文中是否已被删除。QWeakPointer对象只能通过从QSharedPointer赋值创建。
需要注意的是,QWeakPointer未提供自动强制转换操作符,因此即使QWeakPointer跟踪一个指针,它本身也不能被视为一个有效指针。访问QWeakPointer所跟踪的指针时,必须首先将其提升到QSharedPointer,并验证结果是否为空。QSharedPointer保证对象不会被删除,因此如果获得一个非空对象,可以安全使用该指针。
QScopedArrayPointer
QScopedArrayPointer是QScopedPointer的变体,默认情况下使用delete[]操作符释放所指向的对象。它还提供了操作符[],例如:
void foo(){QScopedArrayPointer<int> i(new int[10]);i[2] = 42;...return; // our integer array is now deleted using delete[]}
QSharedDataPointer
QSharedDataPointer类表示指向隐式共享对象的指针。通过QSharedDataPointer,您可以轻松实现自己的隐式共享类。QSharedDataPointer实现了线程安全的引用计数,确保在可重入类中不会导致不可重入。
许多Qt类都使用隐式共享,以结合指针速度和内存效率以及类的易用性。有关更多信息,请参见共享类页面。
假设您想让Employee类实现隐式共享,步骤如下:
- 定义Employee类,包含一个类型为QSharedDataPointer的数据成员。
- 定义从QSharedData派生的EmployeeData类,包含通常放入Employee类中的所有数据成员。
以下是隐式共享Employee类的示例代码:
#include <QSharedData>#include <QString>class EmployeeData : public QSharedData{public:EmployeeData() : id(-1) { }EmployeeData(const EmployeeData &other): QSharedData(other), id(other.id), name(other.name) { }~EmployeeData() { }int id;QString name;};class Employee{public:Employee() { d = new EmployeeData; }Employee(int id, const QString &name) {d = new EmployeeData;setId(id);setName(name);}Employee(const Employee &other): d (other.d){}void setId(int id) { d->id = id; }void setName(const QString &name) { d->name = name; }int id() const { return d->id; }QString name() const { return d->name; }private:QSharedDataPointer<EmployeeData> d;};
在上述Employee类中,注意到数据成员d为QSharedDataPointer类型。所有对员工数据的访问均应通过d指针的operator->()进行。对于写访问,操作符将自动调用detach(),如果共享数据对象的引用计数大于1,detach()将创建共享数据对象的副本。这样可以确保对一个Employee对象的写入不会影响共享相同EmployeeData对象的其他Employee对象。
在Employee类的构造函数中,创建新的EmployeeData实例并将其赋值给d指针。
Employee() { d = new EmployeeData; }Employee(int id, const QString &name)
{d = new EmployeeData;setId(id);setName(name);
}
请注意,Employee类定义了简单的复制构造函数,但在此示例中并非严格要求。
Employee(const Employee &other) : d(other.d) {}
尽管在包含QSharedDataPointer的公共类的同一文件中包含QSharedData的私有子类并不典型,通常的做法是将QSharedData的私有子类放在一个单独的文件中,以隐蔽其实现细节。如果在这里将EmployeeData类放在单独的文件中,则需在employee.h中声明该类。
class EmployeeData;
在幕后,QSharedDataPointer会自动增加引用计数,在复制、分配或作为参数传递Employee对象时进行。当Employee对象被删除或超出作用域时,引用计数会减少。当引用计数达到0时,共享的EmployeeData对象将被自动删除。
在Employee的非常量成员函数中,每当d指针被解引用时,QSharedDataPointer会自动调用detach(),以确保函数对自身数据的副本进行操作。注意,若因多次解引用而在成员函数中多次调用detach(),detach()只会在第一次调用时创建数据副本。
在Employee的const成员函数中,解引用d指针不会导致调用detach()。
int id() const { return d->id; }QString name() const { return d->name; }
需要说明的是,不必为Employee类实现复制构造函数或赋值操作符,因为C++编译器提供的默认实现已经满足需要,逐个成员进行浅复制。唯一需要复制的是d指针,它是一个QSharedDataPointer,其operator=()仅增加共享EmployeeData对象的引用计数。
隐式与显式共享
隐式共享可能不适用于Employee类。考虑创建两个隐式共享Employee类实例的示例:
#include "employee.h"int main()
{Employee e1(1001, "Albrecht Durer");Employee e2 = e1;e1.setName("Hans Holbein");
}
在第二个雇员e2被创建并被分配给e1之后,e1和e2都指向雇员1001 Albrecht Durer。两个Employee对象都指向EmployeeData的同一个实例,该实例的引用计数为2。然后e1。setName(“Hans Holbein”)被调用来更改员工名,但由于引用计数大于1,所以在更改名称之前会执行写时拷贝。现在e1和e2指向不同的EmployeeData对象。它们有不同的名称,但都有ID 1001,这可能不是您想要的。当然,如果您真的想创建第二个唯一的雇员,但如果您只想在所有地方更改雇员的名称,则可以继续使用e1.setId(1002),考虑在employee类中使用显式共享而不是隐式共享。
如果将Employee类中的d指针声明为QExplicitlySharedDataPointer<EmployeeData>,则使用显式共享,写时复制操作不会自动执行(即在非const函数中不会调用detach())。在这种情况下,e1之后。setName(“Hans Holbein”),员工的名字已经改变,但是e1和e2仍然引用EmployeeData的同一个实例,所以只有一个员工的ID是1001。
在成员函数文档中,d指针始终指向共享数据对象的内部指针。
优化Qt容器的使用性能
如果隐式共享类类似于上述Employee类,并且使用QSharedDataPointer或QExplicitlySharedDataPointer作为唯一成员,建议使用Q_DECLARE_TYPEINFO()
宏将其标记为可移动类型。这可以提高在使用Qt容器类时的性能和内存效率。
QExplicitlySharedDataPointer
QExplicitlySharedDataPointer类表示指向显式共享对象的指针。QExplicitlySharedDataPointer使您可以轻松编写自己的显式共享类。它实现了线程安全的引用计数,确保将QExplicitlySharedDataPointer添加到可重入类中不会导致不可重入。
QExplicitlySharedDataPointer与QSharedDataPointer类似,但其成员函数在允许修改共享数据对象之前,不会像QSharedDataPointer的非const成员那样自动执行写时复制(detach())。可手动调用detach(),但如果频繁调用detach(),应考虑使用QSharedDataPointer替代。
5. 属性系统
The Property System
Qt提供了一个复杂而强大的属性系统,旨在简化对象属性的管理和访问。该系统与一些编译器供应商提供的属性系统相似,但作为一个独立于编译器和平台的库,Qt并不依赖于像__property
或[property]
这样的非标准编译器特性。因此,Qt的解决方案可以在所有支持的Qt平台上使用任何标准的C++编译器,并基于元对象系统,通过信号和插槽机制实现对象之间的通信。
属性的行为类似于类的数据成员,但它具有通过元对象系统进行访问的附加特性。下面将详细介绍属性的基本用法及其附加功能。
5.1 获取/设置属性值
在Qt中,可以轻松地获取和设置对象的属性。例如,QObject
类具有一个名为objectName
的属性,可以通过以下方式获取其值:
qInfo() << obj->property("objectName").toString();
若要修改该属性的值,可以使用以下代码:
obj->setProperty("objectName","OBJ");
QObject::setProperty()
方法不仅可以用于修改已存在的属性,还可以在运行时向类的实例添加新属性。当调用该方法时,如果QObject
中已存在具有指定名称的属性,并且提供的值与该属性的类型兼容,则该值将被存储到属性中,并返回true
。如果值与属性类型不兼容,则不会更改属性的值,并返回false
。
需要注意的是,如果QObject
中不存在具有指定名称的属性(即未使用Q_PROPERTY()
声明),则会自动将新属性添加到QObject
中,尽管返回值仍然为false
。这意味着仅凭返回值无法确定特定属性是否实际被设置,除非事先确认该属性已存在于QObject
中。
动态属性是在每个实例基础上添加的,即它们是添加到QObject
而不是QMetaObject
中的。通过将属性名称和一个无效的QVariant
值传递给QObject::setProperty()
,可以从实例中删除属性。QVariant
的默认构造函数构造一个无效的QVariant
。
5.2 声明自定义属性
除了通过setProperty
动态添加属性之外,还可以在代码中显式声明属性。要声明属性,需要在继承自QObject
的类中使用Q_PROPERTY()
宏。
Q_PROPERTY(type name(READ getFunction [WRITE setFunction] |MEMBER memberName [(READ getFunction | WRITE setFunction)])[RESET resetFunction][NOTIFY notifySignal][REVISION int | REVISION(int[, int])][DESIGNABLE bool][SCRIPTABLE bool][STORED bool][USER bool][BINDABLE bindableProperty][CONSTANT][FINAL][REQUIRED])
属性声明参数详解
- type:属性的数据类型,可以是
QVariant
支持的任何类型,也可以是用户自定义的类型。 - name:属性的名称。
- READ:指定用于读取属性值的访问器函数。此函数应返回属性的类型或对该类型的
const
引用。 - WRITE:可选项,用于设置属性值的访问器函数。该函数必须返回
void
并只接受一个参数,该参数可以是属性的类型或指向该类型的指针或引用。 - MEMBER:可选项,指定直接关联的成员变量,使其可读可写,而无需显式定义
READ
和WRITE
函数。 - RESET:可选项,指定将属性重置为其特定于上下文的默认值的函数。
- NOTIFY:可选项,指定信号,该信号在属性值更改时发出。该信号必须与属性类型匹配。
- REVISION:可选项,用于定义API中特定修订版中使用的属性。
- DESIGNABLE:指示属性是否应在GUI设计工具(如Qt Designer)的属性编辑器中可见。默认为
true
。 - SCRIPTABLE:指示脚本引擎是否可以访问该属性。默认为
true
。 - STORED:指示该属性是否应被视为独立属性,默认为
true
。 - USER:指示该属性是否为面向用户的属性,默认值为
false
。 - BINDABLE:指示该属性支持绑定。
- CONSTANT:指示该属性的值是常量。
- FINAL:指示该属性不会被派生类覆盖。
- REQUIRED:指示该属性应由类的用户设置。
自定义属性示例
class MyObject : public QObject
{Q_OBJECTQ_PROPERTY(QString name READ getName WRITE setName RESET unsetName NOTIFY nameChanged)public:MyObject(QObject *parent = nullptr) : QObject(parent) {}QString getName() const {qInfo() << __FUNCTION__;return m_name;}void setName(const QString &name) {if (m_name != name) {m_name = name;emit nameChanged(m_name); // 发射信号以通知属性变化}}void unsetName() {m_name = "unknown";}signals:void nameChanged(const QString &);private:QString m_name;
};int main(int argc, char *argv[])
{QApplication a(argc, argv);MyObject obj;QObject::connect(&obj, &MyObject::nameChanged, [](const QString &name) {qInfo() << "slot:" << name;});obj.setName("maye");qInfo() << obj.getName(); // 输出 "maye"obj.unsetName(); // 将name重置为"unknown"obj.setProperty("name", "顽石");qInfo() << obj.property("name").toString(); // 输出 "顽石"obj.setProperty("name", QVariant()); // 将name重置为"unknown"return a.exec();
}
在此示例中,通过属性name
访问和设置值,相当于直接操作成员变量m_name
。当使用setName
方法更改名称时,程序手动发射信号以通知属性变化。
属性关联成员变量示例
class MyObject : public QObject
{Q_OBJECTQ_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)public:MyObject(QObject *parent = nullptr) : QObject(parent) {}signals:void nameChanged(const QString &);private:QString m_name;
};int main(int argc, char *argv[])
{QApplication a(argc, argv);MyObject obj;QObject::connect(&obj, &MyObject::nameChanged, [](const QString &name) {qInfo() << "slot:" << name;});obj.setProperty("name", "顽石"); // 修改属性,会自动触发nameChanged信号qInfo() << obj.property("name").toString(); // 输出 "顽石"return a.exec();
}
在上述代码中,使用属性name
直接修改了成员变量m_name
,通过属性接口访问或设置属性值时,不需要显式的读写函数。如果指定了NOTIFY
信号,则通过属性接口改变name
的值时,信号会自动触发。
5.3 绑定属性
Qt Bindable Properties
Qt引入了可绑定属性,允许开发者创建依赖于其他属性值的属性。这些属性可以具有静态值或通过C++函数(通常是lambda表达式)动态计算的值。当依赖的属性发生变化时,可绑定属性会自动更新其值。
可绑定属性的实现主要依赖于QProperty
类,该类包含数据对象以及指向管理数据结构的指针。QObjectBindableProperty
类用于封装这些功能,适用于QObject
的子类。
QProperty
和QObjectBindableProperty
是在 Qt 6 中引入的特性,因此需要 Qt 6 或更高版本才能使用这些功能。Q_PROPERTY
宏及其相关功能在 Qt 4 及更高版本中均可用。QProperty
是可绑定属性的通用类,而QObjectBindableProperty
只能在QObject
的子类中使用。
QProperty示例
定义一个Rectangle
矩形类,通过构造函数传入宽度和高度,自动计算矩形面积:
struct Rectangle
{int w;int h;int area;Rectangle(int width,int height):w(width),h(height){area = w * h;}
};void test()
{Rectangle rect(2,5);qInfo()<<rect.w<<rect.h<<rect.area; //2 5 10rect.w = 3; //area:15qInfo()<<rect.w<<rect.h<<rect.area; //3 5 10
}
从上面代码可以看出,只有在构造对象时,才能正确计算面积;如果在对象定义之后,修改宽度或者高度,面积都将变得不正确,因为它没有及时更新。
矩形的面积是依赖于矩形的宽度和高度的,那么当高度或者高度变化之后,应该需要自动更新面积,这个应该如何做到呢?
绑定表达式通过读取其他QProperty值来计算该值。在幕后跟踪这种依赖关系。每当检测到任何属性的依赖关系发生更改时,都会重新计算绑定表达式,并将新的结果应用于该属性。例如:
#include <QCoreApplication>
#include <QProperty> // Qt6引入
#include <QDebug>struct Rectangle
{QProperty<int> w{0}; // 宽度QProperty<int> h{0}; // 高度QProperty<int> area{0}; // 面积// 构造函数,初始化宽度和高度Rectangle(int width, int height) : w(width), h(height){// 设置绑定属性area.setBinding([this]() -> int { return w * h; });}
};void test()
{Rectangle rect(2, 5);qInfo() << rect.w << rect.h << rect.area; // 输出: 2 5 10rect.w = 3; // 触发area计算qInfo() << rect.w << rect.h << rect.area; // 输出: 3 5 15rect.h = 4; // 触发area计算qInfo() << rect.w << rect.h << rect.area; // 输出: 3 4 12
}int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);test(); // 调用测试函数return a.exec();
}
在这个例子中,当矩形的宽度或高度发生变化时,面积area
会自动更新。这是因为area
属性通过setBinding
方法与w
和h
属性建立了依赖关系。
QObjectBindableProperty 示例
QObjectBindableProperty是一个通用容器,它保存T的一个实例,其行为主要类似于QProperty。它是实现Qt绑定属性的类之一。与QProperty不同,它将其管理数据结构存储在QObject中。额外的模板参数用于标识周围的类和作为更改处理程序的类的成员函数。
QObjectBindableProperty
允许在使用Q_PROPERTY
的代码中添加绑定支持。以下是一个使用QObjectBindableProperty
的示例:
在这个示例中,定义了一个Rectangle
类,其中的宽度和高度属性被声明为可绑定属性。当宽度或高度改变时,面积属性会自动重新计算并发射信号。
#include <QObject>
#include <QDebug>
#include <QCoreApplication>
#include <QObjectBindableProperty>struct Rectangle : public QObject
{Q_OBJECTQ_PROPERTY(int w MEMBER w)Q_PROPERTY(int h MEMBER h)Q_PROPERTY(int area MEMBER area)public:Rectangle(int width, int height) : w(width), h(height){// 设置绑定属性area.setBinding([&]() { return w * h; });}signals:void areaChanged(int area);public:Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, w); // 定义可绑定属性Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(Rectangle, int, h, 0); // 可以给默认值Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, area, &Rectangle::areaChanged); // 属性改变时,会触发areaChanged信号
}; void test()
{Rectangle rect(2, 5);QObject::connect(&rect, &Rectangle::areaChanged, [&](int area) { qInfo() << "Area changed to:" << area; });qInfo() << rect.w << rect.h << rect.area; // 输出: 2 5 10rect.w = 3; // area: 15qInfo() << rect.w << rect.h << rect.area; // 输出: 3 5 15
}int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);test(); // 调用测试函数return a.exec();
}
通常不会直接使用QObjectBindableProperty,而是通过使用Q_OBJECT_BINDABLE_PROPERTY
宏创建它的实例。
在类声明中使用Q_OBJECT_BINDABLE_PROPERTY
宏将属性声明为可绑定的。
如果需要使用一些非默认值直接初始化属性,可以使用Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS
宏。它接受一个初始化值作为它的参数之一。
6. 实时类型信息
何为内省
内省(Introspection)是面向对象编程语言的一种特性,它允许在运行时查询对象的信息。这种能力使得能够检查对象的类型,从而实现多态性。若一种语言具备在运行期间检查对象类型的能力,则称之为类型内省(Type Introspection)。
- Qt是通过
QObject
、QMetaObject
类实现其内省机制。 QObject
暴露给用户的共有自省方法有objectName()
,inherits()
,isWidgetType()
等。- 大多数自省方法是
QObject
派发给QMetaObject
实现 (QMetaObject::className
),元对象模型编译器moc
负责自省方法的实现。 - 更多自省方法定义在
QMetaObject
,是为了信号槽通讯、事件派发等机制。
C++的内省功能相对有限,主要支持通过运行时类型识别(RTTI,Run-Time Type Information)实现的类型内省。C++的RTTI通过typeid
和dynamic_cast
关键字来实现,以下是一个简要示例:
// Dog类派生自Animal类,jump为虚函数
if (Dog *pdog = dynamic_cast<Dog*>(obj)) {pdog->cry();
}
// 还可以使用typeid获取对象的类型信息,如对象的名称
std::cout << typeid(obj).name() << std::endl;
在Qt中,内省机制得到了扩展。实际上,Qt并未采用C++的RTTI,而是提供了更为强大的元对象(Meta Object)机制来实现内省。理解Qt的内省机制需要首先理解QObject
类,因为QObject
是整个Qt对象模型的核心。Qt对象模型的主要功能是提供无缝的对象通信机制,即信号和槽。QObject
承担着三大主要职责:内存管理、内省和事件处理。接下来将重点讨论内省。
QObject
类提供了多种内省方法,以下是一个示例代码,展示了如何判断一个类是否继承自指定的类:
// 判断该类是否继承自指定的类
bool inherits(const char *className) const;QWidget* w = new QWidget;
bool isQObject = w->inherits("QObject"); // true
bool isQWidget = w->inherits("QWidget"); // false
深入了解QObject::inherits
方法的底层实现,可以发现其实际实现如下:
inline bool inherits(const char *classname) const { return const_cast<QObject *>(this)->qt_metacast(classname) != nullptr;
}
可见,QObject::inherits
方法是通过虚函数qt_metacast()
实现的。每个QObject
的派生类必须实现metaObject()
以及其他qt_metacall()
方法,从而支持内省方法如className
、inherits
等的调用。
用户在派生自QObject
的类中只需声明宏Q_OBJECT
,Qt的元对象编译器(MOC)便会负责实现这些内省方法。
#define Q_OBJECT \
public: \QT_WARNING_PUSH \Q_OBJECT_NO_OVERRIDE_WARNING \static const QMetaObject staticMetaObject; \virtual const QMetaObject *metaObject() const; \virtual void *qt_metacast(const char *); \virtual int qt_metacall(QMetaObject::Call, int, void **); \QT_TR_FUNCTIONS \
private: \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, "")
此外,所有的Qt widgets
类均继承自QObject
, QObject
所提供的isWidgetType
自省方法可以很方便让QObject
子对象查询自己是否是Wideget
, 而且它会比 qobject_cast<QWidget *>(obj)
或者obj->inherits
快很多。原因qobject_cast()
和inherits()
都是借助元对象系统来实现其功能的,isWidgetType()
是QObject
本身的标志位得以实现。
更多自省方法定义在QMetaObject
。
枚举
使用枚举可以便利地表示某些状态标志。然而,在查看枚举值时,通常只能看到数值,无法直接查看枚举的名称。Qt提供了方法,使得在输出时能够显示定义的枚举名称。
命名空间中的枚举
namespace Maye {Q_NAMESPACEenum Type{Player,Enemy,Bullet};Q_ENUM_NS(Type) // 将枚举注册到元对象系统
}
首先定义命名空间,并在命名空间的第一行添加Q_NAMESPACE宏,以将整个命名空间注册到元对象系统中。接着定义枚举类型,最后使用**Q_ENUM_NS(enum type)**将枚举类型注册到元对象系统中。
以下代码能够成功输出枚举名,而非仅仅是数值:
int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);using namespace Maye;Type type = Type::Player;qDebug() << type; // 输出: Maye::Player// 获取枚举的元信息const QMetaObject &metaObject = Maye::staticMetaObject; // 获取命名空间的静态元对象QMetaEnum metaEnum = metaObject.enumerator(metaObject.indexOfEnumerator("Type")); // 获取 Type 枚举的元信息// 将枚举值转换为字符串QString typeStr = metaEnum.valueToKey(static_cast<int>(type));qInfo() << typeStr; // 输出: "Player"return a.exec();
}
类中的枚举
class Test : public QObject
{Q_OBJECT
public:enum Type{Player,Enemy,Bullet};Q_ENUM(Type)
};
在自定义类中,首先需直接继承自QObject
或其子类;然后,在public
权限下定义枚举;最后,使用Q_ENUM(enum type)
将枚举类型注册到元对象系统中。
Test::Type type = Test::Type::Player;
qDebug() << type; // 输出: Test::Player// 转为字符串
// 获取枚举元信息
const QMetaObject &metaObject = Test::staticMetaObject;
QMetaEnum metaEnum = metaObject.enumerator(metaObject.indexOfEnumerator("Type"));// 将枚举值转换为字符串
QString typeStr = metaEnum.valueToKey(static_cast<int>(type));
qDebug() << typeStr; // 输出: "Player"
QMetaEnum
QMetaEnum
类提供了多种功能以处理枚举。其主要功能包括:
- name():返回枚举项的名称。
- key():返回每个枚举项的键(名称)。
- keyCount():查找键的数量。
- isFlag():返回该枚举是否被设计为标志,意味着它的值可以使用OR操作符组合。
- keyToValue()、valueToKey()、keysToValue()**和**valueToKeys():这些转换函数允许在枚举或集合值的整数表示与其文字表示之间进行转换。
- scope():返回声明此枚举的类的范围。
下面是一个使用QMetaEnum
的示例,展示如何定义枚举、注册枚举到元对象系统,并利用QMetaEnum
来获取枚举值和名称。
#include <QCoreApplication>
#include <QMetaEnum>
#include <QObject>
#include <QDebug>class TrafficLight : public QObject
{Q_OBJECT
public:enum State {Red,Yellow,Green};Q_ENUM(State) // 注册枚举到元对象系统TrafficLight(QObject *parent = nullptr) : QObject(parent) {}void printCurrentState(State state) {// 使用QMetaEnum获取状态的名称const QMetaObject *metaObject = this->metaObject();int index = metaObject->indexOfEnumerator("State");QMetaEnum metaEnum = metaObject->enumerator(index);qDebug() << "Current state:" << metaEnum.valueToKey(state);}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);TrafficLight light;light.printCurrentState(TrafficLight::Red); // 输出: Current state: "Red"light.printCurrentState(TrafficLight::Yellow); // 输出: Current state: "Yellow"light.printCurrentState(TrafficLight::Green); // 输出: Current state: "Green"return a.exec();
}#include "main.moc"
QMetaObject
QMetaObject
类包含有关Qt对象的元信息。它提供了一种机制,通过该机制可以在运行时获取有关对象的结构和属性信息。以下是一些重要的成员函数:
- className():返回类的名称。
- classInfoCount():返回类信息的数量。
- classInfo(int index):获取指定索引的类信息。
- classInfoOffset():返回类信息的偏移量。
附加信息
Q_CLASSINFO()
宏可用于将附加的<名称-值>对附加到类的元对象,例如:
Q_CLASSINFO("Version", "3.0.0")
可以通过QMetaObject
的几个函数访问类信息:
QMetaClassInfo classInfo(int index) const
int classInfoCount() const
int classInfoOffset() const
const char *className() const
示例:
下面示例展示了如何使用QMetaObject
类和Q_CLASSINFO()
宏来获取类的元信息。
class MyObject : public QObject
{Q_OBJECTQ_CLASSINFO("version", "1.0")Q_CLASSINFO("author", "顽石")
public:
};class MyObject1 : public MyObject
{Q_OBJECTQ_CLASSINFO("version", "2.0")Q_CLASSINFO("name", "maye")
public:
};int main(int argc, char* argv[])
{QApplication a(argc, argv);MyObject1 obj;const QMetaObject* metaObj = obj.metaObject();int cnt = metaObj->classInfoCount();for (int i = 0; i < cnt; i++){qInfo() << metaObj->classInfo(i).name() << metaObj->classInfo(i).value();}qInfo() << metaObj->classInfoOffset();qInfo() << metaObj->className();return a.exec();
}
输出:
version 1.0
author 顽石
version 2.0
name maye
2
MyObject1
oc"
## QMetaObject`QMetaObject`类包含有关Qt对象的元信息。它提供了一种机制,通过该机制可以在运行时获取有关对象的结构和属性信息。以下是一些重要的成员函数:- **className()**:返回类的名称。
- **classInfoCount()**:返回类信息的数量。
- **classInfo(int index)**:获取指定索引的类信息。
- **classInfoOffset()**:返回类信息的偏移量。### 附加信息`Q_CLASSINFO()`宏可用于将附加的<名称-值>对附加到类的元对象,例如:```cpp
Q_CLASSINFO("Version", "3.0.0")
可以通过QMetaObject
的几个函数访问类信息:
QMetaClassInfo classInfo(int index) const
int classInfoCount() const
int classInfoOffset() const
const char *className() const
示例:
下面示例展示了如何使用QMetaObject
类和Q_CLASSINFO()
宏来获取类的元信息。
class MyObject : public QObject
{Q_OBJECTQ_CLASSINFO("version", "1.0")Q_CLASSINFO("author", "顽石")
public:
};class MyObject1 : public MyObject
{Q_OBJECTQ_CLASSINFO("version", "2.0")Q_CLASSINFO("name", "maye")
public:
};int main(int argc, char* argv[])
{QApplication a(argc, argv);MyObject1 obj;const QMetaObject* metaObj = obj.metaObject();int cnt = metaObj->classInfoCount();for (int i = 0; i < cnt; i++){qInfo() << metaObj->classInfo(i).name() << metaObj->classInfo(i).value();}qInfo() << metaObj->classInfoOffset();qInfo() << metaObj->className();return a.exec();
}
输出:
version 1.0
author 顽石
version 2.0
name maye
2
MyObject1
相关文章:
Qt 元对象系统
Qt 元对象系统 Qt 元对象系统1. 元对象的概念2. 元对象系统的核心组件2.1 QObject2.2 Q_OBJECT 宏2.3 Meta-Object Compiler (MOC) 3. 信号与槽3.1 基本概念信号与槽的本质信号和槽的关键特征 3.2 绑定信号与槽参数解析断开连接 3.3 标准信号与槽查找标准信号与槽使用示例规则与…...
鸿蒙实战:使用隐式Want启动Ability
文章目录 1. 实战概述2. 实现步骤2.1 创建鸿蒙应用项目2.2 修改Index.ets代码2.3 创建LuzhouAbility2.4 创建Luzhou页面2.5 设置模块配置文件 3. 测试效果4. 实战总结 1. 实战概述 本次鸿蒙应用实战,先创建项目“ImplicitWantStartAbility”,接着修改In…...
go-zero(二) api语法和goctl应用
go-zero api语法和goctl应用 在实际开发中,我们更倾向于使用 goctl 来快速生成代码。 goctl 可以根据 api快速生成代码模板,包括模型、逻辑、处理器、路由等,大幅提高开发效率。 一、构建api demo 现在我们通过 goctl 创建一个最小化的 HT…...
java 操作Mongodb
CRUD基础操作 Springboot 操作 MongoDB 有两种方式。 第一种方式是采用 Springboot 官方推荐的 JPA 方式,这种操作方式,使用简单但是灵活性比较差。第二种方式是采用 Spring Data MongoDB 封装的 MongoDB 官方 Java 驱动 MongoTemplate 对 MongoDB 进行…...
以Java为例,实现一个简单的命令行图书管理系统,包括添加图书、删除图书、查找图书等功能。
江河湖海中的代码之旅:打造你的命令行图书管理系统 一、系统简介 1. Java简介 Java,这个编程语言界的“瑞士军刀”,自1995年诞生以来就以其跨平台的特性和强大的生态系统征服了无数开发者的心。想象一下,Java就像是一条蜿蜒曲折…...
[JavaWeb]微头条项目
完整笔记和项目代码: https://pan.baidu.com/s/1PZBO0mfpwDPic4Ezsk8orA?pwdwwp5 提取码: wwp5 JavaWeb-微头条项目开发 1 项目简介 1.1 业务介绍 微头条新闻发布和浏览平台,主要包含业务如下 用户功能 注册功能登录功能 头条新闻 新闻的分页浏览通过标题关键字搜…...
Linux(CentOS)安装达梦数据库 dm8
CentOS版本:CentOS 7,查看操作系统版本信息,请查阅 查看Linux内核版本信息 达梦数据库版本:dm8 一、获取 dm8 安装文件 1、下载安装文件 打开达梦官网:https://www.dameng.com/ 下载的文件 解压后的文件 2、上传安…...
【专题】中国企业出海洞察报告暨解码全球制胜之道报告汇总PDF洞察(附原数据表)
原文链接:https://tecdat.cn/?p38314 在当今全球化的浪潮中,中国企业的出海行动正以前所未有的规模和速度展开,成为全球经济舞台上的重要力量。本报告旨在对 2024 年中国企业出海情况进行深度洞察,涵盖多个领域和视角。 从对外投…...
[ 跨域问题 ] 前后端以及服务端 解决跨域的各种方法
这篇文章主要介绍了跨域问题,包括其定义、产生原因及各种解决方法。原因是浏览器安全策略限制,方法有 JSONP、CORS、Domain、 postMessage、Nginx配置、.NetCore配置。 前言 什么是跨域问题? 在Web应用中,当一个网页的脚本试图去请求另一个域…...
网络安全之信息收集-实战-2
请注意,本文仅供合法和授权的渗透测试使用,任何未经授权的活动都是违法的。 目录 7、网络空间引擎搜索 8、github源码泄露 9、端口信息 10、框架指纹识别 11、WAF识别 12、后台查找 7、网络空间引擎搜索 FOFA:https://fofa.info/ 360 …...
利用飞书多维表格自动发布版本
文章目录 背景尝试1,轮询尝试2,长连接 背景 博主所在的部门比较奇特,每个车型每周都需要发版,所以实际上一周会发布好几个版本。经过之前使用流水线自动发版改造之后,发版的成本已经大大降低了,具体参考&a…...
深入内核讲明白Android Binder【一】
深入内核讲明白Android Binder【一】 前言一、Android Binder应用编写概述二、基于C语言编写Android Binder跨进程通信Demo0. Demo简介1. 服务的管理者server_manager.c2. Binder服务端代码实现 test_service.c2.1 实现思路2.2 完整实现代码 3. Binder客户端代码实现 test_clie…...
Photoshop(PS)——人像磨皮
1.新建一个文件,背景为白色,将图片素材放入文件中 2.利用CtrlJ 复制两个图层出来,选择第一个拷贝图层,选择滤镜---杂色---蒙尘与划痕 3.调整一下数值,大概能够模糊痘印痘坑,点击确定。 4.然后选择拷贝2图层…...
如何用Excel批量提取文件夹内所有文件名?两种简单方法推荐
在日常办公中,我们有时需要将文件夹中的所有文件名整理在Excel表格中,方便管理和查阅。手动复制文件名既费时又易出错,因此本文将介绍两种利用Excel自动提取文件夹中所有文件名的方法,帮助你快速整理文件信息。 方法一࿱…...
YOLOv8改进,YOLOv8通过RFAConv卷积创新空间注意力和标准卷积,包括RFCAConv, RFCBAMConv,二次创新C2f结构,助力涨点
摘要 空间注意力已广泛应用于提升卷积神经网络(CNN)的性能,但它存在一定的局限性。作者提出了一个新的视角,认为空间注意力机制本质上解决了卷积核参数共享的问题。然而,空间注意力生成的注意力图信息对于大尺寸卷积核来说是不足够的。因此,提出了一种新型的注意力机制—…...
【实验11】卷积神经网络(2)-基于LeNet实现手写体数字识别
👉🏼目录👈🏼 🍒1. 数据 1.1 准备数据 1.2 数据预处理 🍒2. 模型构建 2.1 模型测试 2.2 测试网络运算速度 2.3 输出模型参数量 2.4 输出模型计算量 🍒3. 模型训练 🍒4.模…...
chatgpt训练需要什么样的gpu硬件
训练像ChatGPT这样的大型语言模型对GPU硬件提出了极高的要求,因为这类模型的训练过程涉及大量的计算和数据处理。以下是训练ChatGPT所需的GPU硬件的关键要素: ### 1. **高性能计算能力** - **Tensor Cores**: 现代深度学习训练依赖于Tensor Cores&#…...
Kubernetes常用命令
Kubernetes常用命令 一、集群管理 kubectl cluster-info:显示集群信息,包括控制平面地址和服务的 URL。 kubectl get nodes:查看集群中的节点列表,包括节点状态、IP 地址等信息。 kubectl describe node <node-name>&…...
Flutter:key的作用原理(LocalKey ,GlobalKey)
第一段代码实现的内容:创建了3个块,随机3个颜色,每次点击按钮时,把第一个块删除 import dart:math; import package:flutter/material.dart; import package:flutter_one/demo.dart;void main() {runApp(const App()); }class App…...
R语言基础入门详解
文章目录 R语言基础入门详解一、引言二、R语言环境搭建1、安装R和RStudio1.1、步骤1.2、获取工作目录 三、R语言基础2、语法基础2.1、赋值操作2.2、注释 3、数据类型与结构3.1、向量3.2、矩阵 4、基本操作4.1、数据读取4.2、数据可视化 四、R语言使用示例4.1、统计分析示例4.2、…...
django启动项目报错解决办法
在启动此项目报错: 类似于: django.core.exceptions.ImproperlyConfigured: Requested setting EMOJI_IMG_TAG, but settings are not c启动方式选择django方式启动,以普通python方式启动会报错 2. 这句话提供了对遇到的错误的一个重要线索…...
详细描述一下Elasticsearch搜索的过程?
大家好,我是锋哥。今天分享关于【详细描述一下Elasticsearch搜索的过程?】面试题。希望对大家有帮助; 详细描述一下Elasticsearch搜索的过程? Elasticsearch 的搜索过程是其核心功能之一,允许用户对存储在 Elasticsea…...
Spring、SpringMVC、SpringBoot、Mybatis小结
Spring Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器(框架) Spring框架的核心特性包括依赖注入(Dependency Injection ,DI)、面向切面编程(Aspe…...
.NET 9 运行时中的新增功能
本文介绍了适用于 .NET 9 的 .NET 运行时中的新功能和性能改进。 文章目录 一、支持修剪的功能开关的属性模型二、UnsafeAccessorAttribute 支持泛型参数三、垃圾回收四、控制流实施技术.NET 安装搜索行为性能改进循环优化感应变量加宽Arm64 上的索引后寻址强度降低循环计数器可…...
Linux下安装mysql8.0版本
先确定我的下载安装的目录,安装文件是下载在 /opt/install 目录下面 (安装地址不同的话注意修改地址) 1.在线下载 wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz2.解压 tar -xvf mysql-8.0.20-linux-glibc2.12-x86_64.t…...
kvm-dmesg:从宿主机窥探虚拟机内核dmesg日志
在虚拟化环境中,实时获取虚拟机内核日志对于系统管理员和开发者来说至关重要。传统的 dmesg 工具可以方便地查看本地系统的内核日志,但在KVM(基于内核的虚拟机)环境下,获取虚拟机内部的内核日志则复杂得多。为了简化这…...
植物明星大乱斗15
能帮到你的话,就给个赞吧 😘 文章目录 player.hplayer.cppparticle.hparticle.cpp player.h #pragma once #include <graphics.h> #include "vector2.h" #include "animation.h" #include "playerID.h" #include &…...
go-zero(三) 数据库操作
go-zero 数据库操作 在本篇文章中,我们将实现一个用户注册和登录的服务。我们将为此构建一个简单而高效的 API,包括请求参数和响应参数的定义。 一、Mysql连接 1. 创建数据库和表 在 MySQL 中创建名为 test_zero的数据库,并创建user 表 …...
SQL面试题——间隔连续问题
间隔连续问题 某游戏公司记录的用户每日登录数据如下 +----+----------+ | id| date| +----+----------+ |1001|2021-12-12| |1001|2021-12-13| |1001|2021-12-14| |1001|2021-12-16| |1001|2021-12-19| |1001|2021-12-20| |1002|2021-12-12| |1002|2021-12-16| |1002|…...
vim配置 --> 在创建的普通用户下
在目录/etc/ 下面,有个名为vimrc 的文件,这是系统中公共的vim配置文件对所有用户都有效 我们现在创建一个普通用户 dm 创建好以后,我们退出重新链接 再切换到普通用户下 再输入密码(是不显示的,输入完后,…...
推广网站的公司/缅甸最新新闻
应该是非常简单的动态规划了,要随时的记录每个点的状态,这样递归的时候直接用就可以了,不需要再次寻找,大大减少时耗。 重点是状态转移方程 dp[x][y]max(dp[x-1][y], dp[x1][y], dp[x][y-1], dp[x][y1])1 当前的最长长度ÿ…...
网站开发 绩效考核/新东方英语线下培训学校
状态名解释1xx(临时响应)表示临时响应并需要请求者继续执行操作的状态码。100(继续)请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。101(切换协议)请求者…...
国外设计师灵感网站/以图搜图百度识图
目录1. 两者区别的现象2. 部分代码解释2.1 open显示对话框2.1.1 对象方式创建2.1.2 指针方式创建2.2 exec显示对话框3. exec与open的区别4. 全部代码示例4.1 MainWindow.h4.2 MainWindow.cpp1. 两者区别的现象 注意:需要看下方的打印信息 2. 部分代码解释 2.1 open…...
云南旅游网站设计/东莞做网站推广的公司
初探HTML5:制作一份邀请函目的:制作这个简易的邀请函,只是为了让新手入门Web开发。 在制作页面之前,我们先来看看整个邀请函的整体面貌。 一.首先编写HTML代码 1 <!DOCTYPE html>2 <html>3 <head>4 <meta charse…...
山西建设执业注册中心网站/百度域名注册官网
Redis 压缩列表(ziplist)1. 介绍压缩列表(ziplist)是哈希键的底层实现之一。它是经过特殊编码的双向链表,和整数集合(intset)一样,是为了提高内存的存储效率而设计的。当保存的对象是小整数值,或者是长度较短的字符串,那么redis就…...
君山区建设局网站/百度广告投放价格
配置阿里云ECS支持IPv6 前几天有个小伙伴的 iOS App 提交给水果审核没通过,水果给出的原因是应用在 IPv6 的环境下无法使用。原来小伙伴用的服务器是阿里云的 ECS ,而这玩意并不支持 IPv6 。。。这里就不得不吐槽一下阿里云这么大的云服务厂商࿰…...