Qt的学习之路

目录

一、信号槽机制

1.1 基本概念

1.2 特点

1.3 使用方法

1.4 信号槽连接类型

1.5 注意

二、元对象系统

2.1 基本概念

2.2 实现方式

2.3 主要特性

2.4 使用场景

2.5 元对象系统如何识别信号槽

三、国际化

3.1 标记可翻译的文本(tr函数)

3.2 生成翻译源文件(.ts文件)

3.3 翻译

3.4 编译翻译结果(.qm文件)

3.5 加载和使用翻译(QTranslator类)

3.6 注意

四、插件系统

4.1 定义插件接口

4.2 创建插件

4.3 导出插件

 4.4 加载插件

4.5 错误处理

4.6 插件的注册和发现

4.7 编译和部署

五、事件循环机制

5.1 事件循环的概念

5.2 事件处理流程

5.3 事件循环的优先级

5.4 事件循环的进入和退出

5.5 嵌套事件循环

六、多线程

6.1 继承QThread类

6.2 继承QObject类

6.3 线程池QThreadPool

6.4 QMetaObject::invokeMethod()方法使用

6.5 线程同步

6.6 线程与事件循环

七、模型/视图框架

7.1 模型

7.2 视图

7.3 委托

八、对象树

8.1 内存管理

8.2 几何管理

8.3 事件传播

8.4 资源共享

九、绘图引擎(Graphics Engine)

9.1 核心组件

9.2 绘图引擎的工作流程

9.3 坐标系统

十、图形/视图框架

10.1 场景、视图、图形项的介绍

10.2 坐标系统

 10.3 事件处理


一、信号槽机制

Qt 的信号和槽(Signals and Slots)机制是其框架中的一个核心特性,它提供了一种强大而灵活的通信方式,允许对象之间进行交互。这种机制替代了传统的回调函数,使得代码更加清晰、易于理解和维护。

1.1 基本概念

信号(Signal):当某个特定事件发生时,对象会发射(emit)一个信号。这个事件可以是用户交互(如按钮点击)、系统事件(如定时器超时)或其他对象的状态变化。是一种特殊的成员函数,返回类型为void,并且没有函数体(只有声明,没有定义)。
槽(Slot):槽是响应信号的对象成员函数。当一个信号被发射时,与其关联的槽函数会被自动调用。槽可以是任何成员函数,但通常它们都是 public slots,以便其他对象可以将其与信号连接。
连接(Connection):信号和槽之间的关联是通过连接(connect)操作建立的。Qt 提供了一个灵活的连接系统,允许在运行时动态地建立或断开信号和槽之间的关联。

1.2 特点

类型安全:信号和槽的签名(即参数类型和数量)必须匹配,这保证了在连接时不会发生类型错误。
灵活性:一个信号可以连接多个槽,一个槽也可以连接多个信号。此外,信号和槽之间的连接可以是直接的(即一个对象直接调用另一个对象的槽),也可以是间接的(通过信号和槽机制进行)。
解耦:信号和槽机制允许对象之间的解耦,即对象不需要知道彼此的具体实现细节就可以进行交互。这有助于降低代码的耦合度,提高可维护性。
易于扩展:Qt 允许用户自定义信号和槽,这使得开发者可以轻松地扩展框架的功能。

1.3 使用方法

定义信号和槽:在 Qt 中,信号使用 signals 关键字定义,而槽使用 slots 关键字定义。它们都必须是类的成员函数。
连接信号和槽:使用 QObject::connect() 函数将信号和槽连接起来。该函数接受五个参数:发射信号的对象、信号、接收信号的对象、槽函数、连接类型。
发射信号:当某个事件发生时,使用 emit 关键字发射信号。这会导致与该信号连接的所有槽函数被调用。
断开信号和槽:使用 QObject::disconnect() 函数可以断开信号和槽之间的连接。这通常在对象销毁或需要重新配置连接时使用。

1.4 信号槽连接类型

Qt::DirectConnection
当信号被发射时,槽函数立即在同一线程中被调用。
这是最快的连接类型,但只适用于信号和槽在同一线程中的情况。
如果信号和槽在不同的线程中,并且你尝试使用这种连接类型,Qt 会发出警告,并可能不调用槽函数。
Qt::QueuedConnection
当信号被发射时,槽函数的调用被放入接收对象所在线程的事件队列中,等待该线程的事件循环来处理。
这允许信号和槽在不同的线程中安全地通信。
由于涉及线程间通信和事件队列处理,这种连接类型通常比 Qt::DirectConnection 慢。
Qt::AutoConnection
这是默认的连接类型。
如果信号和槽在同一线程中,它使用 Qt::DirectConnection。
如果信号和槽在不同的线程中,它使用 Qt::QueuedConnection。
这是一种灵活的连接类型,可以根据信号和槽的位置自动选择最合适的连接方式。
Qt::BlockingQueuedConnection
当信号被发射时,发射信号的线程会被阻塞,直到接收线程的事件循环处理了该槽函数的调用。
这种连接类型通常用于需要等待槽函数执行完成的情况,但应谨慎使用,因为它可能导致线程阻塞和性能问题。
Qt::UniqueConnection:
确保一个信号只连接到一个槽。如果已存在这样的连接,则连接失败。

1.5 注意

在Qt中,同一个信号可以多次连接到同一个槽,或者连接到不同的槽。这种连接被称为信号的“多播”(multicast)或“多连接”(multiple connections)。
当你使用QObject::connect()函数连接一个信号到一个槽时,Qt并不会检查是否已经存在相同的连接。每次调用connect()都会创建一个新的连接,除非在连接时指定了某种类型的连接类型(如Qt::UniqueConnection),该类型会防止创建重复的连接。
例如,以下代码展示了如何将同一个信号连接到同一个槽两次:

