Qt模块 Core

Qt模块 Core1

1. 核心功能

Qt Core 给C++语言增加了以下特性

  • 信号与槽(signals and slots)机制:用于对象之间无缝通信的强大机制
  • 对象属性(object properties):可查询和可设计的对象属性
  • 对象树(object trees):对象层级树
  • 安全指针(guarded pointers):QPointer
  • 跨库边界的动态类型转换:a dynamic cast that works across library boundaries

1.1 元对象系统(The Meta-Object System)

元对象中的元是什么意思?可以简单的将元对象理解为描述对象的对象,类似的,元类型对象可以理解为描述类型的对象

Qt的元对象系统提供了:用于内部对象间通信的信号和槽机制、运行时获得类型信息的能力、动态属性系统

元对象系统基于以下三个东西:

  • QObject:一个类想要从元对象系统中获得收益,就必须继承自这个类
  • Q_OBJECT宏:放在类声明的private作用域中,用于开启元对象特性,例如动态属性、信号和槽
  • 元对象编译器(moc):作用于每一个QObject的子类,提供必要的代码以实现元对象特性

moc会读取C++源文件,如果他发现了有任何类的声明中包含了Q_OBJECT宏,那么它将产生另一些相应的C++源文件,其中包含了用于这些类的元对象代码(meta-object code)。被生成的源文件被#include进类的源文件,或者,更一般的,被编译和链接进这个类的实现中

除了提供信号和槽机制(引入元对象系统的主要原因)之外,元对象系统还提供了下面这些额外的特性:

  • QObject::metaObject():该函数返回和类相关联的元对象
  • QMetaObject::className():该函数在运行时将类名作为字符串返回
  • QObject::inherits():该函数返回是否一个对象是一个被指定的类的实例
  • QObject::tr()和QObject::trUtf8():翻译字符串,用于国际化
  • QObject::setProperty()和QObject::property():通过属性名动态的设置和获取属性
  • QMetaObject::newInstance():构造类的一个新实例

在QObject类上可以使用qobject_cast()去执行动态类型转换,这个函数的行为和标准C++中的dynamic_cast()很像,但是qobject_cast()不需要RTTI支持。这个函数尝试转换他的参数到尖括号中指定的指针类型,失败时返回空指针,成功时返回一个非空的指针

例如,下面假定MyWidget类继承自QWidget并且声明了Q_OBJECT宏

QObject *obj = new MyWidget;

变量obj的类型为QObject,但实际引用的类型为MyWidget,所以我们可以对他进行合适的转换

QWidget *widget = qobject_cast<QWidget *>(obj);

从QObject到QWidget的转换是成功的,因为对象本身的类型是MyWidget,它是QWidget的一个子类。因为我们知道obj是一个MyWidget,所以也可以转换为MyWidget*

MyWidget *myWidget = qobject_cast<MyWidget *>(obj);

转换成MyWidget是也成功的,因为qobject_cast()不区分内置的Qt类型还是自定义的类型

QLabel *label = qobject_cast<QLabel *>(obj);
// label is 0

转换成QLabel是失败的,label被设置为0,这个结果可以在运行时用来处理不同类型的不同对象

if (QLabel *label = qobject_cast<QLabel *>(obj)) {
        label->setText(tr("Ping"));
} else if (QPushButton *button = qobject_cast<QPushButton *>(obj)) {
    button->setText(tr("Pong!"));
}

一个类继承自QObject但是没有Q_OBJECT宏和元对象代码也是可以的,但是信号和槽机制以及在上面讲到的所有特性都不可用。从元对象系统的角度来看,一个QObject的没有元对象代码的子类相当于离他的最近的具有元对象代码的祖先。这意味着,例如,QMetaObject::className()函数将不会返回类的实际名字,而是他祖先的名字

因此,强烈推荐所有继承自QObject的类都使用Q_OBJECT宏,无论他是否真的需要信号和槽、属性等机制

1.2 属性系统

Qt提供了一些复杂的属性系统,他与编译器厂商提供的类似。然而,作为一个独立于编译器和平台的库,Qt不依赖与非标准的编译器特性,例如__property或[property]。Q提供的解决方案可以被任何Qt支持的平台下的任何标准的C++编译器所支持。它基于Qt中的元对象系统

1.2.1 声明属性

要声明一个属性,在一个继承自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])

这里有一些典型的属性例子,他们来自QWidget类

Q_PROPERTY(bool focus READ hasFocus)
Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
Q_PROPERTY(QCursor cursor READ cursor WRITE setCursor RESET unsetCursor)

下面的例子展示了如何使用MEMBER关键字将成员变量作为Qt属性导出,注意信号NOTIFY必须被指定以允许QML属性的绑定

一个属性的行为像是一个类的数据成员,但是他获得了一些额外的特性通过元对象系统

  • READ访问器函数在没有MEMBER变量被指定的情况下是需要的。它用于读取属性值。理想地,一个const函数被用于这个目的,这个函数的返回类型必须是属性的类型或者是对该类型的const引用,例如,QWidget::focus是一个只读属性,指定的READ函数是QWidget::hasFocus().
  • WRIETE访问器函数是可选的,他用于设置属性值。这个函数必须返回空并且只有一个参数,参数的类型要么是属性的类型,要么是指向这个类型的指针或引用。例如,QWidget::enabled有一个WRITE函数QWidget::setEnabled()。只读属性不需要WRITE函数,例如,QWidget::focus没有WRITE函数
  • MEMBER在没有指定READ访问器函数的情况下需要被关联到一个成员变量。这使得给定的成员变量在不需要READWRITE访问器的情况下可读可写。READWRITE访问器和MEMBER关联的变量一起使用也是可以的(但是不能同时使用)
  • RESET函数是可选的,他用与将属性设置回指定的默认值。例如,QWidget::cursor有READWRITE函数QWidget::cursor()和QWidget::setCursor(),并且也有RESET函数QWidget::unsetCursor(),因为调用QWidget::setCursor()不意味着将光标重置为指定位置。RESET函数必须返回void并且不带参数
  • NOTIFY信号是可选的。如果被定义,他应该指定他所在类中的一个已存在的信号,这个信号当属性值改变时被发射。用于MEMBER变量的NOTIFY信号必须带有0个或1个参数,这个参数必须和属性有相同的类型,属性的新值将被作为参数传递。NOTIFY信号只应该在属性值真的改变时被发射
  • REVISION号是可选的,如果被包含了,那么他定义了属性和他的通知器信号被用于一个特殊的API版本。如果不被包含则默认为0
  • DESIGNABLE表明了在GUI设计工具(例如Qt Designer)的属性编辑器中是否这个属性应该被显示。大多数的属性都是DESIGNABLE(默认为true)。他的值为true或false,你可以指定一个boolean类型的成员函数
  • SCRIPTABLE表明了这个属性是否应该被脚本引擎访问(默认为true)。他的值为true或false,你可以指定一个boolean类型的成员函数
  • STORED表明了这个属性被认为是他自己存在的还是取决于其他值。他也表明了属性值是否必须被存储当存储对象的状态时。大多数的属性都是STORED(默认为true)
  • USER表明了是否这个属性被设计为面向用户或用户可编辑的,每个类中只有一个USER属性(默认为false)
  • CONSTANT的存在表明了属性值为常量。对于一个给定的对象实例,一个const属性的READ方法在每次调用时必须返回相同的值。常量值在不同对象的实例中是不同的。const属性不能有WRITE方法或NOTIFY信号
  • FINAL的存在表明了该属性不能被继承的类重写。这在一些形况下被用于性能优化,但是不会被moc强制执行。需要注意的是不要去重写FINAL属性

