Skip to content

Latest commit

 

History

History
730 lines (537 loc) · 43.2 KB

File metadata and controls

730 lines (537 loc) · 43.2 KB

九、Qt/C++ 反应式图形用户界面编程

Qt(发音为可爱)生态系统是一个全面的基于 C++ 的框架,用于编写跨平台和多平台的 GUI 应用。如果您使用库的可移植核心编写程序,您可以利用框架支持的一次编写和随处编译范例。在某些情况下,人们使用特定于平台的功能,例如支持编写基于 Windows 的应用的 ActiveX 编程模型。

我们遇到过 Qt 比 MFC 更适合在 Windows 中编写应用的情况。一个看似合理的原因可能是易于编程,因为 Qt 使用 C++ 语言特性的一个非常小的子集作为它的库。当然,框架的最初目标是跨平台开发。Qt 跨平台的单一源代码可移植性、特性的丰富性、源代码的可用性以及更新的文档,使它成为一个非常程序员友好的框架。自 1995 年第一次发行以来,这帮助它繁荣了二十多年。

Qt 提供了一个完整的界面环境,支持开发多平台 GUI 应用、Webkit APIs、媒体流、文件系统浏览器、OpenGL APIs 等等。要涵盖这个奇妙图书馆的全部特征,需要一本自己的书。本章的目的是介绍如何利用 Qt 和 RxCpp 库编写反应式图形用户界面应用。我们已经在第 7 章数据流计算和 RxCpp 库介绍第 8 章RxCPP–关键元素中介绍了反应式编程模型的核心。现在,是时候将前几章所学付诸实践了!Qt 框架本身有一个健壮的事件处理系统,在他或她将 RxCpp 结构合并到组合中之前,需要学习这些库特性。

在本章中,我们将探讨:

  • Qt 图形用户界面编程快速入门
  • 你好世界–Qt 计划
  • Qt 事件模型,包括信号/时隙/主运行中心——一个例子
  • 将 RxCpp 库与 Qt 事件模型集成
  • 在 Rxcpp 中创建自定义运算符

Qt 图形用户界面编程快速入门

Qt 是一个跨平台应用开发框架,用于编写软件,该软件可以作为本机应用在许多平台上运行,而无需更改太多代码,具有本机平台功能和速度。除了图形用户界面应用,我们还可以使用该框架编写控制台或命令行应用,但是主要的用例是图形用户界面。

虽然使用 Qt 的应用通常是用 C++ 编写的,但是 QML 对其他语言的绑定也存在。Qt 使用全面而强大的 API 和工具简化了 C++ 开发的许多方面。Qt 支持很多编译器工具链,比如 GCC C++ 编译器和 Visual C++ 编译器。Qt 还提供了 Qt Quick(包括 QML,一种基于 ECMAScript 的声明性脚本语言)来编写逻辑。这有助于移动平台的快速应用开发,尽管可以使用本机代码编写逻辑以获得最佳性能。ECMAScript/C++ 组合提供了最好的声明式开发和本机代码速度。

Qt 目前正由 Qt 公司开发和维护,该框架可通过开源和专有许可证获得。第一次推出时,Qt 通过模仿不同平台的外观和感觉来使用自己的绘画引擎和控件(得益于定制的绘画引擎,人们可以在 GNU Linux 下创建 Windows 外观和感觉)。这有助于开发人员轻松地跨平台移植,因为对目标平台的依赖性最小。由于模拟不完善,Qt 开始为平台使用本机风格的 API,并有自己的本机小部件集。这通过模拟 Qt 自己的油漆引擎解决了这个问题,但代价是平台之间没有更一致的外观和感觉。Qt 库与 Python 编程语言有很好的绑定,命名为 PyQt。

程序员在利用库之前,必须了解一些基本的东西。在接下来的部分中,我们将快速介绍 Qt 对象模型、信号和槽、事件系统和元对象系统的各个方面。

Qt 对象模型

在图形用户界面框架中,运行时效率和高级别灵活性都是关键因素。标准的 C++ 对象模型提供了非常高效的运行时支持,但是它的静态特性在某些有问题的领域中是不灵活的。Qt 框架结合了 C++ 的速度和 Qt 对象模型的灵活性。

Qt 对象模型支持以下特性:

  • 信号和插槽,用于无缝对象通信
  • 可查询和可设计的对象属性
  • 强大的事件和事件过滤器
  • 强大的内部驱动计时器,能够在事件驱动的图形用户界面中流畅、无阻塞地完成许多任务
  • 带上下文字符串翻译的国际化
  • 被引用对象被销毁时自动设置为 0 的保护指针( QPointers )
  • 跨库边界工作的动态转换

这些特性中的许多是作为标准的 C++ 类实现的,基于从QObject的继承。其他的,像信号和插槽以及对象属性系统,需要由 Qt 自己的元对象编译器 ( MOC )提供的元对象系统。元对象系统是 C++ 语言的扩展,使其更适合于图形用户界面编程。主运行中心充当预编译器,它根据源代码中嵌入的提示生成代码,并为 ANSI C++ 编译器移除这些提示以执行其正常编译任务。

让我们看看 Qt 对象模型中的一些类:

