OpenCV
【🔧更新中🔧】基于 Qt 和 OpenCV 的计算机视觉示例实现及教程
[toc]
如果你不了解 Qt 这个框架,建议先学习:
- 关于 Qt 无比详细教程及案例实现:https://github.com/NekoSilverFox/Qt 其中不仅涵盖了 Qt 基本控件的使用及讲解,还包含了大学和培训机构不会讲到的:插件设计及实现、基于 QTest 的静态动态、动态测试、CI/CD的使用、Qt 函数/方法注意事项等
如果你对 OpenGL 计算机图形学感兴趣:
- 基于 Qt & OpenGL 的案例实现及详细教程:[https://github.com/NekoSilverFox/OpenGL](https://github.com/NekoSilverFox/OpenGL
参考:
原文:Computer Vision with OpenCV 3 and Qt5
只要环顾四周,就很可能会看到至少两个不同的设备,例如计算机,智能手机,智能手表或平板电脑,上面运行着一些应用,可以帮助您完成各种日常任务或娱乐音乐,看电影 ,视频游戏等。 每年,市场上都会引入数百种新设备,并且需要新版本的操作系统来跟上它们,以便为应用开发人员提供更好的界面,以创建可更好地利用诸如高分辨率等基础资源的软件。 显示器,各种传感器等。 结果,软件开发框架必须适应并支持不断增长的平台。 考虑到这一点,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并注册,以直接将文件通过电子邮件发送给你。 你可以按照以下步骤下载代码文件:
- 在www.packtpub.com登录或注册。
- 选择SUPPORT标签。
- 点击Code Downloads & Errata。
- 在搜索框中输入书名并按照屏幕上的指示操作。
一旦文件下载完成,请确保你使用最新版本的以下软件解压或提取文件夹:
- 对于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 库的一个模块,我们将在本章的接下来的页面中介绍它,并且它也将在一个完整的章节中进行详细介绍。
计算机视觉是当今计算机科学中最受欢迎的主题之一,它被应用于各种应用程序中,从检测癌组织的医疗工具到帮助制作所有那些闪亮音乐视频和电影的视频编辑软件,再到军用级别的目标检测器,帮助在地图上找到特定位置,以及帮助无人驾驶汽车找到路线的交通标志检测器。嗯,很明显我们无法列出计算机视觉的所有可能性,但我们可以肯定它是一个有趣的主题,将在很长一段时间内存在。还值得一提的是,计算机视觉领域的工作和职业市场正在迅速扩展,而且正在日益增长。
在计算机视觉开发人员和专家中最受欢迎的工具中,有两个最突出的开源框架,它们也是您手头书籍的标题中的两个框架,即 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 中的模块和类是如何实现的,您需要熟悉更多的资源、研究,有时甚至是硬核的数学计算或对计算机或操作系统在现实世界中执行的低级理解,这完全超出了本书的范围。然而,对于本书涵盖的所有算法和方法,您将得到它们是什么,如何以及何时何地使用它们的简要描述,以及足够的指导,让您如果愿意的话可以继续深入挖掘。
你可能已经听说过它,甚至在不知情的情况下使用过它。它是许多世界著名的商业和开源应用程序的基础,例如 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 所承诺在所有支持的平台上可用的模块。它们基本上是 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 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。
当我们谈论开发应用程序时,平台可能有许多不同的含义,包括操作系统类型、操作系统版本、编译器类型、编译器版本和处理器架构(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 应用程序的 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 包含的内容是有意义的,就像本章前面看到的 Qt 一样。这里它们是:
- 核心功能或简称为
core
模块包含所有其他 OpenCV 模块使用的所有基本结构,常量和函数。 例如,在此模块中定义 OpenCVMat
类,在本书的其余部分中,我们几乎将在每个 OpenCV 示例中使用该类。 第 4 章,“Mat
和QImage
”将涵盖这个模块以及与之密切相关的 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。正如您稍后将看到的,并与本节的标题相反,我们并没有像在Qt安装中那样真正“安装”OpenCV。这是因为 OpenCV 通常不提供针对所有编译器和平台的预构建二进制文件,事实上,它根本不为 macOS 和 Linux 提供预构建二进制文件。在最新的 OpenCV Win 包中,只包含了针对 MSVC 2015 64 位的预构建二进制文件,这与我们将要使用的 32 位版本不兼容,因此学习如何自己构建 OpenCV 是一个非常好的主意。这也有利于构建适合您需求的 OpenCV 框架库。您可能希望排除一些选项以使您的 OpenCV 安装更轻量化,或者您可能希望为其他编译器(如 MSVC 2013)构建。因此,有很多理由自己从源代码构建 OpenCV。
互联网上大多数开源框架和库,或者至少那些希望保持 IDE 中立的项目(这意味着可以使用任何 IDE 配置和构建的项目,不依赖于特定 IDE 即可工作的项目),使用 CMake 或类似的所谓“构建”系统。我想这也回答了诸如“我为什么需要 CMake?”、“他们为什么不直接给出库并完成呢?”或类似这样的问题。因此,我们需要 CMake 能够使用源代码配置和构建 OpenCV。CMake 是一个开源的跨平台应用程序,允许配置和构建开源项目(或应用程序、库等),您可以在之前的章节中提到的所有操作系统上下载和使用它。在撰写本书的时候,CMake 版本 3.9.1 可以从 CMake 网站下载页面 (https://cmake.org/download/) 下载。
在继续之前,请确保在计算机上下载并安装它。CMake 安装没有特别需要注意的地方,除了您应该确保安装 GUI 版本,因为这是我们将在下一节中使用的版本,也是提供的链接中的默认选项。
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 本身的依赖关系,需要在配置和构建它之前就位:
sudo apt-get install libgtk2.0-dev and pkg-config
运行 CMake GUI 应用后,需要设置以下两个文件夹:
- “源代码在哪里”文件夹应设置为您下载和提取 OpenCV 源代码的位置
- 可以将“生成二进制文件的位置”文件夹设置为任何文件夹,但是通常在源代码文件夹下创建一个名为
build
的子文件夹并将其选择为二进制文件文件夹
设置这两个文件夹后,您可以通过单击“配置”按钮继续前进,如以下屏幕截图所示:
单击配置按钮将启动配置过程。 如果构建文件夹尚不存在,可能会要求您创建该文件夹,您需要通过单击“是”按钮来对其进行回答。 如果您仍然觉得自己只是在重复书中的内容,请不要担心。 当您继续阅读本书和说明时,所有这些都会陷入。 现在,让我们仅关注在计算机上构建和安装 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_BUILD
和INSTALL
启用了复选框,如以下屏幕截图所示:
对于 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 是一个框架,你将学习如何在 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 安装,该应用将从硬盘读取图像文件并仅显示它。
首先运行 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 详细的介绍请参考: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 框架不是一回事,它只是由 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 中的窗口来创建,添加或删除它们。 设计器模式,然后从右键菜单中选择相关项目。 它们是:

- 菜单栏(
QMenuBar
)菜单栏是显示在窗口顶部的典型水平主菜单栏。 菜单中可以有任意数量的项目和子项目,每个项目和子项目都可以触发一个动作(QAction
)。 您将在接下来的章节中了解有关操作的更多信息。 以下是菜单栏示例:
- 工具栏(
QToolBar
)工具栏是一个可移动面板,其中可以包含与特定任务相对应的工具按钮。 这是一个示例工具栏。 请注意,它们可以在 Qt 窗口内移动甚至移出:
- 状态栏(
QStatusBar
) 状态栏**是底部的一个简单的水平信息栏,对于大多数基于窗口的应用是通用的。 **
**每当在 Qt 中创建一个新的主窗口时,这三种类型的条形都将添加到该窗口中。 请注意,一个窗口上只能有一个菜单栏和一个状态栏,但是可以有任意数量的状态栏。 如果不需要它们,则需要将它们从“设计器”窗口右侧的对象层次结构中删除。 现在您已经熟悉了 Qt 中的三个不同的条形,可以从“Qt 欢迎”模式中的示例中搜索Application Example
,以进一步了解它们,以及是否可以进一步自定义它们。
以下是对 Qt Creator 设计模式(或从现在开始简称为 Designer)中可用小部件的简要说明,如前面的屏幕快照所示。 在设计器模式下,小部件基于其行为的相似性进行分组。 在继续进行列表操作时,请自己亲自尝试设计器中的每个功能,以感觉到将它们放置在用户界面上时的外观。 为此,您可以使用设计器模式将每个窗口小部件拖放到窗口上:
-
Layouts - 布局:这些布局用于管理窗口小部件的显示方式。在外观上,它们是不可见的(因为它们不是
QWidget
子类),并且它们仅影响添加到它们的小部件。 请注意,布局根本不是小部件,它们是用来管理小部件的显示方式的逻辑类。 尝试在用户界面上放置任何布局小部件,然后在其中添加一些按钮或显示小部件,以查看其布局如何根据布局类型进行更改。 查看每个示例图片以了解它们的行为。 -
Spacers - 分隔符:类似于弹簧,它们在视觉上不可见,但会影响将其他窗口小部件添加到布局时的显示方式。在用户更改窗口大小时候小控件间隙可以动态缩放。间隔符的类型为
QSpacerItem
,但是通常,它们绝不能直接在代码中使用。分隔符(英) 分隔符(中) 效果 Horizontal Spacer 水平分隔符 Vertical Spacer 垂直分隔符
-
Buttons - 按钮:这些只是按钮。 它们用于提示操作。 您可能会注意到,单选按钮和复选框也在该组中,这是因为它们都继承自
QAbstractButton
类,该类是一个抽象类,提供了类按钮小部件所需的所有接口。
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 View:
QUndoView
是一个展示撤销堆栈内容的Qt小部件。通过点击视图中的命令,可以使文档的状态向前或向后回滚到该命令。这提供了一个直观的方式,让用户可以轻松地浏览并选择撤销或重做的操作。更多详情,请访问官方文档。
- 列表视图 - List View:这以一个简单的列表形式展示模型中的项,没有任何层次结构(对应的Qt类为
- 项目小部件(基于项目)Item Widgets (Item-Based):这类似于基于模型的项目视图,不同之处在于它们不是基于 MVC 设计模式,并且它们提供了简单的 API 来添加,删除或修改他们的项目
- 列表小部件 - List Widget:类似于列表视图,但是具有基于项目的 API,可以添加,删除和修改其项目(此小部件的等效 Qt 类称为
QListWidget
) - 树形小部件 - Tree Widget:这类似于树形视图,但具有基于项目的 API,可以添加,删除和修改其项目(此小部件的等效 Qt 类称为
QTreeWidget
) - 表格小部件 - Table Widget:这类似于表视图,但是具有基于项目的 API,用于添加,删除和修改其项目(此窗口小部件的等效 Qt 类称为
QTableWidget
)
- 列表小部件 - List Widget:类似于列表视图,但是具有基于项目的 API,可以添加,删除和修改其项目(此小部件的等效 Qt 类称为
- 输入小部件:听起来完全一样。 您可以使用以下小部件获取用户输入数据。
- 组合框:有时称为下拉列表; 它可以用来选择列表中的选项,而屏幕上的空间却很少。 任何时候,只有选定的选项可见。 用户甚至可以输入自己的输入值,具体取决于其配置。 (此小部件的等效 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
)。
- 组合框:有时称为下拉列表; 它可以用来选择列表中的选项,而屏幕上的空间却很少。 任何时候,只有选定的选项可见。 用户甚至可以输入自己的输入值,具体取决于其配置。 (此小部件的等效 Qt 类称为
不应将此与QKeySequence
类混淆,该类根本不是小部件。 QKeySequenceEdit
用于从用户那里获取QKeySequence
。 在拥有QKeySequence
之后,我们可以将其与QShortcut
或QAction
类结合使用以触发不同的函数/插槽。 本章稍后将介绍信号/插槽的介绍。
- 显示小部件:可用于显示输出数据,如数字、文本、图片、日期等:
- 标签:可用于显示数字、文本、图片或电影(此小部件对应的 Qt 类称为
QLabel
)。 - 文本浏览器:与文本编辑小部件几乎相同,但增加了在链接之间导航的功能(此小部件对应的 Qt 类称为
QTextBrowser
)。 - 图形视图:可用于显示图形场景的内容(此小部件对应的 Qt 类称为
QGraphicsView
)。
- 标签:可用于显示数字、文本、图片或电影(此小部件对应的 Qt 类称为
我们在本书中将会使用到的最重要的小部件可能是图形场景(或 QGraphicsScene
),并且将在第5章,图形视图框架中进行介绍。
- 日历小部件:可用于从月历中查看和选择日期(此小部件对应的 Qt 类称为
QCalendarWidget
)。- LCD数字:可用于在类似LCD的显示屏上显示数字(此小部件对应的 Qt 类称为
QLCDNumber
)。 - 进度条:可用于显示垂直或水平的进度指示器(此小部件对应的 Qt 类称为
QProgressBar
)。 - 水平/垂直线:可用于绘制简单的垂直或水平线。特别适用于不同小部件组之间的分隔线。
- OpenGL小部件:此类可用作渲染OpenGL输出的表面(此小部件对应的 Qt 类称为
QOpenGLWidget
)。
- LCD数字:可用于在类似LCD的显示屏上显示数字(此小部件对应的 Qt 类称为
请注意,OpenGL是计算机图形学中一个完全独立和高级的主题,完全超出了本书的范围;然而,如前所述,了解Qt中存在的工具和小部件对于可能的进一步学习是一个好主意。
- QQuickWidget:此小部件可用于显示Qt Quick用户界面。Qt Quick界面使用QML语言来设计用户界面(此小部件对应的 Qt 类称为
QQuickWidget
)。
第12章,Qt Quick应用程序中将介绍QML。现在,让我们确保我们的用户界面中不添加任何QQuickWidget小部件,因为我们需要向项目中添加额外的模块才能使其工作。如何向Qt项目中添加模块将在本章中介绍。
现在,我们可以开始为我们的 Hello_Qt_OpenCV 项目设计用户界面了。对于一个项目来说,拥有一份清晰的规格说明书总是一个好主意,然后根据需求设计一个用户友好的UI,先在一张纸上(或者如果项目不大的话,在你的脑海中)画出用户界面,最后开始使用 Designer 创建它。当然,这个过程需要对现有的 Qt 小部件有经验,同时也需要足够的经验来创建你自己的小部件,但这是最终会发生的事情,你只需要继续练习就可以了。
因此,首先,让我们来看看我们需要开发的应用程序的规格说明。比如说:
- 这个应用程序必须能够接受图像作为输入(接受的图像类型至少应该包括 .jpg、.png 和 *.bmp 文件)。
- 这个应用程序必须能够应用模糊滤镜。用户必须能够选择中值模糊或高斯模糊类型来过滤输入图像(使用默认的参数集)。
- 这个应用程序必须能够保存输出图像,而且输出图像的文件类型(或者换句话说,扩展名)必须可以由用户选择(.jpg、.png 或 *.bmp)。
- 用户应该能够在保存时可选地查看输出图像。
- 用户界面上设置的所有选项,包括模糊滤镜类型和最后打开和保存图像文件,应该在应用程序重新启动时被保留和重新加载。
- 当用户想要关闭应用程序时,应该提示用户。
这对我们的案例来说应该足够了。通常,你不应该超出或不满足需求。这是设计用户界面时的一个重要规则。这意味着你应该确保所有需求都被成功满足,同时,你没有添加任何不需要的东西(或者在需求列表中不需要的东西)。
对于这样一份需求列表(或规格说明),可以有无数种用户界面设计;然而,这里是我们将要创建的一个。请注意,这是我们的程序执行时的外观。显然,标题栏和样式可能因操作系统而异,但基本上就是这样:
尽管它看起来可能很简单,但它包含了这样一个任务所需的所有必要组件,界面几乎是不言自明的。因此,打算使用这个应用程序的人实际上不需要知道很多关于它的功能,他们可以简单地猜测所有输入框、单选按钮、复选框等的用途。
这是在 Designer 中查看同一UI时的样子:
是时候为我们的项目创建用户界面了:
-
创建这个用户界面,你需要首先从主窗口中移除菜单栏、状态栏和工具栏,因为我们不需要它们。右键点击顶部的菜单栏并选择移除菜单栏。接下来,在窗口的任何位置右键点击并选择移除状态栏。最后,右键点击顶部的工具栏并点击移除工具栏。
-
现在,在你的窗口中添加一个水平布局;这就是前面图片顶部可见的布局。然后,在其中添加一个标签、行编辑和推送按钮,如前图所示。
-
通过双击标签并输入
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 时为我们节省大量时间。
-
添加一个组框,然后添加两个单选按钮,类似于上图所示。
-
接下来,添加另一个水平布局,然后在其中添加
Label
,Line Edit
和Push Button
。 这将是在复选框正上方的底部看到的布局。 -
最后,在窗口中添加一个复选框。这是底部的复选框。
-
现在,根据前面的图片更改窗口上所有小部件的文本。你的 UI 几乎准备好了。你现在可以通过点击屏幕左下角的运行按钮来尝试运行它。确保你没有按带有错误的运行按钮。这是按钮:
这将产生与您之前看到的相同的用户界面。现在,如果您尝试调整窗口的大小,您会注意到在调整窗口大小或最大化窗口时,所有内容都保持原样,并且它不会响应应用大小的更改。 要使您的应用窗口响应大小更改,您需要为centralWidget
设置布局。 还需要对屏幕上的分组框执行此操作。
Qt 小部件均具有centralWidget
属性。 这是 Qt 设计器中特别用于 Windows 和容器小部件的东西。 使用它,您可以设置容器或窗口的布局,而无需在中央窗口小部件上拖放布局窗口小部件,只需使用设计器顶部的工具栏即可:
您可能已经注意到工具栏中的四个小按钮(如前面的屏幕快照所示),它们看起来与左侧小部件工具箱中的布局完全一样(如下所示):
因此,让我们就整本书中的简单快速解释达成另一条规则。 每当我们说Set the Layout of X to Y
时,我们的意思是首先选择小部件(实际上是容器小部件或窗口),然后使用顶部工具栏上的布局按钮选择正确的布局类型。
-
根据前面信息框中的描述,选择窗口(这意味着,单击窗口上的空白而不是任何小部件上的空白)并将其布局设置为
Vertical
。 -
对组框执行相同操作; 但是,这一次,将布局设置为水平。 现在,您可以尝试再次运行程序。 如您现在所见,它会调整其所有小部件的大小,并在需要时移动它们,以防更改窗口大小。 窗口内的组框也发生了同样的情况。
-
接下来需要更改的是小部件的
objectName
属性。 这些名称非常重要,因为在 C++ 代码中使用它们来访问窗口上的小部件并与其进行交互。 对于每个小部件,请使用以下屏幕截图中显示的名称。 请注意,该图像显示了对象层次结构。 您还可以通过双击对象层次结构窗格中的小部件来更改objectName
属性:
从理论上讲,您可以为objectName
属性使用任何 C++ 有效的变量名,但实际上,最好始终使用有意义的名称。考虑对本书中使用的变量或小部件名称遵循相同或相似的命名约定。它基本上是 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
属性值引用用户界面上的任何窗口小部件。 将它们视为可以在代码中使用以访问这些小部件的变量名。
这是我们项目的编码部分所需的步骤:
- 再次切换到设计器,然后右键单击
inputPushButton
。 然后,从出现的菜单中选择“转到插槽...”。 将显示的窗口包括此小部件发出的所有信号。 选择pressed()
,然后单击确定:
- 您会注意到,您是从设计器自动转到代码编辑器的。 另外,现在
mainwindow.h
文件中添加了新函数。 - 在
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(一个静态代码分析器)会发出警告:
- 隐式行为可能导致错误:自动连接是一个隐式过程,开发者可能不清楚某个槽函数是否被自动连接,或者错误地认为某个槽函数会被自动连接。这可能导致调试困难,因为行为的预期与实际可能不符。
- 重构风险:如果对象名称或信号名称在未来发生变化,与之相关的自动连接也会受到影响,可能会导致槽不再被正确连接,而编译器不会报错,因为这些连接是在运行时解析的。
- 代码可读性降低:对于不熟悉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
这仅表示core
和gui
模块应包含在您的项目中。 它们是两个最基本的 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
应用。
- 所有
OpenCV
函数都包含在cv
名称空间中,因此我们确保我们是 OpenCVnamespace cv
的using
。 - 然后,为了读取输入图像,我们使用了
imread
函数。这里要注意的重要一点是 OpenCV 使用 C++std::string
类,而 Qt 的QString
应该转换为该格式,否则,当您尝试运行该程序时会遇到错误。 只需使用QString
的toStdString
函数即可完成。注意,在这种情况下,QString
是inputLineEdit
小部件的text()
函数返回的值。 - 接下来,根据选择的过滤器类型,我们使用
medianBlur
或gaussianBlur
函数进行简单的 OpenCV 过滤。请注意,在这种情况下,我们为这些 OpenCV 函数使用了一些默认参数,但是如果我们使用小部件从用户那里获得它们,那就更好了。您将在章节“创建全面的 Qt + OpenCV 项目”中学习如何使用更多小部件,甚至创建自己的小部件。 - 最后,已过滤的输出图像
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
,它的文档页面都将在编辑模式下的代码编辑器中获取:

您可以通过点击主菜单中的“工具(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 框架已经有了实现它的手段,那就是以模块化的方式构建应用程序。请注意,在这里模块化不仅仅意味着库或不同的源代码模块,而是意味着应用程序的每个职责和能力都是独立于其他职责和能力创建和构建的。这实际上正是 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
工具是一个帮助使用 *.pro
文件中的信息创建 makefile
的程序。这简单地意味着,使用非常简单的语法(与其他 make
系统中的更复杂语法相比),qmake
生成了编译和构建应用程序所需的所有必要命令,并将所有这些生成的文件放在 Build
文件夹中。
当构建 Qt 项目时,它首先创建一个新的构建文件夹,默认情况下,该文件夹与项目文件夹位于同一级别。在我们的例子中,这个文件夹应该有一个类似于 build-Hello_Qt_OpenCV-Desktop_Qt_5_9_1_*-Debug
的名称,其中 *
可能会有所不同,取决于平台,你可以在项目文件夹所在的同一个文件夹中找到它。Qt(使用 qmake
和本章中您将了解到的一些其他工具)和 C++ 编译器生成的所有文件位于此文件夹及其子文件夹中。这称为项目的构建文件夹。这也是您的应用程序被创建和执行的地方。例如,如果您使用的是 Windows,您可以在 Build
文件夹的 debug
或 release
子文件夹中找到 Hello_Qt_OpenCV.exe
文件(以及许多其他文件)。因此,从现在开始我们将称这个文件夹(及其子文件夹)为构建文件夹。
例如,我们已经知道在我们的 Qt 项目文件中包含以下行会导致将 Qt 的 core
和 gui
模块添加到我们的应用程序中:
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() 进入消息循环机制,避免程序一闪而过,类似死循环
最简单地描述:
- 就是创建了
QApplication
类的一个实例,并将应用程序参数(通常通过命令行或终端传递)传递给名为a
的新实例。 - 然后,创建了我们的
MainWindow
类的一个实例w
,然后通过.show()
显示它。 - 最后,调用
QApplication
类的.exec()
函数,以便应用程序进入主循环,并保持打开状态,直到窗口关闭。
要了解事件循环的真正工作方式,请尝试删除最后一行,看看会发生什么。当你运行你的应用程序时,你可能会注意到窗口实际上显示了非常短暂的时间,然后立即关闭。这是因为我们的应用程序不再有事件循环,它立即到达应用程序的结尾,内存中的所有内容都被清除了,因此窗口被关闭。现在,重新写回那行代码,正如你所期待的,窗口保持打开状态,因为 .exec()
函数只有在代码中某处(任何地方)调用了 .exit()
函数时才返回,并且它返回 .exit()
设置的值。
现在,让我们继续讨论具有相同名称但扩展名不同的接下来的三个文件。它们是 mainwindow
头文件、源文件和用户界面文件。您现在将了解负责我们在创建第一个 Qt 和 OpenCV 项目中创建的应用程序的代码和用户界面的实际文件。这使我们了解到另外两个 Qt 内部工具,称为元对象编译器(moc)和用户界面编译器(uic)。
元对象编译器(moc, Meta-Object Compiler)
我们已经知道,在标准 C++ 代码中并不存在信号和槽这样的东西。那么,使用 Qt,我们是如何在 C++ 代码中拥有这些额外能力的呢?而且这还不是全部。正如你稍后将学到的,你甚至可以向 Qt 对象添加新属性(称为动态属性)并执行许多类似的操作,这些都不是标准 C++ 编程的能力。嗯,这些是通过使用一个名为 moc
的 Qt 内部编译器实现的。在你的 Qt 代码实际传递给真正的 C++ 编译器之前,moc
工具会处理你的类头文件(在我们的案例中是 mainwindow.h
文件),以生成启用刚刚提到的 Qt 特定能力所需的代码。你可以在构建文件夹中找到这些生成的源文件。它们的名称以 moc_
开头。
你可以在 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 内部编译器。
[!note]
用户界面编译器(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
的类,其中有两个函数:setupUi
和 retranslateUi
。
setupUi
函数被自动添加到mainwindow.h
中的MainWindow
类构造函数中。该函数主要负责根据mainwindow.ui
文件中的设置来设置用户界面上的所有内容。- 本章稍后将介绍
retranslateUi
函数,以及在制作多语言 Qt 应用程序时如何使用该函数。
当所有 Qt 生成的文件都放入 Build
文件夹后,它们就会像其他 C++ 程序一样,被传递给 C++ 编译器进行编译,然后链接到 Build
文件夹中创建我们的应用程序。Windows 用户应注意,当你使用 Qt Creator 运行应用程序时,所有 DLL
文件(动态链接库(Dynamic Link Library)文件)路径都会被 Qt Creator 解析,但如果你试图从 Build
文件夹内运行程序,你将会看到多个错误信息,应用程序会崩溃或根本无法启动。你将在"调试和测试*"中学习如何解决这个问题,在那里你将学习如何正确地将你的应用程序交付给你的用户。
总结:
Qt中的moc
(Meta-Object Compiler)和uic
(User Interface Compiler)是特定的工具,它们在Qt应用程序的构建过程中起着重要的角色。
- moc (Meta-Object Compiler):
moc
是Qt的一个元对象编译器。它用于处理Qt的信号和槽机制,以及Qt中的一些其他元编程机制如属性系统、运行时类型信息和动态对象通信。- 当你在类声明中使用
Q_OBJECT
宏时,moc
会处理这个类,并生成一个附加的C++源文件,这个源文件包含了实现元对象所需的元信息和标准的信号和槽机制。
- uic (User Interface Compiler):
uic
是用于转换由Qt Designer创建的用户界面文件(.ui
文件)为C++代码的工具。- 当你设计一个界面并保存为
.ui
文件时,uic
会将这个文件转换成一个或多个头文件,这些文件将在编译时包含在你的应用程序中。
编译成最终应用程序的流程通常如下:
- 编写源代码:你写好 C++ 源代码和 Qt 特有的标记(如
Q_OBJECT
)。 - 预处理:运行
moc
来处理所有定义了Q_OBJECT
宏的类,生成含有元信息的 C++ 源文件。 - 设计UI:在 Qt Designer 中创建 GUI,并保存为
.ui
文件。 - UI编译:运行
uic
将.ui
文件转换为C++头文件。 - 资源编译:如果你使用了Qt资源系统,
rcc
(Qt Resource Compiler)会被用来将资源文件(如图片、翻译文件)编译成为应用程序可用的二进制形式。 - 编译:编译器(如g++, clang++)编译
源代码
和由moc
和uic
生成的代码。 - 链接:链接器将编译后的代码和所有相关的库(包括Qt库)链接在一起,生成最终的可执行文件。
设计模式 - Design pattern
非常有必要提醒我们自己设计模式存在的原因,以及为什么像 Qt 这样成功的框架会广泛使用不同的设计模式。首先,设计模式只是软件开发任务的众多解决方案之一,它不是唯一的解决方案;事实上,大多数时候它甚至不是最快的解决方案。然而,设计模式绝对是解决软件开发问题最有结构的方式,它有助于确保你对程序中添加的每件事都使用一些预定义的模板式结构。
设计模式有不同种类的问题的名称,例如创建对象、它们的运行方式、它们如何处理数据等。Eric Gamma、Richard Helm、Ralph E. Johnson 和 John Vlissides(被称为 四人帮)在他们的书 设计模式:可复用面向对象软件的基础 中描述了许多最广泛使用的设计模式,这本书被认为是计算机科学中设计模式的事实上的参考书。如果你不熟悉设计模式,你绝对应该花一些时间了解这个主题。学习软件开发中的**反模式(Anti-Pattern)**也是一个好主意。如果你是这个话题的新手,你可能会惊讶地发现一些反模式有多常见,确保你始终避免它们是至关重要的。
“反模式”(Anti-Pattern)是一种常见的但低效或有问题的设计、编程或管理实践,这些实践表面上看似提供了一个解决方案,但实际上可能会引入更多的问题。反模式通常是因为缺乏经验或对现有问题理解不足而产生的,而且它们可能会在团队或项目中不知不觉地得到推广。
反模式的关键特征包括:
- 反生产性:它们可能会阻碍过程的效率,导致产出质量下降。
- 反直觉:虽然表面上解决了问题,但实际上可能会掩盖根本问题,使得问题更加难以解决。
- 重复性:它们往往会在不同的项目或团队中重复出现,因为人们可能不认识到它们的负面影响。
- 教训性:识别和理解反模式可以作为学习工具,帮助人们避免在未来犯同样的错误。
一些常见的反模式例子包括:
- 金锤子(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 支持signal 和slot 机制,这是使用观察者设计模式的更方便,更高级的方法。 |
序列化 | 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 通过使用 *.qrc
文件(资源集合文件)来支持资源管理,这些文件实际上是包含了需要包含在我们应用程序中的资源文件信息的 XML
文件。让我们通过一个简单的例子来了解 Qt 资源系统的工作原理,我们将在我们的 Hello_Qt_openCV
应用程序中包含一个图标:
- 确保您已经在 Qt Creator 中打开了
Hello_Qt_OpenCV
项目。选择文件,然后新建文件或项目。在新文件窗口
中,确保您从左侧第二个列表中选择了 Qt,然后选择 Qt 资源文件。参考以下截图:
-
点击
Choose...
按钮,在下一个屏幕中,设置名称为resources
。路径默认应设置为您的项目文件夹,因此保持原样即可。点击下一步,然后完成。您将得到一个名为resources.qrc
的新文件添加到您的项目中。如果您在 Qt Creator 中打开这个文件(通过右键点击并选择在编辑器中打开),您将看到 Qt Creator 中的资源编辑器。 -
在这里,您可以使用
Add
按钮打开以下两个选项:-
添加文件
-
添加前缀
-
在这里,文件仅仅是您想要添加到项目中的任何文件。然而,前缀基本上是一个伪文件夹(或者您可以称之为容器),它包含了许多文件。注意,这并不一定代表您项目文件夹中的文件夹或子文件夹,而仅仅是一种表示方式和分组您的资源文件的方式。
- 首先点击
Add Prefix
然后在前缀字段中输入 images。 - 然后,点击
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 小部件样式,配有您可以复制和修改以适应自己需要的清晰示例,以及您希望在应用程序中拥有的外观和感觉。以下是我们刚刚使用的两个简单样式表的结果。如您所见,我们的浏览按钮现在有了不同的外观:
在前面的示例中,我们还避免设置适当的样式规则。**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;
}
如果你现在运行你的应用程序,你可以看到外观的变化。你还会注意到,即使关闭确认对话框小部件的样式也发生了变化,原因很简单,我们在其父窗口中设置了样式表。这里有一个截图:
不用说,你也可以通过保存样式表到一个文本文件中,并在运行时加载和设置它,正如我们将在本章后面构建我们的综合计算机视觉应用程序的基础时所做的那样。**你甚至可以存储一个默认样式表在应用程序内部,正如你在本章早些时候学到的(参考 Qt 资源系统),并默认加载它,也许如果计算机的特定位置存储了一个自定义文件则跳过它。**这样,你可以轻松地拥有可定制的应用程序。你甚至可以分担任务,要求专业设计师简单地为你提供一个样式表,以便你在你的应用程序中使用。这基本上展示了在 Qt 应用程序中样式设计的简便性。
为了获取更多关于样式表特定语法和帮助,始终最好关注 Qt Creator 帮助模式中的样式表语法文章,因为 Qt 样式表基本上是特定于 Qt 的,并且在某些情况下与标准 CSS 有所不同。
在本节中,你将学习如何使用 Qt 框架创建支持多种语言的应用程序。实际上,这一切都归结为一个非常易于使用的类。QTranslator
类是 Qt 主要负责处理输出(显示)文本国际化的类。你只需要确保以下几点:
-
在构建项目时使用默认语言(例如英语)。这意味着,对于显示的所有内容,简单地使用默认语言中的句子和单词。
-
确保==代码==中的所有字面句子,或者更具体地说,所有在选择不同语言时需要翻译的字面句子都用
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 会自动检测。
-
-
在
*.pro
文件中指定你的翻译文件的文件名。为此,你需要用TRANSLATIONS = translation_XX.ts
指定它们,就像在项目文件中的SOURCES
和HEADERS
一样。例如,如果你想在应用程序中添加俄语(
ru
)和中文(zh_CN
)翻译,将以下内容添加到你的项目(*.pro
)文件中:TRANSLATIONS = translation_ru.ts translation_zh_CN.ts
确保为每个翻译文件使用清晰的名称。尽管你可以随意命名它们,但最好是包含语言代码(
zh_CN
代表中文,de
代表德语等),如前面的示例所示。这也帮助Qt Linguist
工具(正如你稍后将学习的)知道翻译的目标语言。 -
使用 Qt Creater 内置的
lupdate
工具创建刚刚在.pro
文件中指定的.ts
文件(或如果它们已经存在,则更新它们)。为此需要点击主菜单中的Tools / External / Linguist / Update Translations (lupdate)
从 Qt Creator 中执行lupdate
。运行此命令后,如果你进入你的项目文件夹,你会注意到项目文件中之前指定的
.ts
文件现在已经创建。随着你的应用程序越来越大,定期运行
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
-
使用
Qt Linguist
工具翻译所有必需的字符串。它已经安装在你的计算机上,因为它是默认 Qt 安装的一部分。简单地选择
File / Open
并从你的项目文件夹中选择所有刚刚创建的.ts
文件并打开它们。如果你已经按照所有指示操作到现在,那么在 Qt Linguist 中打开.ts
文件后,你应该会看到以下界面:Qt Linguist 允许快速轻松地翻译你项目中的所有可翻译元素。只需为所有显示的语言编写每个项目的翻译,并使用顶部的工具栏将它们标记为
Done
。确保在退出 Qt Linguist 工具之前保存。 -
使用翻译好的
.ts
文件创建.qm
文件,这些文件是压缩的二进制 Qt 语言文件。为此,你需要回到 Qt Creater,运行lrelease
工具。使用lrelease
与你在前面步骤中学到的lupdate
类似: -
将
.qm
文件(二进制语言文件)添加到你的应用程序资源中。你已经学习了如何使用 Qt 资源系统。简单地创建一个名为
translations
的新前缀,并在该前缀下添加新创建的.qm
文件。如果正确完成,你的项目中应该有以下内容: -
你现在可以开始使用
QTranslator
类在你的应用程序中拥有多种语言,并且在运行时切换语言。让我们再次回到我们的示例项目
Hello_Qt_OpenCV
。在应用程序中使用翻译器有不同的方法,但现在我们将从最简单的方法开始。在你的mainwindow.h
文件中添加#include <QTranslator>
头文件,并在MainWindow
类中定义两个私有的QTranslator
对象,如下所示:#include <QTranslator> ... private: QTranslator* translator_ru; QTranslator* translator_zh_CN;
-
在
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
-
现在,是时候在我们的项目中添加一个主菜单,并允许用户切换语言了。你可以通过在 Qt Creator 设计模式下右键点击窗口并选择创建菜单栏来做到这一点。然后,在顶部菜单栏中添加一个名为
Language
的项目。通过简单地点击并输入以下内容来添加三个子项目:在设计器的底部,你可以找到操作编辑器。显然,你现在在这里有三个条目,这些条目是当你创建主菜单时自动创建的。它们中的每一个都对应于你在主菜单中输入的每一个语言名称。
-
通过信号和槽实现语言配置安装(注意
qApp->installTranslator
后,界面是不会变化的。还需要在 13 步中刷新界面)右键点击中文并选择转到槽,然后从列表中选择
trigger()
并点击OK
。为actionChinese
和actionRussia
对象的触发槽编写以下代码行: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); }
-
回到默认语言。对
actionEnglish
对象做同样的处理。这次,你需要从你的应用程序中移除翻译器,因为英语是我们应用程序的默认语言:void Hello_Qt_OpenCV::on_actionEnglish_triggered() { qApp->removeTranslator(this->translator_zh_CN); qApp->removeTranslator(this->translator_ru); }
但是这不是一个合理的解决方案,以下是两种更好地解决方案:
- 重新加载默认翻译文件:如果你有默认语言的翻译文件(即使它可能只是原始文本的直接复制),你可以通过加载这个默认语言的翻译文件来恢复默认语言。这类似于加载任何其他语言,只不过翻译文件中的文本是你的默认文本
- 使用空的翻译文件:理论上,你可以创建一个没有任何翻译条目的翻译文件,并加载它,这样由于没有任何翻译应用,应用程序将显示源代码中的文本,即默认语言
-
好吧,我们现在已经将翻译放到(install)我们的 Qt 应用程序中了,我们还需要在更改语言的时候刷新 UI。为此,我们需要使用
QMainWindow
类的changeEvent
。每次使用前面的installTranslator
和removeTranslator
函数安装或移除翻译器时,都会向应用程序中的所有窗口发送语言更改事件。要捕获此事件,并确保我们的窗口在语言更改时重新加载,我们需要在程序中重写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 项目,然后添加头文件,
然后添加C++头文件
,输入CvPluginInterface
作为文件的名称,并继续直到您处于代码编辑模式。将代码更改为以下内容:
/**
* 插件接口 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++编程错误时,你甚至不会收到警告。
虚析构函数的作用:当你有一个基类指针指向一个派生类对象时,如果基类的析构函数不是虚函数,那么当通过基类指针删除对象时,只有基类的析构函数会被调用。这将导致派生类中为释放资源而定义的析构逻辑不会执行,可能导致资源泄露。而将基类的析构函数声明为虚函数后,删除对象时会首先调用派生类的析构函数,然后再调用基类的析构函数,从而保证了资源的正确释放。
让我给你举一个具体的例子来展示如果基类的析构函数不是虚的,可能会导致什么样的问题。
假设我们有一个基类
Base
和一个从Base
继承的派生类Derived
。Derived
类有自己的资源管理,比如动态分配的内存。如果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; } --- 输出: 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"; } // 现在是虚的 }; --- 输出: Base Constructor Derived Constructor Derived Destructor // 可以看到子类的析构函数被执行了 Base Destructor 进程已结束,退出代码为 0这样,当我们通过基类指针删除派生类对象时,析构函数的调用会遵循动态绑定,即先调用
Derived
的析构函数,然后调用Base
的析构函数(构造-爸爸盖房子,析构-孩子拆房子),从而确保所有资源都被正确管理和释放。多态的重要性:在C++中,多态允许我们通过基类的指针或引用来调用派生类的方法。如果基类将要被用作多态基类(即通过基类的指针或引用来访问派生类对象),则必须为这个基类提供虚析构函数。这样做确保了当通过基类的指针删除派生类对象时,能够正确地调用派生类的析构函数,避免内存泄漏。
因此,我们的插件接口包括:
- 一个名为
description()
的函数,旨在返回任何插件的描述和有关它的有用信息 - 一个名为
processImage()
的函数,该函数将OpenCV的Mat
类作为输入并返回一个作为输出。显然,在这个函数中,我们期望每个插件执行某种图像处理、滤镜等,并给出结果。
之后,我们使用Q_DECLARE_INTERFACE
宏将我们的类定义为接口。不包含这个宏,Qt将无法将我们的类识别为插件接口。CVPLUGININTERFACE_IID
应该是一个独一无二的字符串,采用类似包名格式,但你基本上可以根据自己的偏好进行更改。
确保将cvplugininterface.h
文件保存到您选择的任何位置,然后关闭它。我们现在将创建一个使用此接口的插件。让我们使用我们之前在第3章创建我们的第一个Qt和OpenCV项目中看到的OpenCV函数之一:medianBlur
。
我们现在将创建一个名为median_filter_plugin
的插件,该插件使用我们的CvPluginInterface
接口类。首先从主菜单中选择文件
,然后新建文件或项目
。然后,选择库
和C++库
,如下图所示:
确保选择了共享库 (Shared Lihrary)
作为类型,然后输入MedianFilterPlugin
作为名称并点击下一步
。选择桌面作为套件类型并点击前进。在选择所需模块
页面,确保只选中了QtCore
并继续点击下一步
(最终点击完成
),直到你进入Qt Creator的代码编辑器。
我们基本上创建了一个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库类定义的样子。然而,正是我们的添加使它变成了一个有趣的插件。让我们回顾前面的代码,看看实际上添加了什么:
-
首先,我们包含了
cvplugininterface.h
头文件。 -
然后,我们确保
Median_filter_plugin
类继承了QObject
和CvPluginInterface
。 -
之后,我们添加了Qt所必须的宏,以便我们的库被识别为插件。这意味着以下三行代码,
// 支持信号和槽 Q_OBJECT // 在插件类定义中声明元数据,IID(Interface Identifier 接口标识符) Q_PLUGIN_METADATA(IID "com.amin.cvplugininterface") // 声明插件中实现的接口 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()
,需要声明插件中实现的接口
- 首先是
-
然后,我们为我们的类添加了
description
和processImage
函数的定义。这是我们真正定义插件做什么的地方,与仅仅有声明而没有实现的接口类相反。 -
最后,我们可以添加必要的更改和实际实现到
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); }
我们刚刚添加了类析构函数、
description
和processImage
函数的实现。如你所见,description
函数返回有关插件的有用信息,在这种情况下没有复杂的帮助页面,只是几句话;processImage
函数简单地将medianBlur
应用于图像
现在你可以在项目上右键点击并选择重新构建
,或者从主菜单的构建
项中选择。这将创建一个插件文件,我们将在下一节中使用,通常位于与项目同级的 build-*
文件夹下。
插件文件的扩展名可能因操作系统而异。例如,在Windows上应该是.dll
,在macOS和Linux上是.dylib
或.so
等。
现在,我们将使用上一节中创建的插件。首先,创建一个新的 Qt Widgets Application 项目。我们将其命名为 Plugin_User
。项目创建后,首先将 OpenCV 框架添加到 *.pro 文件中(你已经见过很多次了),然后开始创建类似于下面的用户界面:
- 显然,你需要修改
mainwindow.ui
文件,设计它使其看起来像下图,并设置所有对象名称,如下图所示:
确保使用与前图中相同类型的布局。
-
接下来,将
cvplugininterface.h
文件添加到此项目的文件夹中,然后,使用“添加现有文件”选项,像你创建插件时那样将其添加到项目中。 -
现在,我们可以开始编写我们的用户界面代码以及加载、检查和使用插件所需的代码。首先,向
mainwindow.h
文件添加所需的头文件,如下所示:#include <QDir> #include <QFileDialog> #include <QMessageBox> #include <QPluginLoader> #include <QFileInfoList> #include "opencv2/opencv.hpp" #include "cvplugininterface.h"
-
然后,在 MainWindow 类的 private 成员中,就在
};
之前,添加一个私有方法:void getPluginsList();
-
接下来,切换到
mainwindow.cpp
并在文件顶部添加以下定义,紧跟在任何现有的 #include 行之后:void getPluginsList();
-
然后,在
mainwindow.cpp
中添加以下函数,这基本上是getPluginsList
函数的实现:void MainWindow::getPluginsList() { QString findDir = qApp->applicationDirPath() + FILTERS_SUBFOLDER; qDebug() << "Search plugin in dir:" << findDir; QDir filtersDir(findDir); 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->lwFilters->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->lwFilters->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
函数从文件夹中提取QFileInfoList
。QFileInfoList
类本身基本上是一个包含 QFileInfo 项的 QList 类(QList<QFileInfo>
),每个 QFileInfo 项提供有关磁盘上文件的信息。在这种情况下,每个文件将是一个插件。 -
之后,通过在 foreach 循环中迭代文件列表,它检查插件文件夹中的每个文件,以确保只接受插件(库)文件,使用以下函数:
QLibrary::isLibrary
-
每个通过前一步骤的库文件接着被检查以确保它与我们的插件接口兼容。我们不会仅仅让任何库文件被接受为插件,因此我们使用以下代码进行此目的:
dynamic_cast<CvPluginInterface*>(pluginLoader.instance())
-
如果一个库通过了上一步的测试,则被视为正确的插件(与 CvPluginInterface 兼容),添加到我们窗口中的列表小部件中,然后卸载。我们可以简单地重新加载并在需要时使用它。
-
在每一步,如果有问题,使用 QMessageBox 向用户显示有用的信息。此外,如果列表为空,意味着没有可用的插件,窗口上的小部件被禁用,应用程序不可用。
-
-
不要忘记从 MainWindow 构造函数中调用此函数,紧跟在
setupUi
调用之后。 -
我们还需要为
inputImgButton
编写代码,该按钮用于打开图像文件。代码如下:
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);
}
}
我们之前已经见过这段代码,它不需要解释。它简单地允许你打开一个图像文件,并确保它被正确选择。
- 现在,我们将编写
helpButton
的代码,该按钮将显示插件中description
函数的结果。
void MainWindow::showPluginDescription()
{
if(ui->lwFilters->currentRow() <= 0)
{
QMessageBox::warning(this, tr("Warning"), QString(tr("First select a filter plugin from the list.")));
return;
}
QString pluginPath = qApp->applicationDirPath() + FILTERS_SUBFOLDER + ui->lwFilters->currentItem()->text();
qDebug() << "Plugin path" << pluginPath;
QPluginLoader pluginLoader(pluginPath);
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->lwFilters->currentItem()->text()));
}
}
我们使用 QPluginLoader
类从列表中正确加载插件,然后使用 instance
函数获取其实例,最后,我们将通过接口调用插件中的函数。
-
同样的逻辑也适用于
filterButton
。唯一不同的是,这次我们将调用实际的过滤函数:void MainWindow::useFilterOnImage() { if(ui->lwFilters->currentRow() < 0 || ui->leInputImage->text().isEmpty()) { QMessageBox::warning(this, tr("Warning"), QString(tr( "First select a filter plugin from the list." ))); return; } QPluginLoader pluginLoader(qApp->applicationDirPath() + FILTERS_SUBFOLDER + ui->lwFilters->currentItem()->text()); CvPluginInterface *plugin = dynamic_cast<CvPluginInterface*>(pluginLoader.instance()); if(nullptr == plugin) { QMessageBox::warning(this, tr("Warning"), QString(tr("Make sure plugin %1 exists and is usable.")).arg(ui->lwFilters->currentItem()->text())); } if(QFile::exists(ui->leInputImage->text())) { using namespace cv; Mat inputImage, outputImage; inputImage = imread(ui->leInputImage-> 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->leInputImage->text())); } }
[!note]
务必始终通过
QMessageBox
或其他信息提示机制让用户知晓当前状态及可能发生的问题。如你所见,这些提示代码通常比实际功能代码还要多,但这对于避免应用程序崩溃至关重要。默认情况下,Qt 不支持异常处理,它信任开发者会通过足够的if
和else
指令来处理所有可能的崩溃场景。前述代码示例中另一个重要注意事项是tr
函数——请始终对字面量字符串使用它,这将便于后续实现多语言支持。即使你不打算支持多语言,养成在字面量字符串中添加tr
函数的习惯也大有裨益,且不会有任何副作用。现在我们可以运行
Plugin_User
应用程序了。若立即运行,会看到我们预设的错误提示,警告当前没有可用插件。要使Plugin_User
正常工作,需执行以下步骤:- 在
Plugin_User
项目的构建目录(即生成可执行文件的目录)中创建名为filter_plugins
的文件夹 - 复制已构建的插件文件(即
median_filter_plugin
项目构建目录中的库文件)到上一步创建的filter_plugins
文件夹。如前所述,插件文件与可执行程序类似,其扩展名取决于操作系统。
此时再次运行
Plugin_User
,一切应正常工作:你将在列表中看到该插件,可选中它后点击帮助按钮查看信息,或点击过滤按钮对图像应用插件中的滤波器。效果如下图所示:尝试创建另一个名为
gaussian_filter_plugin
的插件,完全遵循median_filter_plugin
的创建流程,但这次需使用第2章《创建首个Qt与OpenCV项目》中介绍的gaussianBlur
函数。构建后将其放入filter_plugins
文件夹并重新运行Plugin_User
。你还可以放入随机库文件(及其他非库文件)来测试应用程序在这些场景下的表现。需特别注意:切勿将Debug模式构建的插件用于Release模式构建的应用程序,反之亦然。插件加载还有其他重要规则:
- 高版本Qt构建的插件不能用于低版本Qt构建的应用程序
- 低主版本号Qt构建的插件不能用于高主版本号Qt构建的应用程序
关于插件使用的最新信息,请始终参考Qt文档中的《部署插件》文章或Qt Creator帮助模式。
- 在
本章所学的一切都是为了让你能够开始构建一个功能全面的计算机视觉应用程序,该程序将具备以下特性:
- 通过插件扩展功能
- 使用Qt样式表自定义界面风格
- 支持多语言
从现在开始,我们将综合本章及前几章的知识点来构建应用程序的基础框架:
- 用户偏好设置持久化:通过已学习的
QSettings
类实现配置的保存与加载 - 动态主题加载:默认采用系统原生风格,允许用户在设置页面选择主题。主题实际上是存储在应用程序同级
themes
文件夹中的Qt样式表文件(扩展名为.thm
),运行时动态加载 - 多语言支持:将Qt二进制语言文件存放在应用程序同级的
languages
文件夹中。优先加载系统默认语言(若有对应翻译文件),否则回退到默认英语。用户可在运行时通过设置页面切换语言 - 图像处理核心能力:构建支持单幅图像和视频帧处理的计算机视觉应用。插件接口设计类似本章的
CvPluginInterface
,要求插件存放在cvplugins
文件夹中
在构建过程中还需前瞻性考虑以下技术挑战:
- 多媒体输入处理:需处理来自文件、摄像头、网络流等多种图像/视频源(详见第4章《Mat与QImage》)
- 可视化交互工具:计算机视觉应用离不开强大的图像查看与操作工具(详见第5章《图形视图框架》)
- 视频处理扩展:当前插件接口无法满足连续帧处理需求,需在第8章《多线程》)学习Qt并行处理机制后,开发支持多线程的视频处理插件接口(最终应用于第9章《视频分析》)
现在请按以下步骤开始构建:
- 使用Qt Creator创建名为
Computer_Vision
的Qt Widgets Application项目 - 建议先自行实现基础功能(主题/语言/插件支持),这将是很好的练习
- 后续章节将逐步扩展该应用,到第5章结束时,可下载完整基础项目代码
基础项目将包含以下增强功能:
-
能加载/显示带GUI插件的
MainWindow
类 -
扩展版插件接口(在原有基础上新增):
QString getTitle() const; // 获取插件标题 QString getHelp() const; // 获取帮助信息 QWidget* getGUI() const; // 获取插件专属GUI容器 int getType() const; // 判断插件类型(图像处理型/GUI信息型) void processImage(cv::Mat& in, cv::Mat& out); // 图像处理接口
本章总结
在开发者职业生涯或学术研究中,"可持续性"将成为高频关键词。本章旨在传授构建可持续应用程序的基础理念,特别是基于Qt和OpenCV的计算机视觉应用。你现在已经掌握了:
- 插件化开发:能够创建可通过第三方插件(或自主开发)扩展功能的应用,且无需重新编译主程序
- 界面定制:使用Qt样式表灵活定制应用视觉风格
- 多语言支持:构建支持国际化部署的Qt应用
虽然本章内容密集,但付出必有回报。若你完成了所有示例实践,现在应该已经掌握Qt跨平台开发的核心技术:
- Qt样式系统:通过样式表开发美观界面
- 国际化必备技能:在应用商店全球分发的时代,多语言支持已成为刚需而非可选功能
- 插件体系实战:通过完整案例掌握了插件开发与集成的全流程
在第4章《Mat与QImage》中,你将深入学习:
- OpenCV
Mat
与QtQImage
这两个核心图像处理类 - 多种图像读写方式(文件/摄像头等输入源)
- 图像格式相互转换技术
- 在Qt控件中优雅显示图像(告别OpenCV简陋的
imshow
窗口)
在第3章《创建全面的 Qt+OpenCV 项目》中,我们学习了创建全面可持续应用程序的基本规则,该程序可以通过 Qt 的插件系统实现美观界面、多语言支持和轻松扩展。现在我们将通过了解负责处理计算机视觉数据类型的类和结构,进一步扩展对计算机视觉应用基础知识的理解。学习 OpenCV 和 Qt 框架所需的基本结构和数据类型,是理解底层计算机视觉函数在应用程序中执行时如何处理这些数据的第一步。OpenCV 是追求速度和性能的计算机视觉框架,而 Qt 则是拥有大量类和功能的应用程序开发框架。因此它们都需要定义良好的类和结构来处理计算机视觉应用中需要被处理、显示甚至保存或打印的图像数据。保持对 Qt 和 OpenCV 现有结构的细节了解始终是良好实践。
您已经使用过 OpenCV 的 Mat
类来快速读取和处理图像。正如本章将学习的,尽管 Mat
是 OpenCV 中负责处理图像数据的主要类(至少传统上是这样),但 Mat
类有几个变体非常有用,其中一些甚至是后续章节中特定函数所必需的。在 Qt 框架中情况类似,虽然 QImage
是 Qt 中处理图像数据的主要类,但还有一些其他类(有些名称非常相似)被用来支持计算机视觉和图像数据、视频等的处理。
本章我们将从最重要的 OpenCV 类 Mat
开始,然后介绍其不同变体(有些是 Mat
的子类),最后介绍 OpenCV 3 新增的 UMat
类。我们将学习使用新的 UMat
类(实际上与 Mat
兼容)相比 Mat
类的优势。接着我们将转向 Qt 的 QImage
类,学习如何通过在这两种数据类型之间转换来传递图像数据。我们还将学习 QPixmap
、QPainter
和其他几个 Qt 类,这些是所有进入计算机视觉领域开发者必须掌握的类。
最后,我们将学习 OpenCV 和 Qt 框架从文件、摄像头、网络源等读取、写入和显示图像及视频的多种方式。到本章结束时您将明白,根据所需的计算机视觉任务选择最合适的类始终是最佳实践,因此我们应该充分了解处理图像数据输入/输出时的不同选项。
本章涵盖的主题包括:
Mat
类及其子类和新的UMat
类介绍QImage
和计算机视觉中主要 Qt 类的介绍- 如何读取、写入和显示图像及视频
- 如何在 Qt 和 OpenCV 框架之间传递图像数据
- 如何在 Qt 中创建自定义控件并使用
QPainter
进行绘制
在前几章中您已简要接触过 OpenCV 框架的 Mat
类,现在我们将进行更深入的探讨。Mat
类(其名称源于矩阵)是一个 n 维数组,能够存储和处理单通道或多通道的不同数学数据类型。为简化理解,让我们从计算机视觉的角度看看图像的本质:计算机视觉中的图像是一个由像素组成的矩阵(即二维数组),具有特定的宽度(矩阵列数)和高度(矩阵行数)。此外:
- 单通道数值:灰度图像中的每个像素用单个数字表示(单通道)
- 数值范围:最小值(通常为 0)代表黑色,最大值(通常为 255,即单字节最大值)代表白色,中间值对应不同灰度等级
下图为放大后的灰度图像局部,每个像素标注了对应的灰度值:
- 三通道结构:标准 RGB 彩色图像中每个像素包含红、绿、蓝三个分量(三通道)
- 通道组合:三个通道的数值组合决定最终颜色
下图展示了放大后的彩色图像局部,每个像素标注了对应的 RGB 分量值:
一般来说,介绍 Mat 类和 OpenCV 函数的数学细节并不是我们目前最感兴趣的内容,因此我们只需进行介绍即可,接下来我们将更多地关注 OpenCV 中 Mat 类及其底层方法的使用。
构造 Mat
类可以通过多种方式实现。截至本书撰写时,Mat
类拥有超过二十种不同的构造函数。其中一些是便捷构造方法,另一些则是为了创建三维及更高维数组而设计的。以下是使用最广泛的部分构造函数及示例:
创建一个 10x10 矩阵,每个元素为单通道 8 位无符号整型(即字节):
Mat matrix(10, 10, CV_8UC(1));
创建相同矩阵并将所有元素初始化为 0:
Mat matrix(10, 10, CV_8UC(1), Scalar(0));
上述代码中构造函数的第一个参数是矩阵的行数,第二个参数是列数。第三个参数(极为重要)将类型、位深和通道数整合为一个宏。该宏的格式及可用值如下:
CV_<bits><type>C(<channels>)
宏的各个部分含义如下:
<bits>
可替换为:
- 8:用于无符号/有符号整型
- 16:用于无符号/有符号整型
- 32:用于无符号/有符号整型及浮点型
- 64:用于无符号/有符号浮点型
<type>
可替换为:
- U:无符号整型
- S:有符号整型
- F:有符号浮点型
理论上 <channels>
可为任意值,但常规计算机视觉函数/算法中通道数通常不超过四。
若通道数 ≤4 时可省略括号。当通道数为 1 时,可省略 C
及通道数。建议始终使用标准格式以保持代码可读性和一致性。
创建边长为 10 的立方体(三维数组),元素为双精度浮点型(64 位)双通道,所有值初始化为 1.0:
int sizes[] = {10, 10, 10};
Mat cube(3, sizes, CV_64FC(2), Scalar::all(1.0));
可通过 create
方法修改 Mat
的尺寸和类型:
Mat matrix;
// ...
matrix.create(10, 10, CV_8UC(1));
原 Mat
内容将被完全清除(内存安全释放回操作系统),新建空白矩阵。
可创建作为其他 Mat
子集的 感兴趣区域(ROI, region of interest),常用于对图像局部进行独立操作(如滤波)。以下示例创建从图像 (25,25) 坐标起始的 50x50 像素 ROI:
Mat roi(image, Rect(25,25,50,50));
在 OpenCV 中指定 Mat
尺寸时通常采用 (行,列)/(高,宽) 顺序,这与许多框架的 (宽,高) 习惯不同。若需使用后者格式,可通过 Size
类创建 Mat
。
本节示例中除非特别说明,均假设 image
变量(Mat
类型)通过 imread
函数从之前章节的测试图像加载。这将确保 Mat
类的信息完整性,imread
等函数将在本章后续详细讲解。
通过下图可以更直观理解 OpenCV 中 Mat
类的 ROI、尺寸与坐标系统:
- 原点位置:图像左上角坐标为 (0,0)
- 右下角坐标:若图像宽度为
cols
,高度为rows
,则右下角坐标为 (cols-1
,rows-1
)
使用 Rect
类创建 ROI 时需提供左上角坐标及宽度/高度。注意:ROI 是浅拷贝,修改 ROI 内容将直接影响原图像。如需独立副本需使用 clone()
方法:
Mat imageCopy = image.clone();
以下代码示例选择图中 ROI 区域(假设原图已通过 imread
加载到 image
),并将该区域像素设为黑色:
Mat roi(image, Rect(500, 138, 65, 65));
roi = Scalar(0);
通过以下函数可提取特定行/列(操作方式与 ROI 类似):
Mat r = image.row(0); // 提取第 0 行
Mat c = image.col(0); // 提取第 0 列
使用 rowRange
/colRange
可提取行/列范围。以下代码在图像中心生成 20 像素宽的十字线:
Mat centralRows = image.rowRange(image.rows/2 - 10, image.rows/2 + 10);
Mat centralColumns = image.colRange(image.cols/2 - 10, image.cols/2 + 10);
centralRows = Scalar(0);
centralColumns = Scalar(0);
执行结果示例:
使用 locateROI()
可获取 ROI 在父图像中的位置信息:
Mat centralRows = image.rowRange(image.rows/2 - 10, image.rows/2 + 10);
Size parentSize;
Point offset;
centralRows.locateROI(parentSize, offset);
int parentWidth = parentSize.width;
int parentHeight = parentSize.height;
int x = offset.x;
int y = offset.y;
执行后:
parentWidth
:父图像宽度(对应cols
)parentHeight
:父图像高度(对应rows
)x
,y
:ROI 左上角在父图像中的坐标
Mat
类包含多个信息性属性和函数,可用于获取每个 Mat
类实例的详细信息。这些成员能够提供关于像素、通道、色彩深度、宽度、高度等详细数据,主要包括:
depth
:表示Mat
类的深度值。深度值对应矩阵元素的类型和位数,可能取值为:CV_8U
:8位无符号整数CV_8S
:8位有符号整数CV_16U
:16位无符号整数CV_16S
:16位有符号整数CV_32S
:32位有符号整数CV_32F
:32位浮点数CV_64F
:64位浮点数
channels
:表示Mat
类每个元素的通道数。对于标准图像,通常为3通道。type
:表示Mat
类的类型标识符,与创建Mat
时使用的类型常量一致。cols
:对应矩阵的列数(即图像宽度)。rows
:对应矩阵的行数(即图像高度)。elemSize
:获取每个元素的字节大小(包含所有通道)。elemSize1
:获取单通道元素的字节大小。例如三通道图像中,elemSize1 = elemSize / 3
。empty
:若矩阵无元素则返回true
,否则返回false
。isContinuous
:检查矩阵元素是否连续存储。单行矩阵总是连续的。
使用 create
函数创建的 Mat
类始终是连续的。需要注意的是,Mat
类的二维表示在此情况下通过 step
值处理。这意味着在连续存储的元素数组中,每 step
个元素对应二维表示中的一行。
isSubmatrix
:如果当前Mat
类是另一个Mat
类的子矩阵则返回true
。在前面的示例中,所有使用其他图像创建 ROI 的情况,该属性将返回true
,而父Mat
类中会返回false
。total
:返回Mat
类中元素的总数。例如对于图像,该值等于宽度乘以高度。step
:返回Mat
类中单步对应的元素数量。例如在标准图像(非连续存储)中,step
包含Mat
类的宽度(即cols
)。
除了信息性成员外,Mat
类还包含多个用于访问(及操作)其单个元素(或像素)的函数,主要包括:
-
at
:这是一个模板函数,可用于访问Mat
类中的元素。特别适用于访问图像中的元素(像素)。例如,假设我们有一个标准三通道彩色图像存储在名为image
的Mat
类中(类型为CV_8UC(3)
),则可通过以下代码访问位置 (X,Y) 的像素并设置其颜色值为 C:image.at<Vec3b>(X,Y) = C;
OpenCV 提供了 Vec
(向量)类及其变体以简化数据访问和处理。您可以通过以下 typedef
创建自定义命名的 Vec
类型:
typedef Vec<Type, C> NewType;
例如,在前述代码中,您可以自定义一个 3 字节向量(如 QCvVec3B
)替代 Vec3b
:
typedef Vec<quint8,3> QCvVec3B;
您也可以直接使用 OpenCV 预定义的以下 Vec
类型(配合 at
函数使用):
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;
typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;
typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;
begin
和end
:可通过类似 C++ STL 的迭代器访问Mat
类中的元素。forEach
:可并行地对Mat
类的所有元素运行指定函数。需提供函数对象、函数指针或 lambda 表达式。
注意:lambda 表达式仅适用于 C++11 及更高版本。若尚未升级,这是迁移到 C++11+ 的重要理由。
以下三个示例代码使用前述访问方法实现相同目标:将图像每个像素值除以 5 使其变暗。
方法 1:使用 at
函数:
for(int i=0; i<image.rows; i++)
{
for(int j=0; j<image.cols; j++)
{
image.at<Vec3b>(i, j) /= 5;
}
}
方法 2:使用 STL 风格迭代器(begin
和 end
函数):
MatIterator_<Vec3b> it_begin = image.begin<Vec3b>();
MatIterator_<Vec3b> it_end = image.end<Vec3b>();
for( ; it_begin != it_end; it_begin++)
{
*it_begin /= 5;
}
方法 3:使用 forEach
函数(配合 lambda 表达式):
image.forEach<Vec3b>([](Vec3b &p, const int *)
{
p /= 5;
});
以下是三种代码处理后生成的相同暗化效果图像:
如您所见,Mat
类包含大量方法,它是 OpenCV 图像处理的核心基础结构。除了已介绍的属性和函数外,以下函数也需要掌握:
adjustROI
:用于调整子矩阵(严格来说是 ROI 矩阵)的尺寸。clone
:广泛用于创建Mat
类的深拷贝。典型场景:处理图像时保留原始副本用于后续对比。convertTo
:修改Mat
类的数据类型,可选进行数值缩放。copyTo
:复制全部或部分图像到另一个Mat
类。ptr
:获取指针以访问矩阵数据。通过重载版本可获取特定行或位置的指针。release
:在Mat
析构函数中调用,负责内存清理。reserve
:为指定行数预分配内存。reserveBuffer
:类似reserve
,但按字节数预分配内存。reshape
:改变通道数以重构矩阵数据。例如将单通道(元素为Vec3b
)转为三通道矩阵,此时行数会乘3。后续可通过转置调整行列关系。resize
:修改矩阵的行数。setTo
:将全部或部分元素设为指定值。
Mat
类还支持以下矩阵运算方法:
cross
:计算两个三元矩阵的叉积diag
:提取矩阵对角线dot
:计算两个矩阵的点积eye
(静态函数):创建单位矩阵inv
:生成逆矩阵mul
:执行矩阵元素级乘/除ones
(静态函数):创建全1矩阵t
:生成转置矩阵。注意图像转置等效于镜像+90度旋转(见下图)zeros
:创建全零矩阵(即纯黑图像)
下图中左侧为原始图像,右侧为转置结果(左右互为转置关系),展示了 t
函数的效果:
重要说明:
Mat
类支持标准算术运算。例如前文通过遍历像素实现暗化效果的操作,可直接简化为:
Mat darkerImage = image / 5; // 或 image * 0.2
Mat_<Tp>
类是 Mat
类的模板子类,其成员与 Mat
完全一致,但在编译时已知矩阵类型(或图像元素类型)的场景下非常有用。相较于 Mat
的 at
函数,它提供了更友好(更易读)的访问方式。示例如下:
Mat_<Vec3b> imageCopy(image); // image 是 Mat 类型
imageCopy(10, 10) = Vec3b(0,0,0); // imageCopy 可使用 () 运算符
要注意类型匹配,可以将 Mat_<Tp>
类传递给任何接受 Mat
类的函数而不会出错。
Matx
类专用于小型矩阵(编译时已知类型、宽度和高度)。其方法与 Mat
类似,支持矩阵运算。但通常建议使用 Mat
类代替 Matx
,因其提供更高的灵活性和功能。
UMat
类是 OpenCV 3.0+ 新增的类(类似 Mat
)。其优势取决于运行平台是否支持 OpenCL 加速层。简而言之:若平台支持 OpenCL,使用 UMat
会调用底层 OpenCL 指令(需函数已实现相关支持),从而提升计算性能;否则 UMat
会自动回退为 Mat
并调用 CPU 实现。这种统一抽象机制("U" 即 Unified)简化了高性能 OpenCL 的使用,不同于旧版 OpenCV 需通过 ocl
命名空间单独调用。
在支持 OpenCL 的 CPU 密集型函数中,建议优先使用 UMat
。需注意 Mat
与 UMat
的显式转换方法:
Mat::getUMat(access_flag) // 将 Mat 转为 UMat
UMat::getMat(access_flag) // 将 UMat 转为 Mat
访问标志 access_flag
可选值:
ACCESS_READ
ACCESS_WRITE
ACCESS_RW
ACCESS_FAST
本书将尽量交替使用 Mat
和 UMat
。随着 OpenCL 支持的扩展,熟悉 UMat
的使用将带来显著优势。
您会注意到大多数 OpenCV 函数接收这些参数类型而非直接使用 Mat
及其类似类型。这些是代理数据类型,用于提升可读性和数据类型兼容性。这意味着您可以将以下任一类型传递给接受这些参数的 OpenCV 函数:
- Mat
- Mat_<T>
- Matx<T, m, n>
- std::vector<T>
- std::vector<std::vector<T> >
- std::vector<Mat>
- std::vector<Mat_<T> >
- UMat
- std::vector<UMat>
- double
注意:OpenCV 将标准 C++ 向量(std::vector
)视为 Mat
类处理,因其底层数据结构具有相似性。
重要原则:永远不要显式创建
InputArray
、OutputArray
或InputOutputArray
。直接传递上述支持的类型即可正常工作。
了解 Mat
类后,我们可学习如何读取图像并用其填充 Mat
类以便后续处理。如先前章节所见,imread
函数可用于从磁盘读取图像:
Mat image = imread("c:/dev/test.jpg", IMREAD_GRAYSCALE | IMREAD_IGNORE_ORIENTATION);
imread
接收两个参数:
std::string
类型文件路径ImreadModes
枚举标志
若读取失败,返回空 Mat
类(data == NULL
);成功则返回包含图像像素的 Mat
类,类型和色彩模式由第二个参数指定。支持读取以下图像格式:
- Windows 位图:
*.bmp
,*.dib
- JPEG 文件:
*.jpeg
,*.jpg
,*.jpe
- JPEG 2000 文件:
*.jp2
- 便携式网络图形:
*.png
- WebP:
*.webp
- 便携式图像格式:
*.pbm
,*.pgm
,*.ppm
,*.pxm
,*.pnm
- Sun 光栅图:
*.sr
,*.ras
- TIFF 文件:
*.tiff
,*.tif
- OpenEXR 图像文件:
*.exr
- Radiance HDR:
*.hdr
,*.pic
- GDAL 支持的栅格/矢量地理空间数据
示例中使用的标志组合:
IMREAD_GRAYSCALE | IMREAD_IGNORE_ORIENTATION
表示以灰度模式加载图像,并忽略 EXIF 中的方向信息。
OpenCV 还支持读取多页图像文件,需使用 imreadmulti
函数:
std::vector<Mat> multiplePages;
bool success = imreadmulti("c:/dev/multi-page.tif", multiplePages,
IMREAD_COLOR);
此外,可通过 imdecode
从内存缓冲区读取图像(适用于非磁盘存储或网络流场景):
// 伪代码示例:从内存缓冲区读取
std::vector<uchar> buffer = ...; // 图像数据缓冲区
Mat image = imdecode(buffer, IMREAD_COLOR);
imdecode
用法与 imread
类似,区别在于需传入数据缓冲区而非文件路径。
OpenCV 的 imwrite
函数可用于将图像写入磁盘文件。该函数通过文件扩展名确定输出格式。要自定义压缩率等参数,需使用 ImwriteFlags
、ImwritePNGFlags
等标志。以下示例演示如何将图像写入 JPG 文件并启用渐进模式(低质量/高压缩率):
std::vector<int> params;
params.push_back(IMWRITE_JPEG_QUALITY); // 设置 JPG 质量
params.push_back(20); // 质量值 (0-100)
params.push_back(IMWRITE_JPEG_PROGRESSIVE); // 启用渐进模式
params.push_back(1); // 1=启用,0=禁用
imwrite("c:/dev/output.jpg", image, params);
若使用默认设置,可省略参数直接写入:
imwrite("c:/dev/output.jpg", image);
imwrite
支持的文件格式与 imread
函数相同(参见前文列表)。
除 imwrite
外,OpenCV 还支持通过 imencode
将图像写入内存缓冲区(适用于网络传输等场景,无需保存文件)。用法与 imwrite
类似,但需提供数据缓冲区和格式扩展名:
std::vector<uchar> buffer;
std::vector<int> params;
bool success = imencode(".jpg", image, buffer, params);
注意:因无文件名指定格式,
imencode
需通过扩展名参数(如.jpg
)确定输出格式。
OpenCV 提供 VideoCapture
类用于从磁盘文件、摄像头、网络视频流(如 RTSP 地址)读取视频或图像序列。使用 open
函数打开视频源,read
函数逐帧读取视频:
VideoCapture video;
video.open("c:/dev/test.avi");
if(video.isOpened())
{
Mat frame;
while(true)
{
if(video.read(frame))
{
// 处理帧...
}
else
{
break;
}
}
}
video.release();
读取图像序列时,可使用文件路径模式。例如 image_%02d.png
将读取 image_00.png
、image_01.png
等文件。
读取网络流时,直接提供 URL 作为文件名。
注意:上述示例不完整。若在 GUI 应用中运行,需在循环内添加
qApp->processEvents();
避免界面卡死。更佳方案参见第 8 章 多线程 和第 9 章 视频分析。
VideoCapture
类提供 set
和 get
函数配置参数。完整参数列表参考 VideoCaptureProperties
枚举。
快速查找参数技巧:在 Qt Creator 中输入 CAP_PROP_
可自动补全相关参数。按住 Ctrl 点击枚举名称可跳转源码查看详细定义。
获取视频总帧数示例:
double frameCount = video.get(CAP_PROP_FRAME_COUNT);
跳转到第 100 帧示例:
video.set(CAP_PROP_POS_FRAMES, 100);
使用 VideoWriter
类写入视频:
VideoWriter video;
video.open("c:/dev/output.avi", CAP_ANY, CV_FOURCC('M','P','G','4'), 30.0, Size(640, 480), true);
if(video.isOpened())
{
while(framesRemain())
{
video.write(getFrame());
}
}
video.release();
参数说明:
CAP_ANY
:捕获 API(可省略)CV_FOURCC
:FourCC 编码(如 MPG4)30.0
:帧率Size(640,480)
:帧尺寸true
:是否为彩色
重要提示:
- FourCC 编码列表参考:http://www.fourcc.org/codecs.php
- 不同平台支持的编码可能不同,部署应用时需验证编解码器可用性
OpenCV 的 HighGUI
模块用于快速创建简单 GUI。本书第 3 章 创建 Qt+OpenCV 综合项目 中已使用该模块的 imshow
函数快速显示图像。但由于我们将使用 Qt 构建更完善的 GUI 框架,后续将完全跳过此模块。以下引用 OpenCV 文档对 HighGUI
模块的说明:
"尽管 OpenCV 设计用于全功能应用程序,并能集成到 Qt、WinForms、Cocoa 等成熟 UI 框架中(或无 UI 运行),但有时需要快速验证功能并可视化结果。这正是
HighGUI
模块的设计目标。"
如本章后续内容所示,我们将弃用 imshow
函数,转用 Qt 的图像显示机制以确保界面统一性和功能性。
Qt 使用多个不同的类来处理图像数据、视频、摄像头及计算机视觉相关任务。本节我们将学习这些类,并掌握如何在 Qt 类与 OpenCV 类之间建立连接,以实现更灵活的计算机视觉应用开发。
作为 Qt 中最重要的计算机视觉相关类,QImage
是处理图像数据的核心类,提供像素级访问及多种图像操作功能。我们将重点介绍其与 OpenCV 协同使用时关键的构造函数和功能。
创建空图像:
QImage image(320, 240, QImage::Format_RGB888); // 直接指定宽高
QImage image(QSize(320, 240), QImage::Format_RGB888); // 使用 QSize
完整支持的格式列表请参考
QImage::Format
枚举。
从 OpenCV Mat 转换:
Mat mat = imread("c:/dev/test.jpg");
cvtColor(mat, mat, CV_BGR2RGB); // OpenCV 默认 BGR 需转为 RGB
QImage image(mat.data, mat.cols, mat.rows, QImage::Format_RGB888);
[!note]
若省略
cvtColor
转换,会导致红蓝通道错位。
推荐转换方式(包含 step
参数):
Mat mat = imread("c:/dev/test.jpg");
cvtColor(mat, mat, CV_BGR2RGB);
QImage image(mat.data,
mat.cols,
mat.rows,
mat.step, // 对应 Mat 的 step 值
QImage::Format_RGB888);
此方法支持连续存储的内存数据转换。
从文件读取:
QImage image("c:/dev/test.jpg");
Qt 与 OpenCV 支持的格式相互独立。Qt 默认支持以下格式:
格式 | 描述 | 支持 |
---|---|---|
BMP | Windows 位图 | 读/写 |
GIF | 图形交换格式(可选) | 读 |
JPG | 联合图像专家组 | 读/写 |
JPEG | 联合图像专家组 | 读/写 |
PNG | 便携式网络图形 | 读/写 |
PBM | 便携式位图 | 读 |
PGM | 便携式灰度图 | 读 |
PPM | 便携式像素图 | 读/写 |
XBM | X11 位图 | 读/写 |
XPM | X11 像素图 | 读/写 |
除构造函数外,QImage
还包含以下实用成员:
allGray
:检查图像是否全为灰度。该函数验证所有像素的 RGB 通道值是否相等。bits
和constBits
(常量版本):用于访问底层图像数据。可将QImage
转换为 OpenCV 的Mat
进行处理。转换时需确保格式兼容,推荐使用convertToFormat
函数标准化为三通道 RGB 格式。示例如下:
QImage image("c:/dev/test.jpg");
image = image.convertToFormat(QImage::Format_RGB888);
Mat mat = Mat(image.height(),
image.width(),
CV_8UC(3),
image.bits(),
image.bytesPerLine());
需要注意的是,在像这样传递数据时,就像我们在将 Mat 转换为 QImage 时看到的那样,在 Qt 和 OpenCV 的类之间传递的内存空间是相同的,这一点非常重要。这意味着,如果您修改了上例中的 Mat 类,实际上也是在修改QImage 类,因为您只是将其数据指针传递给了 Mat 类。这既可能非常有用(更容易操作图像),同时也可能非常危险(应用程序崩溃),因此在使用 Qt 和 OpenCV 时必须小心谨慎。如果要确保 QImage 和 Mat 类拥有完全独立的数据,可以使用 Mat 类中的克隆函数或 QImage 中的复制函数。
byteCount
:返回图像数据占用的总字节数。bytesPerLine
:类似Mat
的step
参数,表示每行字节数(等于byteCount / height
)。convertToFormat
:转换图像格式(如前文用于标准化为Format_RGB888
)。copy
:复制全部或部分图像到新QImage
。depth
:返回图像深度(每像素位数)。fill
:用指定颜色填充所有像素。支持:QColor
对象Qt::GlobalColor
枚举- 像素值整数
这些函数以及 Qt 框架中的许多其他类似函数都使用三种颜色类型:QColor、Qt::GlobalColor 以及与像素位相对应的整数值。尽管这些函数非常容易使用,但在继续之前,最好还是花几分钟时间在 Qt Creator 帮助模式下阅读一下它们的文档页面。
format
:获取当前图像格式(推荐使用Format_RGB888
实现 Qt-OpenCV 兼容)。hasAlphaChannel
:检测是否包含透明通道。height/width/size
:获取图像尺寸信息。isNull
:判断是否为空图像。load/loadFromData/fromData
:从文件或缓冲区加载图像(类似 OpenCV 的imdecode
)。mirrored
:镜像翻转图像(水平/垂直/双向)。pixel/pixelColor
:获取像素值(返回整数或QColor
)。rect
:获取图像边界矩形(QRect
对象)。rgbSwapped
:交换红蓝通道(无需修改数据,用于快速显示 OpenCV 的 BGR 图像)。save
:保存图像到文件。scaled/scaledToHeight/scaledToWidth
:缩放图像,可选比例模式:Qt::IgnoreAspectRatio
:忽略宽高比Qt::KeepAspectRatio
:保持宽高比Qt::KeepAspectRatioByExpanding
:保持比例并扩展
setPixel/setPixelColor
:设置单个像素值。setText/text
:读写图像元数据文本(支持格式需具备此功能)。transformed
:应用矩阵变换(如旋转)。示例:
QImage image("c:/dev/test.jpg");
QTransform trans;
trans.rotate(45); // 创建 45 度旋转矩阵
image = image.transformed(trans); // 应用变换
trueMatrix
:获取实际应用的变换矩阵。valid
:检查坐标点是否在图像范围内。
QPixmap
类在某些方面与 QImage
类似,但 QPixmap
主要用于在屏幕上显示图像。QPixmap
可以用于加载和保存图像(就像 QImage
一样),但它不提供灵活的图像数据操作功能,我们通常只会在完成所有修改、处理操作后需要显示图像时使用它。大多数 QPixmap
方法与 QImage
方法同名且使用方式基本相同。对于我们来说重要且 QImage
中没有的两个函数如下:
convertFromImage
: 该函数可用于通过QImage
的图像数据填充QPixmap
的数据fromImage
: 这是一个静态函数,本质上与convertFromImage
的功能相同
// 示例:将 QImage 转换为 QPixmap
QImage image("path/to/image.png");
QPixmap pixmap;
pixmap.convertFromImage(image); // 使用 convertFromImage 方法
// 或者使用静态方法
QPixmap pixmap = QPixmap::fromImage(image);
我们现在将创建一个示例项目来练习目前所学的知识。没有实际动手项目,本章学习的所有激动人心的技术都将被浪费,让我们从图像查看示例应用开始:
-
创建新项目
在 Qt Creator 中新建一个 Qt Widgets Application 项目,命名为
ImageViewer
-
界面设计
打开
mainwindow.ui
文件,使用设计器:- 移除菜单栏、状态栏和工具栏
- 添加一个标签部件(
QLabel
)到窗口 - 点击窗口空白处,按
Ctrl + G
应用网格布局,确保部件随窗口自动调整大小 - 将标签的:
alignment/Horizontal
属性设为AlignHCenter
Horizontal
和Vertical sizePolicy
均设为Ignored
-
添加头文件包含
在
mainwindow.h
中添加以下包含语句:#include <QPixmap> #include <QDragEnterEvent> #include <QDropEvent> #include <QMimeData> #include <QFileInfo> #include <QMessageBox> #include <QResizeEvent>
-
添加保护函数声明
在
MainWindow
类定义中添加:protected: void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; void resizeEvent(QResizeEvent *event) override;
-
添加私有成员
在
mainwindow.h
中添加私有成员:private: QPixmap _pixmap;
-
构造函数初始化
现在,切换到 mainwindow.cpp,在 MainWindow 构造函数中添加以下内容,以便在程序开始时调用:
setAcceptDrops(true);
-
实现拖放处理
添加文件拖放处理:
/** * @brief MainWindow::dragEnterEvent 用户拖拽文件进入窗口 → 触发 dragEnterEvent 进行验证 * @param event */ void MainWindow::dragEnterEvent(QDragEnterEvent *event) { QStringList acceptedFileTypes{"jpg", "png", "bmp"}; // 允许的文件类型 /* 从文件管理器拖拽一个文件到应用程序时, * 操作系统并不是直接传递文件内容, * 而是传递一个文件路径的 URL(例如 file:///C:/Users/YourName/image.jpg) * * MIME(Multipurpose Internet Mail Extensions) * 最初是为了标记电子邮件附件的类型(比如文本、图片、音频), * 现在被广泛用于标识数据格式。 * */ if (event->mimeData()->hasUrls() && event->mimeData()->urls().count() == 1) { QUrl url = event->mimeData()->urls().at(0); // 获取第一个URL QString localPath = url.toLocalFile(); // 转换成本地文件路径(如 "C:/image.jpg") QFileInfo file(localPath); // 用文件路径做后续操作 if(acceptedFileTypes.contains(file.suffix().toLower())) { event->acceptProposedAction(); } } } void MainWindow::dropEvent(QDropEvent *event) { QFileInfo file(event->mimeData()->urls().at(0).toLocalFile()); if(_pixmap.load(file.absoluteFilePath())) { ui->label->setPixmap(_pixmap.scaled(ui->label->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } else { QMessageBox::critical(this, tr("Error"), tr("The image file cannot be read!")); } }
-
最后添加窗口缩放事件处理:
void MainWindow::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); if(!pixmap.isNull()) { ui->label->setPixmap(pixmap.scaled(ui->label->width()-5, ui->label->height()-5, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } }
正如你所猜测的,我们刚刚编写了一个能够显示拖放图像的应用程序。通过向MainWindow
添加dragEnterEvent
函数,我们能够检查被拖动的对象是否是文件,特别是是否为单个文件。接着我们检查了图像类型以确保其被支持。
在dropEvent
函数中,我们简单地用拖放到应用程序窗口的图像文件加载了QPixmap
。然后将QLabel
类的pixmap
属性设置为我们的像素映射。
最后在resizeEvent
函数中,我们确保无论窗口大小如何变化,图像始终按正确宽高比缩放以适应窗口。
如果漏掉上述步骤中的任何一步,你将会遇到Qt拖放编程技术的问题。例如,如果没有在MainWindow
类构造函数中添加以下代码行,无论MainWindow
类添加了什么函数,都不会接受任何拖放内容:
setAcceptDrops(true);
这是最终应用程序的截图。尝试将不同图像拖放到应用程序窗口中观察效果。你甚至可以尝试拖放非图像文件来验证它们是否被拒绝:
这本质上是一个关于如何在Qt中显示图像,以及如何为Qt应用程序添加拖放功能的教程。正如前例所示,QPixmap
可以轻松地与QLabel
部件配合显示。QLabel
部件的名称有时会产生误导,但实际上它不仅可以显示纯文本,还能显示富文本、像素映射甚至动画(使用QMovie
类)。由于我们已经知道如何将Mat
转换为QImage
(反之亦然),以及如何将QImage
转换为QPixmap
,我们可以编写如下代码来使用OpenCV加载图像,通过计算机视觉算法进行处理(我们将在第六章《OpenCV中的图像处理》及后续章节深入学习),然后将其转换为QImage
继而转换为QPixmap
,最终在QLabel
上显示结果,如示例代码所示:
cv::Mat mat = cv::imread("c:/dev/test.jpg");
QImage image(mat.data,
mat.cols,
mat.rows,
mat.step,
QImage::Format_RGB888);
ui->label->setPixmap(QPixmap::fromImage(image.rgbSwapped()));
QImageReader
和 QImageWriter
类可用于对图像读写过程进行更多控制。它们支持与QImage
和QPixmap
相同的文件类型,但提供了更大的灵活性:在图像读写过程出现问题时能够提供错误信息,且使用这些类时还可以设置和获取更多图像属性。正如后续章节将看到的,我们将在综合计算机视觉应用中使用这些同类类来实现更好的图像读写控制。目前我们仅作简要介绍,接下来继续下一节内容。
QPainter
类可用于在QPaintDevice
类的任何Qt子类上进行绘制(本质上是绘画)。这意味着什么?基本上包括所有具有可视界面并能进行绘制的Qt部件。例如,QPainter
可用于在QWidget
类(即所有现有和自定义的Qt部件)、QImage
、QPixmap
及其他许多Qt类上绘制。可通过Qt Creator帮助模式查看QPaintDevice
类文档页面,获取继承QPaintDevice
的完整Qt类列表。QPainter
拥有众多以draw
开头的函数,完整介绍需要单独章节,但我们将通过QWidget
和QImage
的基础示例演示其用法。本质上,相同逻辑适用于所有可与QPainter
配合使用的类。
如前所述,可通过自定义Qt部件并使用QPainter
创建(或绘制)其视觉界面。这实际上是创建新Qt部件的常用方法之一。我们通过创建显示闪烁圆形的自定义部件来示例说明:
- 新建名为Painter_Test的Qt Widgets Application项目。
- 通过主菜单选择File / New File or Project。
- 在新建窗口中选择C++和C++ Class,点击Choose。
- 在弹出窗口中设置类名为
QBlinkingWidget
,基类选择QWidget
,确保勾选Include QWidget复选框,其余选项保持默认:
- 点击Next后点击Finish。这将在项目中添加包含头文件和源文件的新类。
- 现在需要重写
QBlinkingWidget
的paintEvent
方法并使用QPainter
进行绘制。首先在qblinkingwidget.h
中添加以下头文件:
#include <QPaintEvent>
#include <QPainter>
#include <QTimer>
- 在
QBlinkingWidget
类中添加以下受保护成员(例如添加在现有公共成员之后):
protected:
void paintEvent(QPaintEvent *event) override;
- 为该类添加私有槽函数:
private slots:
void onBlink();
- 在
qblinkingwidget.h
中添加以下私有成员:
private:
QTimer blinkTimer;
bool blink;
- 在
qblinkingwidget.cpp
的构造函数中添加以下代码:
blink = false;
connect(&blinkTimer, &QTimer::timeout, this, &BlinkingWidget::onBlink);
blinkTimer.start(500);
- 在
qblinkingwidget.cpp
中添加以下两个方法:
void QBlinkingWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
if(blink)
painter.fillRect(this->rect(), QBrush(Qt::red));
else
painter.fillRect(this->rect(), QBrush(Qt::white));
}
void QBlinkingWidget::onBlink()
{
blink = !blink;
this->update();
}
- 通过
mainwindow.ui
进入设计模式,向MainWindow
类添加一个Widget(注意是空部件):
- 右键点击添加的空部件(
QWidget
类),选择Promote to:
- 在弹出窗口中设置Promoted class name为
QBlinkingWidget
,点击Add按钮:
- 点击Promote完成提升。运行程序即可看到部件每500毫秒(半秒)闪烁一次。
这实际上是创建Qt自定义部件的通用方法。总结步骤如下:
- 创建继承
QWidget
的新类 - 重写其
paintEvent
函数 - 使用
QPainter
进行绘制 - 在窗口中添加
QWidget
- 将其提升为新建部件
当使用第三方开发的部件时,也采用相同的提升方法。在前例中,我们通过QPainter::fillRect
根据blink
变量状态切换红白填充色。类似地,可使用drawArc
、drawEllipse
、drawImage
等函数在部件上绘制任意图形。关键点在于:绘制部件时需将this
传递给QPainter
实例。若要在QImage
上绘制,只需确保将QImage
传递给QPainter
构造函数或使用begin
函数。示例:
QImage image(320, 240, QImage::Format_RGB888);
QPainter painter;
painter.begin(&image);
painter.fillRect(image.rect(), Qt::white);
painter.drawLine(0, 0, this->width()-1, this->height()-1);
painter.end();
注意所有绘制函数需包含在begin
和end
调用之间。
由于我们将使用OpenCV接口处理图像、摄像头和视频,因此不会涵盖Qt框架提供的所有视频读写与处理功能。但有时当某一框架对特定功能提供了更优或更简实现时,确实难以抗拒。例如,尽管OpenCV提供了强大的摄像头处理功能(详见第12章《Qt Quick应用》),Qt在Android、iOS等移动平台的摄像头处理方面仍有显著优势。以下简要介绍Qt中重要的摄像头与视频处理类,具体应用将留待第12章展开。
通过Qt Creator帮助索引搜索"Qt Multimedia C++ classes"可获取Qt多媒体模块完整类列表及相关文档:
QCamera
:提供对平台可用摄像头的访问QCameraInfo
:获取平台摄像头信息QMediaPlayer
:用于播放视频文件及其他录制媒体QMediaRecorder
:视频录制工具类QVideoFrame
:访问摄像头捕获的独立帧QVideoProbe
:监控摄像头/视频源的帧数据,支持在Qt中进一步处理QVideoWidget
:显示摄像头/视频源的输入帧
注意:上述类均属于Qt多媒体模块,使用时需在项目PRO文件中添加以下配置:
QT += multimedia
本章是重要里程碑,介绍了将OpenCV与Qt框架结合所需的核心概念。我们深入学习了Mat
类及其变体,探讨了OpenCV中的透明API(Transparent API)及如何通过UMat
类提升计算机视觉应用的性能。同时,掌握了图像/视频的读写方法,以及从摄像头和网络视频流捕获帧的技术。
在Qt相关部分,重点学习了与计算机视觉及图像处理相关的类。QImage
类(相当于OpenCV中的Mat
类)、QPixmap
类、QPainter
类等均有详细解析。实践中,我们还创建了自定义Qt部件,并利用QPainter
在QImage
上进行绘制。最后简要介绍了Qt中处理视频与摄像头的相关类。
第五章《图形视图框架》将通过强大的QGraphicsScene
类和图形视图框架,补全Qt与OpenCV计算机视觉拼图的最后一块。该框架能以高度灵活的方式查看和操作图像数据。完成第五章后,我们将正式进入计算机视觉与图像处理的深度领域——在完善综合计算机视觉应用的过程中,重点构建其核心功能"图像查看与处理工具",并如之前章节所述,通过持续开发新插件来学习更多计算机视觉技术。
熟悉Qt和OpenCV框架中计算机视觉应用的基础构建模块后,我们可以进一步学习可视化部分的开发。谈及计算机视觉应用,用户最直观的需求就是图像/视频预览功能。所有图像编辑软件的用户界面都包含显著的预览区域(通常通过边框区分),视频编辑软件及其他视觉媒体处理工具也遵循相同设计逻辑。对于我们将要开发的计算机视觉应用,同样需要此类可视化组件。当然也存在仅输出数值结果或通过网络传输处理结果的场景,但多数情况下用户需要实时查看处理效果(如实时视频中的目标检测结果)。这种可视化面板本质上是一个场景(Scene),更准确地说是一个图形场景(Graphics Scene)——这正是本章的核心内容。
Qt框架中有一个专注于简化图形处理的图形视图框架(Graphics View Framework),其包含大量以QGraphics
开头的类,能够应对计算机视觉应用开发中的各类图形处理需求。该框架将对象划分为三大类,形成灵活的图形对象管理架构:
- 场景(Scene):对应
QGraphicsScene
类 - 视图(View):对应
QGraphicsView
部件 - 图形项(Graphics Items):对应
QGraphicsItem
及其子类
前几章我们使用OpenCV的imshow
函数和Qt标签部件实现基础图像显示,但这些方式在图形交互(如选中、移动、缩放等操作)上缺乏灵活性。即使实现简单的图形项拖拽功能,也需要编写大量鼠标事件处理代码。而图形视图框架通过内置功能,能以更高性能轻松实现这些交互操作(专为高效处理大量图形对象设计)。
本章将重点学习与计算机视觉应用开发最相关的图形视图框架核心类。通过本章知识,我们将完善第3章《创建综合Qt+OpenCV项目》中创建的Computer_Vision
项目基础。学完本章后,您将能构建类似图像编辑软件的交互场景,实现以下功能:
- 向场景添加新图像
- 选择/删除图形项
- 场景缩放
- 其他图像编辑/查看功能
本章末尾将提供Computer_Vision
项目的基础版本链接,后续章节将基于此继续开发。
本章涵盖以下内容:
- 使用
QGraphicsScene
绘制图形场景 - 通过
QGraphicsItem
及其子类管理图形项 - 使用
QGraphicsView
查看图形场景 - 开发缩放等图像编辑/查看功能
The Scene-View-Item architecture
如前所述,Qt的图形视图框架(以下简称Qt)将需要处理的图形相关对象划分为三大类:场景(Scene)、视图(View)和项(Items)。Qt通过明确的类名对应架构的每个部分。尽管理论上可以区分三者,实际应用中它们紧密关联。这意味着深入探讨任一部分时都需涉及其他组件。移除架构中任意部分都将导致图形功能失效。此外,该架构体现了模型-视图设计模式(Model-View programming),其中模型(此处为场景 Scene)完全无需关心显示方式或显示内容。Qt将其称为基于项的模型-视图编程方法,以下为各组件的实际作用:
- 场景(
QGraphicsScene
)管理项(QGraphicsItem
或其子类的实例),包含这些项,并将事件(如鼠标点击等)传递给项。 - 视图(
QGraphicsView
部件)用于可视化并显示QGraphicsScene
的内容,同时负责将事件传递给场景。需注意场景与视图使用不同的坐标系。当场景进行缩放等变换时,同一位置的坐标值会发生变化。QGraphicsScene
和QGraphicsView
均提供坐标系转换函数以实现位置映射。 - 项(
QGraphicsItem
子类的实例)是场景中包含的图形元素,可以是线条、矩形、图像、文本等。
让我们通过一个入门示例来理解上述架构,随后深入探讨各个类:
-
创建Qt Widgets应用
新建名为
Graphics_Viewer
的Qt Widgets项目(类似第4章项目)。本次仅添加QGraphicsView
部件(保留默认objectName
为graphicsView
),无需标签、菜单栏等组件。 -
添加拖放功能
在
MainWindow
类中实现dragEnterEvent
和dropEvent
事件处理。确保在构造函数中调用setAcceptDrops(true)
。注意:本项目无需QLabel
,故移除相关QPixmap
设置代码。 -
添加场景对象
在
mainwindow.h
的私有成员区添加场景对象QGraphicsScene scene;
scene
基本上就是我们将在添加到MainWindow
类的QGraphicsView
部件中使用和显示的场景。很有可能,你需要为每一个代码编辑器无法识别的类添加一个(#include)语句。你还会遇到编译器错误,这些错误通常会提醒我们忘记在源代码中包含哪些类。因此,从现在起,只要确保为你使用的每一个 Qt 类添加一个类似于下面的 #include 指令就可以了。不过,如果某个类需要任何特殊操作才能使用,书中会有明确说明:#include <QGraphicsScene>
-
关联场景与视图 在
MainWindow
构造函数中添加以下代码:ui->setupUi(this); this->setAcceptDrops(true); ui->graphicsView->setAcceptDrops(false); // 禁止视图自身接收拖放 ui->graphicsView->setScene(&scene); // 将场景绑定到视图
-
实现图像拖放处理 在
dropEvent
函数中,使用以下两种方式之一添加图像到场景:// 方式一:直接添加QPixmap到场景 QFileInfo file(event->mimeData()->urls().at(0).toLocalFile()); QPixmap pixmap; if(pixmap.load(file.absoluteFilePath())) { scene.addPixmap(pixmap); } else { // 错误处理 } // 方式二:显式创建QGraphicsPixmapItem QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); scene.addItem(item); // 场景自动接管内存管理
在这两种情况下,我们都不必担心项目指针的问题,因为在调用 addItem 时,场景将获得项目指针的所有权,并自动将其从内存中清除。当然,如果我们想手动将项目从场景和内存中完全删除,我们可以编写一条简单的删除语句来删除项目,如图所示:
delete item;
我们的简单代码有一个很大的问题,乍一看看不出来,但如果我们继续将图片拖放到窗口中,每次最新的图片都会添加到之前图片的顶部,而之前的图片不会被清理掉。事实上,你不妨自己试试看。不过,首先要在 addItem 所在行之后添加以下一行:
qDebug() << scene.items().count();
现在,如果您运行应用程序并尝试在窗口中拖放图片,您会发现在 Qt Creator 代码编辑器屏幕底部的应用程序输出窗格中,每次拖放图片时显示的数字都会增加,即场景中的项目数:
Qt 中的 qDebug() 与 std::cout 类似,都是用于向控制台(或终端)输出。我们将在第 10 章 “调试和测试 ”中学习更多有关测试和调试的知识,但现在,让我们记下 qDebug(),并在使用 Qt 和 C++ 开发时用它来快速修复代码中的小问题。
-
因此,要解决前面示例中提到的问题,我们显然需要在添加任何内容之前清除场景。因此,只需在调用 addItem(或 addPixmap 等)之前添加以下内容即可:
scene.clear();
再次运行应用程序并查看结果。现在,投放到应用程序窗口中的图像应该只有一张。另外,请注意应用程序的输出,你会发现显示的值始终为 1,这是因为场景中始终只有一幅图像。在刚才的示例项目中,我们使用了 Qt 图形视图框架中所有现有的主要部分,即场景、项目和视图。现在我们将详细了解这些类,同时为我们的综合计算机视觉应用程序(即
Computer_Vision
项目)创建一个功能强大的图形查看器和编辑器。
该类提供操作多个图形项(QGraphicsItem
)所需的几乎所有方法,尽管我们在前例中仅将其用于单个QGraphicsPixmapItem
。本节将回顾该类的部分核心函数。如前所述,我们主要聚焦用例所需属性和方法(完整方法集虽重要但不符本书目标)。跳过QGraphicsScene
构造函数(仅用于设置场景尺寸),其余关键方法如下(部分方法附示例代码,可使用本章创建的Graphics_Viewer
项目测试):
-
addEllipse
、addLine
、addRect
、addPolygon
函数:如名所示,这些函数用于向场景添加基础几何图形。部分提供重载版本简化参数输入。每个函数均返回对应
QGraphicsItem
子类实例指针(如下),可用于后续修改/删除操作:QGraphicsEllipseItem
QGraphicsLineItem
QGraphicsRectItem
QGraphicsPolygonItem
以下是一个示例:
// 在场景中绘制一个椭圆 scene.addEllipse(-100.0, 100.0, 200.0, 100.0, QPen(QBrush(Qt::SolidPattern), 2.0), //实线边框(SolidPattern),线宽为 2 像素 QBrush(Qt::Dense2Pattern)); // 密集点状填充(Dense2Pattern) scene.addLine(-200.0, 200, +200, 200, QPen(QBrush(Qt::SolidPattern), 5.0)); scene.addRect(-150, 150, 300, 140); // 在场景中绘制一个矩形 // 定义多边形的顶点坐标 QVector<QPoint> points; points.append(QPoint(150, 250)); points.append(QPoint(250, 250)); points.append(QPoint(165, 280)); points.append(QPoint(150, 250)); scene.addPolygon(QPolygon(points)); // 绘制多边形
执行结果:
-
addPath
函数用于将
QPainterPath
(记录绘制操作)添加到场景中,需指定QPen
(画笔)和QBrush
(画刷)。返回指向新创建的QGraphicsPathItem
的指针。 -
addSimpleText
与addText
函数:addSimpleText
添加纯文本,返回QGraphicsSimpleTextItem
指针addText
添加富文本,返回QGraphicsTextItem
指针
-
addPixmap
函数:已在前例使用,用于添加图像到场景,返回QGraphicsPixmapItem
指针。 -
addItem
函数:通用方法,接受任何QGraphicsItem
子类并添加到场景(前例已演示)。 -
addWidget
函数:可将Qt部件嵌入场景(除特殊部件如使用Qt::WA_PaintOnScreen
标志或依赖外部库的部件)。此功能为创建交互式场景提供强大支持,示例代码:// 在场景中添加按钮 QPushButton *btn = new QPushButton("Process Image"); QGraphicsProxyWidget *proxy = scene.addWidget(btn); proxy->setGeometry(QRectF(-200.0, -200, 400, 100.0)); // 设置代理对象在场景中的 位置 和 大小。 // 连接按钮信号 connect(btn, &QPushButton::clicked, this, [](){ qDebug() << "Image processing triggered"; });
代码简单地添加了一个按钮,并将其连接到槽。每当场景中的此按钮被按下时,函数就会被调用。这与向窗口添加按钮的行为完全相同
-
setBackgroundBrush
、backgroundBrush
、setForegroundBrush
和foregroundBrush
函数允许访问负责绘制场景背景和前景的QBrush
类。 -
font
和setFont
函数可用于获取或设置QFont
类,以确定场景中使用的字体。 -
当我们需要定义最小尺寸来决定一个项是否有资格被绘制(渲染)时,
minimumRenderSize
和setMinimumRenderSize
函数非常有用。 -
sceneRect
和setSceneRect
函数可用于指定场景的边界矩形。这基本上意味着场景的宽度和高度,加上其在坐标系中的位置。需要注意的是,如果未调用setSceneRect
,或在QGraphicsScene
的构造函数中未设置矩形,则调用sceneRect
将始终返回可以覆盖场景中所有添加项的最大矩形。最好始终手动设置场景矩形(使用setSceneRect
),并根据场景变化等需求重新设置。 -
stickyFocus
和setStickyFocus
函数可用于启用或禁用场景的“粘性焦点”模式。如果启用粘性焦点,点击场景中的空白区域不会对已聚焦的项产生任何影响;否则,焦点将被清除,且所选项将不再被选中。 -
collidingItems
是一个非常有趣的函数,可用于简单判断一个项是否与其他项共享其区域的某部分(即碰撞)。你需要传递一个QGraphicsItem
指针和Qt::ItemSelectionMode
,然后会得到一个包含该项碰撞的QGraphicsItem
实例的QList
。 -
createItemGroup
和destroyItemGroup
函数可用于创建和删除QGraphicsItemGroup
类的实例。QGraphicsItemGroup
本质上是另一个QGraphicsItem
子类(如QGraphicsLineItem
等),可用于将一组图形项分组并表示为单个项。 -
hasFocus
、setFocus
、focusItem
和setFocusItem
函数均用于处理图形场景中当前聚焦的项。 -
width
和height
函数返回与sceneRect.width()
和sceneRect.height()
相同的值,可用于获取场景的宽度和高度。需要注意的是,这些函数返回的值类型为qreal
(默认等同于double
),而非整数,因为场景坐标并非基于像素。除非通过视图绘制场景,否则场景上的所有内容都被视为逻辑和非可视的,这与可视化的QGraphicsView
类领域相反。 -
invalidate
函数在某些情况下等同于update()
,可用于请求完全或部分重绘场景。类似于刷新功能。 -
itemAt
函数可用于获取场景中某个位置的QGraphicsItem
指针。 -
items
函数返回已添加到场景中的项列表,即QGraphicsItem
的QList
。 -
itemsBoundingRect
可用于获取QRectF
类,或包含场景上所有项的最小矩形。此函数在需要将所有项置于视图中或执行类似操作时特别有用。 -
mouseGrabberItem
可用于获取当前被点击但未释放鼠标按钮的项。此函数返回一个QGraphicsItem
指针,通过它我们可以轻松为场景添加“拖拽移动”等功能。 -
removeItem
函数可用于从场景中移除项。此函数不会删除项,调用方需负责必要的清理工作。 -
render
函数可用于在QPaintDevice
上渲染场景。这意味着你可以使用QPainter
类(如第4章 Mat与QImage 中所学)通过将绘制器类指针传递给此函数,在QImage
、QPrinter
等类上绘制场景。可选地,你可以将场景的一部分渲染到QPaintDevice
渲染目标类的一部分,并处理宽高比。 -
selectedItems
、selectionArea
和setSelectionArea
函数结合使用时,可帮助处理单个或多个项的选择。通过提供Qt::ItemSelectionMode
枚举,我们可以基于完全在框中选择项或仅部分选择等方式选择项。我们还可以为此函数提供Qt::ItemSelectionOperation
枚举条目,使选择为累加或替换所有先前选定的项。 -
sendEvent
函数可用于向场景中的项发送QEvent
类(或其子类)。 -
style
和setStyle
函数用于设置和获取场景的样式。 -
update
函数可用于重绘部分或全部场景。此函数最好与QGraphicsScene
类在场景视觉部分发生变化时发出的变更信号结合使用。 -
views
函数可用于获取包含用于显示(或查看)此场景的QGraphicsView
控件的QList
类。
除了上述现有方法,QGraphicsScene
还提供了若干虚函数,可用于进一步自定义和增强 QGraphicsScene
类的行为和外观。因此,与任何其他类似 C++ 类一样,你需要创建 QGraphicsScene
的子类并简单实现这些虚函数。实际上,这是使用 QGraphicsScene
类的最佳方式,能为新创建的子类提供极大的灵活性:
- 可重写
dragEnterEvent
、dragLeaveEvent
、dragMoveEvent
和dropEvent
函数,为场景添加拖放功能。注意这与我们之前在示例中实现图像拖放到窗口的行为非常相似。每个事件都提供了足够的信息和参数来处理整个拖放过程。 - 如果需要为整个场景添加自定义背景或前景,则应重写
drawBackground
和drawForeground
函数。当然,对于简单的背景或前景绘制或着色任务,可以直接调用setBackgroundBrush
和setForegroundBrush
函数,而无需重写这些函数。 - 可使用
mouseDoubleClickEvent
、mouseMoveEvent
、mousePressEvent
、mouseReleaseEvent
和wheelEvent
函数处理场景中的不同鼠标事件。例如,本章稍后将在Computer_Vision
项目中为场景添加缩放功能时使用wheelEvent
。 - 可重写
event
函数以处理场景接收的所有事件。此函数主要负责将事件分派给对应的处理函数,但也可用于处理自定义事件或没有便捷处理函数的事件(如前面提到的所有事件)。
正如你至今所学的所有类(无论是 Qt 还是 OpenCV 中的),本书中提供的方法、属性和功能列表不应被视为类的完整功能清单。始终建议通过框架文档学习新的函数和属性。不过,本书的描述力求简洁易懂,尤其从计算机视觉开发者的视角出发。
这是场景中所有绘制项的基础类。它包含各种方法和属性,用于处理每个项的绘制、碰撞检测(与其他项)、处理鼠标点击和其他事件等功能。尽管你可以通过子类化它来创建自己的图形项,但 Qt 也提供了一系列子类,可用于满足日常图形任务的大部分(甚至全部)需求。以下这些子类中,有些我们已在前面的示例中直接或间接使用过:
QGraphicsEllipseItem
: 用于绘制椭圆或圆形。QGraphicsLineItem
: 用于绘制线段。QGraphicsPathItem
: 用于绘制复杂路径(由QPainterPath
定义)。QGraphicsPixmapItem
: 用于显示图像(QPixmap
),如第4章中处理图像时使用的项。QGraphicsPolygonItem
: 用于绘制多边形。QGraphicsRectItem
: 用于绘制矩形。QGraphicsSimpleTextItem
: 用于显示简单文本。QGraphicsTextItem
: 用于显示富文本(支持 HTML 格式)。QGraphicsProxyWidget
: 用于将 Qt 控件(如按钮、输入框等)嵌入到图形场景中。
如前所述,QGraphicsItem
提供了众多函数和属性来处理图形应用程序中的问题和任务。本节我们将探讨 QGraphicsItem
中一些最重要的成员,从而帮助理解前文提到的子类:
-
acceptDrops
和setAcceptDrops
函数可用于使项接受拖放事件。注意这与之前示例中看到的拖放事件非常相似,但主要区别在于项本身会感知拖放事件。 -
acceptHoverEvents
、setAcceptHoverEvents
、acceptTouchEvents
、setAcceptTouchEvents
、acceptedMouseButtons
和setAcceptedMouseButtons
函数均用于处理项的交互及其对鼠标点击等操作的响应。需注意,项可以根据Qt::MouseButtons
枚举设置响应或忽略不同的鼠标按键。QGraphicsRectItem *item = new QGraphicsRectItem(0, 0, 100, 100, this); item->setAcceptDrops(true); item->setAcceptHoverEvents(true); item->setAcceptedMouseButtons( Qt::LeftButton | Qt::RightButton | Qt::MidButton);
-
boundingRegion
函数可用于获取描述图形项区域的QRegion
类。这是一个非常重要的函数,因为它能获取项需要绘制(或重绘)的精确区域。与项的边界矩形不同,因为简单来说,项可能仅覆盖其边界矩形的一部分(例如线段等)。具体示例可参考下文说明。 -
boundingRegionGranularity
和setBoundingRegionGranularity
函数用于设置和获取计算项boundingRegion
函数时的粒度级别。此处的粒度是介于0和1之间的实数,对应计算时的细节程度:QGraphicsEllipseItem *item = new QGraphicsEllipseItem(0, 0, 100, 100); scene.addItem(item); item->setBoundingRegionGranularity(g); // 0 , 0.1 , 0.75 and 1.0 QTransform transform; QRegion region = item->boundingRegion(transform); QPainterPath painterPath; painterPath.addRegion(region); QGraphicsPathItem *path = new QGraphicsPathItem(painterPath); scene.addItem(path);
在上述代码中,如果将 g
替换为 0.0、0.1、0.75 和 1.0,将得到以下结果。显然,值为 0(默认粒度)会生成单个矩形(边界矩形),这不是精确的估计。随着粒度级别的提高,我们会得到更精确的区域(本质上是一组矩形),覆盖图形形状和项:
-
childItems
函数可用于获取包含该项子项的QList
(存储QGraphicsItem
类)。可将其视为复杂项的子项集合。 -
childrenBoundingRect
、boundingRect
和sceneBoundingRect
函数可用于获取包含该项子项、项本身及场景的边界矩形的QRectF
类。 -
clearFocus
、setFocus
和hasFocus
函数用于清除、设置和获取项的焦点状态。拥有焦点的项会接收键盘事件。 -
collidesWithItem
、collidesWithPath
和collidingItems
函数可用于检查该项是否与给定项发生碰撞,并获取与该项碰撞的项列表。 -
contains
函数接受一个点坐标(精确类型为QPointF
),检查该项是否包含该点。 -
cursor
、setCursor
、unsetCursor
和hasCursor
函数用于为项设置、获取、重置特定鼠标光标类型,并检查项是否已设置光标。设置后,当鼠标悬停在该项上时,光标形状会切换为设定值。 -
hide
、show
、setVisible
、isVisible
、opacity
、setOpacity
和effectiveOpacity
函数均与项的可见性和不透明度相关。这些函数名自解释,唯一需要注意的是effectiveOpacity
(实际不透明度)可能与项本身的不透明度不同,因为其计算基于该项及其父项的不透明度层级。最终,effectiveOpacity
是屏幕上绘制该项时实际使用的不透明度值。 -
flags
、setFlags
和setFlag
函数用于获取或设置项的标志。这里的“标志”本质上是QGraphicsItem::GraphicsItemFlag
枚举值的组合。item->setFlag(QGraphicsItem::ItemIsFocusable, true); item->setFlag(QGraphicsItem::ItemIsMovable, false);
需注意:使用 setFlag
函数时,所有先前设置的标志状态会被保留,仅影响该函数指定的单个标志;而使用 setFlags
时,所有标志将根据给定的组合重置。
grabMouse
、grabKeyboard
、ungrabMouse
和ungrabKeyboard
方法用于控制场景中接收鼠标和键盘事件的项。默认实现中,一次只有一个项能捕获鼠标或键盘事件。除非另一项捕获、当前项主动释放、被删除或隐藏,否则捕获状态保持不变。可通过QGraphicsScene
类的mouseGrabberItem
函数获取当前捕获项(如本章前文所述)。setGraphicsEffect
和graphicsEffect
函数用于设置和获取QGraphicsEffect
类。这是一个强大且易用的功能,可为场景中的项添加滤镜或特效。QGraphicsEffect
是 Qt 中所有图形特效的基类,可通过子类化创建自定义特效,或直接使用 Qt 提供的以下内置图形特效类:QGraphicsBlurEffect
(模糊特效)QGraphicsColorizeEffect
(着色特效)QGraphicsDropShadowEffect
(投影特效)QGraphicsOpacityEffect
(透明度特效)
让我们通过一个自定义图形效果示例并结合 Qt 自带的图形特效来深入理解该概念:
-
使用本章前文创建的
Graphics_Viewer
项目。在 Qt Creator 中打开后,通过主菜单选择 新建文件或项目 → C++ → C++ 类,点击 Choose 按钮。 -
输入类名为
QCustomGraphicsEffect
,选择基类为QObject
,勾选 包含 QObject 复选框(若未默认勾选)。点击 Next → Finish 完成创建。 -
在新建的
qcustomgraphicseffect.h
文件中添加以下头文件包含:
#include <QGraphicsEffect>
#include <QPainter>
-
修改类继承关系,使
QCustomGraphicsEffect
继承自QGraphicsEffect
(修改qcustomgraphicseffect.h
中的类定义):class QCustomGraphicsEffect : public QGraphicsEffect
-
更新类构造函数,确保调用
QGraphicsEffect
的构造函数(修改qcustomgraphicseffect.cpp
文件):QCustomGraphicsEffect::QCustomGraphicsEffect(QObject *parent) : QGraphicsEffect(parent)
-
在
qcustomgraphicseffect.h
的类定义中添加draw
函数声明:protected: void draw(QPainter *painter) override;
-
在
qcustomgraphicseffect.cpp
中实现draw
函数。本例将创建一个简单的阈值滤镜,根据像素灰度值将其设为纯黑或纯白:void QCustomGraphicsEffect::draw(QPainter *painter) { /* * 图像转换为灰度图(QImage::Format_Grayscale8), * 所以每个像素在内存中的表示已经不再是 RGB 值, * 而是一个单独的灰度值(一个字节), * 其取值范围是 0 到 255 */ QImage image = sourcePixmap().toImage(); image = image.convertToFormat(QImage::Format_Grayscale8); for(int i=0; i<image.sizeInBytes(); i++) image.bits()[i] = (image.bits()[i] < 100) ? 0 : 255; painter->drawPixmap(0, 0, QPixmap::fromImage(image)); }
-
在
mainwindow.h
中包含自定义效果类的头文件:#include "qcustomgraphicseffect.h"
-
在项目的
dropEvent
函数中应用此效果(修改Graphics_Viewer
项目的dropEvent
):QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); item->setGraphicsEffect(new QCustomGraphicsEffect(this)); scene.addItem(item);
以上步骤演示了如何通过继承 QGraphicsEffect
创建自定义图形滤镜,实现像素级图像处理
若所有步骤正确执行,当运行应用程序并拖放图像时,将看到阈值处理的效果:
尝试将最后一步中的 QCustomGraphicsEffect
替换为 Qt 提供的任一特效类(如 QGraphicsBlurEffect
),观察不同效果。可见这些类为图形特效提供了极高的灵活性。
接下来继续介绍 QGraphicsItem
类的其他函数和属性:
-
group
和setGroup
函数用于将项加入组或获取包含该项的组(若存在)。QGraphicsItemGroup
类负责处理组,如本章前文所述。 -
isAncestorOf
函数用于检查该项是否为另一项的祖先(父项或其父项递归)。 -
setParentItem
和parentItem
用于设置或获取当前项的父项。若无父项,parentItem
返回空指针。 -
isSelected
和setSelected
函数用于设置项的选中状态,与QGraphicsScene
的setSelectionArea
等功能紧密相关。 -
坐标映射函数组(共12个核心函数):
mapFromItem
,mapToItem
mapFromParent
,mapToParent
mapFromScene
,mapToScene
mapRectFromItem
,mapRectToScene
mapRectFromParent
,mapRectToParent
mapRectFromScene
,mapRectToScene
这些函数用于坐标系转换。每个项和场景都有自己的坐标系系统,当项之间存在父子层级时尤其需要坐标转换。参考下图理解:
假设场景 Scene 为主坐标系(世界坐标系),父项在场景中的坐标为 (D, E),其子项子项1在父项局部坐标系中坐标为 (F, G),子项2为 (H, I)。当层级复杂时,映射函数至关重要。以下是代码示例:
QGraphicsRectItem *item = new QGraphicsRectItem(0,0,100,100);
item->setPos(50,400);
scene.addItem(item);
// 创建父项(作为 scene 的子项)
QGraphicsRectItem *parentItem = new QGraphicsRectItem(0, 0, 320, 240);
parentItem->setPos(300, 50);
scene.addItem(parentItem);
QGraphicsRectItem *childItem1 = new QGraphicsRectItem(0, 0, 50, 50, parentItem);
childItem1->setPos(50,50);
QGraphicsRectItem *childItem2 = new QGraphicsRectItem(0, 0, 75, 75, parentItem);
childItem2->setPos(150,75);
qDebug() << item->mapFromItem(childItem1, 0,0); // QPointF(300,-300) 以 item 为原点
qDebug() << item->mapToItem(childItem1, 0,0); // QPointF(-300,300) 以 childItem1 为原点
qDebug() << childItem1->mapFromScene(0,0); // QPointF(-350,-100)
qDebug() << childItem1->mapToScene(0,0); // QPointF(350,100)
qDebug() << childItem2->mapFromParent(0,0); // QPointF(-150,-75)
qDebug() << childItem2->mapToParent(0,0); // QPointF(150,75)
qDebug() << item->mapRectFromItem(childItem1, childItem1->rect()); // QRectF(300,-300 50x50)
qDebug() << item->mapRectToItem(childItem1, childItem1->rect()); // QRectF(-300,300 50x50)
qDebug() << childItem1->mapRectFromScene(0,0, 25, 25); // QRectF(-350,-100 25x25)
qDebug() << childItem1->mapRectToScene(0,0, 25, 25); // QRectF(350,100 25x25)
qDebug() << childItem2->mapRectFromParent(0,0, 30, 30); // QRectF(-150,-75 30x30)
qDebug() << childItem2->mapRectToParent(0,0, 25, 25); // QRectF(150,75 25x25)
试着在 Qt Creator 和 Qt Widgets 项目中运行前面的代码,你会在 Qt Creator 的应用程序输出窗格中看到下面的内容,这基本上就是 qDebug() 语句的结果。
让我们试着看看产生第一个结果的指令:
item->mapFromItem(childItem1, 0,0);
假设某个项在场景中的位置为 (50,400),而 childItem1
在父项中的位置为 (50,50)。以下代码将 childItem1
坐标系中的 (0,0) 转换为父项的坐标系。可通过类似代码验证其他坐标转换函数,这在移动场景项或进行变换时极为实用:
-
moveBy
、pos
、setPos
、x
、setX
、y
、setY
、rotation
、setRotation
、scale
和setScale
函数用于获取或设置项的几何属性。注意pos
与mapToParent(0,0)
返回值相同,可通过示例代码验证。 -
transform
、setTransform
、setTransformOriginPoint
和resetTransform
函数用于应用或获取项的几何变换。所有变换默认以原点 (0,0) 为基准,可通过setTransformOriginPoint
修改变换原点。 -
scenePos
函数获取项在场景中的位置,等同于mapToScene(0,0)
。可在前述示例中验证结果。 -
data
和setData
函数用于存储/检索项的任意自定义数据。例如,可为QGraphicsPixmapItem
存储图像路径或其他关联信息。 -
zValue
和setZValue
函数控制项的 Z 值。Z 值决定绘制顺序,值越大越靠前显示。
与 QGraphicsScene
类似,QGraphicsItem
也提供多个可重写的保护虚函数,主要用于处理场景传递的事件。重要示例包括:
contextMenuEvent
(上下文菜单事件)dragEnterEvent
、dragLeaveEvent
、dragMoveEvent
、dropEvent
(拖放事件)focusInEvent
、focusOutEvent
(焦点事件)hoverEnterEvent
、hoverLeaveEvent
、hoverMoveEvent
(悬停事件)keyPressEvent
、keyReleaseEvent
(键盘事件)mouseDoubleClickEvent
、mouseMoveEvent
、mousePressEvent
、mouseReleaseEvent
、wheelEvent
(鼠标/滚轮事件)
我们已进入Qt图形视图框架的最后部分。QGraphicsView
类是一个Qt Widget类,可放置在窗口上用于显示QGraphicsScene
(QGraphicsScene 场景本身包含多个QGraphicsItem
子类和/或部件)。与QGraphicsScene
类类似,该类也提供了大量处理图形可视化的方法、属性和功能。我们将在以下列表中回顾其中最重要的部分,然后学习如何子类化QGraphicsView
并扩展其功能,为我们的综合计算机视觉应用添加缩放、项选择等重要能力。以下是计算机视觉项目中需要用到的QGraphicsView
类方法和成员:
-
alignment
和setAlignment
函数可用于设置场景在视图中的对齐方式。需注意:只有当视图能完整显示场景且仍有剩余空间(视图无需滚动条)时,此设置才会生效。 -
dragMode
和setDragMode
函数用于获取/设置视图的拖拽模式。这是视图最重要的功能之一,决定鼠标左键在视图上点击拖拽时的行为。我们将在后续示例中使用该功能,并通过QGraphicsView::DragMode
枚举设置不同拖拽模式。 -
isInteractive
和setInteractive
函数用于获取/设置视图的交互性。交互式视图会响应鼠标和键盘事件(若已实现),否则将忽略所有输入事件,仅作为观察场景内容的非交互式视图。 -
以下函数分别用于获取/设置视图的性能和渲染质量参数,在后续示例项目中我们将实践这些用例:
optimizationFlags() setOptimizationFlags() QGraphicsView::DontSavePainterState: 禁用保存绘画状态的优化。 QGraphicsView::IndirectPainting: 使用间接绘制,以提高性能。 QGraphicsView::DisableViewportUpdate: 禁用视口更新的优化。 renderHints() setRenderHints() QPainter::Antialiasing: 启用抗锯齿,平滑图形。 QPainter::SmoothPixmapTransform: 启用平滑的位图转换。 QPainter::TextAntialiasing: 启用文本抗锯齿。 QPainter::HighQualityAntialiasing: 启用高质量的抗锯齿。 viewportUpdateMode() setViewportUpdateMode() QGraphicsView::FullViewportUpdate: 完全重绘视口。 QGraphicsView::MinimalViewportUpdate: 仅更新最小区域。 QGraphicsView::SmartViewportUpdate: 智能更新视口,根据需要更新区域。
-
rubberBandSelectionMode
和setRubberBandSelectionMode
函数用于设置当拖拽模式为RubberBandDrag
时的项选择模式。可通过Qt::ItemSelectionMode
枚举设置以下模式:Qt::ContainsItemShape
当橡皮筋框(拖拽框)完全覆盖了一个项的形状(即其实际图形部分)时,项才会被选中Qt::IntersectsItemShape
拖拽框与项的形状有交集时选中Qt::ContainsItemBoundingRect
拖拽框完全包含项的边界矩形时才选中Qt::IntersectsItemBoundingRect
拖拽框与项的边界矩形有交集时选中
-
sceneRect
和setSceneRect
函数用于获取/设置视图中的场景可视区域。注意:该值不一定与QGraphicsScene
类的sceneRect
相同。 -
centerOn
函数可确保指定点或项位于视图中心。 -
ensureVisible
函数可滚动视图至指定区域(带边距),确保其显示在视图中。支持点、矩形和图形项作为参数。 -
fitInView
函数功能与centerOn
/ensureVisible
相似,但核心区别在于该函数会缩放视图内容以适应视图显示区域,并可通过以下参数控制宽高比:Qt::IgnoreAspectRatio // 忽略宽高比 Qt::KeepAspectRatio // 保持宽高比 Qt::KeepAspectRatioByExpanding // 通过扩展保持宽高比
-
itemAt
函数可用于获取视图指定位置的图形项。
我们已经了解到场景中的每个元素和场景本身都拥有各自的坐标系,需要通过映射函数在它们之间进行位置转换。视图(View)的情况也是如此。视图同样拥有自己的坐标系,主要区别在于:视图中的位置、矩形等实际上是以像素为度量单位的,因此它们是整数值;而场景和元素的位置等则使用实数。这是因为场景和元素在通过视图显示之前都是逻辑实体,因此当整个(或部分)场景准备在屏幕上显示时,所有实数都会被转换为整数。下图可以帮助你更好地理解这一点:
[!note]
在Qt的图形视图框架中,QGraphicsScene的原点及其与QGraphicsView的坐标关系可总结如下:
- QGraphicsScene的原点
- 默认位置:QGraphicsScene的原点始终位于其自身坐标系中的
(0, 0)
。- 场景范围(sceneRect):
- 若未显式设置
sceneRect
,Qt会自动计算一个==包含所有图元的最小矩形作为场景范围==。此时原点可能在场景的左下角或左上角,具体取决于图元的布局- 若手动设置
sceneRect
(如setSceneRect(0, 0, width, height)
),原点固定为(0, 0)
,场景范围由此矩形定义。
- QGraphicsView的视口坐标
- 视口原点:视口(View的可见区域)的左上角为
(0, 0)
,向右为X轴正方向,向下为Y轴正方向。- 坐标映射:
- 通过
mapToScene()
可将视口坐标转换为场景坐标。- 通过
mapFromScene()
可将场景坐标转换为视口坐标。
- Scene原点在View中的位置
- 依赖因素:
- 视图变换:缩放、旋转或平移操作会改变Scene内容在View中的显示位置。
- 对齐方式:通过
setAlignment()
设置对齐方式。默认(Qt::AlignCenter
)场景内容居中显示,此时Scene的(0, 0)
位于View中心。若设为Qt::AlignTop | Qt::AlignLeft
,则Scene原点对齐到View左上角。- 场景矩形:若场景内容较小且未设置对齐方式,View可能自动调整显示区域。
在上图中,视图View 的中心点实际上位于场景Scene 的右上方四分之一区域。视图提供了类似的映射函数(与我们之前在元素中看到的类似),用于在场景坐标系和视图坐标系之间进行位置转换。以下是这些函数,以及我们需要了解的视图其他剩余函数和方法:
-
mapFromScene
和mapToScene
函数可用于在场景坐标系之间进行位置转换。与之前提到的完全一致的是:mapFromScene
函数接受实数值并返回整数值,- 而
mapToScene
函数接受整数并返回实数。稍后我们在开发视图的缩放功能时会用到这些函数。
-
items
函数可用于获取场景中的元素列表。 -
render
函数可用于渲染整个视图或其部分区域。该函数的使用方式与QGraphicsScene
中的render
完全相同,只是此函数作用于视图。 -
rubberBandRect
函数可用于获取橡皮筋选择的矩形区域。如前所述,这仅在拖动模式设置为RubberBandSelection
时相关。 -
setScene
和scene
函数可用于为视图设置和获取场景。 -
setMatrix
、setTransform
、transform
、rotate
、scale
、shear
和translate
函数都可用于修改或检索视图的几何属性。
// 示例代码:视图的坐标转换使用
QPoint viewPoint = view->mapFromScene(scenePos);
QRectF sceneRect = view->mapToScene(viewRect);
与 QGraphicsScene
和 QGraphicsItem
类相同,QGraphicsView
也提供了许多相同的受保护虚成员,可用于进一步扩展视图的功能。现在我们将扩展 Graphics_Viewer
示例项目,以支持更多元素、元素选择、元素删除和缩放功能。在此过程中,我们将回顾本章学习的视图、场景和元素的一些最重要用例。让我们开始实施:
-
首先在 Qt Creator 中打开
Graphics_Viewer
项目;然后从主菜单中选择 "New File or Project",在新建文件或项目窗口中选择 C++ 和 C++ Class,点击 "Choose" 按钮。 -
确保输入
QEnhancedGraphicsView
作为类名,并选择QWidget
作为基类。同时勾选 "Include QWidget" 复选框(如果未勾选)。点击 "Next",然后点击 "Finish"。 -
在
qenhancedgraphicsview.h
头文件中添加以下包含语句:#include <QGraphicsView>
-
确保
QEnhancedGraphicsView
类继承自QGraphicsView
而不是QWidget
(在qenhancedgraphicsview.h
文件中),如下所示:class QEnhancedGraphicsView : public QGraphicsView
-
需要按以下方式修正
QEnhancedGraphicsView
类的构造函数实现(在qenhancedgraphicsview.cpp
文件中):QEnhancedGraphicsView::QEnhancedGraphicsView(QWidget *parent) : QGraphicsView(parent) { }
-
现在在
qenhancedgraphicsview.h
文件的增强视图类定义中添加以下受保护成员:protected: void wheelEvent(QWheelEvent *event);
-
按照以下代码块将其实现添加到
qenhancedgraphicsview.cpp
文件:void QEnhancedGraphicsView::wheelEvent(QWheelEvent *event) { if (event->angleDelta().y() != 0) { double angleDeltaY = event->angleDelta().y(); double zoomFactor = qPow(1.0015, angleDeltaY); scale(zoomFactor, zoomFactor); this->viewport()->update(); event->accept(); } else { event->ignore(); } }
需要确保
QWheelEvent
和QtMath
已包含在类源文件中,否则会遇到qPow
函数和QWheelEvent
类的编译错误。上述代码逻辑清晰——首先检查鼠标滚轮事件方向,然后根据滚轮移动量应用 X/Y 轴缩放,最后更新视口确保重绘。 -
现在需要通过 Qt Creator 的设计模式提升窗口中的
graphicsView
对象:右键点击选择 "Promote To",输入QEnhancedGraphicsView
作为提升类名,点击 "Add" 按钮,最后点击 "Promote" 按钮。(此提升操作与之前示例相同)由于QGraphicsView
和QEnhancedGraphicsView
是兼容的(前者是后者的父类),我们可以将父类提升为子类,或在不需要时降级。提升本质上是将控件转换为其子控件以支持更多功能。 -
需要在
mainwindow.cpp
的dropEvent
函数顶部添加以下代码,确保加载新图像时重置缩放级别(具体是缩放变换):ui->graphicsView->resetTransform();
现在可以启动应用程序并尝试使用鼠标滚轮进行滚动。当向上或向下滚动滚轮时,可以看到缩放级别的变化。以下是应用程序对图像进行放大和缩小的效果截图:
若进一步测试,会发现缩放始终以图像中心为基准,这种体验非常怪异且不舒适。要解决此问题,我们需要利用本章学到的更多技巧和函数:
-
首先在增强视图类中添加另一个受保护的私有函数。除了之前使用的
wheelEvent
,我们还将利用mouseMoveEvent
。在qenhancedgraphicsview.h
文件的受保护成员部分添加以下代码:void mouseMoveEvent(QMouseEvent *event);
-
同时添加如下私有成员:
private: QPointF sceneMousePos;
-
现在转到实现部分,将以下代码添加到
qenhancedgraphicsview.cpp
文件:void QEnhancedGraphicsView::mouseMoveEvent(QMouseEvent *event) { sceneMousePos = this->mapToScene(event->pos()); }
-
还需略微调整
wheelEvent
函数,使其如下所示:
void EnhancedGraphicsView::wheelEvent(QWheelEvent *event)
{
if (event->angleDelta().y() != 0)
{
double angleDeltaY = event->angleDelta().y();
double zoomFactor = qPow(1.0015, angleDeltaY);
scale(zoomFactor, zoomFactor);
if(angleDeltaY > 0)
{
this->centerOn(sceneMousePos);
sceneMousePos = this->mapToScene(event->position().toPoint());
}
this->viewport()->update();
event->accept();
}
else
{
event->ignore();
}
}
通过函数名即可理解其逻辑:我们实现 mouseMoveEvent
捕获鼠标位置(以场景View坐标系为准,这非常重要),确保放大(非缩小)操作后视图将采集的点置于屏幕中心,最后更新位置以实现更舒适的缩放体验。需注意:此类细节缺陷或功能的处理方式,将直接影响用户使用应用的舒适度,最终成为应用成败的关键参数之一。
现在我们将为 Graphics_Viewer 应用程序添加更多功能。首先确保应用程序能够处理无限数量的图像:
-
首先需要确保视图(及场景)在每次拖入新图像后不会清空已有内容。为此,先在
mainwindow.cpp
的dropEvent
函数中删除以下代码行:scene.clear();
-
同时移除之前在
dropEvent
中添加的用于重置缩放的代码行:ui->graphicsView->resetTransform();
-
现在在
mainwindow.cpp
的dropEvent
函数起始处添加以下两行代码:// 将鼠标坐标从 MainWindow 原点转换到 以 graphicsView 为原点 QPoint viewPos = ui->graphicsView->mapFromParent(event->pos()); // 将鼠标坐标从 graphicsView 原点转换到 以 scene 为原点 QPointF sceneDropPos = ui->graphicsView->mapToScene(viewPos);
-
然后确保将元素位置设置为
sceneDropPos
,如下所示:item->setPos(sceneDropPos);
至此无需其他修改。启动 Graphics_Viewer 应用程序并尝试拖入图像。加载首张图像后,尝试缩小视图并继续添加更多图像(注意:避免过量测试导致内存耗尽,否则可能引发系统问题或程序崩溃)。下图展示了在场景不同位置拖放多个图像的效果:

显然当前应用程序仍缺乏许多功能,但本章将涵盖一些关键能力,其余功能留待读者自行探索。当前缺失的重要功能包括:无法选择元素、删除元素或对其应用特效。我们将一次性为 Graphics_Viewer
应用程序实现这些基础但关键的功能。后续章节中,我们会在综合计算机视觉项目(名为 Computer_Vision
)中使用本章所学技术。以下是 Graphics_Viewer
项目的最终功能补充步骤:
-
首先在增强的图形视图类 enhanced graphics view class 中添加以下受保护成员:
void mousePressEvent(QMouseEvent *event);
-
接着在同类定义中添加以下私有槽函数:
private slots: void clearAll(bool); void clearSelected(bool); void noEffect(bool); void blurEffect(bool); void dropShadowEffect(bool); void colorizeEffect(bool); void customEffect(bool);
-
将所有必要实现添加到视图类源文件(
qenhancedgraphicsview.cpp
)。从mousePressEvent
的实现开始:
void QEnhancedGraphicsView::mousePressEvent(QMouseEvent *event)
{
{
QMenu menu;
QAction *clearAllAction = menu.addAction("Clear All");
connect(clearAllAction, &QAction::triggered, this, &EnhancedGraphicsView::clearAll);
QAction *clearSelectedAction = menu.addAction("Clear Selected");
connect(clearSelectedAction, &QAction::triggered, this, &EnhancedGraphicsView::clearSelected);
QAction *noEffectAction = menu.addAction("No Effect");
connect(noEffectAction, &QAction::triggered, this, &EnhancedGraphicsView::noEffect);
QAction *blurEffectAction = menu.addAction("Blur Effect");
connect(blurEffectAction, &QAction::triggered, this, &EnhancedGraphicsView::blurEffect);
QAction *dropShadowEffectAction = menu.addAction("Drop Shadow Effect");
connect(dropShadowEffectAction, &QAction::triggered, this, &EnhancedGraphicsView::dropShadowEffect);
QAction *colorizeEffectAction = menu.addAction("Colorize Effect");
connect(colorizeEffectAction, &QAction::triggered, this, &EnhancedGraphicsView::colorizeEffect);
QAction *customEffectAction = menu.addAction("Custom Effect");
connect(customEffectAction, &QAction::triggered, this, &EnhancedGraphicsView::customEffect);
menu.exec(event->globalPos());
event->accept();
}
else
{
QGraphicsView::mousePressEvent(event); // 向上传递
}
}
上述代码通过右键菜单创建上下文操作,并将每个操作连接到后续添加的槽函数
-
添加槽函数的实现(其余槽函数按相同模式扩展):
void EnhancedGraphicsView::clearAll(bool) { scene()->clear(); } void EnhancedGraphicsView::clearSelected(bool) { while(scene()->selectedItems().count() > 0) { delete scene()->selectedItems().at(0); scene()->selectedItems().removeAt(0); } } void EnhancedGraphicsView::noEffect(bool) { foreach(QGraphicsItem *item, scene()->selectedItems()) { item->setGraphicsEffect(Q_NULLPTR); } } #include <QGraphicsBlurEffect> void EnhancedGraphicsView::blurEffect(bool) { foreach(QGraphicsItem *item, scene()->selectedItems()) { item->setGraphicsEffect(new QGraphicsBlurEffect(this)); } } #include <QGraphicsDropShadowEffect> void EnhancedGraphicsView::dropShadowEffect(bool) { for (auto i : scene()->selectedItems()) { i->setGraphicsEffect(new QGraphicsDropShadowEffect(this)); } } #include <QGraphicsColorizeEffect> void EnhancedGraphicsView::colorizeEffect(bool) { for (auto i : scene()->selectedItems()) { i->setGraphicsEffect(new QGraphicsColorizeEffect(this)); } } #include "customgraphicseffect.h" void EnhancedGraphicsView::customEffect(bool) { for (auto i : scene()->selectedItems()) { i->setGraphicsEffect(new CustomGraphicsEffect(this)); } }
-
在测试前需完成以下配置:确保增强视图类支持交互和点击拖拽选择。在
mainwindow.cpp
的构造函数中添加:// 设置 QGraphicsView 允许用户通过鼠标和键盘与场景中的图元(QGraphicsItem)交互 ui->graphicsView->setInteractive(true); // RubberBandDrag 是一种拖拽模式,允许用户通过按住鼠标左键并拖拽,在视图中绘制一个矩形区域(橡皮筋),释放鼠标后会自动选中该区域内所有符合条件的图元。 ui->graphicsView->setDragMode(QGraphicsView::RubberBandDrag); // 设置橡皮筋选择的条件为“完全包含图元形状”。 ui->graphicsView->setRubberBandSelectionMode(Qt::ContainsItemShape);
另外还需要再自定义
QEnhancedGraphicsView
中mouseMoveEvent
的方法中,设置将事件继续传递给父类,否则会导致橡皮筋的拖拽过程会被中断void QEnhancedGraphicsView::mouseMoveEvent(QMouseEvent *event) { sceneMousePos = this->mapToScene(event->pos()); QGraphicsView::mouseMoveEvent(event); // QGraphicsView 的默认 mouseMoveEvent 负责处理橡皮筋拖拽过程中的矩形绘制和选择逻辑。如果未调用基类方法,橡皮筋的拖拽过程会被中断。 }
-
最后在
mainwindow.cpp
的dropEvent
函数中添加代码,确保元素可选中:item->setFlag(QGraphicsItem::ItemIsSelectable); item->setAcceptedMouseButtons(Qt::LeftButton);
完成上述步骤后,Graphics_Viewer
应用程序已支持特效添加和元素操作。下图展示了橡皮筋选择模式的效果:

下图展示了应用程序为场景中的图像添加不同特效的效果:

至此,我们成功构建了一个功能强大的图形查看器,可集成到后续章节的 Computer_Vision
项目中(用于学习更多 OpenCV 和 Qt 技术)。完整项目代码可通过以下链接下载:
如我们在前几章反复强调的,本项目的目标是帮助我们专注于计算机视觉主题,同时处理所有必需的 GUI 功能、多语言支持、主题样式等。该项目完整整合了迄今所学的全部知识,是一个支持样式自定义、多语言扩展和插件化开发的应用程序。它还将本章所学内容封装成一个强大的图形查看器,我们将在后续章节持续使用。请务必在继续学习后续章节前下载该项目。
Computer_Vision
项目采用 Qt 多项目结构(更准确说是子目录项目类型),包含两个子项目:mainapp
(主应用)和 template_plugin
(插件模板)。您可以通过复制/克隆并替换代码和 GUI 文件来创建与 Computer_Vision
项目兼容的新插件。这正是我们将在第 6 章《OpenCV 图像处理》中进行的操作——针对所学的 OpenCV 技能创建对应的插件。该项目还包含示例语言包和主题包,可通过简单复制修改来创建新的语言和主题。
请务必仔细阅读整个下载的源代码,确保完全理解 Computer_Vision
项目源码的所有细节。此项目旨在将所有知识点整合到一个综合性强、可复用的示例项目中。
总结:
自本书开篇以来,我们已走过漫长的学习之路。至此,我们已全面掌握了开发计算机视觉应用程序所需的众多实用技术。在前述章节(包括刚完成的第五章)中,我们不仅聚焦于计算机视觉(具体而言是 OpenCV 技能),更深入学习了如何构建功能强大、综合全面的应用程序。您已掌握如何创建支持多语言、多主题样式和插件化的应用程序;在本章中,您还学习了如何在场景和视图中可视化图像与图形元素。现在,我们已具备深入探索计算机视觉应用开发所需的几乎所有工具。
在第 6 章《OpenCV 图像处理》中,您将进一步学习 OpenCV 的各类图像处理技术。针对每个学习主题,我们将假设需要创建一个与 Computer_Vision 项目兼容的插件。这意味着我们将复用 Computer_Vision 项目中的模板插件,通过复制和修改来创建具有特定计算机视觉功能(如图像变换滤镜或计算)的新插件。当然,这并不妨碍您创建具有相同功能的独立应用程序——正如后续章节所示,我们的插件同样包含 GUI 界面,其本质与创建独立 Qt Widgets 应用程序无异(这正是前几章已涵盖的知识)。从本章开始,我们将转向更高级的主题,重点聚焦于应用程序的计算机视觉功能实现。您将学习:
- OpenCV 中丰富的滤波功能
- 多种图像处理技术
- 支持的不同色彩空间
- 各类图像变换方法
- 及其他进阶内容
一切始于未经处理的原始图像,这些图像可能来自智能手机、网络摄像头、单反相机,或任何能够拍摄和记录图像数据的设备。然而,最终呈现的可能是锐利或模糊、明亮、昏暗或平衡、黑白或彩色,以及同一图像数据的许多其他不同表现形式。这通常是计算机视觉算法的第一步(也是最重要的一步),通常被称为图像处理(暂且忽略计算机视觉和图像处理有时会被混用的现象,这是历史专家讨论的话题)。当然,图像处理也可能出现在计算机视觉流程的中间或最终阶段,但一般来说,大多数设备拍摄的照片或视频在后续处理前都会经过某种图像处理算法。这些算法有的仅用于转换图像格式,有的用于调整色彩、去除噪点,还有更多功能不胜枚举。OpenCV 框架提供了丰富的能力来处理不同类型的图像处理任务,如图像滤波、几何变换、绘图、处理不同色彩空间、图像直方图等,这些将是本章的重点。
在本章中,你将学习许多不同的函数和类,尤其是 OpenCV 框架中 imgproc
模块的内容。我们将从图像滤波开始,过程中你将学习如何创建 GUI 以正确使用现有算法。之后,我们将学习 OpenCV 提供的几何变换能力。接着简要介绍色彩空间及其相互转换方法。然后学习 OpenCV 中的绘图函数。如前面章节所见,Qt 框架也提供了非常灵活的绘图功能,甚至通过场景-视图-项架构更便捷地处理屏幕上的图形对象;不过在某些情况下,我们也会使用 OpenCV 的绘图函数,它们通常速度更快且能满足日常图形任务需求。本章最后将介绍 OpenCV 中最强大且易用的匹配检测方法之一——模板匹配。
本章包含大量有趣的示例和实践材料,请务必亲自运行所有示例以观察效果,并通过第一手经验学习,而不仅仅是依赖章节中的截图和示例源码。
本章将涵盖以下主题:
- 如何为
Computer_Vision
项目及每个学习的 OpenCV 技能创建新插件 - 如何对图像进行滤波
- 如何执行图像变换
- 色彩空间及其相互转换方法,如何应用色彩映射
- 图像阈值化
- OpenCV 中可用的绘图函数
- 模板匹配及其在物体检测和计数中的应用
在本节初始部分,你将学习 OpenCV 中不同的线性和非线性图像滤波方法。需要注意的是,本节讨论的所有函数都以 Mat
图像作为输入,并生成相同尺寸和通道数的 Mat
图像。实际上,滤波器是独立应用于每个通道的。通常,滤波方法会从输入图像中获取一个像素及其邻域像素,并基于这些像素的函数响应计算生成图像中对应像素的值。
在计算滤波后的像素结果时,通常需要对不存在的像素进行假设。OpenCV 提供了多种处理此问题的方法,可以通过 cv::BorderTypes
枚举在几乎所有需要处理此现象的 OpenCV 函数中指定。稍后我们将在本章第一个示例中演示其用法,但在此之前,让我们通过下图确保完全理解这一概念:
如上图所示,计算(或本例中的滤波函数)获取区域 A 的像素,并在处理后图像(本例为滤波图像)中生成像素 A。这种情况下没有问题,因为输入图像中像素 A 邻域的所有像素都在图像内部(即区域 A)。但对于图像边缘附近的像素(OpenCV 中称为边界像素)呢?如你所见,像素 B 的邻域像素并非全部位于输入图像中(即区域 B)。此时我们需要假设图像外部的像素值为零、与边界像素相同等。这正是 cv::BorderTypes
枚举的作用,我们将在示例中指定合适的值。
现在,在开始图像滤波函数之前,让我们通过第一个示例演示 cv::BorderTypes
的用法。借此机会,我们还将学习如何为前几章创建的 Computer_Vision
项目创建新插件(或克隆现有插件)。步骤如下:
-
创建插件
若已完整跟随本书示例至当前章节,且已在第五章下载
Computer_Vision
项目,可通过复制template_plugin
文件夹创建新插件。将新文件夹重命名为copymakeborder_plugin
,这将成为我们第一个实际插件。 -
重命名文件
进入copymakeborder_plugin
文件夹,将所有文件名中的template
替换为copymakeborder
。包括:template_plugin.pro
→copymakeborder_plugin.pro
template_plugin.h
→copymakeborder_plugin.h
template_plugin.cpp
→copymakeborder_plugin.cpp
-
修改项目文件
用文本编辑器或 Qt Creator 打开copymakeborder_plugin.pro
,更新TARGET
配置:TARGET = CopyMakeBorder_Plugin
-
更新定义
类似上一步,需更新DEFINES
配置:DEFINES += COPYMAKEBORDER_PLUGIN_LIBRARY
-
更新文件条目
确保.pro
文件中的HEADERS
和SOURCES
条目同步更新:SOURCES += \ copymakeborder_plugin.cpp HEADERS += \ copymakeborder_plugin.h \ copymakeborder_plugin_global.h
保存并关闭
.pro
文件。 -
配置主项目文件
使用 Qt Creator 打开computer_vision.pro
文件(这是 Qt 的多项目容器文件)。该文件通常结构如下:TEMPLATE = subdirs SUBDIRS += \ mainapp \ template_plugin
-
添加新插件
将copymakeborder_plugin
添加到SUBDIRS
列表:TEMPLATE = subdirs SUBDIRS += \ mainapp \ template_plugin \ copymakeborder_plugin
注意:若条目跨多行,需在每行末尾添加
\\
(最后一行除外)。 -
批量替换代码
使用 Qt Creator 的 目录内查找替换 功能更新代码中的类名和宏: -
执行搜索替换
-
完成剩余替换
重复上述步骤:- 将
template_plugin
替换为copymakeborder_plugin
- 将
Template_Plugin
替换为CopyMakeBorder_Plugin
- 将
至此,新插件项目已配置完成,可在 Computer_Vision
项目中编译使用。
本章第一个示例的所有前置步骤仅用于准备插件项目。从现在起,在需要创建新插件时,我们将统一称这些步骤为克隆模板插件以创建X插件(本例中X为copymakeborder_plugin
)。这将避免重复说明,让我们更专注于学习 OpenCV 和 Qt 技能。尽管步骤繁琐,但通过mainapp
子项目(一个Qt Widgets 应用)统一处理图像加载、显示、主题风格等任务,插件只需专注特定计算机视觉功能。后续示例将主要填充插件函数并创建 GUI,构建后复制插件库到cvplugins
目录即可通过mainapp
菜单调用。
提示:修改.pro
文件后,建议手动运行 qmake:在 Qt Creator 的 项目面板 右键项目 → Run qmake。
-
固定标签宽度
将borderTypeLabel
的sizePolicy/Horizontal Policy
属性设为Fixed
,确保标签宽度固定。 -
连接信号槽
connect(ui->cbBorderType, &QComboBox::currentIndexChanged, this, &CopyMakeBorder_Plugin::updateNeeded);
此信号通知
mainapp
更新界面(mainapp
已将此信号连接到插件的processImage
函数)。 -
填充组合框
在copymakeborder_plugin.cpp
的setupUi
函数中添加:QStringList items; items.append("BORDER_CONSTANT"); items.append("BORDER_REPLICATE"); items.append("BORDER_REFLECT"); items.append("BORDER_WRAP"); items.append("BORDER_REFLECT_101"); ui->cbBorderType->addItems(items);
组合框项与
cv::BorderTypes
枚举值对应,需手动连接信号(因插件未使用 Qt 的自动连接机制)。 -
实现图像处理
更新processImage
函数:void CopyMakeBorder_Plugin::processImage(const cv::Mat &inputImage, cv::Mat &outputImage) { int top = inputImage.rows / 2; int bot = inputImage.rows / 2; int left = inputImage.cols / 2; int right = inputImage.cols / 2; cv::copyMakeBorder(inputImage, outputImage, top, bot, left, right, ui->cbBorderType->currentIndex()); }
调用
copyMakeBorder
函数,上下边距为图像高度的一半,左右边距为宽度的一半,边界类型从 GUI 获取。
测试插件
- 右键 Computer_Vision 项目 → Rebuild 完整重建
- 将生成的插件库文件复制到
cvplugins
目录(与mainapp
可执行文件同级) - 运行
mainapp
→ 通过 Plugins 菜单选择新插件
切换组合框中的边界类型,观察图像变化。注意:
至此,我们已准备好探索 OpenCV 的其他滤波函数。