《OpenCV3和Qt5计算机视觉应用开发》学习笔记第12章

第12章Qt Quick应用程序
使用Qt控件应用程序工程项目,允许通过Qt Creator设计模式,或通过在文本编辑器中手动修改GUI文件(*.ui),来创建灵活而强大的图形用户界面。直到现在,在本书的所有章节中,我们依赖Qt控件应用程序作为创建GUI的基础,正如在第3章中学到的那样,可以使用样式表来有效地改变Qt应用程序的外观。但是除了Qt控件应用程序以及使用QtWidgets和QtGui模块之外,Qt框架还提供了另外的方法创建GUI。该方法基于QtQuick模块和QML语言,它允许创建更灵活(在外观、动画、效果等方面)的GUI,并采用更轻松的方式。通过这种方式创建的应用程序称为Qt Quick应用程序。注意,在最新的Qt版本(5.7之后)中,也可以创建Qt Quick Controls 2应用程序,它为Qt Quick应用程序的创建提供了更多改进的类型,我们还将重点关注它。
QtQuick模块和QtQml模块包含了在C++应用程序中使用Qt Quick和QML编程所必需的所有类。另一方面,QML本身是一种高度可读的声明性语言,它使用一种JSON式语法(结合脚本),通过把握不同的组件及其相互之间的交互方式来描述用户界面。本章将介绍QML语言,以及如何使用它来简化创建GUI应用程序的过程。我们将学习有关它的简单易读的语法,并通过创建一个基于QML的GUI应用程序,或者更精确地说是Qt Quick Controls 2应用程序,学习如何在实践中使用它。尽管使用QML语言并不需要对C++语言有深入地了解,但理解Qt Quick工程项目的结构仍然是很有用的,因此将简要介绍最基本的Qt快速应用程序的结构。还将通过浏览一些最重要的QML库,学习现有的可视和非可视的QML类型,这些类型可以用来创建用户界面,向其添加动画、访问硬件等等。我们还将学习如何使用Qt Quick Designer(已集成到Qt Creator中),通过图形化设计器修改QML文件。稍后,通过学习C++和QML的集成,我们将它们连接起来,并学习如何在Qt Quick应用程序内使用OpenCV框架。在这一章的最后,还将学习如何使用与Qt和OpenCV相同的桌面工程项目来创建移动计算机视觉应用程序,并将我们的跨平台扩展到桌面平台以外,进入移动世界。

本章将介绍以下主题:
❑ QML介绍
❑ Qt Quick应用程序工程项目的结构
❑ 创建Qt Quick Controls 2应用程序
❑ Qt Quick Designer的使用
❑ C++和QML集成
❑ 在Android和iOS上运行Qt和OpenCV应用程序12.1 QML介绍
正如在简介中介绍的那样,QML有一个类似于JSON的结构,可以用来描述用户界面上的元素。QML代码可以导入一个或多个库,并且有一个根元素,其中包含所有其他可视和非可视的元素。下面是QML代码的一个例子,其结果将创建一个有指定的宽、高和标题的空窗口(ApplicationWindow类型):

import QtQuick 2.12
import QtQuick.Controls 2.12

ApplicationWindow
{
    visible: true
    width: 300
    height: 500
    title: qsTr("Hello QML")
}


每一个import语句后面必须是QML库名和版本。上述代码中,导入的两个主要的QML库包含了大多数默认类型。例如,在QtQuick.Controls 2.2库内定义了ApplicationWindow。对于现有的QML库及其正确的版本,唯一的了解渠道是Qt文档,所以如果要使用任何其他类,一定要查阅Qt文档。如果使用Qt Creator帮助模式搜索ApplicationWindow,就会发现必需的import语句就是我们刚刚使用过的。另一个值得注意的是,上面代码中的ApplicationWindow是一个根对象元素,所有其他用户界面(UI)的对象元素都必须在其内部创建。让我们进一步扩展代码,添加一个标签(Label)元素以显示一些文本:

ApplicationWindow
{
    visible: true
    width: 300
    height: 500
    title: qsTr("Hello QML")
    
    Label
    {
        x:25
        y:25
        text: "This is a label<br>that contains<br>multiple lines!"
    }
}


因为import语句与之前一样,所以在上面的代码中跳过了import语句。请注意,新添加的标签(Label)有一个文本属性,这就是在标签上显示的文本。x和y指的是ApplicationWindow内的标签位置。可以以一种十分类似的方式添加诸如分组框这样的容器项。下面添加一个分组框,看看是如何实现的:

ApplicationWindow
{
    visible: true
    width: 300
    height: 500
    title: qsTr("Hello QML")
    GroupBox
    {
        x:50
        y:50
        width:150
        height:150
        Label
        {
            x:25
            y:25
            text: "This is a label<br>that contains<br>multiple lines!"
        }
    }
}