READWRITERESET函数可以被继承,他们也可以是虚函数。当用于被使用多重继承的类中时,他们必须来自第一个被继承的类

属性的类型可以为任意被QVariant支持的类型,或者为一个用户自定义的类型。在下面的例子中,类QDate被考虑为一个用户自定义的类型

Q_PROPERTY(QDate date READ getDate WRITE setDate)

因为QDate是自定义的,所以你必须包含<QDate>头文件

由于历史原因,QMap和QList作为属性类型,他们是QVariantMap和QVariantList的同义词

1.2.2 使用元对象系统读写属性

使用泛型函数QObject::property()和QObject::setProperty()可以读写属性,只需要知道属性名即可。在下面的代码中,调用QAbstractButton::setDown()和QObject::setProperty()都在蛇者属性down

QPushButton *button = new QPushButton;
QObject *object = button;

button->setDown(true);
object->setProperty("down", true);

在这两种方式中,使用WRITE访问器访问属性是更好的方式,因为他是更快的并且在编译时可以提供一个更好的诊断,而设置属性这种方式需要你在运行时了解这个类。通过名字访问属性可以让你访问一个在编译时你不知道的类。你可以在运行时发现一个类的属性通过查询他的QObject、QMetaObject和QMetaProperties.

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);
    ...
}

在上面的代码中,QMetaObject::property()被用来获得每一个被定义的属性的metadata。属性名从metadata中得到然后传递给QObject::property()去得到属性值

1.2.3 一个简单的例子

假设我们有一个类MyClass,他继承于QObject并且声明了Q_OBJECT宏。我们想在MyClass中声明一个属性去追踪一个属性值。属性名为priority,类型为被定义在MyClass中的枚举类型Priority

我们使用宏Q_PROPERTY()在类private部分中声明属性。READ函数被命名为priority,WRITE函数被命名为setPriority。枚举类型在元对象系统中被注册,使用宏Q_ENUM()。注册枚举类型使枚举值可以在调用QObject::setProperty()时使用。我们必须提供我们我们自己的用于READWRITE函数的声明。MyClass类的声明如下:

class MyClass : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Priority priority READ priority WRITE setPriority NOTIFY priorityChanged)

public:
    MyClass(QObject *parent = 0);
    ~MyClass();

    enum Priority { High, Low, VeryHigh, VeryLow };
    Q_ENUM(Priority)

    void setPriority(Priority priority)
    {
        m_priority = priority;
        emit priorityChanged(priority);
    }
    Priority priority() const
    { return m_priority; }

signals:
    void priorityChanged(Priority);

private:
    Priority m_priority;
};

READ函数是const的并且返回类型为属性的类型。WRITE函数的返回类型为void并且只有一个类型为属性类型的参数。元对象编译器会强制执行这个要求

用一个指向MyClass实例的指针或一个指向MyClass实例的QObject类型的指针,我们有两种方式去设置他的priority属性

MyClass *myinstance = new MyClass;
QObject *object = myinstance;

myinstance->setPriority(MyClass::VeryHigh);
object->setProperty("priority", "VeryHigh");

在这个例子中,枚举类型被声明在MyClass中并且在元对象系统中使用Q_ENUM()宏注册。这使得枚举值可以在调用setProperty()时作为字符串使用。如果枚举类型是在另一个类中声明的,则需要它的完全限定名(即OtherClass::Priority),同时这个类也需要继承自QObject并且使用Q_ENUM()宏注册枚举类型

一个相似的宏,Q_FLAG(),也是可用的。像Q_ENUM()一样,他也可以注册一个枚举类型,但是标记该类型为一个flags集合,即他的值是可以组合起来的集合(也就是位集)。一个I/O类可能有枚举值ReadWrite,并且QObject::setProperty()可以接受Read | Write。Q_FLAG()被用来注册这种枚举类型

1.2.4 动态属性

QObject::setProperty()也可以用来给一个类的实例在运行时增加一个新的属性。当他调用时传递一个name和一个value,如果给定的name已经在这个QObject中存在,并且给定的值与已存在属性的类型相兼容,那么给定的value将被存储在这个属性中,并且返回true。如果给定的value与这个属性不兼容,那么属性值不会被改变,并且返回false。但是,如果给定的name不存在于QObject中,一个新的属性将被调价到这个QObject中,属性名和属性值为name和value,但是仍然返回false。这意味着返回false不能被用来检测是否一个特殊的属性确实被设置了,除非你提前知道这个属性已经存在于这个QObject中

注意动态属性以每个实例为基础被增加,也就是说,他们被增加到QObject,而不是QMetaObject中。一个属性可以从一个实例中移除,通过传递属性名和一个无效的QVariant值给QObject::setProperty()。QVariant的默认构造器是一个无效的QVariant

动态属性可以被QObject::property()所查询,就像在编译时使用Q_PROPERTY()声明的属性一样

1.2.5 属性和自定义类型

被属性使用的自定义类型需要使用Q_DECLARE_METATYPE()宏进行注册,以便他们的值可以被存储在QVariant对象中。这使得他们在动态属性(在运行时创建的)和静态属性(使用宏声明的)中都能够使用

1.2.6 附加给一个类的额外信息

Q_CLASSINFO()是一个附加的宏,它被连接到属性系统,可以被用来固定一个给类的元对象的name–value对,例如

Q_CLASSINFO("Version", "3.0.0")