| 类名 | 描述 | | QObject | 所有 Qt 对象的基类(http://doc.qt.io/archives/qt-4.8/qobject.html) | | QPointer | 为QObject(http://doc.qt.io/archives/qt-4.8/qpointer.html)提供保护指针的模板类 | | QSignalMapper | 将来自可识别发送方的信号打包(http://doc.qt.io/archives/qt-4.8/qsignalmapper.html) | | QVariant | 就像最常见的 Qt 数据类型(http://doc.qt.io/archives/qt-4.8/qvariant.html)的联合 | | QMetaClassInfo | 关于一个类的附加信息(http://doc.qt.io/archives/qt-4.8/qmetaclassinfo.html) | | QMetaEnum | 关于枚举器的元数据(http://doc.qt.io/archives/qt-4.8/qmetaenum.html) | | QMetaMethod | 关于成员函数的元数据(http://doc.qt.io/archives/qt-4.8/qmetamethod.html) | | QMetaObject | 包含关于 Qt 对象的元信息(http://doc.qt.io/archives/qt-4.8/qmetaobject.html) | | QMetaProperty | 关于某个属性的元数据(http://doc.qt.io/archives/qt-4.8/qmetaproperty.html) | | QMetaType | 管理元对象系统中的命名类型(http://doc.qt.io/archives/qt-4.8/qmetatype.html) | | QObjectCleanupHandler | 观看多个QObject(http://doc.qt.io/archives/qt-4.8/qobjectcleanuphandler.html)的寿命 |

Qt 对象通常被视为身份,而不是值。身份是克隆的,不是复制或分配的;克隆身份比复制或赋值更复杂。因此,QObjectQObject的所有子类(直接或间接)都禁用了它们的复制构造函数和赋值运算符。

信号和插槽

信号和槽是 Qt 中用来实现对象间通信的机制。作为一个图形用户界面框架,信号和槽机制是 Qt 的核心特征。通过这种机制,小部件会得到 Qt 中其他小部件变化的通知。一般来说,任何类型的对象都使用这种机制相互通信。例如,当用户点击关闭按钮时,我们可能希望调用窗口的close()函数。

信号和槽是 C/C++ 中回调技术的替代。特定事件发生时会发出信号。Qt 框架中的所有小部件都有预定义的信号,但是我们总是可以子类化一个小部件来添加我们自己的信号。插槽是响应信号而调用的函数。类似于预定义的信号,Qt 小部件有许多预定义的槽,但是我们可以添加自定义槽来处理我们感兴趣的信号。

Qt 官方文档(http://doc.qt.io/archives/qt-4.8/signalsandslots.html)中的下图展示了对象间通信是如何通过信号和插槽进行的:

信号和插槽是松散耦合的通信机制;发出信号的类不关心接收信号的插槽。信号是火灾和遗忘系统的完美例子。信号和插槽系统确保如果一个信号连接到一个插槽,该插槽将在正确的时间用信号参数调用。信号和槽都可以接受任意类型的任意数量的参数,并且它们是完全类型安全的。信号和接收时隙的签名必须匹配;因此,编译器可以帮助我们检测类型不匹配,这是一个好处。

QObject或其任何子类(如QWidget)继承的所有对象都可以包含信号和槽。一个物体改变状态时会发出信号,这可能会引起其他物体的兴趣。对象不知道(或不关心)接收端是否有任何对象。一个信号可以连接到所需数量的插槽。同样,我们可以将任意多的信号连接到一个插槽。甚至可以将一个信号连接到另一个信号;因此,信号链接是可能的。

因此,信号和系统共同构成了一个极其灵活和可插拔的组件编程机制。

事件系统

在 Qt 中,事件表示应用或应用需要了解的用户活动中发生的事情。在 Qt 中,事件是从抽象的QEvent类派生的对象。事件可以由QObject子类的任何实例接收和处理,但是它们与小部件特别相关。

每当一个事件发生时,一个适当的QEvent子类实例被构造出来,并通过调用其event()函数将其所有权赋予QObject的一个特定实例(或任何相关的子类)。此函数不处理事件本身;根据传递的事件类型,它调用该特定类型事件的事件处理程序,并根据事件是被接受还是被忽略来发送响应。

有些事件,比如QCloseEventQMoveEvent,来自应用本身;有些,如QMouseEventQKeyEvent,来自窗户系统;还有一些,比如QTimerEvent,来自其他渠道。大多数事件都有从QEvent派生的特定子类,有时还有特定于事件的函数来满足扩展事件的特定行为。举例来说,QMouseEvent类添加了x()y()功能,使小部件能够发现鼠标光标的位置。

每个事件都有一个关联的类型,在QEvent::Type下定义,这是一个运行时类型信息的方便来源,用于快速识别事件是从哪个子类构造的。

事件处理程序

通常,事件是通过调用关联的虚函数来呈现的。虚拟功能负责按预期做出响应。如果自定义虚拟函数实现没有执行所有必需的操作,我们可能需要调用基类的实现。

例如,以下示例处理自定义标签小部件上的鼠标左键单击,同时将所有其他按钮单击传递给基础QLabel类:

void my_QLabel::mouseMoveEvent(QMouseEvent *evt)
{
    if (event->button() == Qt::LeftButton) {
        // handle left mouse button here
        qDebug() <<" X: " << evt->x() << "t Y: " << evt->y() << "n";
    }
    else {
        // pass on other buttons to base class
        QLabel::mouseMoveEvent(event);
    }
}

如果我们想替换基类功能,我们必须实现虚函数重写中的所有内容。如果需求是简单地扩展基类功能,我们可以实现我们想要的,并为我们不想处理的任何其他情况调用基类函数。

发送事件

许多使用 Qt 框架的应用想要发送自己的事件,就像框架提供的事件一样。通过使用事件对象并用QCoreApplication::sendEvent()QCoreApplication::postEvent()发送,可以构建合适的自定义事件。

sendEvent()执行同步;因此,它会立即处理该事件。对于很多事件类,有一个叫做isAccepted()的函数,它告诉我们事件是被最后一个被调用的处理程序接受还是拒绝。

postEvent()执行异步;因此,它将事件发布到队列中,以备以后调度。下一次 Qt 的主事件循环运行时,它会调度所有发布的事件,并进行一些优化。例如,如果有几个调整大小事件,它们将被压缩为一个,作为所有调整大小事件的联合,这避免了用户界面中的闪烁。

元对象系统

Qt 元对象系统实现了对象间通信的信号和槽机制、动态属性系统和运行时类型信息。

Qt 元对象系统基于三个关键方面:

  • QObject类:为 Qt 对象提供元对象系统优势的基类
  • Q_OBJECT宏:在类声明的私有部分提供的宏,用于启用元对象特性,如动态属性、信号和槽
  • 主运行中心:它为每个QObject子类提供必要的代码来实现元对象特性

主运行中心在 Qt 源文件的实际编译之前执行。当主运行中心找到包含Q_OBJECT宏的类声明时,它为这些类中的每一个生成另一个带有元对象代码的 C++ 源文件。这个生成的源文件或者使用#include包含在类的源文件中,或者更常见的是,编译并与类的实现链接。

你好世界–Qt 计划

现在,让我们开始使用 Qt/C++ 开发图形用户界面应用。在进入以下部分之前,请从 Qt 的官方网站(https://www.qt.io/download)下载 Qt SDK 和 Qt Creator。我们将在本章中讨论的代码完全与 LGPL 兼容,并将通过编写纯 C++ 代码进行手工编码。Qt 框架被设计得令人愉快和直观,这样您就可以在不使用 Qt Creator IDE 的情况下手工编写整个应用。

Qt Creator is a cross-platform C++, JavaScript, and QML integrated development environment, a part of the SDK for the Qt GUI application development framework. It includes a visual debugger and an integrated GUI layout and forms designer. The editor's features include syntax highlighting and autocompletion. Qt Creator uses the C++ compiler from the GNU Compiler Collection on Linux and FreeBSD. On Windows, it can use MinGW or MSVC, with the default install, and can also use Microsoft Console Debugger, when compiled from source code. Clang is also supported. – Wikipedia (https://en.wikipedia.org/wiki/Qt_Creator)

让我们从一个简单的你好世界程序开始,使用一个标签小部件。在本例中,我们将创建并显示一个标签小部件,文本为Hello World, QT!:

#include <QApplication> 
#include <QLabel> 

int main (int argc, char* argv[]) 
{ 
    QApplication app(argc, argv); 
    QLabel label("Hello World, QT!"); 
    Label.show(); 
    return app.execute(); 
}

在这段代码中,我们包含了两个库:<QApplication><QLabel>QApplication对象是在QApplication库中定义的,它管理应用中的资源,运行任何基于 Qt 图形用户界面的应用都需要它。这个对象接受程序的命令行参数,当调用app.execute()时,Qt 事件循环启动。

An event loop is a program structure that permits events to be prioritized, queued, and dispatched to objects. In an event-based application, certain functions are implemented as passive interfaces that get called in response to certain events. The event loop generally continues running until a terminating event occurs (the user clicks on the QUIT button, for example).

QLabel是所有 Qt 小部件中最简单的小部件,在<QLabel>中定义。在这段代码中,标签用文本Hello World, QT实例化。当label.show()被调用时,一个带有实例化文本的标签将出现在屏幕上自己的窗口框架中。

现在,为了构建和运行应用,我们首先需要的是一个项目文件。要创建项目文件并编译应用,我们需要遵循以下步骤:

  1. 创建一个目录并将源代码保存在一个 CPP 文件中,驻留在这个目录中。
  2. 打开一个外壳,使用qmake -v 命令验证安装的qmake版本。如果找不到qmake,需要将安装路径添加到环境变量中。
  3. 现在,将目录更改为 shell 中的 Qt 文件路径,并执行qmake -project命令。这将为应用创建一个项目文件。
  4. 打开项目文件,在INCLUDEPATH后的.pro文件中添加以下一行:
... 
INCLUDEPATH += . 
QT += widgets 
... 
  1. 然后,在没有参数的情况下运行qmake来创建包含构建应用的规则的make文件。
  2. 运行make ( nmakegmake,视平台而定),根据Makefile中指定的规则构建应用。
  3. 如果你运行应用,一个小窗口,上面有一个标签,写着你好,QT!会出现。

The steps to building any Qt GUI applications are the same, except for the changes that may be required in project files. For all of the future examples that we will discuss in this chapter, build and run means to follow these steps.

在我们继续下一个例子之前,让我们找点乐子。用以下代码替换QLabel实例化:

QLabel label("<h2><i>Hello World</i>, <font color=green>QT!</font></h2>"); 

现在,重建并运行应用。如这段代码所示,通过使用一些简单的 HTML 样式的格式,很容易定制 Qt 的用户界面。

在下一节中,我们将学习如何处理 Qt 事件以及如何使用信号和插槽进行对象通信。

带有信号/时隙/主运行中心的 Qt 事件模型——一个例子

在本节中,我们将创建一个应用来处理QLabel中的鼠标事件。我们将在自定义QLabel中覆盖鼠标事件,并在放置自定义标签的对话框中处理它们。该应用的方法如下:

  1. 创建一个自定义的my_QLabel类,继承自框架QLabel类,并覆盖鼠标事件,如鼠标移动、鼠标按下和鼠标离开。
  2. my_QLabel中定义与这些事件对应的信号,并从相应的事件处理程序中发出。
  3. 创建一个继承自QDialog类的对话框类,并手工编码所有小部件的位置和布局,包括为处理鼠标事件而创建的自定义小部件。
  4. 在对话框类中,定义槽来处理从my_QLabel对象发出的信号,并在对话框中显示适当的结果。
  5. QApplication对象下实例化该对话框,并执行。
  6. 创建项目文件来构建一个小部件应用,并使其启动和运行。

创建自定义小部件

让我们编写头文件my_qlabel.h来声明类my_QLabel:

#include <QLabel> 
#include <QMouseEvent> 

class my_QLabel : public QLabel 
{ 
    Q_OBJECT 
public: 
    explicit my_QLabel(QWidget *parent = nullptr); 

    void mouseMoveEvent(QMouseEvent *evt); 
    void mousePressEvent(QMouseEvent* evt); 
    void leaveEvent(QEvent* evt); 

    int x, y; 

signals: 
    void Mouse_Pressed(); 
    void Mouse_Position(); 
    void Mouse_Left(); 
}; 

QLabelQMouseEvent在包含的库、<QLabel><QMouseEvent>下定义。该类派生自QLabel以继承其默认行为,QObject则恰当地处理信号机制。

在头文件的私有部分,我们添加了一个Q_OBJECT宏,通知 MOC 必须为这个类生成元对象代码。信号和槽机制、运行时类型信息和动态属性系统都需要元对象代码。

在类头中,与构造函数声明一起,鼠标事件(如鼠标移动事件、鼠标按下事件和鼠标离开事件)被重写。此外,公共整数变量保存鼠标指针的当前 XY 坐标。最后,从每个鼠标事件发出的信号在信号部分下声明。

现在,让我们在一个 CPP 文件my_qlabel.cpp中定义这些项目:

#include "my_qlabel.h" 

my_QLabel::my_QLabel(QWidget *parent) : QLabel(parent), x(0), y(0)  {} 

void my_QLabel::mouseMoveEvent(QMouseEvent *evt) 
{ 
    this->x = evt->x(); 
    this->y = evt->y(); 
    emit Mouse_Position(); 
} 

在构造函数中,父类被传递给QLabel基类,继承被覆盖类中未处理的情况,坐标变量被初始化为零。在mouse-move事件处理程序中,保存鼠标坐标的成员变量得到更新,并发出信号Mouse_Position()。使用my_QLabel的对话框可以将该信号连接到父对话框类中相应的mouse-move槽,并更新图形用户界面:

void my_QLabel::mousePressEvent(QMouseEvent *evt) 
{ 
    emit Mouse_Pressed(); 
} 

void my_QLabel::leaveEvent(QEvent *evt) 
{ 
   emit Mouse_Left(); 
} 

mouse-press事件处理程序发出信号Mouse_Pressed(),从mouse-leave事件发出信号Mouse_Left()。这些信号连接到父小部件(Dialog类)的相应插槽,并更新图形用户界面。因此,我们编写了一个自定义标签类来处理鼠标事件。

创建应用对话框

由于标签类已经实现,我们需要实现对话框类来放置所有的小部件,并处理从my_QLabel对象发出的所有信号。让我们从dialog.h头文件开始:

#include <QDialog> 

class my_QLabel; 
class QLabel; 

class Dialog : public QDialog 
{ 
    Q_OBJECT 
public: 
    explicit Dialog(QWidget *parent = 0); 
    ~Dialog(); 

private slots: 
    void Mouse_CurrentPosition(); 
    void Mouse_Pressed(); 
    void Mouse_Left(); 

private: 
    void initializeWidgets(); 
    my_QLabel *label_MouseArea; 
    QLabel *label_Mouse_CurPos; 
    QLabel *label_MouseEvents; 
}; 

这里,我们正在创建一个继承自QDialogDialog类,在<QDialog>库下定义。类QLabelmy_QLabel在这个类头中被正向声明,因为实际的库将包含在类定义文件中。正如我们已经讨论过的,必须包含Q_OBJECT宏来生成用于启用信号和槽机制的元对象代码、运行时类型信息和动态属性系统。

除了构造函数和析构函数声明之外,还声明了私有槽来连接从my_QLabel对象发出的信号。插槽是正常功能,可以正常调用;它们唯一的特点是信号可以连接到它们。Mouse_CurrentPosition()槽将连接到从my_QLabel物体的mouseMoveEvent()发出的信号。同样的,Mouse_Pressed()会连接到mousePressEvent(),而MouseLeft()会连接到my_QLabel对象的leaveEvent()

最后,完成所有小部件指针和一个名为initializeWidgets()的私有函数的声明,以实例化和布局对话框中的小部件。

Dialog类的实现属于dialog.cpp:

#include "dialog.h" 
#include "my_qlabel.h" 
#include <QVBoxLayout> 
#include <QGroupBox> 

Dialog::Dialog(QWidget *parent) : QDialog(parent) 
{ 
    this->setWindowTitle("My Mouse-Event Handling App"); 
    initializeWidgets(); 

    connect(label_MouseArea, SIGNAL(Mouse_Position()), this, SLOT(Mouse_CurrentPosition())); 
    connect(label_MouseArea, SIGNAL(Mouse_Pressed()), this, SLOT(Mouse_Pressed())); 
    connect(label_MouseArea, SIGNAL(Mouse_Left()), this, SLOT(Mouse_Left())); 
} 

在构造器中,应用对话框的标题设置为My Mouse-Event Handling App。然后,initializeWidgets()函数被调用——稍后将解释该函数。创建并设置调用initializeWidgets()的布局后,从my_QLabel对象发出的信号连接到在Dialog类中声明的相应插槽:

void Dialog::Mouse_CurrentPosition() 
{ 
    label_Mouse_CurPos->setText(QString("X = %1, Y = %2") 
                                    .arg(label_MouseArea->x) 
                                    .arg(label_MouseArea->y)); 
    label_MouseEvents->setText("Mouse Moving!"); 
} 

Mouse_CurrentPosition()功能是从my_QLabel对象的鼠标移动事件发出的信号的插槽。在该功能中,标签小部件label_Mouse_CurPos用当前鼠标坐标进行更新,label_MouseEvents将其文本更新为Mouse Moving!:

void Dialog::Mouse_Pressed() 
{ 
    label_MouseEvents->setText("Mouse Pressed!"); 
} 

Mouse_Pressed()功能是鼠标按下事件发出的信号的插槽,每次用户点击鼠标区域(对象my_QLabel内部)时都会调用该功能。该功能将label_MouseEvents标签中的文本更新为"Mouse Pressed!":

void Dialog::Mouse_Left() 
{ 
    label_MouseEvents->setText("Mouse Left!"); 
} 

最后,每当鼠标离开鼠标区域时,my_QLabel对象的鼠标离开事件会发出一个连接到Mouse_Left()插槽功能的信号。然后,它将label_MouseEvents标签中的文本更新为"Mouse Left!"

使用initializeWidgets()功能实例化并设置对话框中的布局,如下所示:

void Dialog::initializeWidgets() 
{ 
    label_MouseArea = new my_QLabel(this); 
    label_MouseArea->setText("Mouse Area"); 
    label_MouseArea->setMouseTracking(true); 
    label_MouseArea->setAlignment(Qt::AlignCenter|Qt::AlignHCenter); 
    label_MouseArea->setFrameStyle(2); 

在这段代码中,label_MouseArea对象用自定义标签类my_QLabel *进行实例化。*然后,修改标签属性(如标签文本修改为"Mouse Area"),在label_MouseArea对象内部启用鼠标跟踪,对齐设置为居中,框架样式设置为粗线。

label_Mouse_CurPos = new QLabel(this);
label_Mouse_CurPos->setText("X = 0, Y = 0");
label_Mouse_CurPos->setAlignment(Qt::AlignCenter|Qt::AlignHCenter);
label_Mouse_CurPos->setFrameStyle(2);
label_MouseEvents = new QLabel(this);
label_MouseEvents->setText("Mouse current events!");
label_MouseEvents->setAlignment(Qt::AlignCenter|Qt::AlignHCenter);
label_MouseEvents->setFrameStyle(2);

标签对象label_Mouse_CurPoslabel_MouseEvents正在更新其属性,例如文本对齐和框架样式,类似于label_MouseArea对象。但是label_Mouse_CurPos中的文本最初设置为"X = 0, Y = 0",而label_MouseEvents标签设置为"Mouse current events!":

    QGroupBox *groupBox = new QGroupBox(tr("Mouse Events"), this); 
    QVBoxLayout *vbox = new QVBoxLayout; 
    vbox->addWidget(label_Mouse_CurPos); 
    vbox->addWidget(label_MouseEvents); 
    vbox->addStretch(0); 
    groupBox->setLayout(vbox); 

    label_MouseArea->move(40, 40); 
    label_MouseArea->resize(280,260); 
    groupBox->move(330,40); 
    groupBox->resize(200,150); 
}

最后,创建一个垂直的方框布局(QVBoxLayout),并在其中添加label_Mouse_CurPoslabel_MouseEvents标签小部件。另外,用标签Mouse Events创建一个分组框,分组框的布局被做成垂直的框布局,用小部件创建。最后,鼠标区域标签和鼠标事件组框的位置和大小被设置为预定义的值。因此,小部件的创建和布局设置就完成了。

执行应用

我们现在可以编写main.cpp来创建Dialog类并显示它:

#include "dialog.h" 
#include <QApplication> 

int main(int argc, char *argv[]) 
{ 
    QApplication app(argc, argv); 
    Dialog dialog; 
    dialog.resize(545, 337); 
    dialog.show(); 
    return app.exec(); 
} 

这段代码与我们讨论的 Hello World Qt 应用完全一样。我们正在实例化我们创建的Dialog类,而不是QLabel,通过使用resize()函数将对话框窗口调整到预定义的值。现在,应用已经准备好构建和运行了。但是,在构建应用之前,让我们手工编码项目文件:

QT += widgets 

SOURCES +=  
        main.cpp  
        dialog.cpp  
    my_qlabel.cpp 

HEADERS +=  
        dialog.h  
    my_qlabel.h 

现在,构建应用并运行它。将弹出如下对话框(Windows 平台):

当我们将鼠标指针悬停在左侧标签(鼠标区域)上时,鼠标的坐标将在右侧的第一个标签中更新,右侧的第二个标签将显示文本,鼠标移动!按下鼠标区域中的任何鼠标按钮,第二个标签中的文本将变为“鼠标按下”!当鼠标指针离开鼠标区域时,文字会更新为鼠标左键!

在本节中,我们学习了如何创建对话框窗口、对话框下的小部件、小部件中的布局等。我们还学习了如何启用自定义小部件(标签小部件),以及如何处理系统事件。然后,我们学习了使用用户定义的信号和插槽创建和连接对象。最后,我们使用了所有这些小部件,包括一个自定义小部件,并创建了一个应用来处理窗口中的 Qt 鼠标事件。

现在,让我们实现一个类似的应用来处理QLabel中的鼠标事件,并在另一个标签中显示鼠标坐标。这里,事件处理通过使用事件订阅和事件过滤来执行,具有RxCpp可观察值和 Qt 事件过滤器。

将 RxCpp 库与 Qt 事件模型集成

在前面的章节中,我们已经从鸟瞰图中看到了 Qt 框架。我们学习了如何处理 Qt 事件,尤其是鼠标事件和信号/槽机制。在前两章中,我们也了解了RxCpp库及其编程模型。在这个过程中,我们遇到了许多重要的反应操作符,这些操作符在利用反应方法编写程序时很重要。

在本节中,我们将编写一个应用来处理标签小部件中的鼠标事件,这与前面的示例类似。在这个例子中,我们将使用RxCpp订阅者订阅 Qt 鼠标事件,并从结果鼠标事件流中过滤不同的鼠标事件,而不是处理鼠标事件来发出信号(就像我们在上一个例子中所做的那样)。事件(未被过滤掉)将与订阅者相关。

Qt 事件过滤器–反应式方法

如前所述,Qt 框架有一个健壮的事件机制。我们需要在 Qt 和 RxCpp 方案之间架起一座桥梁。为了开始使用这个应用,我们将编写一个头文件rx_eventfilter.h,包装所需的 RxCpp 头和 Qt 事件过滤器:

#include <rxcpp/rx.hpp> 
#include <QEvent> 
namespace rxevt { 
    // Event filter object class 
    class EventEater: public QObject  { 
    Public: 
        EventEater(QObject* parent, QEvent::Type type, rxcpp::subscriber<QEvent*> s): 
        QObject(parent), eventType(type), eventSubscriber(s) {} 
       ~EventEater(){ eventSubscriber.on_completed();}

包含<rxcpp/rx.hpp>库是为了得到我们在这个类中使用的RxxCppsubscriberobservable的定义,以及QEvent定义的<QEvent>库。整个头文件在命名空间rxevt下定义。现在,EventEater类是植入到filter-in的 Qt 事件过滤器类,这是成员eventType唯一初始化的 Qt 事件。为此,类有两个成员变量。第一个是eventSubscriber,是QEvent型的rxcpp::subscriber,下一个是eventType,用来握持QEvent::Type

在构造函数中,父类QObject(需要过滤事件的小部件)被传递给基类QObject。成员变量eventTypeeventSubscriber用需要过滤的QEvent::Type和对应事件类型的rxcpp::subscriber初始化:

        bool eventFilter(QObject* obj, QEvent* event) { 
            if(event->type() == eventType) 
            { eventSubscriber.on_next(event);} 
            return QObject::eventFilter(obj, event); 
        } 

只有当事件类型与初始化类型相同时,我们才会覆盖eventFilter()函数来调用on_next()EventEater是一个事件过滤器对象,接收发送到该对象的所有事件。筛选器可以停止该事件,也可以将其转发给此对象。EventEater对象通过其eventFilter()功能接收事件。如果事件应该被过滤(换句话说,停止),则eventFilter()功能(http://doc.qt.io/qt-5/qobject.html#eventFilter)必须返回真;否则,必须返回false:

    private: 
        QEvent::Type eventType; 
        rxcpp::subscriber<QEvent*> eventSubscriber; 
    }; 

因此,让我们在同一个头文件下编写一个实用函数,使用EventEater对象从事件流中创建并返回一个rxcpp::observable:

    // Utility function to retrieve the rxcpp::observable of filtered events 
    rxcpp::observable<QEvent*> from(QObject* qobject, QEvent::Type type) 
    { 
        if(!qobject) return rxcpp::sources::never<QEvent*>(); 
         return rxcpp::observable<>::create<QEvent*>( 
            [qobject, type](rxcpp::subscriber<QEvent*> s) { 
                qobject->installEventFilter(new EventEater(qobject, type, s)); 
            } 
        ); 
    } 
} // rxevt 

在这个函数中,我们从事件流中返回QEvent的可观察值,我们将使用EventEater对象对其进行过滤。一个QObject实例可以被设置为在另一个QObject实例看到它们之前监控它们的事件。这是 Qt 事件模型的一个非常强大的特性。installEventFilter()函数的调用使其成为可能,EventEater类具备执行过滤的条件。

创建窗口-设置布局和路线

现在,让我们编写应用代码来创建小部件窗口,它包含两个标签小部件。一个标签将用作鼠标区域,类似于前面的示例,后者将用于显示过滤后的鼠标事件和鼠标坐标。

让我们将main.cpp中的代码分为两部分来看。首先,我们将讨论创建和设置小部件布局的代码:

#include "rx_eventfilter.h" 
int main(int argc, char *argv[]) 
{ 
    QApplication app(argc, argv); 
    // Create the application window 
    auto widget = std::unique_ptr<QWidget>(new QWidget()); 
    widget->resize(280,200); 
        // Create and set properties of mouse area label 
    auto label_mouseArea   = new QLabel("Mouse Area"); 
    label_mouseArea->setMouseTracking(true); 
    label_mouseArea->setAlignment(Qt::AlignCenter|Qt::AlignHCenter); 
    label_mouseArea->setFrameStyle(2); 
    // Create and set properties of message display label 
    auto label_coordinates = new QLabel("X = 0, Y = 0"); 
    label_coordinates->setAlignment(Qt::AlignCenter|Qt::AlignHCenter); 
    label_coordinates->setFrameStyle(2);

我们已经包含了rx_eventfilter.h头文件,以使用使用RxCpp库实现的事件过滤机制。在这个应用中,不是在对话框中创建这些小部件,而是创建一个QWidget对象,并将两个QLabel小部件添加到一个QVBoxLayout布局中;这被设置为应用小部件的布局。应用窗口的大小是一个预定义值200pixels宽和280pixels高。与前面的应用类似,第一个标签启用了鼠标跟踪:

    // Adjusting the size policy of widgets to allow stretching 
    // inside the vertical layout 
    label_mouseArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); 
    label_coordinates->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); 
    auto layout = new QVBoxLayout; 
    layout->addWidget(label_mouseArea); 
    layout->addWidget(label_coordinates); 
    layout->setStretch(0, 4); 
    layout->setStretch(1, 1); 
    widget->setLayout(layout); 

两个小部件的大小策略都设置为QSizePolicy::Expanding,以允许在垂直布局框内拉伸小部件。这允许我们使鼠标区域标签大于状态显示标签。setStretch()功能将位置索引处的拉伸因子设置为拉伸。

特定于事件类型的可观察值

订阅鼠标事件rxcpp::observable的代码如下:

  • 鼠标移动
  • 鼠标按键
  • 鼠标按键双击

程序如下:

    // Display the mouse move message and the mouse coordinates 
    rxevt::from(label_mouseArea, QEvent::MouseMove) 
            .subscribe([&label_coordinates](const QEvent* e){ 
        auto me = static_cast<const QMouseEvent*>(e); 
        label_coordinates->setText(QString("Mouse Moving : X = %1, Y = %2") 
                                   .arg(me->x()) 
                                   .arg(me->y())); 
    });

rxevt::from()函数根据我们作为参数传递的QEvent::Typelabel_mouseArea返回事件的rxcpp::observable。在这段代码中,我们订阅了label_mouseArea中的一组事件,它们属于QEvent::MouseMove类型。这里,我们用鼠标指针当前的 XY 位置更新label_coordinates文本:

    // Display the mouse signle click message and the mouse coordinates 
    rxevt::from(label_mouseArea, QEvent::MouseButtonPress) 
            .subscribe([&label_coordinates](const QEvent* e){ 
        auto me = static_cast<const QMouseEvent*>(e); 
        label_coordinates->setText(QString("Mouse Single click at X = %1, Y = %2") 
                                   .arg(me->x()) 
                                   .arg(me->y())); 
    }); 

类似于鼠标移动过滤,可观察到的QEventrxevt::from()函数返回,仅包括类型为QEvent::MouseButtonPress的事件。然后,文本在label_coordinates中更新,鼠标点击的位置:

    // Display the mouse double click message and the mouse coordinates 
    rxevt::from(label_mouseArea, QEvent::MouseButtonDblClick) 
            .subscribe([&label_coordinates](const QEvent* e){ 
        auto me = static_cast<const QMouseEvent*>(e); 
        label_coordinates->setText(QString("Mouse Double click at X = %1, Y = %2") 
                                   .arg(me->x()) 
                                   .arg(me->y())); 
    }); 
    widget->show(); 
    return app.exec(); 
} // End of main 

最后事件类型QEvent::MouseButtonDblClick的处理也类似于鼠标的单次点击,label_coordinates中的文本也随着双击位置而更新。然后调用应用窗口小部件的show()函数,调用exec()函数启动事件循环。

项目文件Mouse_EventFilter.pro如下:

QT += core widgets 
CONFIG += c++ 14 

TARGET = Mouse_EventFilter 
INCLUDEPATH += include 

SOURCES +=  
    main.cpp 
HEADERS +=  
    rx_eventfilter.h  

由于 RxCpp 库是一个只包含标题的库,因此在项目目录内创建了一个名为include的文件夹,RxCpp 库文件夹被复制到那里。更新INCLUDEPATH将帮助应用获取指定目录中的任何包含文件。现在,让我们构建并运行该应用。

RxQt 简介

RxQt库是在RxCpp库的基础上编写的公共领域库,可以轻松地用 Qt 事件和信号进行反应式编程。为了理解这个库,让我们跳到一个例子中,这样我们就可以跟踪鼠标事件,并使用库提供的可观察值过滤它们。该库可从https://github.com/tetsurom/rxqt的 GitHub 资源库下载:

#include <QApplication> 
#include <QLabel> 
#include <QMouseEvent> 
#include "rxqt.hpp" 

int main(int argc, char *argv[]) 
{ 
    QApplication app(argc, argv); 

    auto widget = new QWidget(); 
    widget->resize(350,300); 
    widget->setCursor(Qt::OpenHandCursor); 

    auto xDock = new QLabel((QWidget*)widget); 
    xDock->setStyleSheet("QLabel { background-color : red}"); 
    xDock->resize(9,9); 
    xDock->setGeometry(0, 0, 9, 9); 

    auto yDock = new QLabel((QWidget*)widget); 
    yDock->setStyleSheet("QLabel { background-color : blue}"); 
    yDock->resize(9,9); 
    yDock->setGeometry(0, 0, 9, 9); 

前面的代码创建了QWidget,作为另外两个QLabels的父级。创建了两个标签小部件,沿着窗口的顶部和左侧边界在父小部件内部移动。沿 X 轴的可停靠标签为红色,沿 Y 轴的标签为蓝色:

    rxqt::from_event(widget, QEvent::MouseButtonPress) 
            .filter([](const QEvent* e) { 
        auto me = static_cast<const QMouseEvent*>(e); 
        return (Qt::LeftButton == me->buttons()); 
    }) 
            .subscribe([&](const QEvent* e) { 
        auto me = static_cast<const QMouseEvent*>(e); 
        widget->setCursor(Qt::ClosedHandCursor); 
        xDock->move(me->x(), 0); 
        yDock->move(0, me->y()); 
    }); 

在前面的代码中,rxqt::from_event()函数从 widget 类中过滤掉除了QEvent::MouseButtonPress事件之外的所有事件,并返回一个rxcpp::observable<QEvent*>实例。如果按钮是鼠标左键,这里的rxcpp::observable已经用那些鼠标事件过滤了。然后,在subscribe()方法的 Lambda 函数中,我们将光标变为Qt::ClosedHandCursor。我们还将xDock的位置设置为鼠标x-位置值,以及窗口的上边缘,将yDock的位置设置为鼠标y-位置,以及窗口的左边缘:

    rxqt::from_event(widget, QEvent::MouseMove) 
            .filter([](const QEvent* e) { 
        auto me = static_cast<const QMouseEvent*>(e); 
        return (Qt::LeftButton == me->buttons()); 
    }) 
            .subscribe([&](const QEvent* e) { 
        auto me = static_cast<const QMouseEvent*>(e); 
        xDock->move(me->x(), 0); 
        yDock->move(0, me->y()); 
    });

在这段代码中,我们使用RxQt库过滤小部件窗口中的所有鼠标移动事件。这里可以观察到的是一系列鼠标事件,包括鼠标移动和鼠标左键按下事件。在 subscribe 方法中,代码沿着窗口的上边缘和左边缘更新xDockyDock的位置:

    rxqt::from_event(widget, QEvent::MouseButtonRelease) 
            .subscribe([&widget](const QEvent* e) { 
        widget->setCursor(Qt::OpenHandCursor); 
    }); 

    widget->show(); 
    return app.exec(); 
} 

最后过滤掉过滤后的鼠标按键释放事件,将鼠标光标设置回Qt::OpenHandCursor。为了给这个应用增加一些乐趣,让我们再创建一个小部件,类似于xDockyDock;这将是一个重力物体。按下时,重力对象将跟随鼠标光标:

#ifndef GRAVITY_QLABEL_H 
#define GRAVITY_QLABEL_H 

#include <QLabel> 

class Gravity_QLabel : public QLabel 
{ 
   public: 
    explicit Gravity_QLabel(QWidget *parent = nullptr): 
         QLabel(parent), prev_x(0), prev_y(0){} 

    int prev_x, prev_y; 
}; 

#endif // GRAVITY_QLABEL_H 

现在,我们必须在应用窗口下创建一个重力小部件的实例(来自新创建的Gravity_QLabel类):

    auto gravityDock = new Gravity_QLabel((QWidget*)widget); 
    gravityDock->setStyleSheet("QLabel { background-color : green}"); 
    gravityDock->resize(9,9); 
    gravityDock->setGeometry(0, 0, 9, 9);

类似于xDockyDock的创建和大小设置,新的gravityDock对象已经创建。此外,每当抛出press事件时,必须在鼠标坐标值中设置该对象的位置。因此,在QEvent::MouseButtonPress的 subscribe 方法的 Lambda 函数内部,我们需要添加以下代码行:

    gravityDock->move(me->x(),me->y()); 

最后gravityDock的位置需要更新,按照鼠标移动。为此,在QEvent::MouseMovesubscribe方法的 Lambda 函数内部,我们需要添加以下代码:

    gravityDock->prev_x = gravityDock->prev_x * .96 + me->x() * .04; 
    gravityDock->prev_y = gravityDock->prev_y * .96 + me->y() * .04; 
    gravityDock->move(gravityDock->prev_x, gravityDock->prev_y); 

这里gravityDock的位置被更新为一个新的值,该值是先前值的 96%和新位置的 4%之和。因此,我们使用RxQt和 RxCpp 库过滤 Qt 事件,以创建一个 X - Y 鼠标位置指示器和一个重力对象。现在,让我们构建并运行该应用。

摘要

在本章中,我们讨论了使用 Qt 进行反应式图形用户界面编程的主题。我们首先快速概述了使用 Qt 开发图形用户界面应用。我们学习了 Qt 框架中的概念,例如 Qt 对象层次结构、元对象系统以及信号和槽。我们使用一个简单的标签小部件编写了一个基本的你好世界应用。然后,我们使用自定义标签小部件编写了一个鼠标事件处理应用。在那个应用中,我们了解了更多关于 Qt 事件系统如何工作,以及如何使用信号和槽机制进行对象通信。最后,我们编写了一个应用来处理鼠标事件,并通过使用RxCpp订阅模型和 Qt 事件过滤器来过滤它们。我们介绍了如何在图形用户界面框架(如 Qt)中使用 RxCpp 来遵循反应式编程模型。我们还介绍了RxQt库,这是一个集成了 RxCpp 和 Qt 库的公共领域。

在进入下一章之前,您需要了解如何为 RxCpp 可观测值编写自定义运算符。这一主题将在在线部分讨论。您可以参考以下链接:https://www . packtpub . com/sites/default/files/downloads/Creating _ Custom _ Operators _ in _ rxcpp . pdf

阅读完前面提到的主题后,我们可以进入下一章,在这一章中,我们将了解 C++ 反应式编程的设计模式和习惯用法。