这段QML代码将产生一个类似于图12-1的窗口。

     图12-1 QML代码生成的结果

请注意,每一个元素的位置都是其父元素的偏移量。例如,GroupBox内提供给标签(Label)的x和y值需要加上GroupBox自身的x和y属性,才能得到这个用户界面(UI)元素在根元素内的最终位置(在该例中,根元素是ApplicationWindow)。

与Qt控件类似,还可以使用QML代码中的布局来控制和组织UI元素。出于此目的,可以使用GridLayout、ColumnLayout和RowLayout QML类型,但是,首先需要使用下面的语句将其导入:

import QtQuick.Layouts 1.12

现在,可以将QML用户界面元素作为子对象元素添加到一个布局中,并且布局会对它们进行自动管理。让我们在一个ColumnLayout中添加几个按钮,并看看这是如何工作的:

import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.2

ApplicationWindow
{
    visible: true
    width: 300
    height: 500
    title: qsTr("Hello QML")
    ColumnLayout
    {
        anchors.fill: parent

        Button
        {
            text: "First Button"
            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
        }

        Button
        {
            text: "Second Button"
            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
        }

        Button
        {
            text: "Third Button"
            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
        }

    }
}

图12-2 运行上述代码生成的窗口界面截图

这会生成一个与图12-2类似的窗口:

在上述代码中,ColumnLayout的动作方式与在Qt控件应用程序中使用的垂直布局类似。从上到下,作为子对象元素添加到ColumnLayout中的每一个对象元素都会显示在前一个对象元素的后面,不管ColumnLayout的大小如何,其中的对象元素始终会调整和重新定位以保持垂直布局视图。关于上述内容,还有几点需要注意。首先,通过使用下面的代码将ColumnLayout自身的大小设置为其父对象的大小:

anchors.fill: parent

anchors是QML视觉对象元素最重要的属性之一,负责元素的大小和位置。在本例中,通过将anchors的填充值设置为另一个对象(父对象),将ColumnLayout的大小、位置设置为与ApplicationWindow一样。通过正确地使用anchors,可以用更强大而灵活的方式处理对象大小和位置。另一个例子中,用下面的代码行替换anchors.fill代码行,看看会发生什么:
width: 100
height: 100
anchors.centerIn: parent

import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.2

ApplicationWindow
{
    visible: true
    width: 300
    height: 500
    title: qsTr("Hello QML")
    ColumnLayout
    {
        width: 100
        height: 100
        anchors.centerIn: parent

        Button
        {
            text: "First Button"
            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
        }

        Button
        {
            text: "Second Button"
            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
        }

        Button
        {
            text: "Third Button"
            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
        }

    }
}

显然,ColumnLayout现在有一个固定的大小,当调整ApplicationWindow的大小时,ColumnLayout的大小并没有变化,但是,它始终在ApplicationWindow的中心位置。前面代码段的最后一行如下:

Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter

每一个添加到ColumnLayout中的对象元素内的这段代码行,都会让对象元素位于其单元格的垂直和水平中心位置。请注意,从某种意义上说,单元格不包含任何可见的边界,就像布局那样,布局中的单元格也是对其中的对象元素进行组织的不可见方式。

无论添加或需要多少个对象元素,QML代码的扩展都遵循相同的模式。但是,当UI元素数量变得越来越大,最好将用户界面分成单独的文件。在同一个文件夹中的QML文件可以当作预定义的及重要的对象元素来使用。假设有一个名为MyRadios.qml的QML文件,包含了下列代码:

import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12

Item {
    ColumnLayout
    {
        anchors.centerIn: parent
        RadioButton
        {
            text: "Video"
        }
        RadioButton
        {
            text: "Image"
        }
    }
}

你可以在同一个文件夹中的另一个QML文件内使用这个QML文件及其对象元素。假设有一个main.qml文件与MyRadios.qml文件在同一个文件夹中。然后,可以这样使用它:

import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.2

ApplicationWindow
{
    visible: true
    width: 300
    height: 500
    title: "Hello QML"

    ColumnLayout
    {
        anchors.fill: parent
        MyRadios
        {
            width: 100
            height: 200
        }
    }
}

请注意,只要QML文件都在同一个文件夹,就不需要import语句。如果想在代码中使用的QML文件位于一个单独文件夹(同一个文件夹中的子文件夹)中,那么,必须用一条语句将其导入,像这样:

import "other_qml_path"

显然,在上面的代码中,other_qml_path是QML文件的相对路径。

12.2 QML中的用户交互和脚本

在QML代码中对用户操作和事件的响应是通过将脚本添加到对象元素的槽来完成的,这与Qt控件非常类似。此处主要的不同是,在QML类型内部定义的每一个信号都有一个对应的槽,该槽自动地生成,并且可以用一个脚本填充来在发送一个相关的信号时执行一个动作。让我们用另一个例子来了解一下该内容。一个QML Button类型有一个按下信号。这自动意味着有一个onPressed槽,可以用来编码特定按钮所需的动作。下面是示例代码:

Button
{
	onPressed:
	{
		//code goes here
	}
}

关于QML类型中可用槽的列表,可以参考Qt文档。如前所述,通过将信号名称的首字母大写并在前面添加on,可以轻松地猜到每一个信号的槽名。因此,对于按下(pressed)信号,将有一个onPressed槽,对于释放(released)信号,将有一个onReleased槽,等等。

要想能够从一个脚本或一个槽中访问其他QML对象元素,首先,必须为它们分配唯一的标识符。请注意,这只针对你想要访问、修改或与之交互的对象元素。在本章前面的所有例子中,只是创建了对象元素,而没有为其分配任何标识符。通过将唯一的标识符分配给对象元素的id属性,就可以很容易地完成对象元素的标识符分配。id属性值遵循变量的命名约定,就是说要区分大小写,不能以数字作为开始,等等。下面是一个示例代码,展示了如何在QML代码中分配和使用id。

ApplicationWindow
{
    id:mainWindow
    visible: true
    width: 300
    height: 500
    title: "Hello QML"
    ColumnLayout
    {
        anchors.fill: parent
        Button
        {
            text: "Close"
            Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
            onPressed:
            {
                mainWindow.close()
            }
        }
    }
}

在上面的代码中,ApplicationWindow有一个分配给它的ID,即mainWindow,用于在按钮(Button)的onPressed槽内对其进行访问。可以猜到,在上面的代码中按下“Close”按钮,将会关闭mainWindow。在一个QML文件中无论在哪里定义一个ID,都可以在这个特定的QML文件内的任何位置访问它。这就意味着,ID的作用范围不局限于同一组对象元素,或对象元素的子元素,等等。简单地说,任何ID对于QML文件中的所有对象元素都是可见的。但是在单独的QML文件中的对象元素的id怎么访问呢?为了能够访问在一个单独的QML文件中的对象元素,需要为其分配给属性别名来导出该对象元素,如下面的例子所示:

Item {
    property alias videoRadio: videoRadio
    property alias imageRadio: imageRadio

    ColumnLayout
    {
        anchors.centerIn: parent
        RadioButton
        {
            id:videoRadio
            text: "Video"
        }
        RadioButton
        {
            id:imageRadio
            text: "Image"
        }
    }
}

上述代码与MyRadios.qml文件一样,但是这次,通过使用根对象元素的别名属性导出了其中两个RadioButton对象元素。通过这种方式,可以在使用MyRadios的另一个单独的QML文件中访问这些对象元素。除了在一个对象元素内导出对象元素之外,还可以使用属性来包含特定对象元素所需的任何其他值。下面是在QML对象元素内用于定义附加属性的一般语法:

property type name: value

TYPE可以包含任意的QML类型,NAME是属性的给定名称,VALUE是属性值,它必须与所提供的类型兼容。

12.3 Qt Quick Designer的使用

因为简单易读的语法,QML文件很容易用任何代码编辑器进行修改和扩展。但是,还可以使用Qt Creator中集成的Quick Designer来简化对QML文件的设计和修改。如果在Qt Creator中试着打开一个QML文件,并切换到“Design”模式,将会看到图12-3的设计模式,这与标准Qt控件设计器(使用*.ui文件)非常不同,它包含使用QML文件快速设计用户界面所需的大部分功能:

                                 图12-3 Qt Creator中集成的Quick Designer界面

在Qt Quick Designer屏幕的左侧,可以看到在Library窗体中添加到用户界面的QML类型库。这与Qt控件工具箱类似,但是肯定有更多的可以用来设计应用程序用户界面的组件。在用户界面上拖拽其中的每一个组件,它们就会自动添加到QML文件中:

                            图12-4 在Library窗体中添加到用户界面的QML类型库

图12-5 导航(Navigator)窗体界面截图

Library窗体右下方的是导航(Navigator)窗体,它显示用户界面上组件的层次化视图。可以使用导航窗体在QML文件上快速设置对象元素的ID,这只需在其上双击即可。此外,可以将一个对象元素导出为别名,以便可以在其他QML文件中使用,或者在设计时对其进行隐藏(为了能够查看重叠的QML对象元素)。在图12-5的导航窗体上,请注意在设计期间,将button2导出为一个别名以及将button3隐藏之后,组件旁边的小图标是如何变化的。

在Qt Quick Designer的右侧,可以看到属性(Properties)窗体。类似于标准的Qt设计模式中的属性窗体,该窗体可以用来对QML对象元素的属性进行详细的操作和修改。该窗体的内容会根据用户界面上所选择的对象元素发生变化。除了QML对象元素的标准属性之外,该窗体还允许修改与单个对象元素的布局相关的属性。图12-6描述了在用户界面上选择一个Button对象元素时,属性(Properties)窗体的不同视图。