像其他元数据一样,类的信息可以在运行时通过元对象访问,查看QMetaObject::classInfo()获取更多相关的细节

1.3 对象模型

标准C++对象模型提供了非常高效的运行时支持给对象范式。但是他的静态特征在某些问题领域时不灵活的。GUI编程是一个需要运行时效率和高灵活性的领域。Qt提供了这些能力,通过结合C++的速度和Qt对象模型

Qt增加了以下特性给C++:

  • 用于对象间无缝通信的强大机制,信号和槽
  • 可查询和可设计的对象属性
  • 事件和事件过滤器:
  • 用于国际化的上下文翻译字符串
  • 精密的计时器,使在事件驱动的GUI的优雅的集成许多任务成为可能
  • 层次化和可查询的对象树,以自然的方式组织对象的所有权
  • 智能指针(QPointer),当被引用的对象被销毁时自动设置为0
  • 动态类型转换
  • 自定义类型创建的支持

这些Qt特性中的许多使用标准C++技术实现,基于对QObject的继承。其他的特性,例如对象间通信的机制和动态属性系统,需要元对象系统的支持,它被Qt自己的元对象编译器(moc)所提供

元对象系统时一个C++的扩展,让语言更适合真正的组件GUI编程

1.3.1 重要的类

这些类形成了Qt对象模型的基础

QMetaClassInfo关于一个类的额外信息
QMetaEnum关于一个枚举的元数据
QMetaMethod关于一个成员函数的元数据
QMetaObject关于Qt对象包含的元信息
QMetaProperty关于一个属性的元数据
QMetaType在元对象系统中被管理的类型
QObject所有Qt对象的基类
QObjectCleanupHandler监视多个QObject的生命周期
QPointer提供安全指针的模板类
QSignalBlocker异常安全包装器
QSignalMapper捆绑来自可识别发送者的信号
QVariant行为像最常见Qt数据类型的联合体

1.3.2 Qt对象:Ideneity vs Value

用于Qt对象模型的一些额外特性在上面被列出,我们需要将Qt对象看作identities而不是values。values时可拷贝和可赋值的;identities是可克隆的。克隆意味着创建一个新的对象,而不是旧版本的一个精确的拷贝。例如,双胞胎有不同的identities,他们看起来完全相同,但是有不同的名字,不同的位置,可能还有完全不同的社交网络

克隆一个identity是一个比拷贝或赋值一个值更加复杂的操作

一个Qt对象…

  • 可能有一个独特的QObject::objectName()。如果我们拷贝一个Qt对象,我们应该给这个副本一个什么名字?
  • 在对象层级中有一个位置。如果我们拷贝一个Qt对象,我们应该拷贝那个位置?
  • 可以被连接到其他Qt对象以发射信号给他们,或者接受被他们发射的信号。如果我们拷贝一个Qt对象,我们应该如何转移这些连接给新的副本?
  • 可以在运行时添加新的属性。如果我们拷贝一个Qt对象,应该拷贝这些我们添加的属性吗?

因为这些原因,Qt对象应该被看做identities而不是values。identities被克隆,不是拷贝或赋值,并且克隆一个identities是比拷贝或赋值一个值更复杂的操作。因此,QObject和QObject的所有子类的拷贝构造函数和赋值运算符都被禁用了

1.4 对象树和所有权

QObject组织他们自己在一棵对象树中。当你使用另一个对象作为父对象创建一个QObject时,他被添加到父对象的children()列表中,并且当父对象删除时同时也被删除。事实证明,这种方法非常适合GUI对象的需要。例如,一个QShortcut(键盘快捷键)是一个相关窗口的子对象,所以当用户关闭这个窗口时,这个快捷键也会被删除

QQuickItem,Qt Quick模块的基本可视化元素,继承自QObject,但是有一个可视化父对象的概念,与QObject父对象不同。一个元素的可视化父节点可能没有必要和他的父对象相同。查看Concepts - Visual Parent in Qt Quick获取更多细节

QWidget,Qt Widgets模块的基础类,扩展了父子关系。一个子对象通常也是一个子widget,也就是说,它显示在它的父对象的坐标系统中并且图像在它的父对象的边界上被裁剪。例如,当一个应用程序被关闭后删除了一个message box,这个message box的按钮和标签也会被删除,就像我们想要的那样,因为按钮和标签是message box的子对象

你也可以自己删除子对象,并且他们将从他们的父对象中移除。例如,当用户移除一个工具栏时,这可能导致应用程序删除某些QToolBar对象,在这种形况下,工具栏的父对象QMainWindow会检测更改并且重新配置屏幕空间

调试函数QObject::dumpObjectTree()和QObject::dumpObjectInfo()当一个应用程序的动作变得很奇怪时通常是有用的

1.4.1 QObject的构造/析构顺序

当QObject被创建在堆上时(即使用new创建),一棵树会被构造以任何顺序,之后,在这棵树上的对象可以被销毁以任何顺序。当在这棵树上的任何一个QObject被删除时,如果这个对象有父对象,其析构函数自动将该对象从他的父对象中移除。如果这个对象有子对象,其析构函数自动地删除每一个子对象。无论析构的顺序如何,一个QObject对象不会被删除两次

当QObject被创建在栈上时,会产生相同的行为。通常来说,析构的顺序仍然没有问题。考虑下面的代码:

int main()
{
    QWidget window;
    QPushButton quit("Quit", &window);
    ...
}

父对象window和子对象quit,都是QObject。这些代码是正确的:quit的析构函数不会被调用两次,因为C++语言标准指明了局部变量的析构函数以和他们构造的相反的顺序调用。因此,子对象quit的构造函数先被调用,然后它从他的父对象window中移除他自己。

但是,现在考虑交换构造顺序的情况:

int main()
{
    QPushButton quit("Quit");
    QWidget window;

    quit.setParent(&window);
    ...
}

在这个情况下。析构的顺序会造成问题。父对象的析构函数早于子对象调用,然后父对象会调用子对象的析构函数,这是错误的行为,因为quit是一个局部变量。当quit超出作用域时,他的析构函数再一次被调用,这次是正确的,但是已经造成了损害

1.5 信号和槽

信号和槽用于对象间通信。信号和槽机制是Qt的核心特性,并且可能是与其他框架提供的最不相同的特性。Qt的元对象系统让信号和槽机制成为可能

1.5.1 引入

在GUI编程中,当我们改变一个widget时,我们通常希望另一个widget被通知。更一般的,我们希望任何类型的对象都能够和其他对象沟通。例如,如果用户点击了Close按钮,我们可能希望窗口的close()函数被调用