// 第一次连接  
QObject::connect(sender, &Sender::mySignal, receiver, &Receiver::mySlot);  
  
// 第二次连接(也是有效的,信号会两次调用mySlot)  
QObject::connect(sender, &Sender::mySignal, receiver, &Receiver::mySlot);

二、元对象系统

Qt元对象系统(Meta-Object System)是Qt框架中的一个核心概念,它提供了在运行时对对象进行反射和元数据操作的机制。以下是关于Qt元对象系统的详细解释:

2.1 基本概念

Qt元对象系统允许开发者在不了解对象实际类型的情况下,通过对象的元数据来访问和操作对象的属性、信号和槽。
在Qt中,每个从QObject派生的类都会有一个对应的元对象(MetaObject),用于存储类的元数据。这些元数据包括类的属性、信号和槽的信息以及其他相关元信息。

2.2 实现方式

元对象是通过元对象编译器(moc)根据类的声明自动生成的。moc是一个预处理器,它读取包含Q_OBJECT宏的C++源文件,并为每个类生成元对象代码。
这些生成的元对象代码或者被包含进类的源文件中,或者和类的实现同时进行编译和链接。

2.3 主要特性

动态属性系统:允许在运行时添加和访问对象的属性。
信号和槽机制:Qt的核心特性之一,用于实现对象之间的通信。
运行时类型识别(RTTI):通过QObject::inherits()函数和qobject_cast<>()函数,可以在运行时确定对象的类型并进行类型转换。
国际化支持:通过QObject::tr()和QObject::trUtf8()函数进行字符串翻译。
对象树和内存管理:QObject提供了对象树结构,支持父子关系,当父对象被删除时,其子对象也会被自动删除。
关键类和宏
QObject:所有使用元对象系统的类的基类,必须在类的开头使用Q_OBJECT宏。
Q_OBJECT:必须在类的私有声明区声明此宏,以启用元对象系统的特性。
QMetaObject:管理类的元对象,提供访问元数据的方法。
QMetaProperty、QMetaMethod、QMetaClassInfo等:用于访问类的属性、方法和类注释信息的类。

2.4 使用场景

动态连接和断开信号和槽:实现对象之间的通信。
对象复制:通过元对象系统可以复制对象的状态。
动态属性添加和访问:在运行时动态地添加和访问对象的属性。
插件系统:Qt的插件系统依赖于元对象系统来实现插件的动态加载和类型检查。

2.5 元对象系统如何识别信号槽

在Qt中,元对象系统通过特定的机制来识别信号与槽的连接。以下是关于元对象系统如何识别信号与槽连接的详细解释:
元对象编译器(moc):
元对象编译器(moc)是Qt框架中的一个关键工具,用于处理包含Q_OBJECT宏的类。
当moc遇到包含Q_OBJECT宏的类时,它会读取类的声明,并生成一个包含元对象特性实现代码的C++源文件(通常命名为moc_*.cpp)。
这个生成的源文件包含了类的元对象表(meta-object table),该表存储了类的元数据,包括信号、槽以及其他相关信息。
元对象表:
元对象表是一个静态数据结构,它存储了所有QObject派生类的元对象信息。
每个QObject派生类都有一个与之关联的元对象表,该表包含了类的信号、槽、属性等的元数据。
信号与槽的连接:
当使用QObject::connect函数连接信号和槽时,Qt会在运行时将连接关系存储在发送者对象的元对象中。具体来说,Qt会在发送者对象的元对象表中记录与信号匹配的槽函数的索引或指针。
信号的发射:
当某个事件触发信号发射时(如通过emit关键字),Qt会查找发送者对象的元对象表。
在元对象表中,Qt会找到与信号名称匹配的信号索引。
接着,Qt会遍历与该信号连接的槽函数列表,并获取每个槽函数的签名。
然后,Qt会在元对象表中找到与槽函数签名匹配的槽函数索引。
最后,Qt会通过槽函数索引获取槽函数的指针或偏移量,并将槽函数与信号的参数一起调用。
总结:
Qt的元对象系统通过moc生成的元对象表来存储类的元数据,包括信号和槽的信息。
当连接信号和槽时,Qt会在发送者对象的元对象表中记录连接关系。
当信号被发射时,Qt会查找发送者对象的元对象表,并遍历与信号连接的槽函数列表,最终调用相应的槽函数。

三、国际化

Qt国际化的实现主要依赖于几个关键步骤和组件,以下是对其实现的详细解释:

3.1 标记可翻译的文本(tr函数)

在Qt应用程序的源代码中,使用tr()函数来标记需要翻译的字符串。这些字符串将被Qt的国际化工具识别并提取出来用于翻译。

QLabel *label = new QLabel(this);  
label->setText(tr("Hello Qt!"));

3.2 生成翻译源文件(.ts文件)

使用Qt的lupdate工具从C++源代码中提取出所有被tr()函数标记的字符串,并生成一个或多个.ts文件。这些文件是XML格式的,包含了原始字符串和相关的上下文信息。

3.3 翻译

使用Qt Linguist工具打开.ts文件,进行字符串的翻译工作。Linguist提供了一个用户界面,允许开发者在源语言和目标语言之间切换,并输入相应的翻译。

3.4 编译翻译结果(.qm文件)

一旦翻译完成,使用Qt的lrelease工具从.ts文件中生成.qm文件。.qm文件是二进制格式的,包含了所有翻译后的字符串,可以被Qt应用程序在运行时加载和使用。

3.5 加载和使用翻译(QTranslator类)

在Qt应用程序中,使用QTranslator类来加载.qm文件。QTranslator会根据当前设置的语言环境来加载相应的翻译文件。

QTranslator translator;  
translator.load(":/translations/myapp_" + QLocale::system().name() + ".qm");  
app.installTranslator(&translator);

