Skip to content

【🔧更新中🔧】基于 Qt 和 OpenCV 的计算机视觉示例实现及教程

License

Notifications You must be signed in to change notification settings

nekosilverfox/OpenCV

Repository files navigation

NekoSilverfox

OpenCV

【🔧更新中🔧】基于 Qt 和 OpenCV 的计算机视觉示例实现及教程

License Qt OpenCV Language

[toc]

如果你不了解 Qt 这个框架,建议先学习:

  • 关于 Qt 无比详细教程及案例实现:https://github.com/NekoSilverFox/Qt 其中不仅涵盖了 Qt 基本控件的使用及讲解,还包含了大学和培训机构不会讲到的:插件设计及实现、基于 QTest 的静态动态、动态测试、CI/CD的使用、Qt 函数/方法注意事项等

如果你对 OpenGL 计算机图形学感兴趣:

前言

参考:

原文:Computer Vision with OpenCV 3 and Qt5

https://github.com/apachecn/apachecn-cv-zh/blob/0f2e14ca582d398ba3be22a1fff949077f4c85c0/docs/cv-opencv3-qt5

原文:Qt 5 and OpenCV 4 Computer Vision Projects

https://github.com/apachecn/apachecn-cv-zh/tree/0f2e14ca582d398ba3be22a1fff949077f4c85c0/docs/qt5-opencv4-cv-proj

只要环顾四周,就很可能会看到至少两个不同的设备,例如计算机,智能手机,智能手表或平板电脑,上面运行着一些应用,可以帮助您完成各种日常任务或娱乐音乐,看电影 ,视频游戏等。 每年,市场上都会引入数百种新设备,并且需要新版本的操作系统来跟上它们,以便为应用开发人员提供更好的界面,以创建可更好地利用诸如高分辨率等基础资源的软件。 显示器,各种传感器等。 结果,软件开发框架必须适应并支持不断增长的平台。 考虑到这一点,Qt 可能是同时提供功能,速度,灵活性和易用性的最成功的跨平台软件开发框架之一,在创建需要以下功能的软件时,它是首选。 在各种平台上都具有吸引力和一致性。

近年来,特别是随着功能更强大的处理器以较低的价格出现,台式计算机及其手持式对等设备的角色已转向执行更苛刻和更复杂的任务,例如计算机视觉。 无论是用于智能电影或照片编辑,保护敏感建筑物,对生产线中的物体计数,还是通过自动驾驶汽车检测交通标志,车道或行人,计算机视觉正越来越多地用于解决此类实时问题。 曾经只能由人类解决的问题。 这是 OpenCV 框架进入现场的地方。 在过去的几年中,OpenCV 已成长为功能完善的跨平台计算机视觉框架,其重点是速度和性能。 在世界各地,开发人员和研究人员都在使用 OpenCV 来实现其计算机视觉应用的思想和算法。

本书旨在帮助您掌握 Qt 和 OpenCV 框架的基本概念,使您轻松地自己继续开发和交付跨多种平台的计算机视觉应用。 能够轻松遵循本书所涵盖主题的唯一假设是,您熟悉并熟悉 C++ 编程概念,例如类,模板,继承等。 即使整本书中涵盖的教程,屏幕截图和示例都是基于 Windows 操作系统的,但仍会在必要时提及 MacOS 和 Linux 操作系统的区别。

这本书是给谁的

本书面向有兴趣构建计算机视觉应用的读者。 期望具备 C++ 编程的中级知识。 即使没有 Qt5 和 OpenCV 3 知识,但如果您熟悉这些框架,您也会受益

本书涵盖的内容

  • 第1章,OpenCV和Qt简介 介绍了所有必要的初始化步骤。从在哪里以及如何获取Qt和OpenCV框架开始,本章将描述如何安装、配置,以及确保你的开发环境设置正确。

  • 第2章,创建我们的第一个Qt和OpenCV项目 带领你通过Qt Creator IDE,我们将使用它开发我们所有的应用程序。在本章中,你将学习如何创建和运行你的应用程序项目。

  • 第3章,创建一个全面的Qt+OpenCV项目 通过最常见的功能需求,为一个全面的应用程序,包括样式、国际化、支持各种语言、插件等。通过这个过程,我们将自己创建一个全面的计算机视觉应用程序。

  • 第4章,Mat和QImage 奠定基础并教你编写计算机视觉应用程序所需的基本概念。在这一章中,你将了解所有关于OpenCV Mat类和Qt QImage类,如何在两个框架之间转换和传递它们,以及更多。

  • 第5章,图形视图框架 教你如何使用Qt Graphics View框架及其底层类,以便在应用程序中轻松高效地显示和操作图形。

  • 第6章,OpenCV中的图像处理 带你了解OpenCV框架提供的图像处理功能。你将学习关于变换、过滤器、颜色空间、模板匹配等。

  • 第7章,特征和描述符 全面讲解从图像中检测关键点,从关键点提取描述符,并将它们相互匹配。在本章中,你将学习各种关键点和描述符提取算法,并使用它们来检测和定位图像中的已知对象。

  • 第8章,多线程 教你Qt框架提供的所有关于多线程的能力。你将学习关于互斥锁、读写锁、信号量和各种线程同步工具。这章还会教你关于Qt中低级(QThread)和高级(QtConcurrent)多线程技术。

  • 第9章,视频分析 覆盖了使用Qt和OpenCV框架正确处理视频的方法。你将学习使用MeanShift和CAMShift算法进行对象跟踪等视频处理功能。本章还包括视频处理的所有基本和必要概念的综合概述,如直方图和反向投影图像。

  • 第10章,调试和测试 带你了解Qt Creator IDE的调试功能,以及它是如何配置和设置的。在本章中,你还将学习Qt框架提供的单元测试能力,通过编写示例单元测试,这些测试可以手动或每次项目构建时自动运行。

  • 第11章,链接和部署 教你动态或静态地构建OpenCV和Qt框架。在这一章中,你还将学习在各种平台上部署Qt和OpenCV应用程序。在本章的最后,我们将使用Qt Installer Framework创建一个安装程序。

  • 第12章,Qt Quick应用程序 介绍你Qt Quick应用程序和QML语言。在本章中,你将学习QML语言语法,以及如何与Qt Quick Designer一起使用它来为桌面和移动平台创建漂亮的Qt Quick应用程序。你还将学习在本章中整合QML和C++。

为了充分利用本书

尽管书的初章已经涵盖了每一个所需的工具和软件、正确的版本,以及它们是如何被安装和配置的,以下是一个可以作为快速参考的列表:

  • 一台安装了最新版本Windows、macOS或Linux(例如Ubuntu)操作系统的常规计算机。
  • 微软Visual Studio(在Windows上)
  • Xcode(在macOS上)
  • CMake
  • Qt框架
  • OpenCV框架

下载示例代码文件

你可以从你在www.packtpub.com的账户下载本书的示例代码文件。如果你是在别处购买的这本书,你可以访问www.packtpub.com/support并注册,以直接将文件通过电子邮件发送给你。 你可以按照以下步骤下载代码文件:

  1. 在www.packtpub.com登录或注册。
  2. 选择SUPPORT标签。
  3. 点击Code Downloads & Errata。
  4. 在搜索框中输入书名并按照屏幕上的指示操作。

一旦文件下载完成,请确保你使用最新版本的以下软件解压或提取文件夹:

  • 对于Windows,使用WinRAR/7-Zip
  • 对于Mac,使用Zipeg/iZip/UnRarX
  • 对于Linux,使用7-Zip/PeaZip

本书的代码包也托管在GitHub上 https://github.com/PacktPublishing/Computer-Vision-with-OpenCV-3-and-Qt5。我们也在https://github.com/PacktPublishing/ 上提供了我们丰富的图书和视频目录中的其他代码包。去看看吧!

下载彩色图片

我们还提供了一个PDF文件,其中包含了本书使用的截图/图表的彩色图片。你可以在这里下载它:https://www.packtpub.com/sites/default/files/downloads/ComputerVisionwithOpenCV3andQt5_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

  • CodeInText:表示文中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚构的URL、用户输入和Twitter句柄。这里有一个例子:"QApplication类是负责控制应用程序的控制流、设置等的主类。"

  • 代码块如下所示:

    #include "mainwindow.h"
    #include <QApplication>
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        MainWindow w;
        w.show();
        return a.exec();
    }
  • 当我们希望把你的注意力吸引到代码块的特定部分时,相关的行或项将以加粗形式展示:

    #include "mainwindow.h"
    #include <QApplication>
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        MainWindow w;
        **w.show();**
        return a.exec();
    }
  • 任何命令行输入或输出如下所写:

    binarycreator -p packages -c config.xml myinstaller
    
  • 加粗:表示一个新术语、一个重要词汇或你在屏幕上看到的词汇。例如,菜单或对话框中的词汇在文本中如此显示。这里有一个例子:"点击Next按钮将你移动到下一个屏幕。"

第一章、OpenCV 和 Qt 简介

在其最基本的形式和结构中,计算机视觉是一个术语,用来识别所有用于赋予数字设备视觉感知能力的方法和算法。这意味着什么?嗯,这确实意味着它听起来的样子。理想情况下,计算机应该能够通过标准摄像机的镜头(或任何其他类型的摄像机)看到世界,并通过应用各种计算机视觉算法,它们应该能够检测到人脸,甚至识别它们,在图像中计数物体,检测视频流中的运动等等,这些起初可能只被期望人类能够做到。因此,要了解计算机视觉真正是什么,最好了解计算机视觉旨在开发实现所提到的理想的方法,赋予数字设备看到和理解周围环境的能力。值得注意的是,大多数情况下,计算机视觉和图像处理是可以互换使用的(尽管,对该主题的历史研究可能证明应该有所不同)。但无论如何,在本书中,我们将坚持使用计算机视觉这个术语,因为这是当今计算机科学社区中更受欢迎和广泛使用的术语,而且正如我们将在本章后面看到的那样,图像处理是 OpenCV 库的一个模块,我们将在本章的接下来的页面中介绍它,并且它也将在一个完整的章节中进行详细介绍。

计算机视觉是当今计算机科学中最受欢迎的主题之一,它被应用于各种应用程序中,从检测癌组织的医疗工具到帮助制作所有那些闪亮音乐视频和电影的视频编辑软件,再到军用级别的目标检测器,帮助在地图上找到特定位置,以及帮助无人驾驶汽车找到路线的交通标志检测器。嗯,很明显我们无法列出计算机视觉的所有可能性,但我们可以肯定它是一个有趣的主题,将在很长一段时间内存在。还值得一提的是,计算机视觉领域的工作和职业市场正在迅速扩展,而且正在日益增长。

在计算机视觉开发人员和专家中最受欢迎的工具中,有两个最突出的开源框架,它们也是您手头书籍的标题中的两个框架,即 OpenCV 和 Qt。每天,全世界成千上万的开发人员,从成熟的公司到创新的初创公司,都在使用这两个框架来为各种行业构建应用程序,比如我们提到的那些行业,而这正是您将在本书中学到的内容。

在本章中,我们将涵盖以下主题:

  • 介绍 Qt,一个开源的跨平台应用程序开发框架
  • 介绍 OpenCV,一个开源的跨平台计算机视觉框架
  • 【省略】如何在 Windows、macOS 和 Linux 操作系统上安装 Qt
  • 如何从源代码构建 OpenCV 在 Windows、macOS 和 Linux 操作系统上
  • 配置您的开发环境以构建使用 Qt 和 OpenCV 框架的应用程序
  • 使用 Qt 和 OpenCV 构建您的第一个应用程序

需要什么:

这是在本章介绍中提到的最明显的问题,但对它的回答也是我们学习计算机视觉的第一步。本书面向熟悉 C++ 编程语言并希望在不费力气的情况下开发强大且外观优美的计算机视觉应用程序的开发人员。本书旨在通过不同的计算机视觉主题带领您进行一场充满乐趣的旅程,重点放在实践练习和逐步开发您所学内容上。

任何有足够 C++ 经验的人都知道,使用原始的 C++ 代码并依赖于特定于操作系统的 API 来编写视觉丰富的应用程序并不是一件容易的任务。因此,几乎每个 C++ 开发人员(或至少是在 C++ 领域有积极职业生涯的严肃开发人员)都会使用一个或多个框架来简化这个过程。在为 C++ 开发的最广泛知名的框架中,Qt 是其中之一。事实上,如果不是最佳选择,那么它肯定是其中之一的最佳选择。另一方面,如果您的目标是开发处理图像或可视化数据集的应用程序,那么 OpenCV 框架可能是您首选的第一个(也许是最受欢迎的)地址。因此,这就是本书专注于 Qt 和 OpenCV 结合使用的原因。开发适用于不同桌面和移动平台的计算机视觉应用程序,以最高可能的性能运行,这是不可能的,而不使用像 Qt 和 OpenCV 这样的强大框架的组合。

总结所说的,确保您至少具有 C++ 编程语言的中级水平知识。如果诸如类、抽象类、继承、模板或指针等术语对您来说听起来很陌生,那么考虑先阅读一本关于 C++ 的书籍。对于所有其他涉及的主题,特别是所有涉及的实践主题,本书承诺为所有包含的示例和教程提供清晰明了的解释(或指向特定文档页面的引用)。当然,要详细深入地了解 Qt 和 OpenCV 中的模块和类是如何实现的,您需要熟悉更多的资源、研究,有时甚至是硬核的数学计算或对计算机或操作系统在现实世界中执行的低级理解,这完全超出了本书的范围。然而,对于本书涵盖的所有算法和方法,您将得到它们是什么,如何以及何时何地使用它们的简要描述,以及足够的指导,让您如果愿意的话可以继续深入挖掘。

Qt 简介

你可能已经听说过它,甚至在不知情的情况下使用过它。它是许多世界著名的商业和开源应用程序的基础,例如 VLC 播放器、Calibre 等等。Qt 框架被所谓的财富 500 强公司的大多数公司使用,我们甚至无法开始定义它在世界上许多应用程序开发团队和公司中的广泛使用和受欢迎程度。因此,我们将从介绍开始,然后逐步深入。

首先,让我们通过对 Qt 框架的简要介绍来使我们站稳脚跟。没有什么比在脑海中清晰地描绘整个框架更能让您感到舒适的了。所以,我们开始吧,目前由 The Qt Company 构建和管理,Qt 框架是一个开源应用程序开发框架,被广泛用于创建视觉丰富且跨平台的应用程序,这些应用程序可以在不同的操作系统或设备上非常轻松地运行,甚至几乎不需要任何努力。进一步分解,开源是其中最明显的部分。这意味着您可以访问 Qt 的所有源代码。所谓的视觉丰富,是指 Qt 框架中具有足够的资源和功能,可以编写非常漂亮的应用程序。至于最后一部分,跨平台,这基本上意味着,如果您使用 Qt 框架模块和类为 Microsoft Windows 操作系统开发应用程序,那么它可以像原样编译和构建为 macOS 或 Linux,而无需更改一行代码(几乎),前提是您的应用程序不使用任何非 Qt 或特定于平台的库。

在编写本书时,Qt 框架(从现在起简称为 Qt)的版本是 5.9.X,它包含许多模块,几乎可以用于开发应用程序的任何目的。Qt 将这些模块划分为以下四个主要类别:

  • Qt Essentials
  • Qt Add-Ons
  • Value-Add Modules
  • Technology Preview Modules

让我们看看它们是什么以及它们包含了什么,因为我们将在本书中经常处理它们。

Qt Essentials

这些是 Qt 所承诺在所有支持的平台上可用的模块。它们基本上是 Qt 的基础,包含了几乎所有 Qt 应用程序使用的大多数类。要真正关注 通用 这两个词,因为这正是这些模块的用途。以下是现有模块的快速研究和以后参考的简要列表:

模块 描述
Qt Core 这些是其他模块使用的核心非图形类。
Qt GUI 这些是用于图形用户界面 (GUI) 组件的基本类。包括 OpenGL。
Qt Multimedia 这些是用于音频、视频、收音机和摄像头功能的类。
Qt Multimedia Widgets 这些是基于窗口小部件的类,用于实现多媒体功能。
Qt Network 这些是使网络编程更轻松和更可移植的类。
Qt QML 这些是用于 QML 和 JavaScript 语言的类。
Qt Quick 这是一个声明性框架,用于构建具有自定义用户界面的高度动态的应用程序。
Qt Quick Controls 这些是基于 Qt Quick 的可重用 UI 控件,用于创建经典的桌面风格用户界面。
Qt Quick Dialogs 这些是用于从 Qt Quick 应用程序创建和与系统对话框交互的类型。
Qt Quick Layouts 这些布局是用于在用户界面中排列基于 Qt Quick 2 的项目的项目。
Qt SQL 这些是用于使用 SQL 进行数据库集成的类。
Qt Test 这些是用于对 Qt 应用程序和库进行单元测试的类。
Qt Widgets 这些是用于扩展 Qt GUI 的 C++ 窗口小部件类。

要获取更多信息,请参阅 http://doc.qt.io/qt-5/qtmodules.html。

请注意,涵盖本书中的所有模块和所有类可能是不可能的,也许也不是一个好主意。在大多数情况下,我们将坚持使用我们需要的模块和类

Qt 附加模块

这些模块可能在所有平台上可用,也可能不可用。这意味着它们用于开发特定功能,而不是 Qt Essentials 的通用性质。这些类型模块的一些示例包括 Qt 3D、Qt 打印支持、Qt WebEngine、Qt 蓝牙等等。您始终可以参考 Qt 文档以获取这些模块的完整列表,实际上,它们太多了,无法在此列出。大多数情况下,您只需简单浏览一下,就可以对模块的用途有一个简要的了解。

要获取更多信息,您可以参考 http://doc.qt.io/qt-5/qtmodules.html。

价值增值模块

这些模块提供额外的功能,并通过 Qt 提供商提供商业许可证。是的,你猜对了,这些模块只在 Qt 的付费版本中可用,并且不在 Qt 的开源和免费版本中提供,但它们大多数旨在帮助完成本书目的中根本不需要的非常具体的任务。您可以使用 Qt 文档页面获取列表。

要获取更多信息,您可以参考 http://doc.qt.io/qt-5/qtmodules.html。

技术预览模块

正如其名称所示,这些模块通常以不保证对所有情况都有效的状态提供;它们可能包含错误或其他问题,而且它们仍在开发中,作为测试和反馈目的的预览提供。一旦模块开发并足够成熟,它就会在前面提到的其他类别中提供,并从技术预览类别中移除。在撰写本书时,这些类型的模块的一个示例是 Qt Speech,它是一个旨在为 Qt 应用程序添加文本到语音支持的模块。如果您希望成为一名完全合格的 Qt 开发人员,随时关注这些模块总是一个好主意。

要获取更多信息,您可以参考 http://doc.qt.io/qt-5/qtmodules.html。

Qt 支持的平台

当我们谈论开发应用程序时,平台可能有许多不同的含义,包括操作系统类型、操作系统版本、编译器类型、编译器版本和处理器架构(32 位、64 位、Arm 等)。Qt 支持许多(如果不是全部)著名的平台,并且通常在发布新平台时能够迅速跟上。以下是在撰写本书时(Qt 5.9)由 Qt 支持的平台列表。请注意,您可能不会使用这里提到的所有平台,但它让您了解 Qt 真正的强大和跨平台性质:

参考:http://doc.qt.io/qt-5/supported-platforms.html

正如您将在接下来的章节中看到的那样,我们将在 Windows 上使用 Microsoft Visual C++ 2015(或从这里简称为 MSVC 2015)编译器,因为 Qt 和 OpenCV(您将在后面学习到)都高度支持它。我们还将在 Linux 上使用 GCC,在 macOS 操作系统上使用 Clang。所有这些工具要么是免费且开源的,要么是由操作系统提供者提供的。尽管我们的主要开发系统将是 Windows,但在 Windows 和其他版本之间存在差异时,我们将涵盖 Linux 和 macOS 操作系统。因此,本书中的默认截图将是 Windows 的截图,而在 Windows 和其他版本之间存在明显差异而不仅仅是路径、按钮颜色等方面的细微差别时,我们将提供 Linux 和 macOS 的截图。

Qt Creator