其他框架实现这种沟通使用回调机制。回调是一个指向函数的指针,所以如果你想让一个处理函数去通知你的一些事件,你需要传递一个指向另一个函数的指针(回调函数)给这个处理函数。处理函数将在合适的时候进行调用。虽然存在使用这个机制的成功框架,但是,回调可能会在确保回调参数类型的正确性上遭受问题

1.5.2 信号和槽

在Qt中,我们有一个代替回调的技术。我们使用信号和槽。当一个特殊的事件发生时,一个信号被发射。Qt的wingets有却多预定义的信号,但是我们总是会去继承这些widgets以增加我们自己的信号和槽。一个槽是一个函数,它被调用以对一个特殊的信号做出响应。Qt的wingets有许多预定义的槽,但是普遍的做法是去继承这些widgets并且增加你自己的信号和槽,以便于你能处理你感兴趣的那些信号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rMNsxkJw-1587833055435)(abstract-connections.png)]

信号和槽机制是类型安全的:一个信号的签名必须和接受槽的签名相匹配。(事实上一个槽可以有一个更短的签名比它要接受的信号,因为它可以忽略额外的参数。)因为签名是兼容的,所以当使用基于指针的函数的语法时,编译器能帮我们检测类型不匹配。使用基于字符串的语法时将在运行时检测类型不匹配。信号和槽是低耦合的:发射一个信号的类既不知道也不关心哪个槽会接收这个信号。Qt的信号和槽机制确保了,如果你连接了一个信号和槽,槽将在正确的事件使用信号的参数来调用。信号和槽可以传递任意数量任意类型的参数。他们都是类型安全的

所有继承自QObject的类都可以包含信号和槽,当对象改变了他们的状态时,这种改变可能被另一个对象感兴趣,信号被对象发射。对象即不知道也不关心谁接收了它发送的信号。这是正确的信息封装,确保对象可以用作软件组件

槽被用于接受信号,但是他们也是普通的成员函数。就像一个对象不知道谁接收了它的信号一样,槽也不知道是否有任何的信号连接到它了。这确保了能被Qt创建的正确的组件依赖

你可以连接许多信号到一个单独的槽,一个信号也可以被连接到多个你需要的槽。甚至可以直接连接一个信号到另一个信号。(这将立即发射两次信号无论何时发出第一个)

信号和槽共同组成了一个强大的组件编程机制

1.5.3 信号

当一个对象的内部状态被改变且这种改变可能被另一个对象感兴趣时,信号被这个对象发射。信号是public函数并且可以从任何地方发射,但是我们推荐只在定义的这个类和它的子类中发射

当信号被发射时,被连接的槽通常会立即被执行,就像一次普通的函数调用一样。当这件事发生时,信号和槽机制完全独立于任何GUI事件循环。emit语句后面的代码将在所有槽返回时执行。使用队列连接时情况略有不同,在这个情况下,emit语句后面的代码将立即被执行,槽将在之后被执行

如果几个槽被连接到一个信号,当信号被发射时,槽将被一个接一个的执行,顺序和被连接时的顺序一样

信号被moc自动生成并且不能在.cpp文件中实现,他们的返回类型为void

注意:我们的经验告诉我们,如果信号和槽不使用特殊的类型,他们的复用性将更强。如果QScrollBar::valueChanged()使用的是一个特殊类型例如hypothetical QScrollBar::Range,他将只能被连接到QScrollBar,连接不同的input widget将变得不可能

1.5.4 槽

当一个被连接到槽的信号发射时,槽被调用。槽是普通的C++函数;他们唯一特殊的地方就是可以被信号连接到他们

因为槽是普通的C++成员函数,所以当直接调用时遵循普通C++的规则。槽可以被任何组件调用,无论他们的访问权限如何。这意味着一个任意类的实例发射的信号,可以导致一个private的槽被调用

你也可以将槽定义为virtual,我们发现这在实践中是相当有用的

和回调相比,信号和槽的速度更慢,因为他们提供了更好的灵活性,但这种差距在实际的应用程序中是无关紧要的。通常来说,发射一个信号比直接调用接收非virtual函数要慢10倍左右。这些是定位连接对象的开销。10倍听上去有点多,但是他比new或delete操作有更小的开销。信号和槽机制的简单性和灵活性非常值得这些开销,用户甚至不会注意到这种开销

注意:当编译一个基于Qt的应用程序时,其他库中定义的signals和slots变量可能导致编译器警告和错误。要解决这个问题,使用#undef

1.5.5 一个简单的例子

一个C++类的声明

class Counter
{
public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }
    void setValue(int value);

private:
    int m_value;
};

一个基于Qt的类

#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};

基于Qt的版本有相同的内部状态,并且提供了public方法去访问这些状态,但是使用了信号和槽去增加了对组件编程的支持。这个类可以通过发射信号valueChanged()来告诉外部世界他的状态改变了,同时他也有一个接收其他对象发出信号的槽

所有包含信号和槽的类都必须声明Q_OBJECT宏,他们也必须继承自QObject

槽被程序员实现,下面是Counter::setValue()的一个可能实现

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}

emit发射信号valueChanged(),使用新的值作为参数

在下面的代码中,我们创建了两个Counter对象,并且连接了第一个对象的valueChanged()给第二个对象的setValue()使用QObject::connect()

Counter a, b;
QObject::connect(&a, &Counter::valueChanged,
                    &b, &Counter::setValue);

a.setValue(12);     // a.value() == 12, b.value() == 12
b.setValue(48);     // a.value() == 12, b.value() == 48

调用a.setValue(12)产生了一个valueChanged(12)信号,b在他的setValue()槽中接收。然后b发射了相同的信号valueChanged(),但是因为没有槽被它连接,所以这个信号将被忽略

注意setValue()函数设置值并且只在value != m_value是发射信号,这阻止了在循环连接时的无限调用

默认情况下,对于你建立的每一个连接都会发出一个信号,对于重复连接,会发出两个信号。你可以调用disconnect()去打断所有的这些连接,如果你传递了Qt::UniqueConnection类型的参数,连接只在他们不重复的情况下建立,如果他们已经重复了,连接将会失败并且返回false

这些例子说明了对象可以一起工作而不需要知道对方的信息。

1.5.6 一个真实的例子

下面是一个简单的widget头文件,目的是向你展示如何在你的应用程序中使用信号和槽

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
    Q_OBJECT

LcdNumber继承自QObject,Q_OBJECT宏被预处理器展开,它声明了一些被moc实现的成员函数,如果在编译时看到了错误 “undefined reference to vtable for LcdNumber”,你可能忘记运行moc或者在链接命令中包含moc输出了