除了作为设计QML用户界面的助手工具之外,Qt Quick Designer还可以帮助你学习QML语言本身,因为设计器中完成的所有修改都被转换成QML代码并存储在同一个QML文件中。你一定要通过设计你的用户界面来熟悉如何用这个助手工具。例如,可以尝试设计一些与在创建Qt控件应用程序时所设计的用户界面一样的界面,但是这次使用Qt Quick Designer以及QML中的文件来创建。

12.4 Qt Quick应用程序的结构

本节将继续学习Qt Quick应用程序工程项目的结构。与Qt控件应用程序工程项目类似,在使用Qt Creator创建新的工程项目时,会自动创建Qt Quick应用程序工程项目所需要的大多数文件,因此实际上不需要记住所有需要的步骤,但是理解Qt Quick应用程序的一些基本概念还是很重要的,以便能够进一步对其进行扩展,或者在QML文件内集成并使用C++代码,本章后面会学习这部分内容。

          图12-6 在用户界面上选择一个Button对象元素时Properties窗体的不同视图

让我们通过创建一个示例应用程序来完成该内容的学习。首先,打开Qt Creator并从欢迎屏幕界面上按下“New Project”按钮,或者从“File”菜单选择“New File or Project”。选择“Qt Quick Controls 2 Application”作为模板类型,单击“Choose”,如图12-7所示。

                                 图12-7 创建新文件或工程项目的图形界面

将工程项目的名称设置为CvQml,并点击“Next”。在“Define Build System”页面,保留“Build system as qmake”,在默认情况下应该已选择它。在“Define Project Details”页面,对于Qt Quick Controls 2样式,可以选择下面中的一个:

❑ Default

❑ Material

❑ Universal

在该屏幕中选择的选项会影响应用程序的整体样式。Default选项产生默认样式,这使得Qt Quick Controls 2产生最佳性能,因此也适用于我们的Qt Quick应用程序。Material样式可以用来创建基于谷歌素材设计(Google Material Design)准则的应用程序。这就提供了更令人满意的组件,但也需要更多的资源。最后,Universal样式可以用来创建基于微软通用设计(Microsoft Universal Design)准则的应用程序。类似于素材Material样式,这也需要更多的资源,但是提供了另一个理想的用户界面组件集合。

可以参考下面的链接,了解有关用于创建Material和Universal样式准则的更多信息:

https://goo.gl/TiQEYB
https://dev.windows.com/design

图12-8描述了一些常见组件之间的区别,以及选择三个可能样式选项中的每一个样式时,应用程序的外观。

                                        图12-8 常见组件及其对应的应用程序外观

无论选择哪种样式,都可以在自动包含到新工程项目中的一个名为“qtquickcontrols2.conf”的专用设置文件中很轻松地更改样式。甚至为了匹配一个暗色主题或亮色主题,或任何其他的颜色集,也可以在以后更改颜色。在任何情况下,请选择一个喜欢的(或使用默认的)样式,并继续点击“Next”,直到最终进入Qt代码编辑器(Qt Code Editor)。工程项目现在包含了Qt Quick应用程序的几乎最低程度的必需文件。

注意,只要在这一章提到Qt Quick应用程序,实际上都指的是Qt Quick Controls 2应用程序,这是Qt Quick应用程序的一个新的增强类型(适用于Qt 5.7及后续版本),我们刚刚创建它并将继续扩展成一个完整而漂亮的跨平台计算机视觉应用程序

首先,让我们看看项目(*.pro)文件中的不同之处。Qt Quick应用程序与Qt控件应用程序截然相反,不使用QtCore、QtGui和QtWidgets模块,而使用默认的QtQml和QtQuick模块。可以通过打开CvQml.pro来查看这个内容,顶部有下面这个代码行:

QT += qml quick

在Qt工程项目中,无论是Qt控件应用程序,还是Qt Quick应用程序,都有一个工程项目文件和一个包含主函数的C++源文件。因此,除了CvQml.pro文件,还有一个包含下列内容的main.cpp文件:

#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

main.cpp与我们在创建Qt控件应用程序时看到的完全不同。在Qt控件应用程序中,将在main.cpp内以及主函数中创建一个QApplication,然后显示主窗口,之后程序进入事件循环,以便窗口保持活跃状态,并处理所有事件,如下所示:

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


int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Mainwindoww;
    w.show();
    return a.exec();
}

类似地,在Qt Quick应用程序中,将创建QGuiApplication,但是这次不加载任何窗口,而是使用QQmlApplicationEngine加载QML文件,如下所示:

QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

这表明QML文件实际上是在运行时加载的,因此可以从磁盘加载,或者在我们的例子中,从main.qml文件加载(该文件存储为qml.qrc文件内的一个资源类型,并嵌入到可执行文件中)。实际上,这是开发Qt Quick应用程序的常用方法,如果检查新创建的CvQml工程项目,就会注意到该工程项目内包含一个名为“qml.qrc”的Qt资源文件,其中包含所有工程项目的QML文件。在qml.qrc文件中包含下列文件:

❑ main.qml这是在main.cpp文件中加载的QML文件,而且是我们的QML代码的入口点。
❑ Page1.qml包含Page1Form QML类型的交互以及脚本。
❑ Page1Form.ui.qml包含Page1Form类型内的用户交互以及QML对象元素。请注意,一组Page1.qml和Page1Form.ui.qml是将用户界面及其底层代码分开的常用方法,与开发Qt控件应用程序时使用mainwindow.ui、mainwindow.h和mainwindow.cpp文件类似。
❑ qtquickcontrols2.conf文件是可以用来改变Qt Quick应用程序样式的配置文件。包含以下内容:

; This file can be edited to change the style of the application
; See Styling Qt Quick Controls 2 in the documentation for details:
; http://doc.qt.io/qt-5/qtquickcontrols2-styles.html

[Controls]
Style=Default

[Universal]
Theme=Light
;Accent=Steel

[Material]
Theme=Light
;Accent=BlueGrey
;Primary=BlueGray

以分号“;”开头的每一行表示该行只是注释。可以将上述代码中“Style”变量的值改为“Material”和“Universal”,以改变应用程序的整体样式。取决于所设置的样式,在上面的代码中,可以使用“Theme”、“Accent”或“Primary”值来更改应用程序中的主题.

关于主题和颜色的完整列表,以及如何在每一个主题中使用可自定义的附加信息,可以参考下面的链接:

https://goo.gl/jDZGPm(对于Default style)
https://goo.gl/Um9qJ4(对于Material style)
https://goo.gl/U6uxrh(对于Universal style)

这就是Qt Quick应用程序的一般结构。这种结构可以用来为任何平台开发任何类型的应用程序。请注意,不必一定使用自动创建的文件,可以只从一个空项目开始,或移除不必要的默认文件,然后从头开始。例如,在我们的示例Qt Quick应用程序(标题为CvQml)中,我们不需要Page1.qml和Page1Form.ui.qml文件,因此只需从qml.qrc文件内进行选择,并通过单击右键并选择“Remove”文件删除。当然,这将导致main.qml文件中的代码丢失。因此,在继续下一节之前,确保将其更新为下列代码:

import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.2

ApplicationWindow
{
    visible: true
    width: 300
    height: 500
    title: qsTr("CvQml")
}

12.5 集成C++和QML代码
即使QML库已经成长为成熟的类型集合,可以处理视觉效果、网络、相机等等,但是通过使用强有力的C++类,对其进行扩展仍然是很重要的。幸运的是,QML和Qt框架提供了足够的规则来轻松地处理这个问题。本节将学习如何创建非可视的C++类,将其用在QML代码内通过使用OpenCV来处理图像。然后,将创建一个C++类,可以作为QML代码内的视觉对象元素来显示图像。
请注意,在QML中有一个默认的Image类型,通过将URL提供给Image对象元素,可以用来显示保存在磁盘上的图像。但是,我们将创建一个QML类型的图像查看器,用来显示QImage对象,并利用这个机会学习QML代码中C++类(可视)的集成。
首先将OpenCV框架添加到上一节中我们创建的项目。这与创建Qt控件应用程序时所做的完全一样,也要将所需的代码行包含进*.pro文件。然后,在工程项目窗体上右键单击,并选择“Add New”,将一个新的C++类添加到项目中。确保类名是“QImageProcessor”,基类是“QObject”, 如图12-9所示。

                                              图12-9 添加C++类的界面截图

将下列#include指令添加到qimageprocessor.h文件:

#include <QImage>
#include "opencv2/opencv.hpp"

然后将下面的函数添加到QImageProcessor类的公共成员区域:

Q_INVOKABLE void processImage(const QString &path);

Q_INVOKABLE是一个Qt宏,它允许使用Qt元对象(Qt Meta Object)系统调用一个函数。因为QML使用同样的Qt元对象来实现对象之间的底层通信机制,所以用Q_INVOKABLE宏标记函数就足以从QML代码对其进行调用。另外,将下列信号添加到QImageProcessor类:

signals:
    void imageProcessed(const QImage &image);

我们使用该信号将一个经过处理的图像传递给稍后将创建的图像查看器类。最后,对于processImage函数的实现,将下面的内容添加到qimageprocessor.cpp文件中:

void QImageProcessor::processImage(const QString &path)
{
    using namespace cv;
    Mat imageM = imread(path.toStdString());
    if(!imageM.empty())
    {
        bitwise_not(imageM, imageM); // or any OpenCV code
        QImage imageQ(imageM.data, imageM.cols, imageM.rows, imageM.step, QImage::Format_RGB888);
        emit imageProcessed(imageQ.rgbSwapped());
    }
    else
    {
        qDebug() << path << "does not exist!";
    }
}