3.6 注意

动态文本布局:不同语言的文本长度和排版方式可能不同,需要确保界面能够动态地适应这些变化。
日期、时间和货币格式化:根据用户的语言和地区设置,对日期、时间和货币进行格式化,以符合当地的习惯和标准。
语言和区域设置:Qt能够根据用户的地理位置、语言、货币等偏好自动调整显示的界面元素。
文化适配:考虑到用户的文化背景,如图像、符号、颜色等在不同文化中可能具有不同的含义。

四、插件系统

在Qt框架中实现插件系统主要涉及到使用Qt的插件机制来动态加载和卸载插件模块。

4.1 定义插件接口

首先,你需要定义一个或多个接口类,这些类将作为插件和主程序之间的契约。这些接口类通常包含纯虚函数,插件需要实现这些函数。

// MyPluginInterface.h  
class MyPluginInterface  
{  
public:  
    virtual ~MyPluginInterface() {}  
    virtual void load() = 0;  
    virtual void unload() = 0; 
    // 其他纯虚函数...
};  
 
Q_DECLARE_INTERFACE(MyPluginInterface, "com.example.MyPluginInterface/1.0")

4.2 创建插件

接下来,你需要创建一个或多个插件,这些插件将实现你在第一步中定义的接口。每个插件都是一个独立的库(在 Windows 上是 DLL,在 Linux/Unix 上是 .so 文件)。

// MyPlugin.h  
#include "MyPluginInterface.h"  
  
class MyPlugin : public QObject, public MyPluginInterface  
{  
    Q_OBJECT  
    Q_PLUGIN_METADATA(IID "com.example.MyPluginInterface/1.0")  
    Q_INTERFACES(MyPluginInterface)  
  
public:  
    void load() override;
    void unload() override;
};

4.3 导出插件

在你的插件实现文件中,你需要使用 Q_EXPORT_PLUGIN2 宏来导出你的插件类。这个宏告诉 Qt 如何加载你的插件。

// MyPlugin.cpp  
#include "MyPlugin.h"  
  
Q_EXPORT_PLUGIN2(MyPlugin, MyPlugin)

 4.4 加载插件

在主程序中,你可以使用 QPluginLoader 类来动态加载插件。QPluginLoader 可以加载指定的插件库,并返回一个指向插件接口的指针。

QPluginLoader loader("path/to/your/plugin.dll"); // Windows  
// 或者  
QPluginLoader loader("path/to/your/libmyplugin.so"); // Linux/Unix  
if (!loader.load()) {  
    qDebug() << "Plugin failed to load:" << loader.errorString();  
    // 处理错误  
}
QObject *plugin = loader.instance();  
if (plugin) {  
    MyPluginInterface *myPlugin = qobject_cast<MyPluginInterface *>(plugin);  
    if (myPlugin) {  
        myPlugin->load();  
    }
    else {
        qDebug() << "Failed to get plugin instance";  
        // 处理错误
    } 
}

4.5 错误处理

当加载插件时,可能会出现各种错误,如文件不存在、插件版本不匹配等。你需要使用 QPluginLoader 的错误处理功能来检测和处理这些错误。

4.6 插件的注册和发现

Qt 的插件系统还支持插件的自动注册和发现。你可以使用 Qt 的元对象系统(Meta-Object System)和插件元数据(Q_PLUGIN_METADATA)来实现这一点。这样,主程序就可以在不指定插件路径的情况下自动加载插件。

4.7 编译和部署

最后,你需要确保你的插件和主程序都正确编译,并且插件库被放置在主程序可以访问的位置。在部署时,你可能还需要考虑不同平台上的库依赖问题。

五、事件循环机制

Qt事件循环机制是Qt框架中用于处理用户输入、事件响应以及应用程序逻辑的核心机制。它通过事件队列和事件分发机制,实现了对事件的异步处理和优先级管理。

5.1 事件循环的概念

定义:事件循环是一个无限循环,用于从操作系统接收事件并将其分发给合适的对象进行处理。这些事件可以来自用户交互、定时器、网络和其他外部设备。
核心作用:Qt事件循环是Qt框架中的核心概念之一,也被称为事件驱动编程。它使得Qt应用程序能够响应用户输入和系统事件,从而实现交互性和动态性。

5.2 事件处理流程

事件生成:事件可以由多种来源产生,包括用户交互(如鼠标点击、键盘按键)、定时器超时、网络活动等。
事件队列:当事件生成后,它们并不是立即被处理的,而是被放入一个事件队列中等待处理。这个队列按照一定的优先级顺序来管理事件,确保重要的事件能够优先得到处理。
事件分发:事件循环不断地从事件队列中取出事件,并将其分发给合适的对象进行处理。这个过程是通过调用每个QObject派生类的事件处理函数(event handler)来实现的。
事件处理:当某个对象接收到一个事件时,它会首先尝试自己处理该事件。如果该对象不能处理该事件,则会将该事件传递给其父级对象,直到找到能够处理该事件的对象或者最终没有任何对象处理该事件。

5.3 事件循环的优先级

Qt的事件循环中,事件按照一定的优先级顺序被处理。通常情况下,事件队列中最先处理的是以下类型的事件(按优先级从高到低):

QTimerEvent:定时器事件,用于处理定时器超时。
QMouseEvent:鼠标事件,例如鼠标点击、移动等操作。
QKeyEvent:键盘事件,例如按键按下、释放等操作。
QWheelEvent:滚轮事件,用于处理滚轮滚动操作。
QResizeEvent:窗口大小调整事件,当窗口大小发生变化时触发。
QCloseEvent:窗口关闭事件,当窗口被关闭时触发。

5.4 事件循环的进入和退出