public:
    LcdNumber(QWidget *parent = nullptr);

signals:
    void overflow();

当LcdNumber被要求显示一个无法显示的数字是,它发射信号overflow()

如果你不关心溢出,或者你知道不会发生溢出,你可以忽略overflow()信号,也就是说它可以不被连接到其他槽

在数字溢出时,如果你想要调用两个不同的错误处理函数,只需要连接这个信号到两个不同的槽即可

public slots:
    void display(int num);
    void display(double num);
    void display(const QString &str);
    void setHexMode();
    void setDecMode();
    void setOctMode();
    void setBinMode();
    void setSmallDecimalPoint(bool point);
};

#endif

注意:display()被重载了,当你连接一个信号和槽时,Qt将会选择一个合适的版本

1.5.7 带有默认参数的信号和槽

信号和槽的签名可以带有默认参数。考虑QObject::destroyed()

void destroyed(QObject* = nullptr);

当一个对象被删除时,他会发射QObject::destroyed()信号。我们想捕获这个信号,以便我们可以清理对被删除对象的所有引用,一个合适的槽的签名如下

void objectDestroyed(QObject* obj = nullptr);

使用QObject::connect()建立信号和槽的连接,有很多连接信号和槽的方式,第一种是使用函数指针

connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);

使用函数指针有这些优势:它允许编译器去检查信号和槽的参数是否兼容,在需要时编译器会对参数进行隐式类型转换

也可以使用c++11中的lambda表达式

connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });

lambda将在发送者被销毁时断开连接,当信号发射时,你需要注意依然存活着的接收者对象

其他连接信号和槽的方式是使用SIGNALSLOT宏。传递给SIGNAL()宏的参数必须少于SLOT()宏

下面都是正确的

connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

下面是错误的

connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

1.5.8 信号和槽的高级用法

当你需要获取发送信号对象的信息时,Qt提供了QObject::sender()函数,他返回一个执行发送信号对象的指针

lambda表达式是传递自定义参数给槽的一个很方便的方式

connect(action, &QAction::triggered, engine,
        [=]() { engine->processAction(action->text()); });

1.5.9 在第三方信号和槽中使用Qt

在第三方信号和槽机制中使用Qt也是可能的,你甚至可以在相同的项目中同时使用这两种机制,只需要添加下面的东西到你的.pro文件中

CONFIG += no_keywords

它告诉Qt不要定义关键字signals、slots和emit,因为这些名字将被第三方库使用,例如Boost。替换所有Qt关键字使用对应的宏 Q_SIGNALS(or Q_SIGNAL)、Q_SLOTS(or Q_SLOT)、Q_EMIT

1.6 计时器

QObject是所有Qt对象的基类,它提供了基本的计时器支持。使用QObject::startTimer(),启动一个计时器,以毫秒为单位作为参数传递。这个函数返回一个独特的计时器整型ID,使用QObject::killTimer()并传递计时器ID可以销毁一个计时器

使用这个机制的应用程序必须运行在一个事件循环中,使用QApplication::exec()启动一个事件循环。当计时器触发时,应用程序发出一个QTimerEvent对象,并且伴随着事件循环的控制等级直到计时器事件被处理,这说明当应用程序正忙于做其他事的时候计时器不会被触发。换句话说:计时器的准确度取决于应用程序的粒度

在多线程应用程序中,你可以使用计时器在任何处于事件循环的线程中。使用QThread::exec()在非GUI线程中启动事件循环。不可以启动计时器在另一个线程中

间隔值的上限由可以在有符号整数中指定的毫秒数确定(实际上只有24天多一点),精度取决于操作系统。Windows 2000的精度为15毫秒,其他测试过的操作系统可以处理1毫秒的间隔

用于计时器功能的主要API是QTimer。这个类提供了常规的计时器,当计时器触发时发送信号,它继承自QObject。一般的使用方式如下

QTimer *timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(updateCaption()));
timer->start(1000);

QTimer对象为这个widget的子对象,当widget被删除时,这个计时器也被删除。timeout()信号被关联到将要工作的槽。启动计时器,间隔为1000毫秒

QTimer也提供了一个静态函数用于只触发一次的计时器,例如

QTimer::singleShot(200, this, SLOT(updateCaption()));

200毫秒之后,updateCaption()将被调用

计时器事件只在事件循环运行时被传递

模拟时钟案例展示了如何使用QTimer去重绘一个widget,下面的代码来自这个例子中

AnalogClock::AnalogClock(QWidget *parent)
    : QWidget(parent)
{
    QTimer *timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(update()));
    timer->start(1000);
    ...
}

每秒钟,QTimer将调用QWidget::update()去刷新时钟的显示

QBasicTimer可以在你的QObject子类中重新实现timerEvent(),Wiggly例子展示了如何使用QBasicTimer

1.7 QPointer

智能指针QPointer<T>,行为和标准C++的指针T*一样,但在被引用的对象摧毁时自动被清理(标准C++的指针在此时会变成野指针)。T必须为QObject的子类

1.8 创建自定义Qt类型

开发者有时候需要创建新类型来代替Qt已存在的类型,标准类型例如QSize、QColor和QString都可以被存储在QVariant对象中,下面描述了如何将一个自定义的类型集成到Qt对象模型中,以便他能像标准Qt类型一样存储

1.8.1 创建一个自定义类型

在开始之前,我们需要确保自定义的类型完全满足QMetaType的需求,即它必须提供:

  • 一个public的构造函数
  • 一个public的拷贝构造函数
  • 一个public的析构函数

下面的Message类定义了这些成员

class Message
{
public:
    Message() = default;
    ~Message() = default;
    Message(const Message &) = default;
    Message &operator=(const Message &) = default;

    Message(const QString &body, const QStringList &headers);

    QString body() const;
    QStringList headers() const;

private:
    QString m_body;
    QStringList m_headers;
};

这个类提供了获取private成员的两个public成员函数

1.8.2 声明QMetaType类型

此时Message的值不能存储在QVariant中

为了让QVariant知道这个类,在类定义的头文件中调用Q_DECLARE_METATYPE()宏

Q_DECLARE_METATYPE(Message);

现在Message的值可以存储在QVariant对象中

Q_DECLARE_METATYPE()宏可以使该类型作为信号的参数,但是只能用于direct signal-slot的信号和槽。要让自定义类型在通用的信号和槽机制中使用,我们还需要做一些额外的工作

