织梦怎么在本地编辑多个网站,网页设计实训报告格式,安卓app开发框架,旅游网站建设现状Qt 元对象系统 Qt 元对象系统1. 元对象的概念2. 元对象系统的核心组件2.1 QObject2.2 Q_OBJECT 宏2.3 Meta-Object Compiler (MOC) 3. 信号与槽3.1 基本概念信号与槽的本质信号和槽的关键特征 3.2 绑定信号与槽参数解析断开连接 3.3 标准信号与槽查找标准信号与槽使用示例规则与… Qt 元对象系统 Qt 元对象系统1. 元对象的概念2. 元对象系统的核心组件2.1 QObject2.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 智能指针QPointerQScopedPointerQSharedPointerQWeakPointerQScopedArrayPointerQSharedDataPointer隐式与显式共享优化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)
MOCMeta-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 QMainWindowclass 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_funcchecked;
}
//全局函数
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, QOverloadQString::of(MyButton::hungury), this, QOverloadQString::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内存就不会释放会发生内存泄漏
#includeQApplication
#includeQDebug
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();
}使用指定父对象的方式自动管理内存
#includeQApplication
#includeQDebugclass 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和指定的类型TT可以是父类查找子对象。如果没有这样的子对象则返回nullptr。 示例返回名为“button1”的parentWidget的子QPushButton即使该按钮不是父级的直接子级 QPushButton *button parentWidget-findChildQPushButton *(button1);示例返回parentWidget的QListWidget子组件 QListWidget *list parentWidget-findChildQListWidget *();示例返回parentWidget它的直接父元素的一个名为button1的子QPushButton QPushButton *button parentWidget-findChildQPushButton *(button1, Qt::FindDirectChildrenOnly);示例返回parentWidget的QListWidget子组件它的直接父组件 QListWidget *list parentWidget-findChildQListWidget *(QString(), Qt::FindDirectChildrenOnly);通过类型查找对象 QListT findChildren(const QString name QString(), Qt::FindChildOptions options Qt::FindChildrenRecursively) constQListT findChildren(const QRegularExpression re, Qt::FindChildOptions options Qt::FindChildrenRecursively) const根据指定的名称name和指定的类型TT可以是父类查找子对象。如果没有这样的子对象则返回nullptr。 示例查找名为widgetname的指定父Widget的子Widget列表 QListQWidget * widgets parentWidget.findChildrenQWidget *(widgetname);示例返回parentWidget的所有子QPushButton QListQPushButton * allPButtons parentWidget.findChildrenQPushButton *();示例返回所有与parentWidget直接关联的QPushButton QListQPushButton * childButtons parentWidget.findChildrenQPushButton *(QString(), Qt::FindDirectChildrenOnly);4.5 智能指针
在C中为了有效管理内存和其他资源程序员通常采用RAIIResource Acquisition Is Initialization机制在类的构造函数中分配资源在使用完成后通过析构函数释放资源。智能指针的引入使得程序员不再需要手动管理new对象的delete操作也无需编写复杂的异常捕获代码来释放资源因为智能指针能够在作用域结束时无论是正常退出还是异常退出自动调用delete来销毁在堆上动态分配的对象。
在Qt框架中提供了多种类型的智能指针以便于资源管理
智能指针描述QPointerQObject 专享指针当 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的析构过程进行时。
QPointerQLabel 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(){QScopedArrayPointerint 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 QStringclass 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:QSharedDataPointerEmployeeData d;};在上述Employee类中注意到数据成员d为QSharedDataPointer类型。所有对员工数据的访问均应通过d指针的operator-()进行。对于写访问操作符将自动调用detach()如果共享数据对象的引用计数大于1detach()将创建共享数据对象的副本。这样可以确保对一个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.hint 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指针声明为QExplicitlySharedDataPointerEmployeeData则使用显式共享写时复制操作不会自动执行(即在非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(); // 输出 mayeobj.unsetName(); // 将name重置为unknownobj.setProperty(name, 顽石);qInfo() obj.property(name).toString(); // 输出 顽石obj.setProperty(name, QVariant()); // 将name重置为unknownreturn 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.wrect.hrect.area; //2 5 10rect.w 3; //area:15qInfo()rect.wrect.hrect.area; //3 5 10
}从上面代码可以看出只有在构造对象时才能正确计算面积如果在对象定义之后修改宽度或者高度面积都将变得不正确因为它没有及时更新。
矩形的面积是依赖于矩形的宽度和高度的那么当高度或者高度变化之后应该需要自动更新面积这个应该如何做到呢
绑定表达式通过读取其他QProperty值来计算该值。在幕后跟踪这种依赖关系。每当检测到任何属性的依赖关系发生更改时都会重新计算绑定表达式并将新的结果应用于该属性。例如:
#include QCoreApplication
#include QProperty // Qt6引入
#include QDebugstruct Rectangle
{QPropertyint w{0}; // 宽度QPropertyint h{0}; // 高度QPropertyint 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 QObjectBindablePropertystruct 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的内省功能相对有限主要支持通过运行时类型识别RTTIRun-Time Type Information实现的类型内省。C的RTTI通过typeid和dynamic_cast关键字来实现以下是一个简要示例
// Dog类派生自Animal类jump为虚函数
if (Dog *pdog dynamic_castDog*(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_castQObject *(this)-qt_metacast(classname) ! nullptr;
}可见QObject::inherits方法是通过虚函数qt_metacast()实现的。每个QObject的派生类必须实现metaObject()以及其他qt_metacall()方法从而支持内省方法如className、inherits等的调用。
用户在派生自QObject的类中只需声明宏Q_OBJECTQt的元对象编译器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_castQWidget *(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_castint(type));qInfo() typeStr; // 输出: Playerreturn 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_castint(type));
qDebug() typeStr; // 输出: PlayerQMetaEnum
QMetaEnum类提供了多种功能以处理枚举。其主要功能包括
name()返回枚举项的名称。key()返回每个枚举项的键名称。keyCount()查找键的数量。isFlag()返回该枚举是否被设计为标志意味着它的值可以使用OR操作符组合。keyToValue()、valueToKey()、keysToValue()**和**valueToKeys()这些转换函数允许在枚举或集合值的整数表示与其文字表示之间进行转换。scope()返回声明此枚举的类的范围。
下面是一个使用QMetaEnum的示例展示如何定义枚举、注册枚举到元对象系统并利用QMetaEnum来获取枚举值和名称。
#include QCoreApplication
#include QMetaEnum
#include QObject
#include QDebugclass 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: Redlight.printCurrentState(TrafficLight::Yellow); // 输出: Current state: Yellowlight.printCurrentState(TrafficLight::Green); // 输出: Current state: Greenreturn a.exec();
}#include main.mocQMetaObject
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
MyObject1oc
## QMetaObjectQMetaObject类包含有关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