进入事件循环:通过调用QCoreApplication::exec()函数,Qt应用程序就进入了一个事件循环中。这个函数会启动一个无限循环,等待并处理事件。
退出事件循环:当调用QCoreApplication::exit()或QCoreApplication::quit()函数时,事件循环就会终止。这通常发生在应用程序关闭或用户请求退出时。

5.5 嵌套事件循环

Qt应用通常至少有一个事件循环,即main()函数中调用的QCoreApplication::exec()。除此之外,还可能有其他的事件循环,如通过QEventLoop::exec()进入的本地事件循环。这些嵌套的事件循环允许在特定的代码段中处理特定的事件,而不会阻塞整个应用程序。

六、多线程

6.1 继承QThread类

一个QThread类对象管理一个子线程,自定义一个继承自QThread类,并重写虚函数run(),在run()函数里实现线程需要完成的复杂操作(注意QThread只有run函数是在新线程里的)。
一般在主线程创建工作子线程,并调用start(),开始执行工作子线程的任务。start()会在内部调用run()函数,进入工作线程的事件循环,在run()函数里调用exit()或quit()可以结束线程的事件循环,或者在工作主线程里调用terminate()强制结束线程。

class subThread : public QThread
{
    ...
protected:
    void run(){
        //全部在这里处理子线程的复杂业务
    }
};

在主线程中创建子线程,并调用start()方法启动子线程。

subThread* st = new subThread;
st->start();

6.2 继承QObject类

创建一个继承自QObject的业务类,处理相关业务逻辑,然后将该类对象移动到子线程中(调用moveToThread()方法)执行,可读性也更强,更易于维护。

class subObject : public QObject
{
    ...
public:
    void working();    //函数名称随意取,传入的参数根据实际需求添加
}

在主线程中创建一个子线程subThread对象,创建一个业务类subObject对象(创建该类对象千万不要指定父对象),再将业务类对象移动到子线程对象中,最后启动子线程。

QThread* subThread =  new QThread;
// subObject* subObj = new subObject(this);      //error
subObject* subObj = new subObject;            //OK
subObj->moveToThread(subThread);
subThread->start();

 在主线程中通过信号槽调用线程类subObject对象的工作函数,这时候才会到子线程中运行该工作函数。

connect(ui->pushButton,&QPushButton::clicked,subObj,&subObject::working);

6.3 线程池QThreadPool

创建一个继承自 QRunnable 的类,并实现 run() 方法。这个方法将包含你的任务代码。

class MyTask : public QRunnable  
{  
public:  
    void run() override {  
        // 在这里编写你的任务代码  
    }  
};

使用 QThreadPool::globalInstance() 获取全局线程池的实例,然后调用 start() 方法来提交任务。

MyTask *task = new MyTask();  
QThreadPool::globalInstance()->start(task);  
// 注意:task 对象将在任务完成后自动删除,除非你设置了不同的删除策略

 你可以使用 QThreadPool 的各种方法来配置线程池的行为,例如设置最大线程数:

QThreadPool::globalInstance()->setMaxThreadCount(4); // 设置最大线程数为 4

QRunnable类是所有可运行对象的基类,没有继承于QObject,所以就不能使用信号槽功能与外界通信。如果想要任务类MyTask与主线程通信,有两种办法:
(1)使用多继承,就是让线程类同时继承QObject和QRunnable(不推荐),让该线程类能够支持信号槽的使用。
(2)使用QMetaObject::invokeMethod()方法(推荐)。

6.4 QMetaObject::invokeMethod()方法使用

QMetaObject::invokeMethod() 是 Qt 框架中用于跨线程或在当前线程中安全地调用对象的槽(slot)函数的方法。这个函数非常有用,因为它允许你在不直接调用对象方法的情况下,通过元对象系统来调用对象的槽函数。
以下是 QMetaObject::invokeMethod() 的基本使用方式:

#include <QMetaObject>

// 假设你有一个指向 QObject 派生类的指针,名为 obj  
QObject *obj = ...; // 从某个地方获得的对象  
  
// 使用 invokeMethod 调用该对象的槽函数  
// 例如,我们假设 obj 有一个名为 "mySlot" 的槽函数  
QMetaObject::invokeMethod(obj, "mySlot", Qt::QueuedConnection);  
  
// 如果有参数需要传递,你可以这样做:  
QList<QVariant> args;  
args << QVariant(123) << QVariant(QString("Hello"));  
QMetaObject::invokeMethod(obj, "mySlotWithArgs", Qt::QueuedConnection, args.constBegin(), args.size());

注意几点:
连接类型:在上面的例子中,我们使用了 Qt::QueuedConnection,这意味着如果 obj 在另一个线程中,则槽函数的调用将被排队到该线程的事件循环中。如果你在同一线程中调用,并且希望立即执行槽函数,可以使用 Qt::DirectConnection。
参数:如果你需要传递参数给槽函数,你可以使用 QList<QVariant> 来存储这些参数,并将它们作为 invokeMethod 的参数传递。
返回值:invokeMethod 本身不返回槽函数的返回值。如果你需要返回值,你可能需要设计一种不同的通信机制,例如使用信号和槽,并通过信号传递返回值。
线程安全:invokeMethod 是线程安全的,这意味着你可以在一个线程中安全地调用另一个线程中对象的槽函数。但是,你仍然需要确保你传递给槽函数的任何数据都是线程安全的。
错误处理:如果槽函数不存在或无法调用,invokeMethod 将不会抛出异常或返回错误代码。但是,你可以通过连接 QObject::destroyed() 信号来检查对象是否已被销毁,这可能导致 invokeMethod 失败。

6.5 线程同步