1.8.3 创建和销毁自定义类型

在上一节中声明的类型不能用于queued signal-slot的信号和槽,这是因为在运行时元对象系统不知道如何去创建和销毁自定义对象

要让对象可以在运行时被创建,调用qRegisterMetaType()模板函数在元对象系统中注册他,这也使得该类型可用于queued signal-slot的信号和槽

Queued Custom Type Example案例中声明了一个Block类,他在main.cpp中被注册

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    ...
    qRegisterMetaType<Block>();
    ...
    return app.exec();
}

之后在window.cpp中被用于信号和槽的连接

Window::Window(QWidget *parent)
    : QWidget(parent), thread(new RenderThread(this))
{
    ...
    connect(thread, &RenderThread::sendBlock,
            this, &Window::addBlock);
    ...
    setWindowTitle(tr("Queued Custom Type"));
}

如果一个类型没有被注册而使用在queued connection连接中,控制台将打印一些警告

QObject::connect: Cannot queue arguments of type 'Block'
(Make sure 'Block' is registered using qRegisterMetaType().)

1.8.4 让类型可打印

在调试时让一个对象可打印通常是有用的,例如

Message message(body, headers);
qDebug() << "Original:" << message;

给这个类型创建一个流操作来实现,他通常被定义在头文件中

QDebug operator<<(QDebug dbg, const Message &message);

实现

QDebug operator<<(QDebug dbg, const Message &message)
{
    const QString body = message.body();
    QVector<QStringRef> pieces = body.splitRef("\r\n", QString::SkipEmptyParts);
    if (pieces.isEmpty())
        dbg.nospace() << "Message()";
    else if (pieces.size() == 1)
        dbg.nospace() << "Message(" << pieces.first() << ")";
    else
        dbg.nospace() << "Message(" << pieces.first() << " ...)";
    return dbg.maybeSpace();
}

注意:这个函数的返回值是QDebug对象本身

1.9 事件系统

在Qt中,事件是一个对象,它继承自抽象类QEvent,用来代表应用程序中需要知道的活动结果或应用程序中发生的事情。事件可以被一个QObject的实例接收和处理,但是他们与widget密切相关。本文档描述的是在典型的应用程序中事件是如何被传递和处理的

1.9.1 事件是如何传递的

当一个事件发生时,Qt将构造一个合适的QEvent子类的对象来代表此事件,并且通过调用QObject的event()函数将事件传递给一个特殊的QObject的实例

event()函数不处理事件本身,而是基于事件的类型,在这个函数内调用特定事件类型的处理函数,并发出一个是否事件被接受或被忽略的响应

一些事件例如QMouseEvent和QKeyEvent,他们来自window系统;一些事件例如QTimerEvent,来自其他源;一些事件来自应用程序本身

1.9.2 事件的类型

大多数事件都有特定的类,尤其是QResizeEvent、QPaintEvent、QMouseEvent、QKeyEvent和QCloseEvent。每一个类都继承了QEvent并增加了特殊的事件函数。例如QResizeEvent增加了size()和oldsize()让widgets知道他们的尺寸是如何被更改的

有些类支持多个事件类型,QMouseEvent支持鼠标按压、双击、移动和其他相关的操作

每个事件都有一个关联的类型,被定义在QEvent::type中,这是很方便的运行时类型信息资源,它可以用来快速的判断一个给定的事件对象是从哪构造的

因为程序需要用多种复杂的方式做出响应,所以Qt的事件传播机制是很灵活的

1.9.3 处理事件

一个事件被传递的正常方式是通过调用一个虚函数。例如QPaintEvent被传递通过调用QWidget::paintEvent(),虚函数能负责做出正确的反应,通常是重绘widget。如果你不想执行所有在你重写的虚函数中不必要的工作,此时你可以调用父类的实现

例如,下面的代码在一个自定义的check box widget中处理左键单击工作,剩下的工作全部交给基类QCheckBox

void MyCheckBox::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        // handle left mouse button here
    } else {
        // pass on other buttons to base class
        QCheckBox::mousePressEvent(event);
    }
}

有时候,没有这样一个特殊事件的函数,或者说是不够的。常见的例子是Tab键被按下。通常QWidget会拦截移动键盘焦点,但是一些widget需要响应Tab键

这些对象可以重写QObject::event(),这是通用的事件处理器,或者在常规处理之前或之后进行事件处理,或者完全替换里面的函数。一个非同寻常的widget可能有如下的event()函数

bool MyWidget::event(QEvent *event)
{
    if (event->type() == QEvent::KeyPress) {
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
        if (ke->key() == Qt::Key_Tab) {
            // special tab handling here
            return true;
        }
    } else if (event->type() == MyCustomEventType) {
        MyCustomEvent *myEvent = static_cast<MyCustomEvent *>(event);
        // custom event handling here
        return true;
    }

    return QWidget::event(event);
}

注意QWidget::event()在没有被特殊处理的情况下依然被调用,他的返回值表明是否一个事件被处理了,返回true表明事件不会被发送到其他对象

1.9.4 事件的过滤

有时一个对象需要观察一个事件,然后拦截它,防止事件被传递到其他对象。例如,对话框希望过滤键盘按下事件对于一些widget

QObject::installEventFilter()函数安装一个事件过滤器,使指定的过滤器对象在他的QObject::eventFilter()函数中接收目标对象的事件。一个事件过滤器可以在目标对象之前处理事件,在需要的时候可以检查并丢弃事件。可以使用 QObject::removeEventFilter()函数移除一个已存在的事件

当过滤器对象重写的eventFilter()被调用时,它可以接收或拒绝事件,允许或否认事件的进一步处理。如果所有事件过滤器都允许进一步处理事件(返回false),事件将被发送到目标对象本身。如果有一个过滤器阻止处理(返回true),目标对象及之后的事件过滤器都看不到这个事件

bool FilterObject::eventFilter(QObject *object, QEvent *event)
{
    if (object == target && event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        if (keyEvent->key() == Qt::Key_Tab) {
            // Special tab handling
            return true;
        } else
            return false;
    }
    return false;
}

上面的代码展示了另一个拦截Tab键的方法。过滤器处理相关的事件并且返回true以停止事件被进一步地处理。

可以过滤整个应用程序的所有事件,只需要安装事件过滤器在QApplication或QCoreApplication对象上。这是非常强大的,但也会减慢整个应用程序中每个事件的传递速度,通常应该使用被讨论的其他技术代替

1.9.5 发送事件

许多应用程序都想创建和发送他们自己的事件。你可以发送事件使用QCoreApplication::sendEvent()和QCoreApplication::postEvent()