这里的内容我们全都看到或使用过。该函数接受图像路径,然后从磁盘读取图像,执行图像处理,这可以是任何处理,但是为了简单起见,我们使用bitwise_not函数倒置所有通道中的像素值,最后用我们定义的信号发送生成的图像。

图像处理器已经完成了。现在,需要创建一个可用于在QML中显示QImage对象的Visual C++类型。因此,请创建另一个类,并命名为“QImageViewer”,但是这次要保证它是QquickItem的子类,图12-10是新类向导屏幕截图。

                                            图12-10 创建新类的向导界面

修改qimageviewer.h文件,如下所示:

#ifndef QIMAGEVIEWER_H
#define QIMAGEVIEWER_H

#include <QQuickItem>
#include <QQuickPaintedItem>
#include <QImage>
#include <QPainter>

class QImageViewer : public QQuickPaintedItem
{
    Q_OBJECT
public:
    QImageViewer(QQuickItem *parent = Q_NULLPTR);
    Q_INVOKABLE void setImage(const QImage &img);

private:
    QImage currentImage;
    void paint(QPainter *painter);

};

#endif // QIMAGEVIEWER_H

 我们已经让QImageViewer类成为QQuickPaintedItem类的子类。而且,已更新构造函数来匹配这个更改。我们还在该类中用Q_INVOKABLE宏定义了另一个函数,用于设置想要在该类的实例上显示的QImage,或者更确切地说,是在用该类型创建的QML对象元素上。QQuickPaintedItem提供了一种创建新的可视QML类型的简单方法,也就是通过对其进行子类化,并重新实现paint函数,如上面的代码所示。该类中传递给paint函数的painter指针可以用来绘制我们所需的任何内容。在该例中,我们只想在其上绘制一个图像,也就是说,我们定义了currentImage,这是一个QImage,它将存放想要在QImageViewer类中绘制的图像。

现在,我们需要添加setImage的实现和paint函数,并根据在头文件中更改的内容来更新构造函数。因此,请确保qimageviewer.cpp文件与下面的代码相似:

#include "qimageviewer.h"

QImageViewer::QImageViewer(QQuickItem *parent)
    : QQuickPaintedItem(parent)
{
}

void QImageViewer::setImage(const QImage &img)
{
    currentImage = img.copy(); // perform a copy
    update();
}

void QImageViewer::paint(QPainter *painter)
{
    QSizeF scaled = QSizeF(currentImage.width(), currentImage.height()).scaled(boundingRect().size(), Qt::KeepAspectRatio);
    QRect centerRect(qAbs(scaled.width() - width()) / 2.0f, qAbs(scaled.height() - height()) / 2.0f, scaled.width(), scaled.height());
    painter->drawImage(centerRect, currentImage);
}

上面的代码中,setImage函数十分简单,它制作并存放图像的副本,然后调用QImage-Viwer类的update函数。当在QQuickPaintedItem(类似于QWidget)内调用update时,将产生重新绘制,因此将调用paint函数。如果想要在QImageViewer的整个可显示区域内拉伸图像的话,那么该函数只需要最后一行代码(用boundingRect替换centerRect),但是,我们希望生成的图像适合屏幕,同时也保持纵横比。因此,我们做一个比例转换,然后保证图像总是在可显示区域的中心。

我们几乎差不多已经完成了,新的C++类(QImageProcessor和QImageViewer)都已经准备好在QML代码中使用了。剩下要做的事情就是确保它们对于我们的QML代码是可见的。出于这个原因,需要确保通过qmlRegisterType函数注册它们。因此在main.cpp文件中必须调用该函数,如下所示:

qmlRegisterType<QImageProcessor>("com.amin.classes", 1, 0, "ImageProcessor");
qmlRegisterType<QImageViewer>("com.amin.classes", 1, 0, "ImageViewer");

上述代码应放置在main.cpp文件中定义QQmlApplicationEngine位置的前面。显然,还必须使用下面的#include指令在main.cpp文件中包含这两个新类:

#include "qimageprocessor.h"
#include "qimageviewer.h"

请注意,qmlRegisterType函数中的com.amin.classes可以替换为你自己的类似域的标识符,而且它是我们已经给出的包含QimageProcessor和QimageViewer类的库的名称。后面的1和0指的是库的1.0版本,最后一个文字字符串是可以在QML类型中用来访问和使用这些新类的类型标识符。

最后,我们可以开始在main.qml文件中使用C++类。首先,确保导入语句与下面相似:

import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import QtMultimedia 5.8
import com.amin.classes 1.0