Qt 提供了多种线程同步的方式,以确保线程之间的协调和数据的一致性。这些同步机制包括互斥锁(QMutex)、读写锁(QReadWriteLock)、条件变量(QWaitCondition)、信号和槽(Signals and Slots)以及 Qt 的并发类(如 QFuture、QThreadPool)。
以下是使用 QMutex 和 QWaitCondition 实现线程同步的示例代码:

#include <QCoreApplication>  
#include <QThread>  
#include <QMutex>  
#include <QWaitCondition>  
#include <QDebug>  
  
class WorkerThread : public QThread  
{  
    Q_OBJECT  
  
public:  
    WorkerThread(QMutex *mutex, QWaitCondition *condition, QObject *parent = nullptr)  
        : QThread(parent), mutex(mutex), condition(condition), workDone(false) {}  
  
protected:  
    void run() override {  
        // 模拟一些工作  
        qDebug() << "WorkerThread: 开始工作";  
        QThread::sleep(2); // 模拟耗时操作  
  
        // 工作完成后,锁定互斥锁并设置条件  
        QMutexLocker locker(mutex);  
        workDone = true;  
        condition->wakeOne();  
        qDebug() << "WorkerThread: 工作完成";  
    }  
  
    bool workDone;  
  
private:  
    QMutex *mutex;  
    QWaitCondition *condition;  
};  
  
int main(int argc, char *argv[])  
{  
    QCoreApplication a(argc, argv);  
  
    QMutex mutex;  
    QWaitCondition condition;  
  
    WorkerThread thread(&mutex, &condition);  
    thread.start();  
  
    // 等待工作线程完成  
    QMutexLocker locker(&mutex);  
    while (!thread.workDone)  
        condition.wait(&mutex);  
  
    qDebug() << "主线程: 接收到工作完成信号";  
  
    thread.wait(); // 等待线程安全退出  
  
    return a.exec();  
}  

6.6 线程与事件循环

QThread中run()的默认实现调用了exec(),从而创建一个QEventLoop对象,由QEventLoop对象处理线程中事件队列(每一个线程都有一个属于自己的事件队列)中的事件。exec()在其内部不断做着循环遍历事件队列的工作,调用QThread的quit()或exit()方法使退出线程,尽量不要使用terminate()退出线程,terminate()退出线程过于粗暴,造成资源不能释放,甚至互斥锁还处于加锁状态。
线程中的事件循环,使得线程可以使用那些需要事件循环的非GUI 类(如,QTimer,QTcpSocket,QProcess)。
在QApplication前创建的对象,QObject::thread()返回NULL,意味着主线程仅为这些对象处理投递事件,不会为没有所属线程的对象处理另外的事件。可以用QObject::moveToThread()来改变对象及其子对象的线程亲缘关系,假如对象有父亲,不能移动这种关系。在另一个线程(而不是创建它的线程)中delete QObject对象是不安全的。除非可以保证在同一时刻对象不在处理事件。可以用QObject::deleteLater(),它会投递一个DeferredDelete事件,这会被对象线程的事件循环最终选取到。假如没有事件循环运行,事件不会分发给对象。假如在一个线程中创建了一个QTimer对象,但从没有调用过exec(),那么QTimer就不会发射它的timeout()信号,deleteLater()也不会工作。可以手工使用线程安全的函数QCoreApplication::postEvent(),在任何时候,给任何线程中的任何对象投递一个事件,事件会在那个创建了对象的线程中通过事件循环派发。事件过滤器在所有线程中也被支持,不过它限定被监视对象与监视对象生存在同一线程中。QCoreApplication::sendEvent(不是postEvent()),仅用于在调用此函数的线程中向目标对象投递事件。

七、模型/视图框架

Qt中的模型/视图架构用来实现大量的数据存储、处理及显示。


模型(model)用来存储数据;视图(View)用来显示数据;控制(Controller)用来处理数据;委托(Delegate)用来定制数据的渲染和编辑方式。

7.1 模型

所有模型都基于 QAbstractItemModel 类。视图和委托使用此类的接口来访问数据。
数据本身不必存储在模型中,它可以保存在由单独的类、文件、数据库或某些其他应用程序组件提供的数据结构或存储库中。
QAbstractItemModel 提供了一个数据接口,该接口足够灵活,可以处理以表格、列表和树的形式表示数据的视图。但是,在为列表(1列n行)和类似表格(n行m列)的数据结构实现新模型时,QAbstractListModel 和 QAbstractTableModel 类是更好的起点,因为它们提供了常用函数的适当默认实现。这些类中的每一个都可以被子类化以提供支持特殊类型列表和表格的模型。
Qt 提供了一些现成的模型,可用于处理数据项
QStringListModel 用于存储 QString 项的简单列表。
QStandardItemModel 管理更复杂的项目树结构,每个项目都可以包含任意数据。
QFileSystemModel 提供有关本地文件系统中文件和目录的信息。
QSqlQueryModel、QSqlTableModel 、QSqlRelationalTableModel 用于使用模型/视图方式访问数据库。
如果这些标准模型不符合要求,可以将 QAbstractItemModel、QAbstractListModel 、QAbstractTableModel 子类化以创建自定义模型。

7.2 视图

Qt为不同类型的视图提供了完整的实现:
QListView 显示项目列表。
QTableView 在表格中显示模型中的数据。
QTreeView 在分层列表中显示模型数据项。
这些类中都基于 QAbstractItemView 抽象基类。虽然这些类是现成的实现,但它们也可以被子类化以提供自定义视图。

下面是一个简单的Qt模型/视图框架的示例代码,其中使用QStandardItemModel作为模型,QTableView作为视图。 

#include <QApplication>  
#include <QTableView>  
#include <QStandardItemModel>  
  