sendEvent()立即处理事件。当他返回时,事件过滤器或对象本身就已经处理完事件了。许多事件类都有isAccepted()函数,它告诉你事件被接收了还是被拒绝了

postEvent()把事件放入事件分发队列中。下一次Qt的主事件循环执行时,分发所有被放置的事件,经过一些优化。例如,有一些resize事件,他们会被压缩成一个事件。QWidget::update()中调用postEvent(),它通过避免多次重绘来消除闪烁并提高速度

postEvent()也在对象初始化期间调用,因为被放置的事件通常会在对象初始化完成后很快被分发。当重写一个widget时,重要的是要意识到事件可以在其生命周期的早期被传递,所以在他的构造函数中,确保在早期初始化成员变量

要创建一个自定义的事件类型,你需要定义一个事件号码,他必须大于QEvent::User,并且你可能还需要继承QEvent类,查看QEvent文档获取更多信息

1.9.6 再探事件

Another Look at Events

2. 线程和并发编程

3. 输入输出,资源和容器

4. 附加的框架

4.1 动画框架

动画框架提供了创建动画和平滑GUI的简答方式。通过设置Qt的属性动画,这个框架提供了极大的自由给动画widget和其他QObject。这个框架也可以被用于图形视图框架。在这个框架中的许多概念在Qt Quick中也是可用的,从这个框架中获取的许多知识都可以用在Qt Quick上

在这个概述中,我们解释了这个框架的基本内容,我们还展示了框架允许对QObjects和图形项设置动画的最常见技术的示例。

4.1.1 架构

我们将站在一个高层级的角度来看待这个动画框架的体系结构,以及如何使用它来制作Qt的属性动画。下图显示了动画框架中最重要的类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W7DJLIGG-1587833055439)(animations-architecture.png)]

动画框架的基础包括基类QAbstractAnimation,以及他的两个子类QVariantAnimation和QAnimationGroup,QAbstractAnimation是所有动画的祖先类。它表示框架中所有动画通用的基本属性,尤其是启动、停止和暂停动画的能力。它还接收时间更改的通知

动画框架进一步地提供了QPropertyAnimation类,他继承自QVariantAnimation并且执行Qt的属性动画。类使用缓和曲线在属性上执行插值。因此,当您想要设置一个值的动画时,可以将其声明为一个属性,并使类成为一个QObject对象。

复杂的动画可以通过建立一棵QAbstractAnimation的树来构造,这棵树使用QAnimationGroup来建立,它的功能是作为其它动画的容器。因为动画组本身也是QAbstractAnimation的子类,所以动画组也可以包含其他动画组

动画框架可以单独使用,但也被设计为状态机框架的一部分。状态机提供了一个特殊的可以播放动画的状态。当状态进入或退出时,QState也可以设置属性

在幕后,动画由全局计时器控制,它发送更新给所有正在播放的动画

4.1.2 动画框架中的类

这些类提供给框架来创建简答和发杂的动画

QAbstractAnimation所有动画的基类
QAnimationGroup动画组的抽象基类
QEasingCurve控制动画的缓和曲线
QParallelAnimationGroup并行的动画组
QPauseAnimation暂停QSequentialAnimationGroup
QPropertyAnimationQt的属性动画
QSequentialAnimationGroup连续的动画组
QTimeLine控制动画的时间线
QVariantAnimation动画的基类

4.1.3 Qt的属性动画

正如前一节所述,QPropertyAnimation类可以在属性上设置插值。通常这个类应该用于值的动画,实际上它的父类QVariantAnimation的updateCurrentValue()有一个空的实现,并且不改变任何值除非我们自己在valueChanged信号中改变了它

我们选择Qt属性动画的一个主要原因是它赋予我们在Qt API中已经存在的动画类的自由。值得注意的是,QWidget类(我们也可以将其嵌入QGraphicsView)具有其边界、颜色等属性,让我们看一个小例子

QPushButton button("Animated Button");
button.show();

QPropertyAnimation animation(&button, "geometry");
animation.setDuration(10000);
animation.setStartValue(QRect(0, 0, 100, 30));
animation.setEndValue(QRect(250, 250, 100, 30));

animation.start();

这些代码在10秒内将按钮从左上角移动到(250, 250)

上面的例子将在起始值和结束值之间进行线性插值,也可以设置位于起始值和结束值之间的值,然后插值将经过这些点

QPushButton button("Animated Button");
button.show();

QPropertyAnimation animation(&button, "geometry");
animation.setDuration(10000);

animation.setKeyValueAt(0, QRect(0, 0, 100, 30));
animation.setKeyValueAt(0.8, QRect(250, 250, 100, 30));
animation.setKeyValueAt(1, QRect(0, 0, 100, 30));

animation.start();

在上面的例子中,按钮将在8秒内移动到(250, 250),然后在剩下的2秒内回到起始位置,运动将在这些点之间进行线性插值

你还可以设置未声明为Qt属性的QObject的值的动画,唯一的要求是这个值有一个设置器(setter),然后,继承自包含该值的类,并声明使用此设置器的属性。注意,每一个Qt属性都需要一个getter,所以在没有定义的时候你需要自己提供一个getter

class MyGraphicsRectItem : public QObject, public QGraphicsRectItem
{
    Q_OBJECT
    Q_PROPERTY(QRectF geometry READ geometry WRITE setGeometry)
};

在上面的代码中我们继承了类QGraphicsRectItem并且定义了一个geometry属性,现在我们可以播放geometry动画,即使QGraphicsRectItem没有提供geometry属性

4.1.4 动画和图形视图框架

当你想在QGraphicsItem上播放动画时,你也需要使用QPropertyAnimation。然而QGraphicsItem没有继承自QObject。一个好的解决方案是继承你想实现动画的QGraphicsItem,下面的例子展示了如何操作。另一个可能的方式是继承QGraphicsWidget,它是一个QObject