最后一行包括刚刚创建的ImageProcessor和ImageViewer QML类型。我们将要用QML Camera类型来访问相机并用它捕获图像。因此,在main.qml文件中添加下列代码作为ApplicationWindow对象元素的直接子类:

    Camera
    {
        id: camera

        imageCapture
        {
            onImageSaved:
            {
                imgProcessor.processImage(path)
            }
        }
    }

在上述代码中,imgProcessor是ImageProcessor类型的id,还需要将其定义为Application-Window的子类,如下所示:

ImageProcessor                    
{                                 
    id: imgProcessor              
                                  
    onImageProcessed:             
    {                             
        imgViewer.setImage(image);
        imageDrawer.open()        
    }                             
}                                 

请注意,上述代码中的onImageProcessed槽是自动生成的,因为我们在QImage-Processor类中创建了imageProcessed信号。可以猜到,imgViewer是我们之前创建的QImage-Viewer类,而且在onImageProcessed槽内设置了它的图像。在本例中,还使用QML Drawer,当调用它的open函数时,它会滑过另一个窗口,而且已经嵌入imgViewer作为这个Drawer的子对象元素。下面是Drawer和ImageViewer的定义:

Drawer                                                                             
{                                                                                  
    id: imageDrawer                                                                
    width: parent.width                                                            
    height: parent.height                                                          
                                                                                   
    ImageViewer                                                                    
    {                                                                              
        id: imgViewer                                                              
        anchors.fill: parent                                                       
                                                                                   
        Label                                                                      
        {                                                                          
            text: "Swipe from right to left<br>to return to capture mode!"         
            color: "red"                                                           
        }                                                                          
    }                                                                              
}                                                                                  

好了,剩下要做的事情就是添加一个允许预览摄像头的QML VideoOutput。我们将使用该VideoOutput来捕获图像,从而调用QML Camera类型的imageCapture.onImageSaved槽,如下所示:

VideoOutput
{
    source: camera
    anchors.fill: parent

    MouseArea
    {
        anchors.fill: parent
        onClicked:
        {
            camera.imageCapture.capture()
        }
    }

    Label
    {
        text: "Touch the screen to take a photo<br>and process it using OpenCV!"
        color: "red"
    }
}

如果现在启动应用程序,将立即看到计算机默认摄像头的输出。如果单击视频输出,将会捕捉并处理图像,然后在Drawer上显示,即从左到右滑过当前页面。图12-11展示了在执行该应用程序时的两个外观截图。

完整的源码如下:

CvQml.pro

QT += qml quick

CONFIG += c++11

SOURCES += main.cpp \
    qimageprocessor.cpp \
    qimageviewer.cpp

RESOURCES += qml.qrc

# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =

# Additional import path used to resolve QML modules just for Qt Quick Designer
QML_DESIGNER_IMPORT_PATH =

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

win32: {
    include("./opencv.pri")
}

HEADERS += \
    qimageprocessor.h \
    qimageviewer.h
qimageprocessor.h
#ifndef QIMAGEPROCESSOR_H
#define QIMAGEPROCESSOR_H

#include <QObject>
#include <QImage>
#include <QDebug>
#include "opencv2/opencv.hpp"

class QImageProcessor : public QObject
{
    Q_OBJECT
public:
    explicit QImageProcessor(QObject *parent = nullptr);

    Q_INVOKABLE void processImage(const QString &path);

signals:
    void imageProcessed(const QImage &image);

public slots:

};

#endif // QIMAGEPROCESSOR_H

qimageprocessor.cpp

#include "qimageprocessor.h"

QImageProcessor::QImageProcessor(QObject *parent) : QObject(parent)
{

}

void QImageProcessor::processImage(const QString &path)
{
    using namespace cv;
    Mat imageM = imread(path.toStdString());
    if(!imageM.empty())
    {
        bitwise_not(imageM, imageM); // or any OpenCV code
        QImage imageQ(imageM.data, imageM.cols, imageM.rows, imageM.step, QImage::Format_RGB888);
        emit imageProcessed(imageQ.rgbSwapped());
    }
    else
    {
        qDebug() << path << "does not exist!";
    }
}

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>