int main(int argc, char *argv[])  
{  
    QApplication app(argc, argv);  
  
    // 创建一个标准模型  
    QStandardItemModel model(4, 3); // 4行3列  
  
    // 设置模型的水平和垂直表头  
    QStringList headers;  
    headers << "Name" << "Age" << "City";  
    model.setHorizontalHeaderLabels(headers);  
  
    // 填充模型数据  
    for (int row = 0; row < 4; ++row) {  
        for (int column = 0; column < 3; ++column) {  
            QStandardItem *item = new QStandardItem(QString("row %0, column %1").arg(row).arg(column));  
            model.setItem(row, column, item);  
        }  
    }  
  
    // 创建一个表格视图  
    QTableView tableView;  
    tableView.setModel(&model); // 设置模型  
  
    // 显示视图  
    tableView.show();  
  
    return app.exec();  
}

7.3 委托

QAbstractItemDelegate 是模型/视图框架中委托的抽象基类。
默认委托实现由 QStyledItemDelegate 提供,它被 Qt 的标准视图用作默认委托。
QStyledItemDelegate 和 QItemDelegate 是为视图中的项目绘制和提供编辑器的两个独立替代方案。
它们之间的区别在于 QStyledItemDelegate 使用当前样式来绘制其项目。因此建议在实现自定义委托时使用 QStyledItemDelegate 作为基类。详细参见:QStyledItemDelegate的使用方法

八、对象树

Qt对象树在Qt框架中扮演着至关重要的角色,特别是在管理GUI组件(如窗口、按钮、标签等)时。Qt对象树通过提供自动的内存管理、几何管理、事件传播和资源共享等机制,大大简化了GUI应用程序的开发过程。它使得开发者能够更专注于实现应用程序的功能和逻辑,而无需过多关注底层的资源管理和事件处理细节。

8.1 内存管理

当一个QObject对象被销毁时,它会自动删除其所有的子对象。这有助于防止内存泄漏。

8.2 几何管理

在GUI中,对象的位置和大小是相对于其父对象来确定的。对象树使得这种相对定位变得简单和直观。

8.3 事件传播

事件(如鼠标和键盘事件)首先被发送到接收它们的最具体的对象,然后沿着对象树向上传播,直到被处理或到达树的顶部。

8.4 资源共享

在某些情况下,如窗口句柄,父对象可以与其子对象共享资源。

九、绘图引擎(Graphics Engine)

Qt的绘图引擎是其图形绘制系统的核心组件,负责将图形和文本绘制到不同的设备上。Qt提供了多种绘图引擎,以满足不同应用程序的需求和硬件环境。Qt的绘图系统基于QPainter、QPainterDevice和QPaintEngine三个核心类。QPainter用于执行绘图的操作,QPainterDevice是绘图发生的二维空间(例如QWidget、QImage等),而QPaintEngine则提供了QPainter在不同设备上进行绘制的统一接口。

9.1 核心组件

QPainter:这是Qt中用于绘图的主要类。它提供了大量的方法用于绘制各种图形元素,如直线、曲线、文本、图片等。
QPainterDevice:是所有绘图设备的基类。它定义了绘图的基本属性,如宽度、高度和像素格式。QWidget、QPixmap、QImage等都继承自QPainterDevice。
QPaintEngine:这是QPainter用于在不同类型设备上绘制的接口。它负责将QPainter的绘图指令转化为具体的绘制操作。

9.2 绘图引擎的工作流程

事件触发与调度:
当用户与Qt应用程序的GUI进行交互时(如点击按钮、移动鼠标等),Qt的事件系统会捕捉这些事件并生成相应的事件对象。
这些事件对象被发送到事件循环(Event Loop)中进行分发和处理。
其中,绘图事件(QPaintEvent)是在控件需要重新绘制时触发的。
更新请求与调度:
当Qt检测到界面需要重绘时(例如,控件被遮挡后重新露出、控件内容发生改变等),会生成一个更新请求(Update Request)。这个更新请求会被事件循环检测到,并触发重绘事件(Repaint Event)。
重绘事件的处理:
在重绘事件发生时,Qt会调用涉及控件的paintEvent()函数。
在paintEvent()函数中,开发者可以创建一个QPainter对象,并将其绑定到当前的控件(即QPainterDevice)。然后,开发者可以使用QPainter的各种方法来绘制图形。
绘图工具与操作:
在绘图过程中,Qt提供了多种绘图工具,如画笔(QPen)、画刷(QBrush)、字体(QFont)等,用于定义线条、填充区域和文本的样式。开发者还可以使用QPainterPath类来绘制复杂的矢量图形。
渲染引擎:
Qt提供了多种渲染引擎,如基于CPU的栅格渲染器以及OpenGL等。
默认情况下,Qt使用基于CPU的栅格渲染器进行绘图。但在需要更高性能或更丰富视觉效果的情况下,开发者可以选择使用OpenGL等硬件加速的渲染引擎。

9.3 坐标系统

Qt的绘图操作通常在一个二维的坐标系统中进行。这个坐标系统的原点(0,0)通常位于控件的左上角。坐标系统中的x值向右增大,y值向下增大。QPainter还支持坐标变换(如旋转、缩放等),使得开发者可以更方便地进行复杂的绘图操作。

QPainterPath类来绘制复杂的矢量图形,示例代码如下:

#include <QApplication>  
#include <QWidget>  
#include <QPainter>  
#include <QPainterPath>  
  
class PathDemo : public QWidget  
{  
protected:  
    void paintEvent(QPaintEvent *event) override {  
        QPainter painter(this);  
        painter.setRenderHint(QPainter::Antialiasing);  
  
        QPainterPath path;  
  
        // 绘制一个矩形  
        path.addRect(10, 10, 100, 50);  
  
        // 移动到某个点并绘制一个椭圆  
        path.moveTo(150, 10);  
        path.addEllipse(150, 10, 80, 40);  
  
        // 绘制一条贝塞尔曲线  
        path.moveTo(270, 50);  
        path.cubicTo(300, 0, 350, 50, 400, 50);  
  
        // 绘制一个多边形  
        path.moveTo(50, 100);  
        path.lineTo(70, 80);  
        path.lineTo(100, 100);  
        path.lineTo(70, 120);  
        path.closeSubpath(); // 关闭子路径,使多边形封闭  
  
        // 使用 QPainter 绘制路径  
        painter.drawPath(path);  
    }  
}; 