class Pixmap : public QObject, public QGraphicsPixmapItem
{
    Q_OBJECT
    Q_PROPERTY(QPointF pos READ pos WRITE setPos)
    ...

如前一节所述,我们需要定义希望设置动画的属性

注意:QObject必须是多重继承的第一个类,因为元对象系统要求这样做

4.1.5 缓和曲线

如前所述,QPropertyAnimation在起始和结束属性值之间设置插值,除了为动画添加更多的关键值之外,你也可以使用缓和曲线。缓和曲线描述一个函数,该函数控制0到1之间的插值速度,如果你想在不更改插值路径的情况下控制动画的速度,这将是非常有用的

QPushButton button("Animated Button");
button.show();

QPropertyAnimation animation(&button, "geometry");
animation.setDuration(3000);
animation.setStartValue(QRect(0, 0, 100, 30));
animation.setEndValue(QRect(250, 250, 100, 30));

animation.setEasingCurve(QEasingCurve::OutBounce);

animation.start();

在这里,动画将跟随一条曲线,使其像一个球一样反弹,就好像它是从开始位置下降到结束位置一样。QEasingCurve有大量曲线供您选择。它们由QEasingCurve::Type枚举定义。如果你需要其他的曲线,你也可以自己实现一个,并在QEasingCurve中注册它

4.1.6 将动画组合在一起

一个应用程序通常包含多个动画,例如,您可能希望同时移动多个图形项,或者将它们依次移动。

QAnimationGroup的子类(QSequentialAnimationGroup和QParallelAnimationGroup)是其他动画的容器,因此这些动画可以以顺序或并行的方式展示动画。QAnimatinGroup是一个不设置属性动画的例子,但它会定期收到时间更改的通知。这使它能够将这些时间更改转发到被包含的动画,从而控制播放动画的时间。

让我们看看同时使用QSequentialAnimationGroup和QParallelAnimationGroup的代码示例,从后者开始

QPushButton *bonnie = new QPushButton("Bonnie");
bonnie->show();

QPushButton *clyde = new QPushButton("Clyde");
clyde->show();

QPropertyAnimation *anim1 = new QPropertyAnimation(bonnie, "geometry");
// Set up anim1

QPropertyAnimation *anim2 = new QPropertyAnimation(clyde, "geometry");
// Set up anim2

QParallelAnimationGroup *group = new QParallelAnimationGroup;
group->addAnimation(anim1);
group->addAnimation(anim2);

group->start();

并行动画组同时播放多个动画。调用它的start()函数将启动它控制的所有动画

QPushButton button("Animated Button");
button.show();

QPropertyAnimation anim1(&button, "geometry");
anim1.setDuration(3000);
anim1.setStartValue(QRect(0, 0, 100, 30));
anim1.setEndValue(QRect(500, 500, 100, 30));

QPropertyAnimation anim2(&button, "geometry");
anim2.setDuration(3000);
anim2.setStartValue(QRect(500, 500, 100, 30));
anim2.setEndValue(QRect(1000, 500, 100, 30));

QSequentialAnimationGroup group;

group.addAnimation(&anim1);
group.addAnimation(&anim2);

group.start();

如您所料,QSequentialAnimationGroup按顺序播放动画。它将在上一个动画完成后开始列表中的下一个动画

由于动画组是本身也是动画,因此可以将其添加到另一个组中。通过这种方式,你可以构建动画的树结构,该结构指定动画在何时被播放

4.1.7 动画和状态

当使用一个状态机时,我们可以使用QSignalTransition或QEventTransition类将一个或多个动画关联到状态之间的转换。这些类都是从QAbstractTransition派生的,它定义了方便的函数addAnimation(),该函数允许在发生转换时附加触发的一个或多个动画

我们还可以将属性与状态关联起来,而不是自己设置开始和结束值。下面是QPushButton几何动画的完整代码示例

QPushButton *button = new QPushButton("Animated Button");
button->show();

QStateMachine *machine = new QStateMachine;

QState *state1 = new QState(machine);
state1->assignProperty(button, "geometry", QRect(0, 0, 100, 30));
machine->setInitialState(state1);

QState *state2 = new QState(machine);
state2->assignProperty(button, "geometry", QRect(250, 250, 100, 30));

QSignalTransition *transition1 = state1->addTransition(button,
    SIGNAL(clicked()), state2);
transition1->addAnimation(new QPropertyAnimation(button, "geometry"));

QSignalTransition *transition2 = state2->addTransition(button,
    SIGNAL(clicked()), state1);
transition2->addAnimation(new QPropertyAnimation(button, "geometry"));

machine->start();

有关如何将状态机框架用于动画的更全面的示例,请参见状态示例(它位于examples/animation/states目录中)

4.2 JSON支持

Qt对处理JSON数据提供了支持,JSON是一种编码从Javascript派生的对象数据的格式,但现在被广泛用作互联网上的数据交换格式

Qt中的JSON支持提供了一个易于使用的C++ API来解析、修改和保存JSON数据。它还支持以二进制格式保存这些数据,这种格式可以直接“mmap”,而且访问速度非常快。

有关JSON数据格式的更多详细信息,请访问JSON.orgRFC-4627

4.2.1 概述

JSON是一种存储结构化数据的格式。它有6种基本数据类型:

  • bool
  • double
  • string
  • array
  • object
  • null

一个值可以具有以上任何类型。布尔值在JSON中由字符串true或false表示。JSON没有显式地指定数字的有效范围,但是Qt中的支持仅限于double的有效范围和精度。字符串可以是任何有效的unicode字符串。数组是值的列表,对象是键/值对的集合。对象中的所有键都是字符串,并且对象不能包含任何重复键。

JSON的文本表示将数组括在方括号中([…])和花括号中的对象({…})。数组和对象中的条目用逗号分隔。对象中键和值之间的分隔符是冒号(😃

一个简单的JSON文档编码一个人、他/她的年龄、地址和电话号码可能如下:

{
    "FirstName": "John",
    "LastName": "Doe",
    "Age": 43,
    "Address": {
        "Street": "Downing Street 10",
        "City": "London",
        "Country": "Great Britain"
    },
    "Phone numbers": [
        "+44 1234567",
        "+44 2345678"
    ]
}

上面的示例包含一个具有5个键/值对的对象。其中两个值是字符串,一个是数字,一个是对象,最后一个是数组。

有效的JSON文档可以是数组或对象,因此文档总是以方括号或花括号开头。

4.2.2 JSON类

所有JSON类都是基于值的隐式共享类

Qt中的JSON支持包括以下类:

QJsonArray封装JSON数组
QJsonDocument读写JSON文档的方法
QJsonObject封装JSON对象
QJsonObject::const_iterator为QJsonObject提供STL样式的常量迭代器
QJsonObject::iterator为QJsonObject提供STL样式的非常量迭代器
QJsonParseError用于在JSON解析期间报告错误
QJsonValue用JSON封装值

查看JSON Save Game Example

4.3 状态机框架

4.4 创建Qt插件


  1. 翻译自官方文档https://doc.qt.io/qt-5/qtcore-index.html,部分缺失。 ↩︎

相关推荐
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页