Qt Creator 是用于开发 Qt 应用程序的 IDE (集成开发环境) 的名称。它也是我们在本书中将用来创建和构建项目的 IDE。值得注意的是,Qt 应用程序可以使用任何其他 IDE(例如 Visual Studio 或 Xcode)来创建,并且 Qt Creator 不是构建 Qt 应用程序的必需品,但它是一个轻量级且功能强大的 IDE,默认情况下随 Qt Framework 安装程序一起提供。因此,它最大的优势在于与 Qt 框架的轻松集成。

以下是 Qt Creator 的截图,显示了 IDE 的代码编辑模式。关于如何使用 Qt Creator 的详细信息将在下一章中介绍,尽管我们将在本章稍后的一些测试中尝试使用它,但不会过多地详细介绍它:

OpenCV 简介

现在,是时候介绍 OpenCV,即开源计算机视觉库,或者如果您愿意的话,也可以称之为框架,因为 OpenCV 本身会互换使用它们,在本书中也可能会发生这种情况。但是,在大多数情况下,我们将简单地坚持使用 OpenCV。好的,让我们先听听它到底是什么,然后在需要的地方进行详细说明。

OpenCV 是一个开源跨平台库,用于开发计算机视觉应用程序。它专注于速度和性能,并包含了许多算法在各种模块中。这些模块也分为两种类型:主要模块和额外模块。主要的 OpenCV 模块简单地是指 OpenCV 社区内建立和维护的所有模块,它们是 OpenCV 提供的默认包的一部分。

这与 OpenCV 的额外模块形成对比,后者多多少少是第三方库的包装器和接口,用于将它们集成到 OpenCV 构建中。以下是一些不同模块类型的示例,并附有简要说明。值得注意的是,随着时间的推移,OpenCV 中的模块数量(有时甚至顺序)可能会发生变化,因此要牢记的关于这一点的最佳方法就是只需访问 OpenCV 文档页面,每当有些事情似乎不合时宜时,或者如果某些东西不在原来的位置时。

主要模块

以下是一些 OpenCV 主要模块的示例。请注意,它们只是 OpenCV 中的一小部分(可能是最常用的部分),覆盖所有模块超出了本书的范围,但了解 OpenCV 包含的内容是有意义的,就像本章前面看到的 Qt 一样。这里它们是:

  • 核心功能或简称为core模块包含所有其他 OpenCV 模块使用的所有基本结构,常量和函数。 例如,在此模块中定义 OpenCV Mat类,在本书的其余部分中,我们几乎将在每个 OpenCV 示例中使用该类。 第 4 章,“MatQImage”将涵盖这个模块以及与之密切相关的 OpenCV 模块以及 Qt 框架的相应部分。
  • 图像处理或imgproc模块包含许多用于图像过滤,图像转换的算法,顾名思义,它用于一般图像处理。 我们将在第 6 章,“OpenCV 中的图像处理”中介绍此模块及其功能。
  • 2D 特征框架模块或features2d包含用于特征提取和匹配的类和方法。 它们将在第 7 章,“特征和描述符”中进行详细介绍。
  • 视频模块包含用于主题的算法,例如运动估计,背景减法和跟踪。 该模块以及 OpenCV 的其他类似模块,将在第 9 章,“视频分析”中介绍。

额外模块

正如之前提到的,额外模块主要是第三方库的包装器,这意味着它们只包含用于集成这些模块的接口或方法。一个例子是文本模块。该模块包含用于在图像中使用文本检测或 OCR (光学字符识别) 的接口,您还将需要这些第三方模块,它们不作为本书的一部分进行涵盖,但您可以随时查看 OpenCV 文档以获取更新的额外模块列表以及它们的使用方法。

有关更多信息,请参阅 http://docs.opencv.org/master/index.html。

OpenCV 支持的平台:如前所述,在应用程序开发中,平台不仅仅是操作系统。因此,我们需要知道 OpenCV 支持哪些操作系统、处理器架构和编译器。OpenCV 是高度跨平台的,几乎与 Qt 类似,您可以为所有主要操作系统(包括 Windows、Linux、macOS、Android 和 iOS)开发 OpenCV 应用程序。稍后我们将看到,我们将在 Windows 上使用 MSVC 2015 (32 位) 编译器,在 Linux 上使用 GCC,在 macOS 上使用 Clang。还要注意,我们将需要自己使用其源代码构建 OpenCV,因为目前并没有为上述编译器提供预构建的二进制文件。然而,稍后您将看到,如果您有正确的工具和说明,OpenCV 对于任何操作系统都相当容易构建。

安装 OpenCV

在本章的这一部分,您将学习如何使用其源代码构建 OpenCV。正如您稍后将看到的,并与本节的标题相反,我们并没有像在Qt安装中那样真正“安装”OpenCV。这是因为 OpenCV 通常不提供针对所有编译器和平台的预构建二进制文件,事实上,它根本不为 macOS 和 Linux 提供预构建二进制文件。在最新的 OpenCV Win 包中,只包含了针对 MSVC 2015 64 位的预构建二进制文件,这与我们将要使用的 32 位版本不兼容,因此学习如何自己构建 OpenCV 是一个非常好的主意。这也有利于构建适合您需求的 OpenCV 框架库。您可能希望排除一些选项以使您的 OpenCV 安装更轻量化,或者您可能希望为其他编译器(如 MSVC 2013)构建。因此,有很多理由自己从源代码构建 OpenCV。

准备构建 OpenCV