效果:

十、图形/视图框架

图形视图框架由以下三个部分组成:场景QGraphicsScene视图QGraphicsView图形项QGraphicsItem

10.1 场景、视图、图形项的介绍

QGraphicsScene: 该类提供了图形视图框架中的场景,是图形项对象的容器,拥有以下功能
1.提供用于管理大量图形项的高速接口
2.传播事件到每一个图形项
3.管理图形项的状态,比如选择和处理焦点
4.提供无变换的渲染功能,主要用于打印

QGraphicsView:该类提供了视图部件,用来显示场景中的内容
1.可以连接多个视图到同一个场景,为相同的数据集提供多个视口
2.视图部件是一个可滚动的区域,提供了一个滚动条来浏览大的场景
3.通过setDragMode(QGraphicsView::ScrollHandDrag)将光标变为手掌形状,可以拖动场景
4.通过setDragMode(QGraphicsView::RubberBandDrag)实现鼠标拖出矩形框来选择场景中的图形项
5.通过setViewport()设置QOpenGLWidget作为视口,使用OpenGL进行渲染

QGraphicsItem: 该类是场景中图形项的基类,在图形视图框架中有提供一些典型形状的图形项
1.鼠标按下、移动、释放、双击、悬停、滚轮和右键菜单事件
2.键盘输入焦点和键盘事件
3.拖放事件
4.使用QGraphicsItemGroup实现分组
5.碰撞检测

10.2 坐标系统

图形视图框架中有三个有效的坐标系统:场景坐标视图坐标图形项坐标,这三个坐标系统可以通过特定函数进行坐标映射。

场景坐标:场景坐标是所有图形项的基础坐标系统,原点在场景的中心,x和y坐标分别向右和向下增大
视图坐标:视图坐标就是视图部件的坐标,原点在视口的左上角,x和y坐标分别向右和向下增大
图形项坐标:图形项使用自己的本地坐标系统,原点在图形项的中心,而这也是所有变换的中心
坐标映射:实现坐标变换,不仅可以在视图、场景和图形项之间使用坐标映射,还可以在父子图形项等之间进行映射:

 10.3 事件处理

图形视图框架中的事件都是先由视图进行接收,然后传递给场景,再由场景传递给相应的图形项。

1.键盘事件和图形效果:这里对图形项的键盘按下事件进行处理,并为图形项添加图形效果

//键盘按下事件处理,移动图形项
void MyItem::keyPressEvent(QKeyEvent *event)
{
    switch(event->key())
    {
    //移动图形项
    case Qt::Key_Up:   //上移
    {
        moveBy(0,-10);
        break;
    }
    case Qt::Key_Down:   //下移
    {
        moveBy(0,10);
        break;
    }
    case Qt::Key_Left:   //左移
    {
        moveBy(-10,0);
        break;
    }
    case Qt::Key_Right:   //右移
    {
        moveBy(10,0);
        break;
    }

    //添加图形效果
    case Qt::Key_1:   //模糊效果
    {
        QGraphicsBlurEffect *blurEffect = new QGraphicsBlurEffect;
        blurEffect->setBlurHints(QGraphicsBlurEffect::QualityHint);
        blurEffect->setBlurRadius(8);
        setGraphicsEffect(blurEffect);
        break;
    }
    case Qt::Key_2:   //染色效果
    {
        QGraphicsColorizeEffect *ColorizeEffect = new QGraphicsColorizeEffect;
        ColorizeEffect->setColor(Qt::white);
        ColorizeEffect->setStrength(0.6);
        setGraphicsEffect(ColorizeEffect);
        break;
    }
    case Qt::Key_3:   //阴影效果
    {
        QGraphicsDropShadowEffect *dropShadowEffect = new QGraphicsDropShadowEffect;
        dropShadowEffect->setColor(QColor(63,63,63,100));
        dropShadowEffect->setBlurRadius(2);
        dropShadowEffect->setXOffset(10);
        setGraphicsEffect(dropShadowEffect);
        break;
    }
    case Qt::Key_4:   //透明效果
    {
        QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect;
        opacityEffect->setOpacity(0.4);
        setGraphicsEffect(opacityEffect);
        break;
    }
    case Qt::Key_5:   //取消图形项的图形效果
        graphicsEffect()->setEnabled(false);
        break;
    }
}

 2.鼠标悬停效果:设置鼠标悬停在图形项上面时的光标外观和提示

//悬停事件处理,设置光标外观和提示
void MyItem::hoverEnterEvent(QGraphicsSceneHoverEvent *)
{
    setCursor(Qt::OpenHandCursor);
    setToolTip(QString("我是%1号图形项").arg(m_id));
}

3.鼠标移动事件和右键菜单:实现用鼠标拖动图形项,并为图形项添加一个右键菜单

//鼠标移动事件处理,获得焦点并改变光标外观
void MyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    setFocus();
    setCursor(Qt::ClosedHandCursor);

    //鼠标拖动设置图形项的场景坐标
    //QPointF scenePos = mapToScene(event->pos());
    //setPos(scenePos);

    //直接用这一句顶上面两句
    QGraphicsItem::mouseMoveEvent(event);
}