#include "qimageprocessor.h"
#include "qimageviewer.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    qmlRegisterType<QImageProcessor>("com.amin.classes", 1, 0, "ImageProcessor");
    qmlRegisterType<QImageViewer>("com.amin.classes", 1, 0, "ImageViewer");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QLatin1String("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}

qtquickcontrols2.conf

; This file can be edited to change the style of the application
; See Styling Qt Quick Controls 2 in the documentation for details:
; http://doc.qt.io/qt-5/qtquickcontrols2-styles.html

[Controls]
Style=Default

[Universal]
Theme=Light
;Accent=Steel

[Material]
Theme=Light
;Accent=BlueGrey
;Primary=BlueGray

main.qml

import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import QtMultimedia 5.8
import com.amin.classes 1.0

ApplicationWindow
{
    visible: true
    width: 640
    height: 480
    title: qsTr("CvQml")

    ImageProcessor
    {
        id: imgProcessor

        onImageProcessed:
        {
            imgViewer.setImage(image);
            imageDrawer.open()
        }
    }

    Camera
    {
        id: camera

        imageCapture
        {
            onImageSaved:
            {
                imgProcessor.processImage(path)
            }
        }
    }

VideoOutput
{
    source: camera
    anchors.fill: parent

    MouseArea
    {
        anchors.fill: parent
        onClicked:
        {
            camera.imageCapture.capture()
        }
    }

    Label
    {
        text: "Touch the screen to take a photo<br>and process it using OpenCV!"
        color: "red"
    }
}

    Drawer
    {
        id: imageDrawer
        width: parent.width
        height: parent.height

        ImageViewer
        {
            id: imgViewer
            anchors.fill: parent

            Label
            {
                text: "Swipe from right to left<br>to return to capture mode!"
                color: "red"
            }
        }
    }
}

运行结果:(要接一个摄像头)

                                 图12-11 应用程序执行结果的外观截图

12.6 Android和iOS上的Qt和OpenCV应用程序

理想情况下,可以同样地在桌面和移动平台上建立并运行用Qt和OpenCV框架创建的应用程序,而不需要编写任何特定于平台的代码。但是,在实践中,这并不像看起来那么简单,因为(某些情况下)用于将像Qt和OpenCV这样的框架充当操作系统自身功能的封装器的这项技术仍处在广泛开发的过程中,所以在特定操作系统(例如,Android或iOS)中,可能还有一些没有完全实现的功能。令人欣慰的是,随着新版本Qt和OpenCV框架的发布,这种情况越来越少见了,甚至现在(Qt 5.9和OpenCV 3.3)在Windows、Linux、macOS、Android和iOS操作系统中都可以很容易地使用这两个框架中的大多数类和函数。

因此,首先要记住刚才介绍的内容,然后可以说,实际上(与理想情况相反)为了能够在Android和iOS上建立并运行用Qt和OpenCV编写的应用程序,需要确保以下条件:

❑ 必须安装相应的Android和iOS的Qt工具包。这是在Qt框架的初始安装过程中完成的(有关该内容的更多详细信息,请参阅第1章)。[插图]值得注意的是,Android工具包在Windows、Linux和macOS上可用,而iOS工具包只适用于macOS,因为(目前)使用Qt的iOS应用程序的开发仅限于macOS。

❑ 必须从OpenCV网站下载Android和iOS的预构建OpenCV库(目前,由opencv.org提供),并解压到你的计算机上。必须将它们添加到Qt工程项目文件中,其方式与添加到Windows或任何其他桌面平台时一样。

❑ 对于iOS,在macOS操作系统上有最新版本的Xcode就足够了。

❑ 对于Android,必须确保在计算机上安装了JDK、Android SDK、Android NDK以及Apache Ant。Qt Creator简化了Android开发环境的配置,通过使用Qt Creator“Options”内“Devices”页面上的“Android”选项卡,可以将所需的程序下载并安装到计算机上(见图12-12)。

                         图12-12 Qt Creator选项中“Devices”页面上的“Android”选项卡的截图 

请注意图12-12中“Browse”按钮旁边的按钮。这些按钮提供了下载页面的链接,以及可以从中获得所有必需依赖项的副本的在线链接。

如果想为Android和iOS操作系统建立应用程序,那么这就是需要处理的所有内容。使用Qt和OpenCV构建的应用程序还可以在Windows、macOS、Android和iOS的应用程序商店中发布。这个过程包括作为开发人员向这些操作系统的提供者注册。可以在上述应用程序商店中找到在网上和全球发布app的指南和要求。

12.7 小结

在本章中,我们学习了Qt Quick应用程序开发和QML语言。我们从可读性极强以及易用的语言的基本语法开始,然后介绍如何开发包含组件的应用程序,这些组件可以彼此交互以实现一个共同的目标。我们学习了如何填补QML和C++代码之间的空白,然后建立一个可视类和一个非可视类,以处理和显示用OpenCV进行处理的图像。我们还简要介绍了在Android和iOS平台上构建和运行同样的应用程序所需的工具。本书的最后一章专门介绍如何使用新的Qt Quick Controls 2模块从头开始开发快速而漂亮的应用程序,同时还介绍如何将C++代码的强大功能与诸如OpenCV这样的第三方框架相结合,在开发移动和桌面应用程序时获得最大的功能和灵活性。

建立跨平台和外观漂亮的应用程序从来都不是那么容易的。通过使用Qt和OpenCV框架,特别是利用QML快速而简单地构建应用程序的能力,现在就可以开始实现所有计算机视觉的创意了。这一章只是对Qt Quick以及QML语言必须提供的所有功能的介绍,而你需要将这些片段组合起来,以便构建能够解决该领域中现有问题的应用程序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值