互联网上大多数开源框架和库,或者至少那些希望保持 IDE 中立的项目(这意味着可以使用任何 IDE 配置和构建的项目,不依赖于特定 IDE 即可工作的项目),使用 CMake 或类似的所谓“构建”系统。我想这也回答了诸如“我为什么需要 CMake?”、“他们为什么不直接给出库并完成呢?”或类似这样的问题。因此,我们需要 CMake 能够使用源代码配置和构建 OpenCV。CMake 是一个开源的跨平台应用程序,允许配置和构建开源项目(或应用程序、库等),您可以在之前的章节中提到的所有操作系统上下载和使用它。在撰写本书的时候,CMake 版本 3.9.1 可以从 CMake 网站下载页面 (https://cmake.org/download/) 下载。

在继续之前,请确保在计算机上下载并安装它。CMake 安装没有特别需要注意的地方,除了您应该确保安装 GUI 版本,因为这是我们将在下一节中使用的版本,也是提供的链接中的默认选项。

获取 OpenCV?

OpenCV 在其网站的 Releases 页面维护其官方和稳定的发布版本 (http://opencv.org/releases.html):

在这里,您始终可以找到适用于 Windows,Android 和 iOS 的最新版本的 OpenCV 源代码,文档和预构建的二进制文件。 随着新版本的发布,它们会添加到页面顶部。 在撰写本书时,版本 3.3.0 是 OpenCV 的最新版本,这就是我们将使用的版本。 因此,事不宜迟,您应该继续进行操作,并通过单击 3.3.0 版的“源”链接来下载源。 将source zip文件下载到您选择的文件夹中,将其提取出来,并记下提取的路径,因为稍后我们将使用它。

如何构建?

现在,我们拥有构建 OpenCV 所需的所有工具和文件,我们可以通过运行 CMake GUI 应用来启动该过程。 如果正确安装了 CMake,则应该能够从桌面,开始菜单或扩展坞运行它,具体取决于您的操作系统。

Linux 用户应在终端中运行以下命令,然后再继续进行 OpenCV 构建。 这些基本上是 OpenCV 本身的依赖关系,需要在配置和构建它之前就位:

sudp apt-get install libgtk2.0-dev and pkg-config 

运行 CMake GUI 应用后,需要设置以下两个文件夹:

  • “源代码在哪里”文件夹应设置为您下载和提取 OpenCV 源代码的位置
  • 可以将“生成二进制文件的位置”文件夹设置为任何文件夹,但是通常在源代码文件夹下创建一个名为build的子文件夹并将其选择为二进制文件文件夹

设置这两个文件夹后,您可以通过单击“配置”按钮继续前进,如以下屏幕截图所示:

image-20240324165156767

单击配置按钮将启动配置过程。 如果构建文件夹尚不存在,可能会要求您创建该文件夹,您需要通过单击“是”按钮来对其进行回答。 如果您仍然觉得自己只是在重复书中的内容,请不要担心。 当您继续阅读本书和说明时,所有这些都会陷入。 现在,让我们仅关注在计算机上构建和安装 OpenCV。 考虑到此安装过程并不像单击几个“下一步”按钮那样简单,并且一旦开始使用 OpenCV,一切都会变得有意义。 因此,在接下来出现的窗口中,选择正确的生成器,然后单击“完成”。


有关每个操作系统上正确的生成器类型,请参阅以下说明:

Windows 用户:您需要选择Visual Studio 142015。请确保您未选择 ARM 或 Win64 版本或其他 Visual Studio 版本。


MacOS 和 Linux 用户:您需要选择Unix Makefile

您将在 CMake 中看到一个简短的过程,完成后,您将能够设置各种参数来配置您的 OpenCV 构建。 有许多参数需要配置,因此我们将直接影响那些直接影响我们的参数。

确保选中BUILD_opencv_world选项旁边的复选框。 这将允许将所有 OpenCV 模块构建到单个库中。 因此,如果您使用的是 Windows,则只有一个包含所有 OpenCV 功能的 DLL 文件。 正如您将在后面看到的那样,当您要部署计算机视觉应用时,这样做的好处是仅使用一个 DLL 文件即可。 当然,这样做的明显缺点是您的应用安装程序的大小会稍大一些。 但是同样,易于部署将在以后证明更加有用。

更改构建参数后,您需要再次单击“配置”按钮。 等待重新配置完成,最后单击“生成”按钮。 这将使您的 OpenCV 内部版本可以编译。

在下一部分中,如果使用 Windows,MacOS 或 Linux 操作系统,则需要执行一些不同的命令。 因此,它们是:


Windows 用户:转到您先前在 CMake 中设置的 OpenCV 构建文件夹(在我们的示例中为c:\dev\opencv\build)。 应该有一个 Visual Studio 2015 解决方案(即 MSVC 项目的类型),您可以轻松地执行和构建 OpenCV。 您也可以立即单击 CMake 上“生成”按钮旁边的“打开项目”按钮。 您也可以只运行 Visual Studio 2015 并打开您刚为 OpenCV 创建的解决方案文件。

打开 Visual Studio 之后,需要从 Visual Studio 主菜单中选择“批量生成”。 就在Build下:

确保在Build列中为ALL_BUILDINSTALL启用了复选框,如以下屏幕截图所示:


对于 MacOS 和 Linux 用户:在切换到在 CMake 中选择的Binaries文件夹后( build 文件夹),运行终端实例并执行以下命令。 要切换到特定文件夹,您需要使用cd命令。 进入 OpenCV 构建文件夹(应该是打开 CMake 时选择的主文件夹)之后,需要执行以下命令。 系统将要求您提供管理密码,只需提供密码,然后按Enter即可继续构建 OpenCV:

 sudo make

这将触发构建过程,并且可能需要花费一些时间,具体取决于您的计算机速度。 等到所有库的构建完成后,进度将达到 100%。

在漫长的等待之后,对于 MacOS 和 Linux 用户来说,只剩下一条命令需要执行了。如果您使用的是 Windows 系统,则可以关闭 Visual Studio IDE 并继续下一步。

构建完成后,在关闭终端实例之前,请在仍位于 OpenCV build文件夹中的情况下执行以下命令:

sudo make install

对于非 Windows 用户,这最后一个命令将确保您的计算机上已安装 OpenCV,并且可以完全使用。 如果您没有错过本节中的任何命令,则可以继续进行。 您已经准备好使用 OpenCV 框架来构建计算机视觉应用。

配置 OpenCV 的安装

记得我们提到过 OpenCV 是一个框架,你将学习如何在 Qt 中使用它吗?好吧,Qt 提供了一种非常易于使用的方法来包含任何第三方库,比如 OpenCV,在你的 Qt 项目中。要在 Qt 中使用 OpenCV,您需要使用一种特殊的文件,称为 PRI 文件。PRI 文件用于添加第三方模块并将它们包含到您的 Qt 项目中。请注意,您只需要执行此操作一次,在本书的其余部分中,您将在所有项目中使用此文件,因此这是 Qt 配置中非常关键(但非常容易)的一部分。

首先,在您选择的文件夹中创建一个文本文件。我建议使用与 OpenCV 构建相同的(build)文件夹,因为这可以确保您的所有与 OpenCV 相关的文件都在一个文件夹中。但从技术上讲,这个文件可以位于计算机上的任何位置。将文件重命名为opencv.pri,并使用任何文本编辑器打开它,然后在此 PRI 文件中写入以下内容:


Windows 用户:到目前为止,您的 OpenCV 库文件应该位于您先前在 CMake 上设置的 OpenCV 构建文件夹中。 build文件夹中应该有一个名为install的子文件夹,其中包含所有必需的 OpenCV 文件。 实际上,现在您可以删除所有其他内容,如果需要在计算机上保留一些空间,则只保留这些文件,但是将 OpenCV 源代码保留在计算机上始终是一个好主意,我们将在最后几章中特别需要它,并且将涵盖更高级的 OpenCV 主题。 因此,这是 PRI 文件中需要的内容(请注意路径分隔符,无论使用什么操作系统,都始终需要在 PRI 文件中使用/):

INCLUDEPATH += c:/dev/opencv/build/install/include 
Debug: { 
LIBS += -lc:/dev/opencv/build/install/x86/vc14/lib/opencv_world330d 
} 
Release: { 
LIBS += -lc:/dev/opencv/build/install/x86/vc14/lib/opencv_world330 
} 

无需说明,在前面的代码中,如果在 CMake 配置期间使用了其他文件夹,则需要替换路径。

Windows 用户还有一件事,那就是将 OpenCV DLLs文件夹添加到PATH环境变量中。 只需打开“系统属性”窗口,然后在PATH中添加一个新条目。 它们通常用;隔开,因此之后只需添加一个新的即可。 请注意,此路径仅与 Windows 操作系统相关,并且可以在其中找到 OpenCV 的DLL文件,从而简化了构建过程。 Linux 和 MacOS 的用户不需要为此做任何事情。


MacOS 和 Linux 用户:只需将以下内容放入opencv.pri文件中:

 INCLUDEPATH += /usr/local/include 
 LIBS += -L/usr/local/lib \ 
    -lopencv_world 

测试 OpenCV 的安装

如果您按照描述的一切操作,并按照正确的顺序执行了所有说明,那么到现在为止,您不应该担心任何事情,但最好是进行验证,这就是我们现在要做的。 因此,我们将使用一个非常简单的应用来验证我们的 OpenCV 安装,该应用将从硬盘读取图像文件并仅显示它。

1a704eb5-fafe-4688-9271-ea7f003b598e

首先运行 Qt Creator,然后创建一个新的控制台应用。 在测试 Qt 安装之前,您已经完成了非常相似的任务。 您需要遵循完全相同的说明,除了必须使用 Qt Widget 之外,还必须确保选择Qt Console Application。 像以前一样重复所有类似的步骤,直到最终进入 Qt Creator 编辑模式。

如果询问您有关构建系统的信息,请选择qmake,默认情况下应选择qmake,因此您只需要继续前进即可。 确保为您的项目命名,例如QtCvTest。 这次,不用单击“运行”按钮,而是双击项目的 .pro 文件,您可以在 Qt Creator 屏幕左侧的资源管理器中找到该文件,然后在项目的 .pro 文件末尾添加以下行 :

include(c:/dev/opencv/opencv.pri) 

请注意,实际上,这是应始终避免的硬编码类型,正如我们将在后面的章节中看到的那样,我们将编写适用于所有操作系统的更复杂的 PRO 文件。 无需更改任何一行; 但是,由于我们只是在测试我们的 OpenCV 安装,因此现在可以进行一些硬编码来简化一些事情,而不会因更多配置细节而使您不知所措。

因此,回到我们正在做的事情,当您通过按Ctrl + S保存 .pro 文件时,您会注意到快速的过程并在项目浏览器和opencv.pri文件将出现在资源管理器中。 您可以随时从此处更改opencv.pri的内容,但是您可能永远不需要这样做。 忽略类似注释的行,并确保您的 .pro 文件与我在此处的文件相似:

 QT += core 
 QT -= gui 
 CONFIG += c++11 
 TARGET = QtCvTest 
 CONFIG += console 
 CONFIG -= app_bundle 
 TEMPLATE = app 
 SOURCES += main.cpp 
 DEFINES += QT_DEPRECATED_WARNINGS 
 include(c:/dev/opencv/opencv.pri) 

现在,您实际上可以编写一些 OpenCV 代码。 打开您的main.cpp文件并更改其内容,使其与此类似:

 #include <QCoreApplication> 
 #include "opencv2/opencv.hpp" 
 int main(int argc, char *argv[]) 
 { 
    QCoreApplication a(argc, argv); 
   
    using namespace cv; 
    Mat image = imread("c:/dev/test.jpg"); 
    imshow("Output", image); 
   
    return a.exec(); 
 } 

您应该在计算机上看到类似于以下屏幕截图的内容:

总结

在本章中,向您介绍了计算机视觉的一般概念以及 Qt 和 OpenCV 框架,并了解了它们的整体模块化结构,还简要了解了它们在所有平台上跨平台的重要性。 两者都支持。 您还学习了如何在计算机上安装 Qt 以及如何使用其源代码构建 OpenCV。 到目前为止,除了本章中提到的标准构建之外,您应该有足够的信心甚至可以尝试一些其他配置来构建 OpenCV。 通过简单地查看它们包含的文件夹和文件,探索这些巨大框架的一些未知而又深入的部分总是一个好主意。 最后,您学习了如何配置开发计算机以使用 Qt 和 OpenCV 构建应用,甚至还构建了第一个应用。 在下一章中,您将首先构建控制台应用,然后继续构建 Qt 小部件应用,以了解有关 Qt Creator 的更多信息。 您还将了解 Qt 项目的结构以及如何在 Qt 和 OpenCV 框架之间创建跨平台集成。 下一章将是本书中实际计算机视觉开发和编程示例的开始,并将为整本书中的动手示例奠定基础。

第二章、Qt 控件介绍

Qt 详细的介绍请参考:https://github.com/NekoSilverFox/opencv

在本章中,我们将通过学习有关 Qt Creator IDE 的所有内容并学习如何使用它来开始我们的实践工作,因为我们在整本书中都会直接使用 Qt Creator 来构建任何项目。您将了解到它提供的所有优势,并了解为什么它在所有的简洁、外观和感觉上都是一个非常强大的 IDE。您将了解 Qt Creator 的设置和详细信息以及如何更改它们以满足您的需求。您还将了解 Qt 项目文件、源代码、用户界面等等。

您应该注意,本章中学到的内容将帮助您在未来节省大量时间,但只有当您真正在您的计算机上重复执行所有内容并尝试始终使用它来进行使用 Qt Creator 进行 C++ 编程时,才会如此。

最后,我们将通过创建一个实际的计算机视觉应用程序并对图像应用一些基本的图像处理算法来结束本章。本章的目标是为您准备好本书的其余部分,并使您熟悉您在整本书中将遇到的一些关键字,如信号、槽、小部件等。

在本章中,我们将涵盖以下主题:

  • 配置和使用 Qt Creator IDE
  • 创建 Qt 项目
  • Qt Creator 中的小部件
  • 创建跨平台的 Qt+OpenCV 项目文件
  • 使用 Qt Creator 设计用户界面
  • 使用 Qt Creator 为用户界面编写代码

什么是 Qt Creator?

Qt Creator 与 Qt 框架不是一回事,它只是由 Qt 框架创建的一个 IDE。以下是 Qt Creator 的欢迎模式的屏幕截图:

请注意,我们并不一定会使用 Qt Creator 的所有功能,但在更深入地了解之前了解它的功能是个好主意。以下是 Qt Creator 的一些最重要的特性:

  • 使用会话管理多个 IDE 状态
  • 管理多个 Qt 项目
  • 设计用户界面
  • 编辑代码
  • 在所有 Qt 支持的平台上构建和运行应用程序
  • 调试应用程序
  • 上下文相关帮助

根据您认为重要的内容,您可能可以将此列表扩展为更多项目,但在上述列表中提到的内容本质上是 IDE(集成开发环境)的定义,它应该是一个提供应用程序开发所需的所有必要工具的应用程序。此外,您还可以随时查看 Qt Creator 的额外功能的 Qt 文档。

Qt 框架对标准 C++ 编程的最重要添加是信号和插槽机制,这也是使 Qt 如此易于学习且功能强大的原因。 这绝对也是 Qt 与其他框架之间最重要的区别。 可以将它视为 Qt 对象和类之间的消息传递方法(或顾名思义,只是发出信号)。 每个 Qt 对象都可以发出可以连接到另一个(或相同)对象中的插槽的信号。 让我们通过一个简单的例子进一步分解它。 QPushButton是一个 Qt 小部件类,您可以将其添加到 Qt 用户界面中以创建按钮。 它包含许多信号,包括明显的按下信号。 另一方面,在我们创建Hello_Qt_OpenCV项目时自动创建的MainWindow(以及所有 Qt 窗口)包含一个名为close的插槽,可用于简单地关闭项目的主窗口。我相信您可以想象如果将按钮的按下信号连接到窗口的关闭插槽会发生什么。 有很多方法可以将信号连接到插槽,因此,从现在开始,在本书的其余部分中,只要需要在示例中使用它们,我们就会学习它们的每一种。

设计用户界面

!!!!关于设计用户界面,更多和更详细的说明请参考这里:https://github.com/NekoSilverFox/Qt !!!!

从这里开始学习如何将 Qt 小部件添加到用户界面,并使它们对用户输入和其他事件做出反应。 Qt Creator 提供了非常简单的工具来设计用户界面并为其编写代码。您已经看到了设计模式下可用的不同窗格和工具,因此我们可以从示例开始。 通过选择mainwindow.ui文件(这是我们从编辑模式进入主窗口的用户界面文件),确保首先切换到设计模式(如果尚未进入设计模式)。

在设计模式下,您可以在用户界面上查看可使用的 Qt 小部件列表。从这些图标和名称可以立即识别出大多数这些小部件的用途,但是仍然有一些特定于 Qt 的小部件。 这是默认情况下 Qt Creator 中代表所有可用布局和小部件的屏幕截图:


Qt 窗口共有 3 种不同类型的条(实际上,一般来说是 Windows),它们在小部件工具箱中不可用,但是可以通过右键单击 Windows 中的窗口来创建,添加或删除它们。 设计器模式,然后从右键菜单中选择相关项目。 它们是:

image-20240325000503738
  1. 菜单栏(QMenuBar菜单栏是显示在窗口顶部的典型水平主菜单栏。 菜单中可以有任意数量的项目和子项目,每个项目和子项目都可以触发一个动作(QAction)。 您将在接下来的章节中了解有关操作的更多信息。 以下是菜单栏示例:

  1. 工具栏(QToolBar工具栏是一个可移动面板,其中可以包含与特定任务相对应的工具按钮。 这是一个示例工具栏。 请注意,它们可以在 Qt 窗口内移动甚至移出:

  1. 状态栏(QStatusBar) 状态栏**是底部的一个简单的水平信息栏,对于大多数基于窗口的应用是通用的。 **

**每当在 Qt 中创建一个新的主窗口时,这三种类型的条形都将添加到该窗口中。 请注意,一个窗口上只能有一个菜单栏和一个状态栏,但是可以有任意数量的状态栏。 如果不需要它们,则需要将它们从“设计器”窗口右侧的对象层次结构中删除。 现在您已经熟悉了 Qt 中的三个不同的条形,可以从“Qt 欢迎”模式中的示例中搜索Application Example,以进一步了解它们,以及是否可以进一步自定义它们。


以下是对 Qt Creator 设计模式(或从现在开始简称为 Designer)中可用小部件的简要说明,如前面的屏幕快照所示。 在设计器模式下,小部件基于其行为的相似性进行分组。 在继续进行列表操作时,请自己亲自尝试设计器中的每个功能,以感觉到将它们放置在用户界面上时的外观。 为此,您可以使用设计器模式将每个窗口小部件拖放到窗口上:

  • Layouts - 布局:这些布局用于管理窗口小部件的显示方式。在外观上,它们是不可见的(因为它们不是QWidget子类),并且它们仅影响添加到它们的小部件。 请注意,布局根本不是小部件,它们是用来管理小部件的显示方式的逻辑类。 尝试在用户界面上放置任何布局小部件,然后在其中添加一些按钮或显示小部件,以查看其布局如何根据布局类型进行更改。 查看每个示例图片以了解它们的行为。

    image-20240324234655285
    布局(英) 布局(中) 说明 效果 Qt等价类(相当于哪个类)
    Vertical Layout 垂直布局 它们用于具有垂直布局,即一列小部件 QVBoxLayout
    Horizontal Layout 水平布局 用于水平排列小部件 QHBoxLayout
    Grid Layout 网格布 可用于创建具有任意行和列数的小部件网格 QGridLayout
    Form Layout 表单布局 可用于使用一些标签和它们对应的输入小部件呈现类似表单的外观(2列n行,并且其中某一行可以对应多个列)。想象一下填写表单,您就会明白 QFormLayout
  • Spacers - 分隔符:类似于弹簧,它们在视觉上不可见,但会影响将其他窗口小部件添加到布局时的显示方式。在用户更改窗口大小时候小控件间隙可以动态缩放。间隔符的类型为QSpacerItem,但是通常,它们绝不能直接在代码中使用。

    分隔符(英) 分隔符(中) 效果
    Horizontal Spacer 水平分隔符 image-20240326195926664
    Vertical Spacer 垂直分隔符 image-20240326195931201

  • Buttons - 按钮:这些只是按钮。 它们用于提示操作。 您可能会注意到,单选按钮和复选框也在该组中,这是因为它们都继承自QAbstractButton类,该类是一个抽象类,提供了类按钮小部件所需的所有接口。

    控件 效果 说明
    文字按钮 image-20221130164311887 image-20221130164629710 文字按钮(可设置图标)
    图标按钮 image-20221130164712179 image-20221130165040902 多用于只显示图标的按钮,可配置是否是透明风格或者显示按钮文字 image-20221130165149890
    单选框 image-20221130165204798 image-20221130165733293 可与 Group Box 搭配使用进行分组,避免交叉 可使用代码设置默认选中那个 ui->rBtnMan->setChecked(true) (此小部件的等效 Qt 类称为QCommandLinkButton
    多选框、复选框 image-20221130165745758 image-20221130233207709 可与 Group Box 搭配使用进行分组,根据是否选中有不同的状态(State) 0 - 未选中1 - 半选中2 - 全选中。可作为 Qt::QCheckBox::stateChanged 进行监听,但是要想让复选框支持半选中需要开启以下选项: image-20221130230942923
    命令链接按钮 image-20240325002753950 【不常用】这是一个 Windows Vista 风格的命令链接按钮。它们基本上是推按钮,旨在替代向导中的单选按钮,因此,当按下命令链接按钮时,它类似于在向导对话框上使用单选框选择选项,然后单击“下一步”(相当于 Qt 中的 QCommandLinkButton 类)。
    对话框按钮框 image-20240325003038151 image-20240325003017933 如果您希望您的按钮在对话框中适应操作系统的样式,那么这个功能非常有用。它有助于以更适合当前系统样式的方式在对话框上呈现按钮(相当于 Qt 中的 QDialogButtonBox 类)。

https://cloud.tencent.com/developer/article/1845045

Item Views(表项视图)和Item Widgets(部件)区别

  • 两者的关系:Item Views(Model-Based)类内的控件是Item Widgets(Item-Based)内对应控件的父类,如QTreeWidget是从QTreeView派生的。
  • 两者的区别
    • Item Views(Model-Based)的对象进行数据操作相对比较复杂,但处理及展示大数据量时性能高
    • Item Widgets的数据操作比较简单,但处理及展示大数据量时性能相对低。Item Widgets在开发中没有Item Views灵活,实际上Item Widgets就是在Item Views的基础上绑定了一个默认的存储并提供了相关方法。
  • 项目==视图==(基于模型)Item Views (Model-based):这基于模型-视图-控制器(MVC, Model-view-controller)设计模式; 它们可用于表示不同类型容器中的模型数据。

    如果您完全不熟悉 MVC 设计模式,那么我建议您在这里停顿一下,首先通读一本综合性的文章,以确保至少对它是什么以及如何使用 MVC(尤其是 Qt)有一个基本的了解。 阅读 Qt 文档中名为“模型/视图编程(Model/View Programming)”的文章。 出于本书的目的,我们不需要非常详细的信息和对 MVC 模式的理解。 但是,由于它是非常重要的架构,您肯定会在以后的项目中遇到它,因此我建议您花一些时间来学习它。 不过,在第 3 章,“创建全面的 Qt + OpenCV 项目”中,我们将介绍 Qt 和 OpenCV 中使用的不同设计模式。

    • 列表视图 - List View:这以一个简单的列表形式展示模型中的项,没有任何层次结构(对应的Qt类为QListView)。
    • 树视图 - Tree View:这以层次化的树视图展示模型中的项。(对应的Qt类为QTreeView)。
    • 表视图 - Table View:这用于以表格形式展示模型中的数据,可以有任意数量的行和列。这在展示SQL数据库或查询的表格时特别有用(对应的Qt类为QTableView)。
    • 列视图 - Column View:这与列表视图相似,不同之处在于列视图还展示存储在模型中的层次化数据(对应的Qt类为QColumnView)。
    • 撤销视图 - Undo ViewQUndoView 是一个展示撤销堆栈内容的Qt小部件。通过点击视图中的命令,可以使文档的状态向前或向后回滚到该命令。这提供了一个直观的方式,让用户可以轻松地浏览并选择撤销或重做的操作。更多详情,请访问官方文档

  • 项目小部件(基于项目)Item Widgets (Item-Based):这类似于基于模型的项目视图,不同之处在于它们不是基于 MVC 设计模式,并且它们提供了简单的 API 来添加,删除或修改他们的项目
    • 列表小部件 - List Widget:类似于列表视图,但是具有基于项目的 API,可以添加,删除和修改其项目(此小部件的等效 Qt 类称为QListWidget
    • 树形小部件 - Tree Widget:这类似于树形视图,但具有基于项目的 API,可以添加,删除和修改其项目(此小部件的等效 Qt 类称为QTreeWidget
    • 表格小部件 - Table Widget:这类似于表视图,但是具有基于项目的 API,用于添加,删除和修改其项目(此窗口小部件的等效 Qt 类称为QTableWidget

  • 输入小部件:听起来完全一样。 您可以使用以下小部件获取用户输入数据。
    • 组合框:有时称为下拉列表; 它可以用来选择列表中的选项,而屏幕上的空间却很少。 任何时候,只有选定的选项可见。 用户甚至可以输入自己的输入值,具体取决于其配置。 (此小部件的等效 Qt 类称为QComboBox):
    • 字体组合框:类似于组合框,但可用于选择字体系列。 字体列表是使用计算机上的可用字体创建的。
    • 行编辑:可用于输入和显示单行文本(此小部件的等效 Qt 类称为QLineEdit)。
    • 文本编辑:可用于输入和显示多行富文本格式。 重要的是要注意,这个小部件实际上是成熟的 WYIWYG 富文本编辑器(此小部件的等效 Qt 类称为QTextEdit)。
    • 纯文本编辑:可用于查看和编辑多行文本。 可以将其视为类似于记事本的简单小部件(此小部件的等效 Qt 类称为QPlainTextEdit)。
    • 旋转框:用于输入整数或离散的值集,例如月份名称(此小部件的等效 Qt 类称为QSpinBox)。
    • 双重旋转框:类似于旋转框,但是它接受双精度值(此小部件的等效 Qt 类称为QDoubleSpinBox)。
    • 时间编辑:可用于输入时间值。(此小部件的等效 Qt 类称为QTimeEdit)。
    • 日期编辑:可用于输入日期值(此小部件的等效 Qt 类称为QDateEdit)。
    • 日期/时间编辑:可用于输入日期和时间值(此小部件的等效 Qt 类称为QDateTimeEdit)。
    • 拨盘:类似于滑块,但具有圆形和类似拨盘的形状。 它可用于输入指定范围内的整数值(此小部件的等效 Qt 类称为QDial)。
    • 水平/垂直条:可用于添加水平和垂直滚动功能(此小部件的等效 Qt 类称为QScrollBar)。
    • 水平/垂直滑块:可用于输入指定范围内的整数值(此小部件的等效 Qt 类称为QSlider)。
    • 按键序列编辑:可用于输入键盘快捷键(此小部件的等效 Qt 类称为QKeySequenceEdit)。

不应将此与QKeySequence类混淆,该类根本不是小部件。 QKeySequenceEdit用于从用户那里获取QKeySequence。 在拥有QKeySequence之后,我们可以将其与QShortcutQAction类结合使用以触发不同的函数/插槽。 本章稍后将介绍信号/插槽的介绍。


  • 显示小部件:可用于显示输出数据,如数字、文本、图片、日期等:
    • 标签:可用于显示数字、文本、图片或电影(此小部件对应的 Qt 类称为 QLabel)。
    • 文本浏览器:与文本编辑小部件几乎相同,但增加了在链接之间导航的功能(此小部件对应的 Qt 类称为 QTextBrowser)。
    • 图形视图:可用于显示图形场景的内容(此小部件对应的 Qt 类称为 QGraphicsView)。

我们在本书中将会使用到的最重要的小部件可能是图形场景(或 QGraphicsScene),并且将在第5章图形视图框架中进行介绍。

  • 日历小部件:可用于从月历中查看和选择日期(此小部件对应的 Qt 类称为 QCalendarWidget)。
    • LCD数字:可用于在类似LCD的显示屏上显示数字(此小部件对应的 Qt 类称为 QLCDNumber)。
    • 进度条:可用于显示垂直或水平的进度指示器(此小部件对应的 Qt 类称为 QProgressBar)。
    • 水平/垂直线:可用于绘制简单的垂直或水平线。特别适用于不同小部件组之间的分隔线。
    • OpenGL小部件:此类可用作渲染OpenGL输出的表面(此小部件对应的 Qt 类称为 QOpenGLWidget)。

请注意,OpenGL是计算机图形学中一个完全独立和高级的主题,完全超出了本书的范围;然而,如前所述,了解Qt中存在的工具和小部件对于可能的进一步学习是一个好主意。

  • QQuickWidget:此小部件可用于显示Qt Quick用户界面。Qt Quick界面使用QML语言来设计用户界面(此小部件对应的 Qt 类称为 QQuickWidget)。

第12章Qt Quick应用程序中将介绍QML。现在,让我们确保我们的用户界面中不添加任何QQuickWidget小部件,因为我们需要向项目中添加额外的模块才能使其工作。如何向Qt项目中添加模块将在本章中介绍。

第三章、创建第一个 Qt 和 OpenCV 项目

创建工程

现在,我们可以开始为我们的 Hello_Qt_OpenCV 项目设计用户界面了。对于一个项目来说,拥有一份清晰的规格说明书总是一个好主意,然后根据需求设计一个用户友好的UI,先在一张纸上(或者如果项目不大的话,在你的脑海中)画出用户界面,最后开始使用 Designer 创建它。当然,这个过程需要对现有的 Qt 小部件有经验,同时也需要足够的经验来创建你自己的小部件,但这是最终会发生的事情,你只需要继续练习就可以了。

因此,首先,让我们来看看我们需要开发的应用程序的规格说明。比如说:

  • 这个应用程序必须能够接受图像作为输入(接受的图像类型至少应该包括 .jpg、.png 和 *.bmp 文件)。
  • 这个应用程序必须能够应用模糊滤镜。用户必须能够选择中值模糊或高斯模糊类型来过滤输入图像(使用默认的参数集)。
  • 这个应用程序必须能够保存输出图像,而且输出图像的文件类型(或者换句话说,扩展名)必须可以由用户选择(.jpg、.png 或 *.bmp)。
  • 用户应该能够在保存时可选地查看输出图像。
  • 用户界面上设置的所有选项,包括模糊滤镜类型和最后打开和保存图像文件,应该在应用程序重新启动时被保留和重新加载。
  • 当用户想要关闭应用程序时,应该提示用户。

这对我们的案例来说应该足够了。通常,你不应该超出或不满足需求。这是设计用户界面时的一个重要规则。这意味着你应该确保所有需求都被成功满足,同时,你没有添加任何不需要的东西(或者在需求列表中不需要的东西)。

对于这样一份需求列表(或规格说明),可以有无数种用户界面设计;然而,这里是我们将要创建的一个。请注意,这是我们的程序执行时的外观。显然,标题栏和样式可能因操作系统而异,但基本上就是这样:

尽管它看起来可能很简单,但它包含了这样一个任务所需的所有必要组件,界面几乎是不言自明的。因此,打算使用这个应用程序的人实际上不需要知道很多关于它的功能,他们可以简单地猜测所有输入框、单选按钮、复选框等的用途。

这是在 Designer 中查看同一UI时的样子:

是时候为我们的项目创建用户界面了:

  1. 创建这个用户界面,你需要首先从主窗口中移除菜单栏、状态栏和工具栏,因为我们不需要它们。右键点击顶部的菜单栏并选择移除菜单栏。接下来,在窗口的任何位置右键点击并选择移除状态栏。最后,右键点击顶部的工具栏并点击移除工具栏。

  2. 现在,在你的窗口中添加一个水平布局;这就是前面图片顶部可见的布局。然后,在其中添加一个标签、行编辑和推送按钮,如前图所示。

  3. 通过双击标签并输入Input Image :来更改标签的文本。 (这与选择标签并使用屏幕右侧的属性编辑器将文本属性值设置为Input Image :相同。)

几乎所有具有text属性的 Qt 小部件都允许使用其文本进行这种类型的编辑。 因此,从现在开始,当我们说Change the text of the widget X to Y时,这意味着双击并设置文本或使用设计器中的属性编辑器。 我们可以很容易地将此​​规则扩展到属性编辑器中可见的窗口小部件的所有属性,并说Change the W of X to Y。 在这里,显然,W是设计者的属性编辑器中的属性名称,X是小部件名称,Y是需要设置的值。 这将在设计 UI 时为我们节省大量时间。

  1. 添加一个组框,然后添加两个单选按钮,类似于上图所示。

  2. 接下来,添加另一个水平布局,然后在其中添加LabelLine EditPush Button。 这将是在复选框正上方的底部看到的布局。

  3. 最后,在窗口中添加一个复选框。这是底部的复选框。

  4. 现在,根据前面的图片更改窗口上所有小部件的文本。你的 UI 几乎准备好了。你现在可以通过点击屏幕左下角的运行按钮来尝试运行它。确保你没有按带有错误的运行按钮。这是按钮:

这将产生与您之前看到的相同的用户界面。现在,如果您尝试调整窗口的大小,您会注意到在调整窗口大小或最大化窗口时,所有内容都保持原样,并且它不会响应应用大小的更改。 要使您的应用窗口响应大小更改,您需要为centralWidget设置布局。 还需要对屏幕上的分组框执行此操作。

Qt 小部件均具有centralWidget属性。 这是 Qt 设计器中特别用于 Windows 和容器小部件的东西。 使用它,您可以设置容器或窗口的布局,而无需在中央窗口小部件上拖放布局窗口小部件,只需使用设计器顶部的工具栏即可:

您可能已经注意到工具栏中的四个小按钮(如前面的屏幕快照所示),它们看起来与左侧小部件工具箱中的布局完全一样(如下所示):

因此,让我们就整本书中的简单快速解释达成另一条规则。 每当我们说Set the Layout of X to Y时,我们的意思是首先选择小部件(实际上是容器小部件或窗口),然后使用顶部工具栏上的布局按钮选择正确的布局类型。

  1. 根据前面信息框中的描述,选择窗口(这意味着,单击窗口上的空白而不是任何小部件上的空白)并将其布局设置为Vertical

  2. 对组框执行相同操作; 但是,这一次,将布局设置为水平。 现在,您可以尝试再次运行程序。 如您现在所见,它会调整其所有小部件的大小,并在需要时移动它们,以防更改窗口大小。 窗口内的组框也发生了同样的情况。

  3. 接下来需要更改的是小部件的objectName属性。 这些名称非常重要,因为在 C++ 代码中使用它们来访问窗口上的小部件并与其进行交互。 对于每个小部件,请使用以下屏幕截图中显示的名称。 请注意,该图像显示了对象层次结构。 您还可以通过双击对象层次结构窗格中的小部件来更改objectName属性:

image-20240329175751918

从理论上讲,您可以为objectName属性使用任何 C++ 有效的变量名,但实际上,最好始终使用有意义的名称。考虑对本书中使用的变量或小部件名称遵循相同或相似的命名约定。它基本上是 Qt 开发人员遵循的命名约定,它还有助于提高代码的可读性。

编写 Qt 项目的代码

现在我们的用户界面已经完全设计好了,我们可以开始为我们的应用程序编写代码了。目前,我们的应用程序基本上只不过是一个用户界面,并且实际上什么也做不了。我们需要从将 OpenCV 添加到我们的项目开始。在第1章 OpenCV 和 Qt 的介绍中,你已经简要了解了如何将 OpenCV 添加到 Qt 项目中。现在,我们将更进一步,确保我们的项目可以在三大主流操作系统上编译和构建。

因此,首先在代码编辑器中打开项目的 .pro 文件。将以下代码添加到这个文件的末尾:

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

 unix: !macx { 
    CONFIG += link_pkgconfig 
    PKGCONFIG += opencv 
 } 

 unix: macx { 
   INCLUDEPATH += "/usr/local/include" 
   LIBS += -L"/usr/local/lib" \ 
    -lopencv_world 
 } 

注意右括号前的代码; win32表示 Windows 操作系统(仅适用于桌面应用,不适用于 Windows 8、8.1 或 10 特定应用),unix: !macx表示 Linux 操作系统,unix: macx表示 MacOS 操作系统。

您的PRO文件中的这段代码允许 OpenCV 包含在内并在您的 Qt 项目中可用。 还记得我们在第 1 章,“OpenCV 和 Qt 简介”中创建了一个PRI文件吗? Linux 和 MacOS 用户可以将其删除,因为在那些操作系统中不再需要该文件。 只有 Windows 用户可以保留它。

请注意,在 Windows OS 中,您可以将前面的include行替换为 PRO 文件的内容,但这在实践中并不常见。 另外,值得提醒的是,您需要在PATH中包含 OpenCV DLLs 文件夹,否则当您尝试运行它时,应用将崩溃。 但是,它仍然可以正确编译和构建。 要更加熟悉 Qt PRO 文件的内容,可以在 Qt 文档中搜索qmake并阅读有关内容。 不过,我们还将在第 3 章,“创建综合的 Qt + OpenCV 项目”中进行简要介绍。

我们不会讨论这些代码行在每个操作系统上的确切含义,因为这不在本书的讨论范围之内,但是值得注意并足以知道何时构建应用(换句话说,编译、编译、链接),这些行将转换为所有 OpenCV 头文件,库和二进制文件,并包含在您的项目中,以便您可以轻松地在代码中使用 OpenCV 函数。

现在我们已经完成了配置工作,让我们开始为用户界面上的每个需求及其相关的小部件编写代码。 让我们从inputPushButton开始。

从现在开始,我们将使用其唯一的objectName属性值引用用户界面上的任何窗口小部件。 将它们视为可以在代码中使用以访问这些小部件的变量名。

这是我们项目的编码部分所需的步骤:

  1. 再次切换到设计器,然后右键单击inputPushButton。 然后,从出现的菜单中选择“转到插槽...”。 将显示的窗口包括此小部件发出的所有信号。 选择pressed(),然后单击确定:

  1. 您会注意到,您是从设计器自动转到代码编辑器的。 另外,现在mainwindow.h文件中添加了新函数。
  2. mainwindow.h中,添加了以下内容:
private slots: 
  void on_inputPushButton_clicked(); 

这是自动添加到mainwindow.cpp的代码:

void MainWindow::on_inputPushButton_clicked() 
{ } 

因此,显然需要在刚刚创建的on_inputPushButton_pressed()函数中编写负责inputPushButton的代码。 如本章前面所述,这是将信号从小部件连接到另一个小部件上的插槽的多种方法之一。 让我们退后一步,看看发生了什么。 同时,请注意刚刚创建的函数的名称。 inputPushButton小部件具有一个称为被按下的信号 signal(因为它是一个按钮),该信号仅在被按下时才发出。 在我们的单个窗口小部件(MainWindow)中创建了一个新插槽,称为on_inputPushButton_clicked。总而言之,每当inputPushButton小部件发出按下信号时,Qt 都会自动理解它需要在on_inputPushButton_clicked()中执行代码。

在 Qt 开发中,这被称为按名称连接插槽slots,它仅遵循以下约定自动将信号连接至插槽on_OBJECTNAME_SIGNAL(PARAMETERS)

在此,OBJECTNAME应该替换为发送信号的小部件的OBJECTNAME属性的值,SIGNAL替换为信号名称,PARAMETERS替换为确切的信号编号和参数类型。

但是注意,这种创建方式是不推荐的,因为这是使用 Qt 的==自动连接机制==

在Qt中,存在一种自动连接信号和槽的机制,这是通过QObject的QMetaObject::connectSlotsByName()函数实现的。当一个QWidget(包括其子类)对象被创建时,Qt会自动查找该对象中==所有==的槽函数,如果槽函数的命名遵循on_<objectName>_<signalName>的模式,Qt将==自动==将这些槽连接到名称为<objectName>的对象发出的名为<signalName>的信号(也就是不通过写 connect 他就自动连接上了)。

为什么是错误倾向的?

虽然这个特性可以简化某些情况下的信号与槽的连接过程,减少编码工作量,但它也带来了一些潜在的问题,这就是为什么Clazy(一个静态代码分析器)会发出警告:

  1. 隐式行为可能导致错误:自动连接是一个隐式过程,开发者可能不清楚某个槽函数是否被自动连接,或者错误地认为某个槽函数会被自动连接。这可能导致调试困难,因为行为的预期与实际可能不符。
  2. 重构风险如果对象名称或信号名称在未来发生变化,与之相关的自动连接也会受到影响,可能会导致槽不再被正确连接,而编译器不会报错,因为这些连接是在运行时解析的。
  3. 代码可读性降低:对于不熟悉Qt自动连接机制的开发者来说,可能会对这种隐式的连接方式感到困惑,这影响了代码的清晰度和可维护性。

根据应用的要求,我们需要确保用户可以打开图像文件。 成功打开图像文件后,我们会将路径写入inputLineEdit小部件的text属性,以便用户可以看到他们选择的完整文件名和路径。 首先让我们看一下代码的外观,然后逐步介绍它:

void MainWindow::on_inputPushButton_clicked() 
{ 
  QString fileName = QFileDialog::getOpenFileName(
    this, 
    "Open Input Image", 
    QDir::currentPath(), 
    "Images (*.jpg *.png *.bmp)"); 

   if(QFile::exists(fileName)) 
   { 
     ui->inputLineEdit->setText(fileName); 
   } 
} 

要访问用户界面上的小部件或其他元素,只需使用ui对象。例如,可以通过ui类并通过编写以下行来简单地访问用户界面中的inputLineEdit小部件:

ui-> inputLineEdit

第一行实际上是大代码的简化版本。 正如您将在本书中学习的那样,Qt 提供了许多方便的函数和类来满足日常编程需求,例如将它们打包成非常短的函数。 首先让我们看看我们刚刚使用了哪些 Qt 类:

  • QString:这可能是 Qt 最重要和广泛使用的类别之一。 它代表 Unicode 字符串。 您可以使用它来存储,转换,修改字符串以及对字符串进行无数其他操作。 在此示例中,我们仅使用它来存储QFileDialog类读取的文件名。
  • QFileDialog:可以用来选择计算机上的文件或文件夹。它使用底层操作系统 API,因此对话框的外观可能有所不同,具体取决于操作系统。
  • QDir:此类可用于访问计算机上的文件夹并获取有关它们的各种信息。
  • QFile:可用于访问文件以及从文件中读取或写入文件。

前面提到的将是对每个类的非常简短的描述,并且如您从前面的代码中所见,它们每个都提供了更多的功能。 例如,我们仅在QFile中使用了静态函数来检查文件是否存在。 我们还使用了QDir类来获取当前路径(通常是应用从中运行的路径)。 代码中唯一需要更多说明的是getOpenFileName函数。 第一个参数应该是parent小部件。 这在 Qt 中非常重要,它用于自动清除内存,如果出现对话框和窗口,则要确定父窗口。 这意味着每个对象在销毁子对象时也应负责清理其子对象,如果是窗户,则由其父窗口打开它们。 因此,通过将this设置为第一个参数,我们告诉编译器(当然还有 Qt)此类负责QFileDialog类实例。 getOpenFileName函数的第二个参数显然是文件选择对话框窗口的标题,下一个参数是当前路径。 我们提供的最后一个参数可确保仅显示应用需求中的三种文件类型:*.jpg*.png*.bmp文件。

仅当首先将其模块添加到您的项目中,然后将其头文件包含在您的源文件中时,才可以使用任何 Qt 类。 要将 Qt 模块添加到 Qt 项目,您需要在项目的PRO文件中添加类似于以下内容的行:

QT += module_name1 module_name2 module_name3 ...

module_name1等可以替换为可以在 Qt 文档中找到的每个类的实际 Qt 模块名称。 您可能已经注意到项目的 PRO 文件中已经存在以下代码行:

QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

这仅表示coregui模块应包含在您的项目中。 它们是两个最基本的 Qt 模块,包括许多 Qt 基础类。第二行表示,如果您使用的 Qt 框架的主要版本号高于4,则还应包含widgets模块。 这是因为以下事实:在 Qt 5 之前,widgets模块是gui模块的一部分,因此无需将其包含在PRO文件中。 至于头文件,它始终与类名本身相同。 因此,在我们的情况下,我们需要在源代码中添加以下类,以使前面的代码起作用。最好的位置通常是头文件的顶部,因此在我们的例子中就是mainwindow.h文件。 确保在顶部具有以下类别:

#include <QMainWindow> #include <QFileDialog> #include <QDir> #include <QFile>

尝试一下,然后运行程序以查看结果。然后,将其关闭并再次返回到设计器。现在,我们需要将代码添加到outputPushButton小部件。只需重复与inputPushButton相同的过程,但是这次,在outputPushButton上进行此操作,并为其编写以下代码:

void MainWindow::on_outputPushButton_clicked() 
{ 
    QString fileName = QFileDialog::getSaveFileName(this, "Select output image", QDir::currentPath(), "*.jpg *.png *.bmp");

    if (!fileName.isEmpty())
    {
        ui->leOutput->setText(fileName);
        cv::Mat img_in = cv::imread(ui->leInput->text().toStdString());

        cv::Mat img_out;
        if (ui->rbtnMedianBlur->isChecked())
        {
            cv::medianBlur(img_in, img_out, 5);
        }
        else if (ui->rbtnGaussianBlur->isChecked())
        {
            cv::GaussianBlur(img_in, img_out, cv::Size(5, 5), 1.25);
        }

        cv::imwrite(fileName.toStdString(), img_out);
        if (ui->cbDisplayAfterSave->isChecked())
        {
            cv::imshow("Output image", img_out);
        }
    }
} 

您还需要向项目添加OpenCV标头。 将它们添加到mainwindow.h文件顶部的添加 Qt 类头的位置,如下所示:

#include "opencv2/opencv.hpp"  

现在,让我们回顾一下我们刚刚编写的代码。这一次,我们在QFileDialog类和标题中使用了getSaveFileName函数,并且过滤器也有所不同。 这是必需的,以便用户在要保存输出图像时分别选择每种图像类型,而不是在打开它们时看到所有图像。 这次,**我们也没有检查文件的存在,因为这将由QFileDialog自动完成,**因此仅检查用户是否确实选择了某项就足够了。 在以下几行中,我们编写了一些特定于 OpenCV 的代码,在接下来的章节中,我们将越来越多地了解这些功能。我们将再次简短地讨论它们,并继续介绍 IDE 和Hello_Qt_OpenCV应用。

  1. 所有OpenCV函数都包含在cv名称空间中,因此我们确保我们是 OpenCV namespace cvusing
  2. 然后,为了读取输入图像,我们使用了imread函数。这里要注意的重要一点是 OpenCV 使用 C++ std::string类,而 Qt 的QString应该转换为该格式,否则,当您尝试运行该程序时会遇到错误。 只需使用QStringtoStdString函数即可完成。注意,在这种情况下,QStringinputLineEdit小部件的text()函数返回的值。
  3. 接下来,根据选择的过滤器类型,我们使用medianBlurgaussianBlur函数进行简单的 OpenCV 过滤。请注意,在这种情况下,我们为这些 OpenCV 函数使用了一些默认参数,但是如果我们使用小部件从用户那里获得它们,那就更好了。您将在章节“创建全面的 Qt + OpenCV 项目”中学习如何使用更多小部件,甚至创建自己的小部件。
  4. 最后,已过滤的输出图像img_out被写入所选文件。 根据displayImageCheckBox小部件设置的条件也会显示它。

到这个时候,我们还有两个要求:

  • 首先是,在关闭程序时将所有小部件的状态保存在窗口中并在重新打开程序时将其重新加载
  • 另一个要求是在用户想要关闭程序时提示他们。

让我们从最后一个要求开始,因为这意味着我们需要知道如何编写在关闭窗口时需要执行的代码。这非常简单,因为 Qt 的QMainWindow类(我们的窗口所基于的类)是QWidget,并且它已经具有一个虚函数,我们可以覆盖和使用它。 只需将以下代码行添加到您的MainWindow类中:

#include <QCloseEvent>  // 如果报错 `Member access into incomplete type 'QCloseEvent'` 可以添加头文件来解决

...

protected:
	virtual void closeEvent(QCloseEvent* event); 

现在,切换到mainwindow.cpp并将以下代码段添加到文件末尾:

void Hello_Qt_OpenCV::closeEvent(QCloseEvent* event)
{
    QMessageBox::StandardButton result =
        QMessageBox::warning(this,
                            "Exit",
                            "Are you sure you want to close this program?",
                            QMessageBox::No | QMessageBox::Yes,
                            QMessageBox::No);

    if (QMessageBox::No == result) event->accept();
    else event->ignore();

    QWidget::closeEvent(event);  // 向上传递
}

我想您已经注意到我们现在又引入了两个 Qt 类,这意味着我们也需要将它们的包含标头添加到mainwindow.h。 考虑以下:

  • QMessageBox:根据消息的目的,它可以用于显示带有简单图标,文本和按钮的消息
  • QCloseEvent:这是许多 Qt 事件(QEvent)类之一,其目的是传递有关窗口关闭事件

该代码几乎是不言自明的,因为您已经知道警告函数的第一个参数是什么。这是用来告诉 Qt 我们的MainWindow类负责此消息框。记录用户选择的结果,然后,基于此结果,关闭事件被接受或忽略。

除此之外,我们仍然需要保存设置(小部件上的文本以及复选框和单选框的状态)并加载它们。如您所知,保存设置的最佳位置是closeEvent函数。 在代码的event->accept();行之前怎么样?让我们向MainWindow类添加两个私有函数,一个私有函数加载名为loadSettings的设置,另一个私有函数保存名为saveSettings的设置。

在本章中,我们将学习最后一个 Qt 类,它称为QSettings。因此,首先将其包含行添加到mainwindow.h中,然后将以下两个函数定义添加到MainWindow类中,再次在Ui::MainWindow *ui;行正下方的mainwindow.h中,在私有成员中:

void loadSettings(); 
void saveSettings(); 

这是给saveSettings的:

void Hello_Qt_OpenCV::saveSettings()
{
    QSettings settings("Packt", "Hello_OpenCV_Qt", this);

    settings.setValue("leInput", ui->leInput->text());
    settings.setValue("leOutput", ui->leOutput->text());
    settings.setValue("rbtnMedianBlur", ui->rbtnMedianBlur->isChecked());
    settings.setValue("rbtnGaussianBlur", ui->rbtnGaussianBlur->isChecked());
    settings.setValue("cbDisplayAfterSave", ui->cbDisplayAfterSave->isChecked());
}

这是loadSettings函数所需的代码:

void Hello_Qt_OpenCV::loadSettings()
{
    QSettings settings("Packt", "Hello_OpenCV_Qt", this);
    ui->leInput->setText(settings.value("leInput", "").toString());
    ui->leOutput->setText(settings.value("leOutput", "").toString());
    ui->rbtnMedianBlur->setChecked(settings.value("rbtnMedianBlur", true).toBool());
    ui->rbtnGaussianBlur->setChecked(settings.value("rbtnGaussianBlur", false).toBool());
    ui->cbDisplayAfterSave->setChecked(settings.value("cbDisplayAfterSave", false).toBool());
}

在构建 QSettings 类时,你需要提供一个组织名称(仅作为示例,我们使用了“Packt”)和一个应用程序名称(在我们的例子中是“Hello_Qt_OpenCV”)。然后,它会记录你传递给 setValue 函数的任何内容,并通过 value 函数返回它。我们所做的就是简单地将我们想要保存的所有内容传递给 setValue 函数,例如 Line Edit 控件中的文本等等,需要时再重新加载它。请注意,像这样使用 QSettings 时,它会自己处理存储位置,并使用每个操作系统的默认位置来保持应用程序特定的配置。

现在,只需将 loadSettings 函数添加到 MainWindow 类的构造函数中。你应该有一个看起来像这样的构造函数:

ui->setupUi(this);
loadSettings();

closeEvent 中,紧接在 event->accept() 之前添加 saveSettings 函数,就是这样。我们现在可以尝试运行我们的第一个应用程序了。让我们尝试运行并过滤一个图像。选择两种滤镜中的每一种,并查看它们之间的区别。尝试玩转应用程序并找出其问题。尝试通过添加更多参数来改进它,等等。以下是应用程序运行时的屏幕截图:

尝试关闭它,并使用我们的退出确认代码查看一切是否正常。

我们编写的程序显然并不完美,但是它列出了您从 Qt Creator IDE 入门到本书各章所需要了解的几乎所有内容。 Qt Creator 中还有另外三个Modes尚未见过,我们将把调试模式和项目模式留给第 12 章,“Qt Quick 应用”,其中我们将深入研究构建,测试和调试计算机视觉应用的概念。 因此,让我们简要地通过 Qt Creator 的非常重要的“帮助”模式以及Options之后,结束我们的 IDE 之旅。

帮助模式

使用 Qt Creator 左侧的帮助按钮切换到帮助模式:

关于 Qt Creator 帮助模式最重要的一点,除了你可以字面上搜索与 Qt 相关的一切内容,并且能看到每个类和模块的无数示例外,就是你必须使用它来找出每个类所需的正确模块。要做到这一点,只需切换到索引模式并搜索你想在应用程序中使用的 Qt 类。这里有一个示例:

如你所见,可以使用索引并搜索它来轻松访问 QMessageBox 类的文档页面。注意描述之后的前两行:

#include <QMessageBox> 
QT += widgets 

这基本上意味着,为了在项目中使用QMessageBox,必须在源文件中包含QMessageBox头文件,并将小部件模块添加到PRO文件中。 尝试搜索本章中使用的所有类,然后在文档中查看其示例。 Qt Creator 还提供了非常强大的上下文相关帮助。 您只需在任何 Qt 类上用鼠标单击F1,它的文档页面都将在编辑模式下的代码编辑器中获取:

Qt Creator 选项窗口设置

您可以通过点击主菜单中的“工具(Tools)”然后选择“选项(Options)”来访问 Qt Creator 的选项窗口。Qt Creator 允许非常高级别的自定义,因此您会发现其选项页面和标签页中有相当多的参数可以配置。对于大多数人(包括我自己)而言,Qt Creator 的默认选项几乎足以满足他们需要做的所有事情,但有些任务如果不知道如何配置 IDE,您将无法完成。请参考下面的截图:

您可以使用左侧的按钮在页面之间切换。每个页面包含多个标签,但它们都属于同一组。以下是每组选项主要用途:

  • 环境(Environment):这包含了与 Qt Creator 的整体外观和感觉相关的设置。在这里您可以更改主题(这在本章开头提到过)、字体和文字大小、语言及其所有设置。

  • 文本编辑器(Text Editor):这组设置包括所有与代码编辑器相关的内容。这里您可以更改诸如代码高亮、代码补全等设置。

  • FakeVim:这是针对熟悉 Vim 编辑器的人的。在这里,他们可以在 Qt Creator 中启用 Vim 风格的代码编辑并进行配置。

  • 帮助(Help):正如可以猜测的,这包含了与 Qt Creator 的帮助模式和上下文敏感帮助功能相关的所有选项。

  • C++:在这里,您可以找到与 C++ 编码和代码编辑相关的设置。

  • Qt Quick:影响 Qt Quick 设计师和 QML 代码编辑的选项可以在这里找到。我们将在第12章Qt Quick 应用程序中了解更多关于 QML 的信息。

  • 构建与运行(Build & Run):这可能是 Qt Creator 中最重要的选项页面。这里的设置直接影响您的应用程序构建和运行体验。我们将在第11章链接和部署中配置一些设置,届时您将学习到 Qt 的静态链接。

  • 调试器(Debugger):这包含了与 Qt Creator 的调试模式相关的设置。您将在第10章调试和测试中了解更多此内容。

  • 设计师(Designer):这可以用来配置 Qt Creator 模板项目和与设计模式相关的其他设置。

  • 分析器(Analyzer):这包括与 Clang 代码分析器、QML 分析器等相关的设置。覆盖它们超出了本书的范围。

  • 版本控制(Version Control):Qt 提供了与许多版本控制系统(如 Git 和 SVN)的非常可靠的集成。在这里,您可以配置 Qt Creator 中所有与版本控制相关的设置。

  • 设备(Devices):正如您将在第12章Qt Quick 应用程序中看到的,您将使用它来为 Android 开发配置 Qt Creator,包括与设备相关的所有设置。

  • 代码粘贴(Code Pasting):这可以用来配置 Qt Creator 用于诸如代码共享等任务的一些第三方服务。

  • Qbs:完全超出了我们书籍的范围,我们不需要它。

  • 测试设置(Test Settings):这包含与 Qt Test 等相关的设置。我们将在第10章调试和测试中介绍 Qt Test,在那里您将学习如何为我们的 Qt 应用程序编写单元测试。

除此之外,您始终可以使用 Qt Creator 的过滤工具(Filter tool)立即定位到您在选项窗口中需要的设置:

总结

本章更多的是对 Qt Creator 的介绍,而这正是我们为了能够舒适地继续进行下一章节所需要的,集中精力构建东西,而不是重复的指令和配置技巧和提示。我们学习了如何使用 Qt Creator 设计用户界面和为用户界面编写代码。我们被介绍到了一些最广泛使用的 Qt 类以及它们是如何在不同模块中打包的。通过学习不同的 Qt Creator 模式并同时构建一个应用程序,我们现在可以通过自己的练习来提升,甚至改进我们写的应用程序。下一章将是我们构建一个可扩展的插件式计算机视觉应用程序骨架的章节,这将几乎持续到本书的最后几章。在下一章中,我们将学习 Qt 和 OpenCV 中不同的设计模式,以及我们如何使用类似的模式来构建易于维护和扩展的应用程序。

第四章、创建一个全面的 Qt+OpenCV 项目

专业的应用程序之所以专业,并不是因为一些随机的情况,而是从一开始就是这样设计的。当然,说起来容易做起来难,但如果你已经知道了如何创建可以轻松扩展、维护、扩大规模和自定义的应用程序的黄金法则,那么这实际上还是相当容易的。这里的黄金法则只有一个简单的概念,幸运的是,Qt 框架已经有了实现它的手段,那就是以模块化的方式构建应用程序。请注意,在这里模块化不仅仅意味着库或不同的源代码模块,而是意味着应用程序的每个职责和能力都是独立于其他职责和能力创建和构建的。这实际上正是 Qt 和 OpenCV 本身创建的方式。一个模块化的应用程序可以很容易地扩展,即使是不同背景的不同开发者也是如此。一个模块化的应用程序可以扩展以支持许多不同的语言、主题(样式或外观),或者更好的是,许多不同的功能。

在本章中,我们将承担一个非常重要和关键的任务,即为使用 Qt 和 OpenCV 框架的全面计算机视觉应用程序构建基础设施(或架构)。你将学习如何创建即使在部署后(交付给用户)也可以扩展的 Qt 应用程序。这实际上意味着许多事情,包括如何向应用程序添加新语言、如何向应用程序添加新样式,最重要的是,如何构建一个基于插件的 Qt 应用程序,通过添加新插件来扩展它。

我们将从了解构建 Qt 应用程序时一般背后的情况开始,通过浏览 Qt 项目的结构和包含的文件。然后,我们将了解 Qt 和 OpenCV 中最广泛使用的设计模式,以及这两个框架如何享受使用这些设计模式的优势。然后,我们将学习如何创建一个可以通过插件扩展的应用程序。我们还将学习如何向我们的应用程序添加新样式和新语言。到本章结束时,我们将能够创建一个全面的计算机视觉应用程序的基础,该应用程序是跨平台的、多语言的、基于插件的,并具有可定制的外观和感觉。这个基础应用程序将在接下来的两章中扩展,第5章 Mat 和 QImage,以及第6章 图形视图框架,并在之后使用插件扩展本书的其余部分,特别是在第7章 OpenCV 中的图像处理之后,当我们开始真正深入计算机视觉主题和 OpenCV 库时。

在本章中,我们将覆盖以下主题:

  • Qt 项目的结构和 Qt 构建过程
  • Qt 和 OpenCV 中的设计模式
  • Qt 应用程序中的样式
  • Qt 应用程序中的语言
  • 如何使用 Qt Linguist 工具
  • 如何在 Qt 中创建和使用插件

背景

创建我们的第一个 Qt 和 OpenCV 项目中,你学习了如何创建一个简单的 Qt+OpenCV 应用程序,名为 Hello_Qt_OpenCV。这个项目包含了 Qt 提供的几乎所有基本功能,尽管我们没有详细讨论我们的项目是如何构建成一个具有用户界面和(几乎可以接受的)行为的应用程序的。**在本节中,你将了解当我们点击运行按钮时背后发生了什么。**这将帮助我们更好地了解 Qt 项目的结构和项目文件夹中每个文件的用途。让我们开始打开项目文件夹,逐个查看几个文件。因此,我们在 Hello_Qt_OpenCV 文件夹中有以下内容:

Hello_Qt_OpenCV.pro
Hello_Qt_OpenCV.pro.user
main.cpp
mainwindow.cpp
mainwindow.h
mainwindow.ui

Hello_Qt_OpenCV.pro 文件基本上是 Qt 在构建我们的项目时首先处理的文件。这称为Qt 项目文件一个名为 qmake 的内部 Qt 程序负责处理它。让我们看看它是什么。

qmake 工具

qmake 工具是一个帮助使用 *.pro 文件中的信息创建 makefile 的程序。这简单地意味着,使用非常简单的语法(与其他 make 系统中的更复杂语法相比),qmake 生成了编译和构建应用程序所需的所有必要命令,并将所有这些生成的文件放在 Build 文件夹中。

当构建 Qt 项目时,它首先创建一个新的构建文件夹,默认情况下,该文件夹与项目文件夹位于同一级别。在我们的例子中,这个文件夹应该有一个类似于 build-Hello_Qt_OpenCV-Desktop_Qt_5_9_1_*-Debug 的名称,其中 * 可能会有所不同,取决于平台,你可以在项目文件夹所在的同一个文件夹中找到它。Qt(使用 qmake 和本章中您将了解到的一些其他工具)和 C++ 编译器生成的所有文件位于此文件夹及其子文件夹中。这称为项目的构建文件夹。这也是您的应用程序被创建和执行的地方。例如,如果您使用的是 Windows,您可以在 Build 文件夹的 debugrelease 子文件夹中找到 Hello_Qt_OpenCV.exe 文件(以及许多其他文件)。因此,从现在开始我们将称这个文件夹(及其子文件夹)为构建文件夹

例如,我们已经知道在我们的 Qt 项目文件中包含以下行会导致将 Qt 的 coregui 模块添加到我们的应用程序中:

QT += core gui

让我们进一步查看 Hello_Qt_OpenCV.pro 文件;以下几行立即引人注意:

TARGET = Hello_Qt_OpenCV
TEMPLATE = app

这几行简单地意味着 TARGET 名称是 Hello_Qt_OpenCV,这是我们项目的名称,TEMPLATE 类型 app 意味着我们的项目是一个应用程序。我们还有以下内容:

SOURCES += \
    main.cpp \
    mainwindow.cpp
HEADERS += \
    mainwindow.h
FORMS += \
    mainwindow.ui

很明显,这就是头文件、源文件和用户界面文件(表单)如何包含在我们的项目中的方式。我们甚至向项目文件中添加了我们自己的代码,如下所示:

win32: {
  include("c:/dev/opencv/opencv.pri")
}
unix: !macx{
  CONFIG += link_pkgconfig
  PKGCONFIG += opencv
}
unix: macx{
  INCLUDEPATH += "/usr/local/include"
  LIBS += -L"/usr/local/lib" \
-lopencv_world
}

你已经学会了这是 Qt 如何看到 OpenCV 并在 Qt 项目中使用它的方式。搜索 Qt 帮助索引中的 qmake 手册以获取有关 qmake 中所有可能的命令和函数以及更详细的工作方式的更多信息。

qmake 处理了我们的 Qt 项目文件后,它开始寻找项目中提到的源文件。自然地,每个 C++ 程序在其源文件中都有一个 main 函数(一个单一且唯一的 main 函数)(不在头文件中),我们的应用程序也不例外。我们应用程序的 main 函数由 Qt Creator 自动生成,它位于 main.cpp 文件中。让我们打开 main.cpp 文件,看看它包含什么:

#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
  QApplication a(argc, argv); // 应用程序对象 a,在 Qt 中有且仅有一个
  MainWindow w;  // 窗口对象
  w.show();  // 弹出窗口,以新窗口的的方式弹出(窗口默认不会弹出)
  return a.exec(); // a.exec() 进入消息循环机制,避免程序一闪而过,类似死循环
}

前两行用于包含我们当前的 mainwindow.h 头文件和 QApplication 头文件。**QApplication 类是负责控制应用程序的控制流、设置等的主类。**您在 main 函数中看到的,是 Qt 创建事件循环以及其底层信号/槽机制和事件处理系统工作方式的基础:

QApplication a(argc, argv); // 应用程序对象 a,在 Qt 中有且仅有一个
MainWindow w;  // 窗口对象
w.show();  // 弹出窗口,以新窗口的的方式弹出(窗口默认不会弹出)
return a.exec(); // a.exec() 进入消息循环机制,避免程序一闪而过,类似死循环

最简单地描述:

  1. 就是创建了 QApplication 类的一个实例,并将应用程序参数(通常通过命令行或终端传递)传递给名为 a 的新实例。
  2. 然后,创建了我们的 MainWindow 类的一个实例 w,然后通过 .show() 显示它。
  3. 最后,调用 QApplication 类的 .exec() 函数,以便应用程序进入主循环,并保持打开状态,直到窗口关闭。

要了解事件循环的真正工作方式,请尝试删除最后一行,看看会发生什么。当你运行你的应用程序时,你可能会注意到窗口实际上显示了非常短暂的时间,然后立即关闭。这是因为我们的应用程序不再有事件循环,它立即到达应用程序的结尾,内存中的所有内容都被清除了,因此窗口被关闭。现在,重新写回那行代码,正如你所期待的,窗口保持打开状态,因为 .exec() 函数只有在代码中某处(任何地方)调用了 .exit() 函数时才返回,并且它返回 .exit() 设置的值。

现在,让我们继续讨论具有相同名称但扩展名不同的接下来的三个文件。它们是 mainwindow 头文件、源文件和用户界面文件。您现在将了解负责我们在创建第一个 Qt 和 OpenCV 项目中创建的应用程序的代码和用户界面的实际文件。这使我们了解到另外两个 Qt 内部工具,称为元对象编译器(moc)和用户界面编译器(uic)

元对象编译器(moc)

元对象编译器(moc, Meta-Object Compiler

我们已经知道,在标准 C++ 代码中并不存在信号和槽这样的东西。那么,使用 Qt,我们是如何在 C++ 代码中拥有这些额外能力的呢?而且这还不是全部。正如你稍后将学到的,你甚至可以向 Qt 对象添加新属性(称为动态属性)并执行许多类似的操作,这些都不是标准 C++ 编程的能力。嗯,这些是通过使用一个名为 moc 的 Qt 内部编译器实现的。在你的 Qt 代码实际传递给真正的 C++ 编译器之前,moc 工具会处理你的类头文件(在我们的案例中是 mainwindow.h 文件),以生成启用刚刚提到的 Qt 特定能力所需的代码。你可以在构建文件夹中找到这些生成的源文件。它们的名称以 moc_ 开头。

iShot_2024-04-03_11.45.14

你可以在 Qt 文档中阅读关于 moc 工具的所有信息,但值得一提的是,moc 会搜索所有包含 Q_OBJECT 宏的 Qt 类定义的头文件。这个宏必须始终包含在希望支持信号、槽和其他 Qt 支持功能的 Qt 类中。

这是我们在 mainwindow.h 文件中的内容:

...
class MainWindow : public QMainWindow
{
  Q_OBJECT // <-- 这里
    
  public:
   explicit MainWindow(QWidget *parent = 0);
 ~MainWindow();
...

如你所见,我们自动生成的类头文件已经在其私有部分包含了 Q_OBJECT 宏。因此,这基本上是创建类的标准方式(不仅仅是窗口类,而是一般的任何 Qt 类),这些类是 QObject(或任何其他 Qt 对象)的子类,将支持 Qt 支持的功能,如信号和槽。

现在,让我们继续看看我们是如何通过 C++ 代码访问 Qt 用户界面文件中的小部件的。如果你尝试在编辑模式或任何其他文本编辑器中查看 mainwindow.ui 文件,你会注意到它们实际上是 XML 文件,只包括属性和一些其他仅与小部件显示方式相关的信息。答案在于你将在本章中了解的最后一个 Qt 内部编译器。

用户界面编译器(uic)

用户界面编译器(uic, User Interface Compiler)

每当构建具有用户界面的 Qt 应用程序时,都会执行一个名为 uic 的 Qt 内部工具来处理和转换 *.ui 文件,使其成为 C++ 代码中可用的类和源代码。在我们的案例中,mainwindow.h 被转换为 ui_mainwindow.h 文件,同样,你可以在构建文件夹中找到它。你可能已经注意到了这一点,但让我们提一下,你的 mainwindow.cpp 文件已经包含了这个头文件。检查文件的最顶部部分,你会找到以下两个 include 行:

#include "mainwindow.h"
#include "ui_mainwindow.h"

你已经知道 mainwindow.h 文件是什么以及在哪里(在你的项目文件夹中),现在你刚刚了解到 ui_mainwindow.h 实际上是位于构建文件夹内的生成的源文件。

如果你查看 ui_mainwindow.h 文件的内容,你会注意到一个名为 Ui_MainWindow 的类,其中有两个函数:setupUiretranslateUi

  • setupUi 函数被自动添加到 mainwindow.h 中的 MainWindow构造函数中。该函数主要负责根据 mainwindow.ui 文件中的设置来设置用户界面上的所有内容。
  • 本章稍后将介绍 retranslateUi 函数,以及在制作多语言 Qt 应用程序时如何使用该函数。

当所有 Qt 生成的文件都放入 Build 文件夹后,它们就会像其他 C++ 程序一样,被传递给 C++ 编译器进行编译,然后链接到 Build 文件夹中创建我们的应用程序。Windows 用户应注意,当你使用 Qt Creator 运行应用程序时,所有 DLL 文件(动态链接库(Dynamic Link Library)文件)路径都会被 Qt Creator 解析,但如果你试图从 Build 文件夹内运行程序,你将会看到多个错误信息,应用程序会崩溃或根本无法启动。你将在"调试和测试*"中学习如何解决这个问题,在那里你将学习如何正确地将你的应用程序交付给你的用户。


总结:

Qt中的mocMeta-Object Compiler)和uicUser Interface Compiler)是特定的工具,它们在Qt应用程序的构建过程中起着重要的角色。

  1. moc (Meta-Object Compiler)
    • moc是Qt的一个元对象编译器。它用于处理Qt的信号和槽机制,以及Qt中的一些其他元编程机制如属性系统、运行时类型信息和动态对象通信。
    • 当你在类声明中使用Q_OBJECT宏时,moc 会处理这个类,并生成一个附加的C++源文件,这个源文件包含了实现元对象所需的元信息和标准的信号和槽机制。
  2. uic (User Interface Compiler)
    • uic是用于转换由Qt Designer创建的用户界面文件(.ui文件)为C++代码的工具。
    • 当你设计一个界面并保存为.ui文件时,uic会将这个文件转换成一个或多个头文件,这些文件将在编译时包含在你的应用程序中。

编译成最终应用程序的流程通常如下:

  1. 编写源代码:你写好C++源代码和Qt特有的标记(如Q_OBJECT)。
  2. 预处理:运行moc来处理所有定义了Q_OBJECT宏的类,生成含有元信息的C++源文件。
  3. 设计UI:在Qt Designer中创建GUI,并保存为.ui文件。
  4. UI编译:运行uic.ui文件转换为C++头文件。
  5. 资源编译:如果你使用了Qt资源系统,rcc(Qt Resource Compiler)会被用来将资源文件(如图片、翻译文件)编译成为应用程序可用的二进制形式。
  6. 编译:编译器(如g++, clang++)编译源代码和由mocuic生成的代码。
  7. 链接:链接器将编译后的代码和所有相关的库(包括Qt库)链接在一起,生成最终的可执行文件。

设计模式

设计模式 - Design pattern

非常有必要提醒我们自己设计模式存在的原因,以及为什么像 Qt 这样成功的框架会广泛使用不同的设计模式。首先,设计模式只是软件开发任务的众多解决方案之一,它不是唯一的解决方案;事实上,大多数时候它甚至不是最快的解决方案。然而,设计模式绝对是解决软件开发问题最有结构的方式,它有助于确保你对程序中添加的每件事都使用一些预定义的模板式结构。

设计模式有不同种类的问题的名称,例如创建对象、它们的运行方式、它们如何处理数据等。Eric Gamma、Richard Helm、Ralph E. Johnson 和 John Vlissides(被称为 四人帮)在他们的书 设计模式:可复用面向对象软件的基础 中描述了许多最广泛使用的设计模式,这本书被认为是计算机科学中设计模式的事实上的参考书。如果你不熟悉设计模式,你绝对应该花一些时间了解这个主题。学习软件开发中的**反模式(Anti-Pattern)**也是一个好主意。如果你是这个话题的新手,你可能会惊讶地发现一些反模式有多常见,确保你始终避免它们是至关重要的。

“反模式”(Anti-Pattern)是一种常见的但低效或有问题的设计、编程或管理实践,这些实践表面上看似提供了一个解决方案,但实际上可能会引入更多的问题。反模式通常是因为缺乏经验或对现有问题理解不足而产生的,而且它们可能会在团队或项目中不知不觉地得到推广。

反模式的关键特征包括:

  1. 反生产性:它们可能会阻碍过程的效率,导致产出质量下降。
  2. 反直觉:虽然表面上解决了问题,但实际上可能会掩盖根本问题,使得问题更加难以解决。
  3. 重复性:它们往往会在不同的项目或团队中重复出现,因为人们可能不认识到它们的负面影响。
  4. 教训性:识别和理解反模式可以作为学习工具,帮助人们避免在未来犯同样的错误。

一些常见的反模式例子包括:

  • 金锤子(Golden Hammer):对某一技术或工具有过度的依赖,认为它可以解决所有问题。
  • 货物崇拜(Cargo Cult Programming):程序员盲目地复制某些代码或做法,而没有理解其背后的原理。
  • 剪贴板编程(Copy-Paste Programming):频繁地复制和粘贴代码,而不是理解代码的功能或考虑代码重用。
  • 神对象(God Object):创建一个过分庞大和复杂的对象,它几乎控制了程序中的所有过程。
  • 过早优化(Premature Optimization):在理解性能瓶颈之前过分关注优化。

反模式的提出目的是为了帮助开发者识别和避免这些常见的错误做法,从而改进他们的软件设计和开发过程。了解反模式同样重要,因为它们提供了不良实践的有力例子,从而使开发者能够学习如何采取更好的策略。

以下是 Qt 和 OpenCV 框架中使用的一些最重要的设计模式(按字母顺序排列),以及对这些设计模式的简要描述和实现这些设计模式的一些类或函数的示例。请仔细注意以下表格中的示例案例,以了解与每种设计模式相关的一些类或函数的概述。然而,在本书的过程中,通过各种示例,你将通过实践经验了解到使用的类。

由于 OpenCV 框架的性质,以及它不是用于构建日常应用程序、复杂用户界面等的通用框架的事实,它没有实现 Qt 使用的所有设计模式,相比之下,只有很小一部分这些模式在 OpenCV 中实现。特别是由于 OpenCV 的速度和效率目标,大多数时间更倾向于使用全局函数和低级别的实现。尽管如此,有一些 OpenCV 类实现了设计模式,例如当速度和效率不是目标时的抽象工厂。请参阅下一个示例案例列以获取示例:

设计模式 英文名 描述 示例案例
抽象工厂模式 Abstract Factory 用来创建所谓的工厂类,这些类可以创建各种对象,并控制新对象的创建,如防止创建一个对象的多个实例。 我们将学习如何使用此设计模式来编写基于插件的 Qt 应用程序
命令模式 Command 使用此设计模式,动作可以表示为对象。这些对象的功能包括组织动作的顺序、记录日志、撤销动作等。 QAction:这个类允许创建特定动作并将它们分配给小部件。例如,QAction 类可以用来创建一个带有图标和文本的打开文件动作,并且可以分配主菜单项和键盘快捷键(如 Ctrl+O 等)
组合模式 Composite 此模式用来创建由子对象组成的对象。这对于管理复杂对象非常有用,这些复杂对象本身可以由许多更简单的对象构成。 QObject:这是所有 Qt 类的基础。QWidget:这是所有 Qt 小部件的基类。任何具有树状设计架构的 Qt 类都是组合模式的示例。(Qt 中对象树的概念)
门面模式/外观模式 Facade 可用于封装较低级别的功能,如操作系统或任何系统的接口,提供更简单的接口。外观和适配器设计模式通常被认为在定义上是相似的。 QFile:这些可用于读取/写入文件。基本上,所有 Qt 类中作为包装器围绕较低级别的 API 的类都是外观设计模式的例子
享元模式(或桥接或私有实现) Flyweight (or Bridge or Private-Implementation) 设计模式的目标是避免数据复制并使用共享数据之间的相关对象(除非另有需要)。 QString:这个类可以用来存储和操作 Unicode 字符串。实际上,许多 Qt 类享有这些设计模式,帮助将指针指向共享数据,以便在不需要复制对象时进行更快的复制并减少内存使用。当然,具有更复杂的代码
备忘录模式 Memento 这可以用来保存和(稍后)加载对象的状态。 这个设计模式会保存涉及对象的所有属性的类的元信息,以便恢复它们以创建一个新的
元对象(或反射) MetaObject (or Reflection) 在这个设计模式中,所谓的元对象用来描述对象,以获得更强大的访问权限。 QMetaObject:这可能是包含有关 Qt 类的元信息的类。每个 Qt 程序都首先使用 Qt 元对象编译器(MOC)进行编译以生成所需的元对象,然后再由实际的C++ 编译器进行编译
单状态 Monostate 这允许同一类的多个实例以相同的方式行为,比如访问相同的数据或执行相同的函数 QSettings:这用于提供应用程序设置的保存/加载。
MVC(模型-视图-控制器) MVC (Model-view-controller) 这是一个广泛使用的设计模式,用于将应用程序或数据存储机制(模型)从用户界面(视图)和数据操纵(控制器)分离。 QTreeView:这是一个树形实现的模型-视图。QFileSystemModel:用于基于本地文件系统的内容获取数据模型。QFileSystemModel(或任何其他QAbstractItemModel)与QTreeView(或任何其他QAbstractItemView)的组合可以是 MVC 设计模式的实现。
观察者(或发布/订阅) Observer (or Publish/Subscribe) 此设计模式用于使对象能够监听(或观察)其他对象中的变化并相应地做出反应。 QEvent:这是所有 Qt 事件类的基础(信号和槽的实现机制)。将QEvent(及其所有众多子类)视为观察者设计模式的低级实现。 另一方面,Qt 支持signalslot机制,这是使用观察者设计模式的更方便,更高级的方法。
序列化 Serializer 当创建类(或对象)时,可以使用此模式,用于读取或写入其他对象。 QTextStream:可用于在文件或其他 IO 设备中读取和写入文本QDataStream:可用于从 IO 设备和文件读取或写入二进制数据。
单例模式 Singleton 可以用来限制一个类只有一个实例。 QApplication:可用于以各种方式处理 Qt 小部件应用。确切地说,QApplication中的instance()函数(或全局qApp指针)是单例设计模式的示例。OpenCV 中的cv::theRNG()函数(用于获取默认的随机数生成器RNG))是单例实现的示例。 请注意,RNG 类本身不是单例。

参考文献:

Design Patterns: Elements of Reusable Object-Oriented Software, by Eric Gamma, Richard Helm, Ralph E. Johnson and John Vlissides (referred to as the Gang of Four)

An Introduction to Design Patterns in C++ with Qt, second Edition, by Alan Ezust and Paul Ezust

通常,前面的列表不应该被视为设计模式的完整列表,因为它仅关注 Qt 和 OpenCV 设计模式,而仅针对本书而言就足够了。 如果您对该主题感兴趣,请考虑阅读提到的参考书,但是正如前面所提到的,就本书而言,您只需要上述清单即可。

检查上一个列表中提到的每个类的文档页面是一个很好的主意。 您可以为此使用 Qt Creator 帮助模式,并在索引中搜索每个类,查看每个类的代码示例,甚至尝试自己使用它们。 这不仅是学习 Qt 的最佳方法,而且是学习不同设计模式的实际实现和行为的最佳方法之一。

Qt 资源系统

在接下来的部分中,您将学习如何为我们的应用程序添加样式和多语言支持,但在此之前,我们必须熟悉 Qt 资源系统。简单来说,它是 Qt 中添加资源文件(如字体、图标、图片、翻译文件、样式表文件等)到我们的应用程序(和库)的一种方式。

Qt 通过使用 *.qrc 文件(资源集合文件)来支持资源管理,这些文件实际上是包含了需要包含在我们应用程序中的资源文件信息的 XML 文件。让我们通过一个简单的例子来了解 Qt 资源系统的工作原理,我们将在我们的 Hello_Qt_openCV 应用程序中包含一个图标:

  1. 确保您已经在 Qt Creator 中打开了 Hello_Qt_OpenCV 项目。选择文件,然后新建文件或项目。在新文件窗口中,确保您从左侧第二个列表中选择了 Qt,然后选择 Qt 资源文件。参考以下截图:

  1. 点击 Choose... 按钮,在下一个屏幕中,设置名称为 resources。路径默认应设置为您的项目文件夹,因此保持原样即可。点击下一步,然后完成。您将得到一个名为 resources.qrc 的新文件添加到您的项目中。如果您在 Qt Creator 中打开这个文件(通过右键点击并选择在编辑器中打开),您将看到 Qt Creator 中的资源编辑器。

  2. 在这里,您可以使用 Add 按钮打开以下两个选项:

    • 添加文件

    • 添加前缀

在这里,文件仅仅是您想要添加到项目中的任何文件。然而,前缀基本上是一个伪文件夹(或者您可以称之为容器),它包含了许多文件。注意,这并不一定代表您项目文件夹中的文件夹或子文件夹,而仅仅是一种表示方式和分组您的资源文件的方式。

  1. 首先点击 Add Prefix 然后在前缀字段中输入 images。
  2. 然后,点击 Add Files 并选择您选择的图片文件(任何计算机上的 *.jpg 文件对我们的示例来说都是可以的):

在这个例子中,我们使用了与第一章 介绍 Qt 和 OpenCV,和第二章 创建我们的第一个 Qt 和 OpenCV 项目中相同的示例 test.jpg 文件。请注意,您的资源文件应该在您的项目文件夹或其内部的子文件夹中。否则,您将如下图所示得到一个确认;如果是这种情况,请点击复制并将资源文件保存在您的项目文件夹中:

就是这样。现在,当您构建并运行您的 Hello_Qt_OpenCV 应用程序时,图片文件将包含在您的应用程序中,并可以像存在于操作系统上的文件一样访问。路径与常规文件路径有些许不同。在我们的示例中,test.jpg 文件的路径如下:

:/images/test.jpg 

您可以在 Qt Creator 中展开您的 \*.qrc 文件,并右键点击每个资源文件,然后选择 Copy Path ***Copy URL *** 选项来复制每个文件的路径或URL。当需要常规路径时可以使用这个路径,而当需要资源文件的 URL(Qt 中的 QUrl 类)时可以使用这个 URL。重要的是要注意,由于 Qt 资源系统是 Qt 的内部能力,OpenCV 可能无法使用这些路径和访问资源文件。然而,这些文件通常仅供应用程序本身使用(通常在与用户界面相关的任务中),因此您可能永远不需要在 OpenCV 中使用它们。

现在,您可以尝试通过将新的图片文件设置为按钮的图标来试用它。例如,尝试选择用户界面上的任何一个按钮,然后在属性编辑器中找到图标属性,然后按下旁边的小下拉按钮选择 Choose Resource。现在,您可以简单地选择您添加的图片作为按钮的图标:

这基本上是一个关于如何为支持图标的 Qt 小部件设置图标的教程。当您想要在应用程序中包含任何其他类型的资源并在运行时使用它时,逻辑完全相同。您只需假设 Qt 资源系统是某种次级文件系统,并像使用文件系统上的常规文件一样使用其中的文件。

应用程序的样式设置

Qt 使用 QStyle 类和 Qt 样式表支持应用程序的样式设置。QStyle 是 Qt 中所有样式的基类,它封装了 Qt 用户界面的样式。虽然本书不涵盖 QStyle 类,但仍应注意,创建一个 QStyle 的子类并在其中实现不同的样式能力,最终是改变 Qt 应用程序外观和感觉的最强大方法。然而,Qt 也提供了样式表来设置应用程序的样式。Qt 样式表在语法上几乎与 HTML CSS层叠样式表)相同,CSS 是网页样式设置中不可分割的一部分。

CSS 是一种样式语言,可以用来定义用户界面上对象的外观。通常,使用 CSS 文件有助于将网页的样式与底层实现分离。Qt 使用非常相似的方法在其样式表中描述小部件的外观。如果您熟悉 CSS 文件,那么 Qt 样式表对您来说将是小菜一碟;然而,即使您是第一次被介绍到这个概念,也请放心,这是一种旨在简单、易学的方法。

让我们看看什么是样式表,以及在 Qt 中如何使用样式表的一个简单示例。让我们再次回到我们的 Hello_Qt_OpenCV 项目。打开项目并转到设计器。选择窗口上的任何小部件,或点击一个空白处以选择窗口小部件本身,您会找到一个叫做 styleSheet 的属性。基本上,每个 Qt 小部件(或换句话说,QWidget 子类)都包含一个可以设置的 styleSheet 属性,以定义每个小部件的外观和感觉。

点击 inputPushButton 小部件,并将其 styleSheet 属性设置为以下内容:

border: 2px solid #222222; 
border-radius: 10px; 
background-color: #9999ff; 
min-width: 80px; 
min-height: 35px; 

outputPushButton 做同样的设置;然而,这次在 styleSheet 属性中使用以下内容:

border: 2px solid #222222; 
border-radius: 10px; 
background-color: #99ff99; 
min-width: 80px; 
min-height: 35px; 

当您在设计器中设置这些样式表时,您会看到两个按钮的新外观。这就是 Qt 中的简单样式设置。唯一需要的是知道可以对任何特定小部件类型应用哪些样式更改。在我们之前的示例中,我们能够改变 QPushButton 的边框、背景颜色和最小接受尺寸。要获取可以应用于任何小部件的样式概述,您可以在 Qt 帮助模式下阅读 Qt 样式表参考。它应该已经在您的计算机上,并且您可以随时从帮助索引中离线访问它。在那里,您将找到所有可能的 Qt 小部件样式,配有您可以复制和修改以适应自己需要的清晰示例,以及您希望在应用程序中拥有的外观和感觉。以下是我们刚刚使用的两个简单样式表的结果。如您所见,我们的浏览按钮现在有了不同的外观:

image-20240405165500501

在前面的示例中,我们还避免设置适当的样式规则。**Qt 样式表中的样式规则由选择器和声明组成。**选择器指定将使用样式的小部件,声明简单地是样式本身。再次,在我们之前的示例中,我们只使用了声明,选择器是(隐式地)获取样式表的小部件。这里是一个示例:

QPushButton 
{ 
  border: 2px solid #222222; 
  border-radius: 10px; 
  background-color: #99ff99; 
  min-width: 80px; 
  min-height: 35px; 
}

这里,QPushButton(或实际上,{之前的所有内容)是选择器,{} 之间的代码部分是声明。

现在,让我们了解在 Qt 中设置样式表时一些重要的概念。

选择器类型

以下是你可以在 Qt 样式表中使用的选择器类型。明智且高效地使用它们可以极大地减少样式表所需的代码量,并改变 Qt 应用程序的外观和感觉:

选择器类型 示例 描述
通用 * 所有的小部件
类型 QPushButton 指定类型及其子类的小部件
属性 QPushButton[text='Browse'] 设置了特定属性为特定值的小部件
.QPushButton 指定类型但不包括其子类的小部件
ID QPushButton# inputPushButton 指定类型及objectName的小部件
后代 QDialog QPushButton 另一小部件的后代(子部件)
子部件 QDialog > QPushButton 另一小部件的直接子部件

子控件

或者更好的说法是,子控件是复杂小部件内部的子小部件。一个例子是 QPinBox 小部件上的向下和向上箭头按钮。它们可以使用 :: 运算符选择,如下例所示:

QSpinBox::down-button 

始终记得参考 Qt Creator 帮助模式中可用的 Qt 样式表参考文章,以获得每个小部件的子控件的(或多或少)完整列表。Qt 是一个不断发展的框架,定期添加新功能,因此没有比它自己的文档更好的参考资料了。

伪状态

每个小部件都可以有一些伪状态,例如 hover(悬停)、pressed(按下)等。它们可以使用 : 运算符在样式表中选择,如下例所示:

QRadioButton:!hover { color: black } 

就像子控件一样,始终参考 Qt Creator 帮助模式中的 Qt 样式表参考,以获取每个小部件适用的伪状态列表。

层叠

你可以为整个应用程序、父小部件或子小部件设置样式表。在我们前面的例子中,我们简单地为两个子小部件设置了样式表。每个小部件的样式将根据层叠规则决定,这简单意味着如果为父小部件或应用程序设置了样式表,每个小部件也将获得在父小部件或应用程序中设置的样式规则。我们可以利用这一事实,避免反复在每个小部件中设置整个应用程序或特定窗口共有的样式规则。

现在,让我们在 MainWindow 中尝试以下样式表,这将结合你所学的所有内容,提供一个简单的示例。确保删除所有之前设置的样式表(对于两个浏览按钮),只需在窗口小部件的 stylesheet 属性中使用以下内容:

* 
{ 
  font: 75 11pt; 
  background-color: rgb(220, 220, 220); 
} 
QPushButton, QLineEdit, QGroupBox 
{ 
  border: 2px solid rgb(0, 0, 0); 
  border-radius: 10px; 
  min-width: 80px; 
  min-height: 35px; 
} 
QPushButton 
{ 
  background-color: rgb(0, 255, 0); 
} 
QLineEdit 
{ 
  background-color: rgb(0, 170, 255); 
} 
  QPushButton:hover, QRadioButton:hover, QCheckBox:hover 
{ 
   color: red; 
} 
QPushButton:!hover, QRadioButton:!hover, QCheckBox:!hover 
{ 
  color: black; 
} 

如果你现在运行你的应用程序,你可以看到外观的变化。你还会注意到,即使关闭确认对话框小部件的样式也发生了变化,原因很简单,我们在其父窗口中设置了样式表。这里有一个截图:

image-20240405213532227

不用说,你也可以通过保存样式表到一个文本文件中,并在运行时加载和设置它,正如我们将在本章后面构建我们的综合计算机视觉应用程序的基础时所做的那样。**你甚至可以存储一个默认样式表在应用程序内部,正如你在本章早些时候学到的(参考 Qt 资源系统),并默认加载它,也许如果计算机的特定位置存储了一个自定义文件则跳过它。**这样,你可以轻松地拥有可定制的应用程序。你甚至可以分担任务,要求专业设计师简单地为你提供一个样式表,以便你在你的应用程序中使用。这基本上展示了在 Qt 应用程序中样式设计的简便性。

为了获取更多关于样式表特定语法和帮助,始终最好关注 Qt Creator 帮助模式中的样式表语法文章,因为 Qt 样式表基本上是特定于 Qt 的,并且在某些情况下与标准 CSS 有所不同。

多语言支持

在本节中,你将学习如何使用 Qt 框架创建支持多种语言的应用程序。实际上,这一切都归结为一个非常易于使用的类。QTranslator 类是 Qt 主要负责处理输出(显示)文本国际化的类。你只需要确保以下几点:

  1. 在构建项目时使用默认语言(例如英语)。这意味着,对于显示的所有内容,简单地使用默认语言中的句子和单词。

  2. 确保==代码==中的所有字面句子,或者更具体地说,所有在选择不同语言时需要翻译的字面句子都用 tr() 函数包围起来

    • 例如,在==代码==中,原本一个 Dialog 的提示是 "Are you sure you want to close this program?",只需将其传递给 tr() 函数并改写为 tr("Are you sure you want to close this program?")再次注意:这不适用于UI设计器,只适用于代码中的字面字符串

      比如我们使用了代码设置一个按钮的文字:

      QMessageBox::warning(this,
                          "Exit",
                          "Are you sure you want to close this program?",
                          QMessageBox::No | QMessageBox::Yes,
                          QMessageBox::No);
      
      如果想让他支持多语言,要改为:
      QMessageBox::warning(this,
                          tr("Exit"),
                          tr("Are you sure you want to close this program?"),
                          QMessageBox::No | QMessageBox::Yes,
                          QMessageBox::No);
    • 当在设计器中设置属性时,只需使用字面字符串,Qt 会自动检测。

  3. *.pro文件中指定你的翻译文件的文件名。为此,你需要用 TRANSLATIONS = translation_XX.ts 指定它们,就像在项目文件中的 SOURCESHEADERS 一样。

    例如,如果你想在应用程序中添加俄语(ru)和中文(zh_CN)翻译,将以下内容添加到你的项目(*.pro)文件中:

    TRANSLATIONS = translation_ru.ts translation_zh_CN.ts

    确保为每个翻译文件使用清晰的名称。尽管你可以随意命名它们,但最好是包含语言代码(zh_CN 代表中文,de 代表德语等),如前面的示例所示。这也帮助 Qt Linguist 工具(正如你稍后将学习的)知道翻译的目标语言。

  4. 使用 Qt Creater 内置的 lupdate 工具创建刚刚在 .pro 文件中指定的 .ts 文件(或如果它们已经存在,则更新它们)。为此需要点击主菜单中的 Tools / External / Linguist / Update Translations (lupdate) 从 Qt Creator 中执行 lupdate

    image-20240405221205641

    运行此命令后,如果你进入你的项目文件夹,你会注意到项目文件中之前指定的 .ts 文件现在已经创建。

    image-20240405221338549

    随着你的应用程序越来越大,定期运行 lupdate 是很重要的,以提取需要翻译的新字符串,并进一步扩展多语言支持。

    lupdate 是一个 Qt 工具,它搜索所有源代码和 UI 文件中的可翻译文本,然后创建或更新上一步中提到的 .ts 文件。负责翻译应用程序的人可以简单地使用 Qt Linguist 工具打开 .ts 文件,并简单地使用简单的用户界面专注于翻译应用程序。

    lupdate 位于 Qt 安装的 bin 文件夹内。例如,在 Windows 操作系统上,它的路径类似于此:

    C:\Qt\Qt5.9.1\5.9.1\msvc2015\bin 

    对于 Windows 用户的重要说明:如果在运行 lupdate 后遇到任何问题,可能是因为 Qt 安装不正常。为了解决它,只需使用开发环境的命令提示符运行 lupdate。

    C:\Qt\Qt5.9.1\5.9.1\msvc2015\bin\lrelease.exe Hello_Qt_OpenCV.pro 
  5. 使用 Qt Linguist 工具翻译所有必需的字符串。它已经安装在你的计算机上,因为它是默认 Qt 安装的一部分。

    image-20240405214901715

    简单地选择 File / Open 并从你的项目文件夹中选择所有刚刚创建的 .ts 文件并打开它们。如果你已经按照所有指示操作到现在,那么在 Qt Linguist 中打开 .ts 文件后,你应该会看到以下界面:

    image-20240405224122531

    Qt Linguist 允许快速轻松地翻译你项目中的所有可翻译元素。只需为所有显示的语言编写每个项目的翻译,并使用顶部的工具栏将它们标记为Done。确保在退出 Qt Linguist 工具之前保存。

  6. 使用翻译好的 .ts 文件创建 .qm 文件,这些文件是压缩的二进制 Qt 语言文件。为此,你需要回到 Qt Creater,运行 lrelease 工具。使用 lrelease 与你在前面步骤中学到的 lupdate 类似:

    image-20240405222211747

  7. .qm 文件(二进制语言文件)添加到你的应用程序资源中。

    你已经学习了如何使用 Qt 资源系统。简单地创建一个名为 translations 的新前缀,并在该前缀下添加新创建的 .qm 文件。如果正确完成,你的项目中应该有以下内容:

    image-20240405222533302

  8. 你现在可以开始使用 QTranslator 类在你的应用程序中拥有多种语言,并且在运行时切换语言。

    让我们再次回到我们的示例项目 Hello_Qt_OpenCV。在应用程序中使用翻译器有不同的方法,但现在我们将从最简单的方法开始。在你的 mainwindow.h 文件中添加 #include <QTranslator> 头文件,并在 MainWindow 类中定义两个私有的 QTranslator 对象,如下所示:

    #include <QTranslator>
    
    ...
    private:  
    QTranslator* translator_ru;
    QTranslator* translator_zh_CN;
  9. MainWindow 构造函数代码中,紧接着对 loadSettings 函数的调用之后,添加以下内容:

    translator_ru = new QTranslator(this);
    translator_ru->load(":/translations/translation_tr.qm"); 
    
    translator_zh_CN = new QTranslator(this);
    translator_zh_CN->load(":/translations/translation_de.qm"); 

    可也以通过不使用指针的形式(直接在 main.h 加载):

    QTranslator language;
    language.load(":/language/language_zh_CN.qm");
    qApp->installTranslator(&language); // 全局对象指针是宏 qApp
  10. 现在,是时候在我们的项目中添加一个主菜单,并允许用户切换语言了。你可以通过在 Qt Creator 设计模式下右键点击窗口并选择创建菜单栏来做到这一点。然后,在顶部菜单栏中添加一个名为 Language 的项目。通过简单地点击并输入以下内容来添加三个子项目:

    image-20240405232202230

    在设计器的底部,你可以找到操作编辑器。显然,你现在在这里有三个条目,这些条目是当你创建主菜单时自动创建的。它们中的每一个都对应于你在主菜单中输入的每一个语言名称。

  11. 通过信号和槽实现语言配置安装(注意 qApp->installTranslator 后,界面是不会变化的。还需要在 13 步中刷新界面)

    右键点击中文并选择转到槽,然后从列表中选择 trigger() 并点击 OK。为 actionChineseactionRussia 对象的触发槽编写以下代码行:

    void Hello_Qt_OpenCV::on_actionChinese_triggered()
    {
        qApp->installTranslator(this->translator_zh_CN);
    }
    
    void Hello_Qt_OpenCV::on_actionRussia_triggered()
    {
        qApp->installTranslator(this->translator_ru);
    }
  12. 回到默认语言。对 actionEnglish 对象做同样的处理。这次,你需要从你的应用程序中移除翻译器,因为英语是我们应用程序的默认语言:

    void Hello_Qt_OpenCV::on_actionEnglish_triggered()
    {
        qApp->removeTranslator(this->translator_zh_CN);
        qApp->removeTranslator(this->translator_ru);
    }

    但是这不是一个合理的解决方案,以下是两种更好地解决方案:

    • 重新加载默认翻译文件:如果你有默认语言的翻译文件(即使它可能只是原始文本的直接复制),你可以通过加载这个默认语言的翻译文件来恢复默认语言。这类似于加载任何其他语言,只不过翻译文件中的文本是你的默认文本
    • 使用空的翻译文件:理论上,你可以创建一个没有任何翻译条目的翻译文件,并加载它,这样由于没有任何翻译应用,应用程序将显示源代码中的文本,即默认语言
  13. 好吧,我们现在已经将翻译放到(install)我们的 Qt 应用程序中了,我们还需要在更改语言的时候刷新 UI。为此,我们需要使用 QMainWindow 类的 changeEvent。每次使用前面的 installTranslatorremoveTranslator 函数安装或移除翻译器时,都会向应用程序中的所有窗口发送语言更改事件。要捕获此事件,并确保我们的窗口在语言更改时重新加载,我们需要在程序中重写 changeEvent() 函数。

    protected:
        virtual void changeEvent(QEvent *event);
    
    ---
      
    void Hello_Qt_OpenCV::changeEvent(QEvent *event)
    {
        if (event->type() == QEvent::LanguageChange)
        {
            ui->retranslateUi(this);
        }
        else
        {
            QMainWindow::changeEvent(event); // 否则,一切应该像平时一样进行
        }
    }

    上述代码简单地意味着,如果更改事件是语言更改,则重新翻译窗口,否则,一切应该像平时一样进行。retranslateUi 函数是使用 uic 生成的(参考 uic 部分),它简单地负责根据应用程序中最新安装的 QTranslator 对象设置正确的翻译字符串。

就是这样。你现在可以运行你的应用程序并尝试切换语言了。重要的是要注意,你在本节中学到的基本上适用于每个 Qt 应用程序,并且是制作多语言应用程序的标准方式。在应用程序中拥有不同语言的更定制化方式几乎会遵循相同的一套指令,但与其使用资源文件将语言文件内置到应用程序中,不如从磁盘上的位置加载语言会更好。这样做的优势是可以更新翻译甚至添加新语言(需要一点更多的代码)而无需重新构建应用程序本身。

创建和使用插件

在应用程序中使用插件是扩展应用程序最强大的方法之一,许多人日常使用的应用程序都从插件的强大功能中受益。**插件仅仅是一个库(在Windows上是*.dll,在Linux上是*.so等),它可以在运行时加载和使用,以处理特定任务,但当然,它不能像独立应用程序那样执行,并且它依赖于使用它的应用程序。**在本书中,我们也将使用插件来扩展我们的计算机视觉应用程序。

在本节中,我们将学习如何创建一个示例应用程序(称为Image_Filter),该应用程序仅仅加载和使用计算机上指定文件夹中的插件。然而,在此之前,我们将学习如何在Qt中创建一个插件,该插件同时使用Qt和OpenCV框架,因为我们的插件很可能需要使用OpenCV库来执行一些计算机视觉魔法。那么,让我们开始吧。

**首先,我们需要定义一组接口,这些接口是我们的应用程序与插件通信所需的。==在C++中,接口的等效物是具有纯虚函数的类==。**因此,我们基本上需要一个接口,其中包含我们期望插件中存在的所有函数。这就是一般创建插件的方式,也是第三方开发者为其他人开发的应用程序编写插件的方式。是的,他们知道插件的接口,并且只需要用真正做某事的实际代码来填充它。

接口

接口比它乍一看时更重要。是的,它基本上是一个什么都不做的类,但是,它为我们的应用程序所需的所有插件勾勒出了草图(框架),这一点将持续很长时间。因此,**我们需要确保从一开始就在插件接口中包含所有必需的函数,否则,之后添加、删除或修改函数可能几乎不可能。虽然目前我们正在处理一个示例项目,这看起来可能不那么严重,但在现实生活中的项目中,这些通常是决定应用程序扩展性的一些关键因素。**所以,现在我们知道了接口的重要性,我们可以开始为我们的示例项目创建一个接口了。

打开 Qt Creator 创建一个 Qt Console Application 项目,然后添加头文件,

image-20240409173503236

然后添加C++头文件,输入CvPluginInterface作为文件的名称,并继续直到您处于代码编辑模式。将代码更改为以下内容:

image-20240409172336330

/**
 * 插件接口 Interface
 */

#ifndef CVPLUGININTERFACE_H
#define CVPLUGININTERFACE_H

#include <QObject>
#include <QString>
#include "opencv2/opencv.hpp"

class CvPluginInterface
{
public:
    virtual ~CvPluginInterface() {}

    virtual QString description() = 0;  // 返回插件说明
    virtual void processImage(const cv::Mat &inputImage, cv::Mat &outputImage) = 0;
};

#define CVPLUGININTERFACE_IID "com.amin.cvplugininterface"  // 一个独一无二的字符串,采用类似包名格式
Q_DECLARE_INTERFACE(CvPluginInterface, CVPLUGININTERFACE_IID)  // 宏将我们的类定义为接口。不包含这个宏,Qt将无法将我们的类识别为插件接口

#endif // CVPLUGININTERFACE_H

您可能已经注意到,使用Qt Creator创建的任何头文件都会自动添加类似于以下的代码行:

#ifndef CVPLUGININTERFACE_H 
#define CVPLUGININTERFACE_H 
... 
#endif // CVPLUGININTERFACE_H 

这些代码简单地确保在应用程序编译期间,每个头文件只被包含/处理一次(防止重定义,也就是 vs studio 中的 #promgramm once)。在C++中,基本上有许多其他方法可以达到同样的目的,但这是最广泛接受和使用的方法,尤其是由Qt和OpenCV框架采用,以实现最高程度的跨平台支持。当使用Qt Creator工作时,它总是自动添加到头文件中,不需要额外工作。

上述代码基本上是Qt中插件接口所需的全部内容。在我们的示例接口中,我们只需要插件支持两种简单的函数类型,但正如我们稍后将看到的,为了支持参数、语言等,我们需要的远不止这些。然而,对于我们的示例来说,这应该就足够了。

【重点】对于C++开发者来说,一个非常重要的注意事项是,前面接口中的第一个公共成员 virtual ~CvPluginInterface() {} ,它在C++中被称为虚析构函数是许多人忘记包含并且不太注意的最重要的方法之一,所以了解它的真正含义并记住==它以避免内存泄漏==是个好主意,特别是在使用Qt插件时。

基本上,任何具有虚拟方法并且意图以==多态==方式使用的C++基类都必须包含==虚析构函数==。这有助于确保即使使用基类的指针访问它们(多态性)时,也能调用子类中的析构函数。不幸的是,使用大多数C++编译器时,当犯这种常见的C++编程错误时,你甚至不会收到警告。

  1. 虚析构函数的作用:当你有一个基类指针指向一个派生类对象时,如果基类的析构函数不是虚函数,那么当通过基类指针删除对象时,只有基类的析构函数会被调用。这将导致派生类中为释放资源而定义的析构逻辑不会执行,可能导致资源泄露。而将基类的析构函数声明为虚函数后,删除对象时会首先调用派生类的析构函数,然后再调用基类的析构函数,从而保证了资源的正确释放。

    让我给你举一个具体的例子来展示如果基类的析构函数不是虚的,可能会导致什么样的问题。

    假设我们有一个基类Base和一个从Base继承的派生类DerivedDerived类有自己的资源管理,比如动态分配的内存。如果Base的析构函数不是虚的,那么当我们通过Base类型的指针来删除一个Derived类型的对象时,只有Base的析构函数会被调用,而Derived的析构函数不会被调用。这可能会导致Derived分配的资源没有被释放,从而引发内存泄露。

    下面是一个示例代码:

    #include <iostream>
    
    class Base {
    public:
        Base() { std::cout << "Base Constructor\n"; }
        ~Base() { std::cout << "Base Destructor\n"; }
    };
    
    class Derived : public Base {
    public:
        Derived() { std::cout << "Derived Constructor\n"; }
        ~Derived() { std::cout << "Derived Destructor\n"; }
    };
    
    int main() {
        Base* b = new Derived();  // 基类指针指向子类
        delete b; // 这里只会调用 Base 的析构函数
        return 0;
    }
    
    ---
    输出:
    /Users/fox/雪狸的文件/Programma/OpenMP/Cpp11Thread/untitled/cmake-build-debug/untitled
    Base Constructor
    Derived Constructor
    Base Destructor
    
    进程已结束,退出代码为 0

    在这个例子中,main函数中我们创建了一个Derived类型的对象,但是用Base类型的指针来引用它。当我们删除这个对象时,由于Base的析构函数不是虚的,所以只有Base的析构函数被调用,Derived的析构函数不会被执行。这意味着如果Derived类中有特殊的资源释放逻辑(比如删除动态分配的内存),那么这些逻辑不会被执行,可能会导致资源泄露。

    要修正这个问题,我们需要将Base类的析构函数声明为虚:

    class Base {
    public:
        Base() { std::cout << "Base Constructor\n"; }
        virtual ~Base() { std::cout << "Base Destructor\n"; } // 现在是虚的
    };
    
    ---
    输出:
    /Users/fox/雪狸的文件/Programma/OpenMP/Cpp11Thread/untitled/cmake-build-debug/untitled
    Base Constructor
    Derived Constructor
    Derived Destructor  // 可以看到子类的析构函数被执行了
    Base Destructor
    
    进程已结束,退出代码为 0

    这样,当我们通过基类指针删除派生类对象时,析构函数的调用会遵循动态绑定,即先调用Derived的析构函数,然后调用Base的析构函数(构造-爸爸盖房子,析构-孩子拆房子),从而确保所有资源都被正确管理和释放。

  2. 多态的重要性:在C++中,多态允许我们通过基类的指针或引用来调用派生类的方法。如果基类将要被用作多态基类(即通过基类的指针或引用来访问派生类对象),则必须为这个基类提供虚析构函数。这样做确保了当通过基类的指针删除派生类对象时,能够正确地调用派生类的析构函数,避免内存泄漏。

因此,我们的插件接口包括:

  • 一个名为description()的函数,旨在返回任何插件的描述和有关它的有用信息
  • 一个名为processImage()的函数,该函数将OpenCV的Mat类作为输入并返回一个作为输出。显然,在这个函数中,我们期望每个插件执行某种图像处理、滤镜等,并给出结果。

之后,我们使用Q_DECLARE_INTERFACE宏将我们的类定义为接口。不包含这个宏,Qt将无法将我们的类识别为插件接口。CVPLUGININTERFACE_IID应该是一个独一无二的字符串,采用类似包名格式,但你基本上可以根据自己的偏好进行更改。

确保将cvplugininterface.h文件保存到您选择的任何位置,然后关闭它。我们现在将创建一个使用此接口的插件。让我们使用我们之前在第3章创建我们的第一个Qt和OpenCV项目中看到的OpenCV函数之一:medianBlur

插件

我们现在将创建一个名为median_filter_plugin的插件,该插件使用我们的CvPluginInterface接口类。首先从主菜单中选择文件,然后新建文件或项目。然后,选择C++库,如下图所示:

image-20240409172608576

确保选择了共享库 (Shared Lihrary)作为类型,然后输入MedianFilterPlugin作为名称并点击下一步。选择桌面作为套件类型并点击前进。在选择所需模块页面,确保只选中了QtCore并继续点击下一步(最终点击完成),直到你进入Qt Creator的代码编辑器。

image-20240409221830554

我们基本上创建了一个Qt插件项目,正如你可能已经注意到的,插件项目的结构与我们到目前为止尝试的所有应用程序项目非常相似(除了它没有UI文件),这是因为插件实际上与应用程序没有什么不同,除了它不能自己运行。

现在,将我们在上一步中创建的cvplugininterface.h文件复制到新创建的插件项目文件夹中。然后,通过在项目窗格中的项目文件夹上简单地右键点击并从弹出菜单中选择添加现有文件来将其添加到项目中,如下所示:

我们需要告诉Qt这是一个插件而不仅仅是任何库。为此,我们需要在我们的*.pro文件中添加以下内容

CONFIG += plugin 

现在,我们需要将OpenCV添加到我们的插件项目中。到目前为止,这对你来说应该是小菜一碟。只需像之前在Hello_Qt_OpenCV项目中所做的那样,将以下内容添加到你的插件的*.pro文件中:

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

unix: !macx {
   CONFIG += link_pkgconfig
   PKGCONFIG += opencv
}

unix: macx {
  include(/Users/fox/AppInstall/opencv-4.9.0/build/opencv.pri)
  INCLUDEPATH += "/usr/local/include"
  LIBS += -L"/usr/local/lib" \
   -lopencv_world
}

当你在*.pro文件中添加一些代码,或者使用Qt Creator主菜单(和其他用户界面快捷方式)添加新类或Qt资源文件时,手动运行qmake是一个非常好的习惯,特别是如果你注意到Qt Creator与你的项目内容不同步。你可以通过选择项目窗格的右键菜单中的运行qmake来轻松做到这一点,如下图所示:

好的,场景已经设置好,我们可以开始编写我们的第一个 Qt+OpenCV 插件的代码了。正如你将在接下来的章节中看到的,我们将通过插件为我们的应用程序添加类似的功能;这样,我们将只关注开发插件,而不是为我们添加的每一个单独的功能修改整个应用程序。所以,熟悉并舒适地进行这一步骤非常重要。

首先打开median_filter_plugin.h文件并按如下修改:

#ifndef MEDIANFILTERPLUGIN_H
#define MEDIANFILTERPLUGIN_H

#include "MedianFilterPlugin_global.h"
#include "../CvPluginInterface/CvPluginInterface.h"

class MEDIANFILTERPLUGIN_EXPORT MedianFilterPlugin :
    public QObject, public CvPluginInterface
{
    Q_OBJECT  // 支持信号和槽
    Q_PLUGIN_METADATA(IID "com.amin.cvplugininterface")  // 用于在插件类定义中声明元数据,IID(Interface Identifier 接口标识符)
    Q_INTERFACES(CvPluginInterface)  // 声明插件中实现的接口

public:
    MedianFilterPlugin();
    ~MedianFilterPlugin() override;

    QString description() override;  // 可以增加 override 标识符,表明为重写
    void processImage(const cv::Mat &inputImage, cv::Mat &outputImage) override;
};

#endif // MEDIANFILTERPLUGIN_H

前面的代码大部分是当你创建median_filter_plugin项目时自动生成的。这就是基本的Qt库类定义的样子。然而,正是我们的添加使它变成了一个有趣的插件。让我们回顾前面的代码,看看实际上添加了什么:

  1. 首先,我们包含了cvplugininterface.h头文件。

  2. 然后,我们确保Median_filter_plugin类继承了QObjectCvPluginInterface

  3. 之后,我们添加了Qt所必须的宏,以便我们的库被识别为插件。这意味着以下三行代码,

    Q_OBJECT  // 支持信号和槽
    Q_PLUGIN_METADATA(IID "com.amin.cvplugininterface")  // 在插件类定义中声明元数据,IID(Interface Identifier 接口标识符)
    Q_INTERFACES(CvPluginInterface)  // 声明插件中实现的接口
    • 首先是Q_OBJECT宏,你在本章前面已经了解过,任何Qt类默认都应该存在,以允许Qt特定的能力(如信号和槽);并且元对象处理器 moc 将会解析它
    • 下一个是Q_PLUGIN_METADATA(IDD "XXX.XXX.XXX")它需要在插件的源代码中恰好出现一次,用于添加关于插件的元数据IID - Interface Identifier 的缩写,即接口标识符。在Qt插件系统中,每个插件都需要实现一个或多个接口,而IID就是用来唯一标识这些接口的。它通常是一个字符串,采用反向域名(包)表示法来保证全球唯一性。这样,Qt的插件加载器(QPluginLoader)就能通过IID来找到并加载提供了特定接口的插件。 用途:当你使用QPluginLoader加载一个插件时,插件加载器会检查插件提供的IID是否与加载器请求的IID相匹配。只有当两者匹配时,插件才能被成功加载。这个机制确保了应用程序能够找到并仅加载那些提供了需要接口的插件。
    • 最后一个Q_INTERFACES(),需要声明插件中实现的接口
  4. 然后,我们为我们的类添加了descriptionprocessImage函数的定义。这是我们真正定义插件做什么的地方,与仅仅有声明而没有实现的接口类相反。

  5. 最后,我们可以添加必要的更改和实际实现到median_filter_plugin.cpp文件。

    Median_filter_plugin::~Median_filter_plugin() 
    {} 
    
    QString Median_filter_plugin::description() 
    { 
      return "This plugin applies median blur filters to any image." 
      " This plugin's goal is to make us more familiar with the" 
      " concept of plugins in general."; 
    } 
    void Median_filter_plugin::processImage(const cv::Mat &inputImage, 
      cv::Mat &outputImage) 
    { 
      cv::medianBlur(inputImage, outputImage, 5); 
    }

    我们刚刚添加了类析构函数、descriptionprocessImage函数的实现。如你所见,

    • description函数返回有关插件的有用信息,在这种情况下没有复杂的帮助页面,只是几句话;
    • processImage函数简单地将medianBlur应用于图像

现在你可以在项目上右键点击并选择重新构建,或者从主菜单的构建项中选择。这将创建一个插件文件,我们将在下一节中使用,通常位于与项目同级的 build-* 文件夹下。

image-20240409231412665

插件文件的扩展名可能因操作系统而异。例如,在Windows上应该是.dll,在macOS和Linux上是.dylib.so等。

插件加载器和用户

现在,我们将使用上一节书中创建的插件。首先,创建一个新的 Qt Widgets 应用项目。让我们将其命名为 Plugin_User。当项目创建好后,首先在 *.pro 文件中添加 OpenCV 框架(你已经见过很多次了),然后继续创建一个类似于下面这样的用户界面:

  1. 显然,你需要修改 mainwindow.ui 文件,设计它使其看起来像下图一样,并设置所有对象名称,如下图所示:

确保使用与前图中相同类型的布局。

  1. 接下来,将 cvplugininterface.h 文件添加到此项目的文件夹中,然后,使用“添加现有文件”选项,将其添加到项目中,就像你在创建插件时所做的那样。

  2. 现在,我们可以开始编写我们的用户界面代码以及加载、检查和使用插件所需的代码。首先,在 mainwindow.h 文件中添加所需的头文件,如下所示:

#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QPluginLoader>
#include <QFileInfoList>
#include "opencv2/opencv.hpp"
#include "cvplugininterface.h"
  1. 然后,在 MainWindow 类的私有成员中,紧接着 }; 前,添加一个单独的函数,这似乎是一个好位置:
void getPluginsList();
  1. 现在,切换到 mainwindow.cpp 并在文件顶部任何现有的 #include 行之后,添加以下定义:
#define FILTERS_SUBFOLDER "/filter_plugins/"
  1. 然后,将以下函数添加到 mainwindow.cpp 中,这基本上是 getPluginsList 函数的实现:
void MainWindow::getPluginsList() 
{ 
  QDir filtersDir(qApp->applicationDirPath() + 
    FILTERS_SUBFOLDER); 
  QFileInfoList filters = filtersDir.entryInfoList( 
  QDir::NoDotAndDotDot | 
  QDir::Files, QDir::Name); 
  foreach(QFileInfo filter, filters) 
  { 
    if(QLibrary::isLibrary(filter.absoluteFilePath())) 
  { 
    QPluginLoader pluginLoader( 
        filter.absoluteFilePath(), 
        this); 
    if(dynamic_cast<CvPluginInterface*>( 
        pluginLoader.instance())) 
    { 
        ui->filtersList->addItem( 
            filter.fileName()); 
        pluginLoader 
            .unload(); // we can unload for now 
    } 
    else 
    { 
        QMessageBox::warning( 
            this, tr("Warning"), 
            QString(tr("Make sure %1 is a correct" 
            " plugin for this application<br>" 
            "and it's not in use by some other" 
            " application!")) 
            .arg(filter.fileName())); 
    } 
  } 
  else 
  { 
    QMessageBox::warning(this, tr("Warning"), 
        QString(tr("Make sure only plugins" 
            " exist in plugins folder.<br>" 
            "%1 is not a plugin.")) 
            .arg(filter.fileName())); 
  } 
  }   

  if(ui->filtersList->count() <= 0) 
  { 
    QMessageBox::critical(this, tr("No Plugins"), 
    tr("This application cannot work without plugins!" 
    "<br>Make sure that filter_plugins folder exists " 
    "in the same folder as the application<br>and that " 
    "there are some filter plugins inside it")); 
    this->setEnabled(false); 
  } 
} 

让我们首先了解这个函数做了什么。上面的函数,我们将在 MainWindow 类的构造函数中调用:

  • 首先,假设在名为 filter_plugins 的子文件夹中存在插件,而这个子文件夹位于与应用程序可执行文件相同的文件夹中。(稍后,我们需要在此项目的构建文件夹内手动创建此文件夹,然后将在前一步骤中构建的插件复制到这个新创建的文件夹中。)以下用法用于获取过滤器插件子文件夹的直接路径:
qApp->applicationDirPath() + FILTERS_SUBFOLDER
  • 接下来,它使用 QDir 类的 entryInfoList 函数从文件夹中提取 QFileInfoListQFileInfoList 类本质上是一个包含 QFileInfo 项的 QList 类(QList<QFileInfo>),每个 QFileInfo 项提供有关磁盘上文件的信息。在这种情况下,每个文件都将是一个插件。
  • 之后,通过在 foreach 循环中迭代文件列表,它检查插件文件夹中的每个文件,以确保只接受插件(库)文件,使用以下函数:
cppCopy code
QLibrary::isLibrary
  • 通过上一步的检查后的每个库文件,然后检查它是否与我们的插件接口兼容。我们不会让任何库文件被接受为插件,因此我们使用以下代码来实现这一目的:
cppCopy code
dynamic_cast<CvPluginInterface*>(pluginLoader.instance())
  • 如果一个库在前一步的测试中通过,则被视为正确的插件(与 CvPluginInterface 兼容),添加到我们窗口中的列表小部件中,然后卸载。我们可以根据需要简单地重新加载和使用它。
  • 在每一步,如果有问题,使用 QMessageBox 向用户显示有用信息。另外,如果最后列表为空,意味着没有可用的插件,窗口上的小部件将被禁用,应用程序将不可用。
  1. 不要忘记在 MainWindow 构造函数中,紧接着 setupUi 调用后调用此函数。
  2. 我们还需要为 inputImgButton 编写代码,用于打开图像文件。代码如下:
cppCopy code
void MainWindow::on_inputImgButton_pressed()
{
    QString fileName =
       QFileDialog::getOpenFileName(
       this,
       tr("Open Input Image"),
       QDir::currentPath(),
       tr("Images") + " (*.jpg *.png *.bmp)");
       if(QFile::exists(fileName))
      {
       ui->inputImgEdit->setText(fileName);
      }
}

我们之前见过这段代码,它不需要解释。它只是允许你打开一个图像文件,并确保它被正确选中。

  1. 现在,我们将编写 helpButton 的代码,它将显示插件中 description 函数的结果:
       void MainWindow::on_helpButton_pressed() 
       { 
         if(ui->filtersList->currentRow() >= 0)
        { 
         QPluginLoader pluginLoader( 
           qApp->applicationDirPath() +
           FILTERS_SUBFOLDER +
           ui->filtersList->currentItem()->text());
           CvPluginInterface *plugin = 
             dynamic_cast<CvPluginInterface*>(
           pluginLoader.instance()); 
           if(plugin) 
           { 
             QMessageBox::information(this, tr("Plugin Description"), 
                plugin->description()); 
           } 
           else 
           { 
            QMessageBox::warning(this, tr("Warning"),
            QString(tr("Make sure plugin %1" " exists and is usable.")) 
           .arg(ui->filtersList->currentItem()->text())); 
           }
        }
        else
        { 
          QMessageBox::warning(this, tr("Warning"), QString(tr("First 
            select a filter" " plugin from the list.")));
        } 
      }

我们使用 QPluginLoader 类来正确地从列表中加载一个插件,然后使用 instance 函数获取它的一个实例,最后,我们将通过接口调用插件中的函数。

  1. 相同的逻辑也适用于 filterButton。唯一的区别是这次,我们将调用实际的过滤函数,如下所示:

    void MainWindow::on_filterButton_pressed() 
    {
      if(ui->filtersList->currentRow() >= 0 && 
        !ui->inputImgEdit->text().isEmpty()) 
      { 
        QPluginLoader pluginLoader(qApp->applicationDirPath() +
          FILTERS_SUBFOLDER + 
          ui->filtersList->currentItem()->text()); 
          CvPluginInterface *plugin = 
            dynamic_cast<CvPluginInterface*>(
              pluginLoader.instance()); 
            if(plugin)
            { 
             if(QFile::exists(ui->inputImgEdit->text()))
             { 
              using namespace cv;
              Mat inputImage, outputImage;
              inputImage = imread(ui->inputImgEdit->
              text().toStdString()); 
              plugin->processImage(inputImage, outputImage); 
              imshow(tr("Filtered Image").toStdString(),
                 outputImage); 
             } 
             else
             { 
               QMessageBox::warning(this, 
                tr("Warning"), 
                QString(tr("Make sure %1 exists.")) 
                .arg(ui->inputImgEdit->text()));
             }
            } 
            else
            { 
             QMessageBox::warning(this, tr("Warning"), 
             QString(tr(
             "Make sure plugin %1 exists and is usable." )) 
             .arg(ui->filtersList->currentItem()->text())); 
            }
           } 
         else
         {
          QMessageBox::warning(this, tr("Warning"), 
          QString(tr( "First select a filter plugin from the list." ))); 
      }
    }

    始终让用户了解正在发生的事情以及可能发生的问题是非常重要的,使用 QMessageBox 或其他类型的信息提供能力可以做到这一点。正如你所看到的,它们通常甚至需要比实际要完成的任务更多的代码,但这对于避免应用程序崩溃至关重要。默认情况下,Qt 不支持异常处理,并信任开发者将使用足够的 ifelse 指令来处理所有可能的崩溃场景。关于前面代码示例的另一个重要说明是 tr 函数。记住,对于字面字符串始终使用它。这样,你可以在以后轻松地使你的应用程序支持多语言。即使你不打算支持多种语言,养成在字面字符串中添加 tr 函数的好习惯也无妨。

    现在,我们准备运行我们的 Plugin_User 应用程序。如果我们现在运行它,我们将看到一个错误消息(我们自己放置的),并且我们将被警告没有插件。为了能够使用我们的 Plugin_User 应用程序,我们需要做以下事情:

    1. Plugin_User 项目的构建文件夹内创建一个名为 filter_plugins 的文件夹。这是项目的可执行文件创建的文件夹。
    2. 复制我们构建的插件文件(即 median_filter_plugin 项目的构建文件夹内的库文件),并将其粘贴到第一步中的 filter_plugins 文件夹中。如前所述,像可执行程序一样,插件文件的扩展名取决于操作系统。

    现在,尝试运行 Plugin_User,一切应该都是好的。你应该能够在列表中看到单个插件,选择它,点击帮助按钮以获取有关它的信息,点击过滤按钮以在图像上应用插件中的过滤器。如下图所示:

    尝试创建另一个名为 gaussian_filter_plugin 的插件,并按照 median_filter_plugin 的相同一套指令操作,这次使用你在第二章 创建我们的第一个 Qt 和 OpenCV 项目中看到的 gaussianBlur 函数。然后构建它,并将其放入 filter_plugins 文件夹,再次运行 Plugin_User 应用程序。同时,尝试放入一些随机的库文件(和其他非库文件)来测试我们在这些场景下编写的应用程序。

    这里有一个非常重要的事情需要注意,那就是你必须确保不要使用在 Debug 模式下构建的插件与在 Release 模式下构建的应用程序一起使用,反之亦然。加载插件还适用其他重要规则,例如用高版本的 Qt 构建的插件不能用于用低版本的 Qt 构建的应用程序。用较低的 Qt 主版本号构建的插件不能用于用较高的 Qt 主版本号构建的应用程序。始终参考 Qt 文档中的 部署插件 文章或 Qt Creator 帮助模式,以获取有关插件及其使用的更新信息。

始终让用户了解正在发生的事情以及可能发生的问题是非常重要的,使用 QMessageBox 或其他类型的信息提供能力可以做到这一点。正如你所看到的,它们通常甚至需要比实际要完成的任务更多的代码,但这对于避免应用程序崩溃至关重要。默认情况下,Qt 不支持异常处理,并信任开发者将使用足够的 ifelse 指令来处理所有可能的崩溃场景。关于前面代码示例的另一个重要说明是 tr 函数。记住,对于字面字符串始终使用它。这样,你可以在以后轻松地使你的应用程序支持多语言。即使你不打算支持多种语言,养成在字面字符串中添加 tr 函数的好习惯也无妨。

现在,我们准备运行我们的 Plugin_User 应用程序。如果我们现在运行它,我们将看到一个错误消息(我们自己放置的),并且我们将被警告没有插件。为了能够使用我们的 Plugin_User 应用程序,我们需要做以下事情:

  1. Plugin_User 项目的构建文件夹内创建一个名为 filter_plugins 的文件夹。这是项目的可执行文件创建的文件夹。
  2. 复制我们构建的插件文件(即 median_filter_plugin 项目的构建文件夹内的库文件),并将其粘贴到第一步中的 filter_plugins 文件夹中。如前所述,像可执行程序一样,插件文件的扩展名取决于操作系统。

现在,尝试运行 Plugin_User,一切应该都是好的。你应该能够在列表中看到单个插件,选择它,点击帮助按钮以获取有关它的信息,点击过滤按钮以在图像上应用插件中的过滤器。如下图所示:

尝试创建另一个名为 gaussian_filter_plugin 的插件,并按照 median_filter_plugin 的相同一套指令操作,这次使用你在第二章 创建我们的第一个 Qt 和 OpenCV 项目中看到的 gaussianBlur 函数。然后构建它,并将其放入 filter_plugins 文件夹,再次运行 Plugin_User 应用程序。同时,尝试放入一些随机的库文件(和其他非库文件)来测试我们在这些场景下编写的应用程序。

这里有一个非常重要的事情需要注意,那就是你必须确保不要使用在 Debug 模式下构建的插件与在 Release 模式下构建的应用程序一起使用,反之亦然。加载插件还适用其他重要规则,例如用高版本的 Qt 构建的插件不能用于用低版本的 Qt 构建的应用程序。用较低的 Qt 主版本号构建的插件不能用于用较高的 Qt 主版本号构建的应用程序。始终参考 Qt 文档中的 部署插件 文章或 Qt Creator 帮助模式,以获取有关插件及其使用的更新信息。

插件加载器和用户

现在,我们将使用上一节中创建的插件。首先,创建一个新的 Qt Widgets Application 项目。我们将其命名为 Plugin_User。项目创建后,首先将 OpenCV 框架添加到 *.pro 文件中(你已经见过很多次了),然后开始创建类似于下面的用户界面:

  1. 显然,你需要修改 mainwindow.ui 文件,设计它使其看起来像下图,并设置所有对象名称,如下图所示:

确保使用与前图中相同类型的布局。

  1. 接下来,将 cvplugininterface.h 文件添加到此项目的文件夹中,然后,使用“添加现有文件”选项,像你创建插件时那样将其添加到项目中。

  2. 现在,我们可以开始编写我们的用户界面代码以及加载、检查和使用插件所需的代码。首先,向 mainwindow.h 文件添加所需的头文件,如下所示:

#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QPluginLoader>
#include <QFileInfoList>
#include "opencv2/opencv.hpp"
#include "cvplugininterface.h"
  1. 然后,在 MainWindow 类的 private 成员中,就在 }; 之前,添加一个单一函数,这似乎是个不错的位置:
cppCopy code
void getPluginsList();
  1. 接下来,切换到 mainwindow.cpp 并在文件顶部添加以下定义,紧跟在任何现有的 #include 行之后:
cppCopy code
#define FILTERS_SUBFOLDER "/filter_plugins/"
  1. 然后,在 mainwindow.cpp 中添加以下函数,这基本上是 getPluginsList 函数的实现:

让我们首先看看这个函数做了什么。上述函数,我们将在 MainWindow 类的构造函数中调用:

  • 首先,假设在一个名为 filter_plugins 的子文件夹中存在插件,这个子文件夹与应用程序可执行文件在同一文件夹中。(稍后,我们需要在此项目的构建文件夹内手动创建此文件夹,然后将之前步骤中构建的插件复制到这个新创建的文件夹中。)使用以下内容获取指向过滤器插件子文件夹的直接路径:
cppCopy code
qApp->applicationDirPath() + FILTERS_SUBFOLDER
  • 接下来,它使用 QDir 类的 entryInfoList 函数从文件夹中提取 QFileInfoListQFileInfoList 类本身基本上是一个包含 QFileInfo 项的 QList 类(QList),每个 QFileInfo 项提供有关磁盘上文件的信息。在这种情况下,每个文件将是一个插件。
  • 之后,通过在 foreach 循环中迭代文件列表,它检查插件文件夹中的每个文件,以确保只接受插件(库)文件,使用以下函数:
cppCopy code
QLibrary::isLibrary
  • 每个通过前一步骤的库文件接着被检查以确保它与我们的插件接口兼容。我们不会仅仅让任何库文件被接受为插件,因此我们使用以下代码进行此目的:
cppCopy code
dynamic_cast<CvPluginInterface*>(pluginLoader.instance())
  • 如果一个库通过了上一步的测试,则被视为正确的插件(与 CvPluginInterface 兼容),添加到我们窗口中的列表小部件中,然后卸载。我们可以简单地重新加载并在需要时使用它。
  • 在每一步,如果有问题,使用 QMessageBox 向用户显示有用的信息。此外,如果列表为空,意味着没有可用的插件,窗口上的小部件被禁用,应用程序不可用。
  1. 不要忘记从 MainWindow 构造函数中调用此函数,紧跟在 setupUi 调用之后。
  2. 我们还需要为 inputImgButton 编写代码,该按钮用于打开图像文件。代码如下:
cppCopy code
void MainWindow::on_inputImgButton_pressed()
{
  QString fileName =
     QFileDialog::getOpenFileName(
     this,
     tr("Open Input Image"),
     QDir::currentPath(),
     tr("Images") + " (*.jpg *.png *.bmp)");
     if(QFile::exists(fileName))
    {
     ui->inputImgEdit->setText(fileName);
    }
}

我们之前已经见过这段代码,它不需要解释。它简单地允许你打开一个图像文件,并确保它被正确选择。

  1. 现在,我们将编写 helpButton 的代码,该按钮将显示插件中 description 函数的结果。
       void MainWindow::on_helpButton_pressed() 
       { 
         if(ui->filtersList->currentRow() >= 0)
        { 
         QPluginLoader pluginLoader( 
           qApp->applicationDirPath() +
           FILTERS_SUBFOLDER +
           ui->filtersList->currentItem()->text());
           CvPluginInterface *plugin = 
             dynamic_cast<CvPluginInterface*>(
           pluginLoader.instance()); 
           if(plugin) 
           { 
             QMessageBox::information(this, tr("Plugin Description"), 
                plugin->description()); 
           } 
           else 
           { 
            QMessageBox::warning(this, tr("Warning"),
            QString(tr("Make sure plugin %1" " exists and is usable.")) 
           .arg(ui->filtersList->currentItem()->text())); 
           }
        }
        else
        { 
          QMessageBox::warning(this, tr("Warning"), QString(tr("First 
            select a filter" " plugin from the list.")));
        } 
      }

我们使用 QPluginLoader 类从列表中正确加载插件,然后使用 instance 函数获取其实例,最后,我们将通过接口调用插件中的函数。

接下来是第 10 步