//右键菜单事件处理,为图形项添加一个右键菜单
void MyItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
    QMenu menu;
    QAction *viewAction = menu.addAction("移动到视图原点");
    QAction *sceneAction = menu.addAction("移动到场景原点");
    QAction *selectedAction = menu.exec(event->screenPos());
    if(selectedAction == viewAction)
    {
        setPos(-200,-150);   //与main函数中设置的场景矩形原点一致
    }
    else if(selectedAction == sceneAction)
    {
        setPos(0,0);
    }
}

 4.动画
(1)使用QPropertyAnimation类来为图形项的某属性创建动画

int main(int argc,char *argv[])
{
	...
	//为图形项的rotation属性创建动画
    MyItem *item_111 = new MyItem;
    item_111->setId(111);
    item_111->setColor(Qt::yellow);
    item_111->setPos(15,50);
    scene.addItem(item_111);
    QPropertyAnimation *animation = new QPropertyAnimation(item_111,"rotation");
    animation->setDuration(2000);
    animation->setStartValue(0);
    animation->setEndValue(360);
    animation->start(QAbstractAnimation::DeleteWhenStopped);
    ...
}

2)使用QGraphicsScene::advance()来推进场景

int main(int argc,char *argv[])
{
	...
	//创建定时器调用场景的advance()函数,并且会自动调用所有图形项的advance()函数
    QTimer timer;
    QObject::connect(&timer,&QTimer::timeout,&scene,&QGraphicsScene::advance);
    //timer.start(300);
    ...
}
    
//动画处理
void MyItem::advance(int phase)
{
    //第一个阶段不进行处理
    if(!phase)
    {
        return;
    }

    //图形项向不同方向随机移动
    int value = qrand() % 100;
    if(value < 25)
    {
        setRotation(45);
        moveBy(qrand() % 10,qrand() % 10);
    }
    else if(value < 50)
    {
        setRotation(-45);
        moveBy(-qrand() % 10,-qrand() % 10);
    }
    else if(value < 75)
    {
        setRotation(30);
        moveBy(-qrand() % 10,qrand() % 10);
    }
    else
    {
        setRotation(-30);
        moveBy(qrand() % 10,-qrand() % 10);
    }
}

5.碰撞检测
(1)重新实现使用QGraphicsItem::shape()函数来返回图形项准确的形状,结合碰撞判断函数使用
(2)重新实现collidesWithItem()函数来提供一个自定义的图形项碰撞算法

QGraphicsItem类中提供了下面这些碰撞判断函数:
collidesWithItem()来判断是否与指定的图形项进行了碰撞
collidesWithPath()来判断是否与指定的路径碰撞
collidingItems()来获取与该图形项碰撞的所有图形项的列表
这几个函数都有一个Qt::ItemSelectionMode参数来指定怎样进行图形项的选取,默认值是Qt::IntersectsItemShape

下面对第一种方式进行代码展示

//返回图形项对应的形状
QPainterPath MyItem::shape() const
{
    QPainterPath path;
    path.addRect(-10,-10,20,20);
    return path;
}

//执行实际的绘图操作
void MyItem::paint(QPainter *painter,const QStyleOptionGraphicsItem *,QWidget *)
{
    if(hasFocus() || !collidingItems().isEmpty())   //是否获得焦点或者有碰撞
    {
        painter->setPen(QPen(QColor(255,255,255,200)));
    }
    else
    {
        painter->setPen(QPen(QColor(100,100,100,100)));
    }
    painter->setBrush(m_brushColor);
    painter->drawRect(-10,-10,20,20);
}

 6.图形项组

QGraphicsItemGroup图形项组为图形项提供了一个容器,下面代码对其使用进行了展示

int main(int argc,char *argv[])
{
	...
	//创建图形项组
    MyItem *item_10 = new MyItem;
    item_10->setId(10);
    item_10->setColor(Qt::blue);
    MyItem *item_11 = new MyItem;
    item_11->setId(11);
    item_11->setColor(Qt::green);
    QGraphicsItemGroup *group = new QGraphicsItemGroup;   //手动创建图形项组
    group->setFlag(QGraphicsItem::ItemIsMovable);
    group->addToGroup(item_10);   //将图形项添加到项组
    group->addToGroup(item_11);
    item_11->setPos(30,0);
    scene.addItem(group);   //将项组添加到场景
    //QGraphicsItemGroup *group = scene.createItemGroup(scene.selectedItems());   //使用场景对象直接创建图形项组
    //group->QGraphicsItemGroup::setHandlesChildEvents(false);   //让项组内的图形项可以捕获自己的相关事件
    //group->removeFromGroup(item1);   //从项组中删除图形项
    //scene.destroyItemGroup(group);   //销毁整个图形项组
    ...
}

 7.打印

图形视图框架提供下面的渲染函数来完成打印功能。
场景坐标上使用QGraphicsScene::render()函数实现打印。
视图坐标上使用QGraphicsView::render()函数实现屏幕快照。

int main(int argc,char *argv[])
{
	...
	//在打印机上进行打印
    QPrinter printer;
    if(QPrintDialog(&printer).exec() == QDialog::Accepted)
    {
        QPainter painter1(&printer);
        painter1.setRenderHint(QPainter::Antialiasing);
        scene.render(&painter1);
    }

    //实现屏幕快照功能,在项目生成的目录中保存图像
    QPixmap pixmap(400,300);
    QPainter painter2(&pixmap);
    painter2.setRenderHint(QPainter::Antialiasing);
    view.render(&painter2);
    painter2.end();
    pixmap.save("view.png");
	...
}

 8.使用OpenGL进行渲染:使用QGraphicsView::setViewport()更改QGraphicsView的视口,就可以使用OpenGL进行渲染了

int main(int argc,char *argv[])
{
	...
	//自定义视图
    MyView view;
    view.setViewport(new QOpenGLWidget);
    //view.setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers)));   //使用OpenGL进行渲染
	...
}

  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值