JUCE-入门指南-全-
JUCE 入门指南(全)
原文:
zh.annas-archive.org/md5/9bc26a17fda2743c9170b7661586c2a8
译者:飞龙
前言
JUCE 是一个用于使用 C++ 开发跨平台软件的框架。JUCE 本身包含了一系列用于解决在软件开发过程中遇到的一些常见问题的类。这些包括处理图形、声音、用户交互、网络等。由于其音频支持水平,JUCE 在开发音频应用程序和音频插件方面非常受欢迎,但这绝对不意味着它的用途仅限于这个领域。使用 JUCE 开始相对容易,并且每个 JUCE 类都提供很少的惊喜。同时,JUCE 功能强大且易于定制。
本书涵盖的内容
第一章,安装 JUCE 和 Introjucer 应用程序,指导用户安装 JUCE,并涵盖了源代码树的结构,包括一些用于创建 JUCE 项目的有用工具。到本章结束时,用户将已安装 JUCE,使用 Introjucer 应用程序创建了一个基本项目,并熟悉了 JUCE 文档。
第二章,构建用户界面,涵盖了 JUCE 的 Component
类,这是在 JUCE 中创建图形用户界面的主要构建块。到本章结束时,用户将能够创建基本用户界面并在组件内执行基本绘图。用户还将具备设计和构建更复杂界面的技能。
第三章,基本数据结构,描述了 JUCE 的重要数据结构,其中许多可以被视为某些标准库类的替代品。本章还介绍了 JUCE 开发的基本类。到本章结束时,用户将能够创建和操作 JUCE 的基本类中的数据。
第四章,使用媒体文件。JUCE 提供了自己的用于读取和写入文件的类以及许多针对特定媒体格式的辅助类。本章介绍了这些类的主要示例。到本章结束时,用户将能够使用 JUCE 操作一系列媒体文件。
第五章,有用的工具。除了前面章节中介绍的基本类之外,JUCE 还包括一系列用于解决应用程序开发中常见问题的类。到本章结束时,用户将了解 JUCE 提供的一些额外有用工具。
您需要这本书的内容
您需要一个支持适当集成开发环境(IDE)的 Mac OS X 或 Windows 计算机。任何相对较新的计算机都应足够。在 Mac OS X 上,您应运行 Mac OS X 10.7 "Lion"操作系统(或更高版本)。大多数相对较新的 Windows 计算机都将支持 Microsoft Visual Studio IDE 的适当版本。JUCE 开发的 IDE 设置在第一章 安装 JUCE 和 Introjucer 应用程序 中介绍。
本书面向的对象
本书面向对 C++有基本了解的程序员。示例从基本水平开始,对基本 C++概念的假设很少。例如,甚至不需要对 C++标准库的理解。没有任何 C++经验的读者应该能够跟随并构建示例,尽管可能需要进一步的支持来理解基本概念。有经验的程序员也应该发现他们能够更快地掌握 JUCE 库。
惯例
在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词显示如下:“我们可以通过使用include
指令来包含其他上下文。”
代码块设置如下:
class MainContentComponent : public Component
{
public:
MainContentComponent()
{
setSize (200, 100);
}
};
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
class MainContentComponent : public Component
{
public:
MainContentComponent()
{
setSize (200, 100);
}
};
任何命令行输入或输出都如下所示:
JUCE v2.1.2
Hello world!
新术语和重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,将以如下方式显示:“点击下一个按钮将您带到下一屏幕”。
小贴士
小技巧和窍门如下所示。
读者反馈
我们欢迎读者的反馈。让我们知道您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需发送电子邮件到 <feedback@packtpub.com>
,并在邮件主题中提及书名。
如果您在某个主题领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com
的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表格链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的任何现有勘误列表中。您可以通过选择您的标题从 www.packtpub.com/support
查看任何现有勘误。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com>
联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com>
联系我们,我们将尽力解决。
第一章:安装 JUCE 和 Introjucer 应用程序
本章将指导您安装 JUCE 库,并涵盖其源代码树的结构,包括一些可用于创建基于 JUCE 的项目的有用工具。在本章中,我们将涵盖以下主题:
-
为 Mac OS X 和 Windows 安装 JUCE
-
构建 和 运行 JUCE 示例项目
-
构建 和 运行 Introjucer 应用程序
-
使用 Introjucer 应用程序创建 JUCE 项目
在本章结束时,您将安装 JUCE 并使用 Introjucer 应用程序创建一个基本项目。
为 Mac OS X 和 Windows 安装 JUCE
JUCE 支持为多种目标平台开发 C++ 应用程序。这些平台包括 Microsoft Windows、Mac OS X、iOS、Linux 和 Android。一般来说,本书涵盖了使用 JUCE 在 Windows 和 Mac OS X 上开发 C++ 应用程序,但将此知识应用于构建其他支持的目标平台的应用程序相对简单。
为了为这些平台编译基于 JUCE 的代码,通常需要一个 集成开发环境(IDE)。要为 Windows 编译代码,建议使用 Microsoft Visual Studio IDE(支持的变体包括 Microsoft Visual Studio 2008、2010 和 2012)。Microsoft Visual Studio 可从 www.microsoft.com/visualstudio
下载(免费 Express 版本足以用于非商业开发)。要为 Mac OS X 或 iOS 编译代码,需要 Xcode IDE。通常,建议使用最新的公共版本。这可以从 Mac App Store 内免费下载。
JUCE 以源代码形式提供(而不是预构建库),分为离散但相互关联的 模块。juce_core
模块根据 Internet Systems Consortium (ISC) 许可证授权,允许在商业和开源项目中免费使用。所有其他 JUCE 模块都采用双重许可。对于开源开发,JUCE 可以根据 GNU 通用公共许可证(版本 2 或更高版本)或 Affero 通用公共许可证(版本 3)的条款进行许可。JUCE 还可用于闭源、商业项目,并使用单独的商业许可证付费。有关 JUCE 许可的更多信息,请参阅 www.juce.com/documentation/commercial-licensing
。
除非有非常具体的原因需要使用 JUCE 的特定版本,否则建议使用项目 GIT 仓库中可用的当前开发版本。这个版本几乎总是保持稳定,并且经常包括有用的新功能和错误修复。源代码可以通过任何 GIT 客户端软件下载,网址为 git://github.com/julianstorer/JUCE.git 或 git://git.code.sf.net/p/juce/code。或者,当前开发版本的代码可以从 github.com/julianstorer/JUCE/archive/master.zip
下载为 ZIP 文件。
应该将 JUCE 源代码保留在其顶级 juce
目录中,但应将其移动到系统上的一个合理位置,以适应您的工作流程。juce
目录具有以下结构(目录使用尾随 /
表示):
amalgamation/
docs/
extras/
juce_amalgamated.cpp
juce_amalgamated.h
juce_amalgamated.mm
juce.h
modules/
README.txt
虽然所有这些文件都很重要,JUCE 库本身的实际代码位于 juce/modules
目录中,但每个模块都包含在其自己的子目录中。例如,之前提到的 juce_core
模块位于 juce/modules/juce_core
目录中。本章的剩余部分将检查 juce/extras
目录中的一些重要项目。这个目录包含了一系列有用的项目,特别是 JUCE 演示项目和 Introjucer 项目。
构建 和 运行 JUCE 演示应用程序
为了概述 JUCE 提供的功能,分发中包含了一个演示项目。这不仅是一个良好的起点,而且是一个有用的资源,其中包含了许多关于整个库中类实现细节的示例。这个 JUCE 演示项目可以在 juce/extras/JuceDemo
中找到。这个目录的结构是 Introjucer 应用程序(将在本章后面介绍)生成的 JUCE 项目的典型结构。
项目目录内容 | 目的 |
---|---|
Binary Data |
包含任何二进制文件的目录,例如图像和音频文件,这些文件将作为代码嵌入到项目中 |
Builds |
包含原生平台 IDE 项目文件的目录 |
Juce Demo.jucer |
Introjucer 项目文件 |
JuceLibraryCode |
通用 JUCE 库代码、配置文件以及转换为源代码的二进制文件,以便包含在项目中 |
Source |
项目特定的源代码 |
要构建和运行 JUCE 演示应用程序,请从 juce/extras/Builds
目录中打开相应的 IDE 项目文件。
在 Windows 上运行 JUCE 演示应用程序
在 Windows 上,打开相应的 Microsoft Visual Studio 解决方案文件。例如,使用 Microsoft Visual Studio 2010,这将是指向 juce/extras/JuceDemo/Builds/VisualStudio2010/Juce Demo.sln
的链接(其他项目和解方案文件版本也适用于 Microsoft Visual Studio 2008 和 2012)。
现在,通过导航到菜单项 调试 | 开始调试 来构建和运行项目。你可能会被询问是否要首先构建项目,如下面的截图所示:
点击 是,如果成功,JUCE 示例应用程序应该会出现。
在 Mac OS X 上运行 JUCE 示例应用程序
在 Mac OS X 上,打开 Xcode 项目文件:juce/extras/JuceDemo/Builds/MacOSX/Juce Demo.xcodeproj
。要构建和运行 JUCE 示例应用程序,导航到菜单项 产品 | 运行。如果成功,JUCE 示例应用程序应该会出现。
JUCE 示例应用程序概述
JUCE 示例应用程序分为一系列演示页面,每个页面都展示了 JUCE 库的一个有用方面。以下截图显示了 Widgets 演示(它在 Mac OS X 上的外观)。这可以通过导航到菜单项 演示 | Widgets 来访问。
Widgets 演示展示了 JUCE 为应用程序开发提供的许多常用 图形用户界面(GUI)控件。在 JUCE 中,这些图形元素被称为 组件,这是 第二章 构建用户界面 的重点。有一系列滑块、旋钮、按钮、文本显示、单选按钮和其他组件,这些都是可定制的。演示菜单中默认提供其他演示,涵盖功能如 图形渲染、字体和文本、多线程、树视图、表格组件、音频、拖放、进程间通信、网络浏览器 和 代码编辑器。在某些平台和某些硬件和软件可用时,还有其他演示可用。这些是 QuickTime、DirectShow、OpenGL 和 摄像头捕获 演示。
自定义外观和感觉
默认情况下,JUCE 示例应用程序使用 JUCE 自带的窗口标题栏、自己的菜单栏外观以及默认的 外观和感觉。标题栏可以配置为使用原生操作系统外观。以下截图显示了 JUCE 示例应用程序在 Windows 平台上的标题栏。请注意,尽管按钮的外观与 Mac OS X 上相同,但它们的位置应该对 Windows 用户来说更为熟悉。
通过导航到菜单项 外观和感觉 | 使用原生窗口标题栏,标题栏可以使用操作系统上可用的标准外观。以下截图显示了 Mac OS X 上原生标题栏的外观:
默认菜单栏外观,其中菜单项出现在标题栏下方的应用程序窗口内,应该对 Windows 用户来说很熟悉。当然,这并不是 Mac OS X 平台上应用程序菜单的默认位置。同样,这可以通过在 JUCE 示例应用程序中导航到菜单项 外观和感觉 | 使用原生 OSX 菜单栏 来指定。这将菜单栏移动到屏幕顶部,这将更符合 Mac OS X 用户的习惯。所有这些选项都可以在基于 JUCE 的代码中进行自定义。
JUCE 还提供了一个机制,可以通过其 LookAndFeel
类来定制许多内置组件的外观和感觉。这种外观和感觉可以应用于特定类型的某些组件或全局应用于整个应用程序。JUCE 本身以及 JUCE 示例应用程序提供了两种外观和感觉选项:默认外观和感觉以及旧版,原始的(即“老式”)外观和感觉。在 JUCE 示例应用程序中,可以通过 外观和感觉 菜单访问这些选项。
在进入下一节之前,你应该探索 JUCE 示例应用程序,下一节将介绍如何构建简化多平台项目管理的 Introjucer 应用程序。
构建 和 运行 Introjucer 应用程序
Introjucer 应用程序是一个基于 JUCE 的应用程序,用于创建和管理多平台 JUCE 项目。Introjucer 应用程序能够生成适用于 Mac OS X 和 iOS 的 Xcode 项目,适用于 Windows 项目的 Microsoft Visual Studio 项目(和解决方案),以及所有其他支持平台的项目文件(以及其他 IDE,如跨平台 IDE CodeBlocks)。Introjucer 应用程序执行多项任务,使得管理此类项目变得更加容易,例如:
-
将项目的源代码文件填充到所有原生 IDE 项目文件中
-
配置 IDE 项目设置以链接到目标平台上的必要库
-
将任何预处理器宏添加到某些或所有目标 IDE 项目中
-
将库和头文件搜索路径添加到 IDE 项目中
-
为产品命名并添加任何图标文件
-
自定义调试和发布配置(例如,代码优化设置)
这些都是在首次设置项目时非常有用的功能,但在项目后期需要做出更改时,它们的价值更大。如果需要在几个不同的项目中更改产品名称,这相对比较繁琐。使用 Introjucer 应用程序,大多数项目设置都可以在 Introjucer 项目文件中设置。保存后,这将修改任何新设置的本地 IDE 项目。您应该知道,这也会覆盖对本地 IDE 项目所做的任何更改。因此,在 Introjucer 应用程序中做出所有必要的更改是明智的。
此外,Introjucer 应用程序还包括一个 GUI 编辑器,用于排列任何 GUI 组件。这减少了某些类型 GUI 开发所需的编码量。Introjucer 应用程序的这部分在您的应用程序运行时生成重建 GUI 所需的 C++ 代码。
Introjucer 应用程序以源代码形式提供;在使用之前,您需要构建它。源代码位于 juce/extras/Introjucer
。与构建 JUCE Demo 应用程序类似,juce/extras/Introjucer/Builds
中提供了各种 IDE 项目(当然,iOS 或 Android 没有 Introjucer 构建版本)。建议使用发布配置构建 Introjucer 应用程序,以利用任何代码优化。
在 Windows 上构建 Introjucer 应用程序
在 Microsoft Visual Studio 中打开 juce/extras/Introjucer/Builds
中的相应解决方案文件。将解决方案配置从 调试 更改为 发布,如图所示:
现在,您应该通过导航到菜单项 构建 | 构建解决方案 来构建 Introjucer 项目。成功完成后,Introjucer 应用程序将在 juce/extras/Introjucer/Builds/VisualStudio2010/Release/Introjucer.exe
(或类似,如果您使用的是 Microsoft Visual Studio 的不同版本)中可用。此时,您应该在 Desktop
或 开始菜单 中添加快捷方式,或者使用适合您典型工作流程的方式。
在 Mac OS X 上构建 Introjucer 应用程序
打开位于 juce/extras/Introjucer/Builds/MacOSX/The Introjucer.xcodeproj
的 Xcode 项目。要在发布配置中构建 Introjucer 应用程序,导航到菜单项 产品 | 构建 | 存档。成功完成后,Introjucer 应用程序将在 juce/extras/Introjucer/Builds/MacOSX/build/Release/Introjucer.app
中可用。此时,您应该在 ~/Desktop
中添加别名,或者使用适合您典型工作流程的方式。
检查 JUCE Demo Introjucer 项目
为了说明 Introjucer 项目的结构和功能,让我们检查 JUCE Demo 应用程序的 Introjucer 项目。打开您在系统上刚刚构建的 Introjucer 应用程序。在 Introjucer 应用程序中,导航到菜单项 文件 | 打开… 并导航到打开 JUCE Demo Introjucer 项目文件(即 juce/extras/JuceDemo/Juce Demo.jucer
)。
Introjucer 项目使用典型的 主从 界面,如下面的截图所示。在左侧,或主部分,有 文件 或 配置 面板,可以使用屏幕标签或通过 视图 菜单进行选择。在右侧,或详细部分,有与主部分中选定的特定项目关联的设置。在主部分的 配置 面板中选择项目名称时,整个 JUCE Demo 项目的全局设置将在详细部分中显示。配置 面板显示了项目针对不同本地 IDE 的可用目标构建的层次结构。
除了 配置 面板中与本地 IDE 目标相关的这些部分之外,还有一个名为 模块 的项目。如前所述,JUCE 代码库被划分为松散耦合的模块。每个模块通常封装了一组特定的功能(例如,图形、数据结构、GUI、视频)。下面的截图显示了可用的模块以及为 JUCE Demo 项目启用的或禁用的模块。
可以根据特定项目所需的功能来启用或禁用模块。例如,一个简单的文本编辑应用程序可能不需要任何视频或音频功能,与该功能相关的模块可以被禁用。
每个模块都有自己的设置和选项。在许多情况下,这些设置可能包括使用本地库以实现某些功能(在这些平台上性能可能是一个高优先级)或是否应该使用跨平台的 JUCE 代码来实现该功能(在这些平台上跨平台的致性是一个更高的优先级)。每个模块可能依赖于一个或多个其他模块,在这种情况下,如果它有缺失的依赖项,它将被突出显示(并且选择该模块将解释需要启用哪些模块来解决这个问题)。为了说明这一点,尝试关闭 juce_core
模块的复选框。所有其他模块都依赖于这个 juce_core
模块,正如其名称所暗示的,它提供了 JUCE 库的核心功能。
每个模块都有一个复制模式(或创建本地副本)选项。当此选项开启(或设置为将模块复制到项目文件夹中)时,Introjucer 应用程序将源代码从 JUCE 源树复制到项目的本地项目层次结构中。当此选项关闭时,原生 IDE 将被指示直接在 JUCE 源树中引用 JUCE 源文件。您在这里的偏好是个人口味和具体情况的问题。
左侧的文件面板显示了所有将在原生 IDE 中可用的源代码的层次结构,以及将转换为跨平台源代码(并由 Introjucer 应用程序包含在原生 IDE 项目中的)的二进制文件(例如,图像、音频、XML、ZIP)。JUCE 演示项目的顶级文件结构如下截图所示:
在文件面板中选择文件可以使您直接在 Introjucer 应用程序中编辑文件。目前,在具有代码补全、错误检查等功能的原生 IDE 中进行大多数代码编辑更为方便。
现在我们已经熟悉了 Introjucer 应用程序,让我们用它从头开始创建一个项目。
使用 Introjucer 应用程序创建 JUCE 项目
本节将指导您创建一个新的 Introjucer 项目,从该项目创建原生 IDE 项目,并运行您的第一个 JUCE 应用程序。首先,通过导航到菜单项文件|关闭项目来关闭任何打开的 Introjucer 项目。接下来,选择菜单项文件|新建项目…,Introjucer 应用程序将呈现其新项目窗口。使用窗口的项目文件夹部分,导航到您想要保存项目的地方(请记住,项目实际上是一个包含代码层次结构和可能包含二进制文件(例如,图像、音频、XML、ZIP)的文件夹)。如图所示,在项目名称字段中命名项目为TestProject001
,并从要自动生成的文件菜单中选择创建 Main.cpp 文件和基本窗口选项:
最后,单击创建…按钮,应会呈现一个熟悉的 Introjucer 项目,类似于以下截图所示:
初始时,Introjucer 应用程序只为用户的当前平台创建一个目标 IDE 平台。在配置面板中右键单击(在 Mac OS X 上,按control键并单击)项目名称。这会显示一系列选项,用于将目标平台添加到项目中,如下面的截图所示:
选择文件面板并注意,Introjucer 应用程序为这个基本项目创建了三个文件:
-
Main.cpp
: 这管理应用程序的生命周期并包含应用程序的主入口点。它还包括将主应用程序窗口呈现给用户的代码。此窗口反过来在这个窗口中呈现一个MainContentComponent
对象,该对象在剩余的两个文件中指定。 -
MainComponent.cpp
: 这包括了将内容绘制到主应用程序窗口中的代码。在这种情况下,这只是一个“Hello world!”消息,但可能包含复杂和层次化的用户界面。 -
MainComponent.h
:MainComponent.cpp
文件的头文件。
建议您使用此 Introjucer 项目页面添加任何新文件。如前所述,这确保了任何新文件都会添加到所有目标平台的所有项目中,而不是您必须单独管理。在这个例子中,您不会添加任何文件。即使在所有其他平台(即这些文件不是为每个平台单独复制)上编译时使用的是完全相同的文件,在本地 IDE 中编辑源文件也不是问题。您可能需要了解一些编译器之间的差异,但尽可能依赖 JUCE 类(其中已经考虑了这一点)将有助于这方面。
要在您的本地 IDE 中打开项目,首先通过导航到菜单项文件 | 保存项目来保存项目。然后,从文件菜单中选择适当的选项以在 IDE 中打开本地项目。在 Mac OS X 上,此菜单项为在 Xcode 中打开…,而在 Windows 上为在 Visual Studio 中打开…。还有一个菜单选项结合这两个操作,并在配置面板底部有一个相应的快捷按钮。
一旦项目被加载到您的 IDE 中,您应该像之前使用 JUCE 演示项目一样构建和运行项目。如果成功,您应该会看到一个如下所示的窗口:
Introjucer 应用程序添加到项目中的三个源文件可以在您的本地 IDE 中看到。以下截图显示了 Mac OS X 上 Xcode 中的项目结构。在 Microsoft Visual Studio 中类似。
编辑MainComponent.cpp
文件(在 Xcode 中单击或 Microsoft Visual Studio 中双击)。检查MainContentComponent::paint()
函数。这个函数包含四个调用以绘制到Component
对象的Graphics
上下文中:
-
Graphics::fillAll()
: 使用特定颜色填充背景 -
Graphics::setFont()
: 将字体设置为给定的字体和大小 -
Graphics::setColour()
: 将前景绘图颜色设置为特定颜色 -
Graphics::drawText()
: 这将在指定位置绘制一些文本
尝试更改这些值中的某些值,并重新构建应用程序。
文档和其他示例
JUCE 在以下 URL 上有完整的文档:
所有 JUCE 类都使用 Doxygen 应用程序进行文档化 (www.doxygen.org
),它将特殊格式化的代码注释转换为可读的文档页面。因此,如果您愿意,您也可以从 JUCE 源代码头文件中阅读注释。这有时更方便,取决于您的 IDE,因为您可以从代码文本编辑器中轻松导航到文档。在本书的剩余部分,您将被指导到正在讨论的关键类的文档。
JUCE 被许多商业开发者用于应用程序和音频插件,特别是。一些例子包括:
-
Tracktion 音乐制作软件有效地启动了 JUCE 库的开发
-
Cycling 74 的旗舰产品 Max 从版本 5 开始使用 JUCE 开发
-
Codex Digital 生产的产品被广泛用于好莱坞电影的制作
-
其他重要的开发者包括 Korg、M-Audio 和 TC Group
还有许多其他软件,其中一些出于商业原因将它们对 JUCE 的使用保密。
摘要
本章已指导您安装适用于您平台的 JUCE,到这一点,您应该已经很好地掌握了源代码树的结构。您应该通过探索 JUCE 示例项目来熟悉 JUCE 的功能。您将安装并使用的 Introjucer 应用程序为使用 JUCE 创建和管理项目提供了基础。您还将知道如何通过 JUCE 网站,或源代码中找到 JUCE 文档。在下一章中,您将更详细地探索 Component
类,以创建各种用户界面并执行绘图操作。
第二章 构建用户界面
本章涵盖了 JUCE 的 Component
类,这是在 JUCE 中创建 图形用户界面(GUI)的主要构建块。在本章中,我们将涵盖以下主题:
-
创建按钮、滑块和其他组件
-
响应用户交互和变化:广播器和监听器
-
使用其他组件类型
-
指定颜色和使用绘图操作
到本章结束时,您将能够创建一个基本的 GUI 并在组件内执行基本的绘图操作。您还将具备设计和构建更复杂界面的技能。
创建按钮、滑块和其他组件
JUCE 的 Component
类是提供在屏幕上绘制和拦截来自指针设备、触摸屏交互和键盘输入的用户交互的基础类。JUCE 发行版包括广泛的 Component
子类,其中许多您可能在探索 第一章 中的 JUCE 示例应用程序时已经遇到,安装 JUCE 和 Introjucer 应用程序。JUCE 坐标系统是分层的,从计算机屏幕(或屏幕)级别开始。以下图示展示了这一点:
每个屏幕上的窗口包含一个 父 组件,其中放置了其他 子 组件(或 子组件)(每个可能包含进一步的子组件)。计算机屏幕的左上角坐标为(0,0),JUCE 窗口内容的左上角都从这个坐标偏移。每个组件都有自己的局部坐标,其左上角也始于(0,0)。
在大多数情况下,您将处理组件相对于其父组件的坐标,但 JUCE 提供了简单的机制将这些值转换为相对于其他组件或主屏幕(即全局坐标)。注意在前面的图中,窗口的左上角位置不包括标题栏区域。
现在您将创建一个简单的 JUCE 应用程序,其中包含一些基本组件类型。由于这个项目的代码将会非常简单,我们将所有代码都写入头文件(.h
)。这虽然在现实世界的项目中并不推荐,除非是相当小的类(或者有其他很好的理由),但这样可以将所有代码放在一个地方,便于我们进行操作。此外,我们将在本章的后面将代码拆分为 .h
和 .cpp
文件。
使用 Introjucer 应用程序创建一个新的 JUCE 项目:
-
选择菜单项 文件 | 新建项目…
-
从 自动生成文件 菜单中选择 创建 Main.cpp 文件和一个基本窗口。
-
选择保存项目的地方,并将其命名为
Chapter02_01
。 -
点击 创建… 按钮
-
导航到 文件 面板。
-
右键单击文件
MainComponent.cpp
,从上下文菜单中选择删除,并确认。 -
选择菜单项文件 | 保存项目。
-
在你的集成开发环境(IDE)中打开项目,无论是 Xcode 还是 Visual Studio。
在你的 IDE 中导航到MainComponent.h
文件。此文件最重要的部分应该看起来类似于以下内容:
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component
{
public:
//==============================================================
MainContentComponent();
~MainContentComponent();
void paint (Graphics&);
void resized();
private:
//==============================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR
(MainContentComponent)
};
当然,我们已经通过删除.cpp
文件从自动生成项目中移除了实际代码。
首先,让我们创建一个空窗口。我们将删除一些元素以简化代码,并为构造函数添加一个函数体。将MainContentComponent
类的声明更改如下:
class MainContentComponent : public Component
{
public:
MainContentComponent()
{
setSize (200, 100);
}
};
构建并运行应用程序,屏幕中央应该有一个名为MainWindow的空窗口。我们的 JUCE 应用程序将创建一个窗口,并将我们的MainContentComponent
类的实例作为其内容(即不包括标题栏)。注意我们的MainContentComponent
类继承自Component
类,因此可以访问Component
类实现的一系列函数。其中第一个是setSize()
函数,它设置我们组件的宽度和高度。
添加子组件
使用组件构建用户界面通常涉及组合其他组件以生成复合用户界面。这样做最简单的方法是在父组件类中包含成员变量,用于存储子组件。对于我们要添加的每个子组件,有五个基本步骤:
-
创建一个成员变量以存储新组件。
-
分配一个新的组件(无论是使用静态还是动态内存分配)。
-
将组件添加为父组件的子组件。
-
使子组件可见。
-
设置子组件在父组件中的大小和位置。
首先,我们将创建一个按钮;将代码更改为如下。前面的编号步骤在代码注释中说明:
class MainContentComponent : public Component
{
public:
MainContentComponent()
: button1 ("Click") // Step [2]
{
addAndMakeVisible (&button); // Step [3] and [4]
setSize (200, 100);
}
void resized()
{
// Step [5]
button1.setBounds (10, 10, getWidth()-20, getHeight()-20);
}
private:
TextButton button1; // Step [1]
};
上述代码的重要部分是:
-
我们在类的
private
部分添加了一个 JUCETextButton
类的实例。此按钮将被静态分配。 -
按钮在构造函数的初始化列表中使用一个字符串初始化,该字符串设置将在按钮上显示的文本。
-
将对组件函数
addAndMakeVisible()
的调用作为按钮实例的指针传递。这会将子组件添加到父组件层次结构中,并在屏幕上使组件可见。 -
组件函数
resized()
被重写以在父组件内部定位我们的按钮,距离边缘 10 像素(这是通过使用组件函数getWidth()
和getHeight()
来发现父组件的大小实现的)。当父组件被调整大小时,会触发对resized()
函数的调用,在这种情况下,当我们在构造函数中调用setSize()
函数时发生。setSize()
函数的参数顺序是:宽度然后是高度。setBounds()
函数的参数顺序是:左、上、宽度和高度。
构建并运行应用程序。注意按钮在鼠标指针悬停时响应,并且在按钮被点击时,尽管按钮还没有做任何事情。
通常,这是定位和调整子组件大小最方便的方法,尽管在这个例子中我们可以在构造函数中轻松设置所有大小。这项技术的真正威力在于父组件变得可调整大小时。在这里,最简单的方法是启用窗口本身的调整大小。为此,导航到 Main.cpp
文件(其中包含设置基本应用程序的样板代码)并将以下突出显示的行添加到 MainWindow
构造函数中:
...
{
setContentOwned (new MainContentComponent(), true);
centreWithSize (getWidth(), getHeight());
setVisible (true);
setResizable (true, true);
}
...
构建并运行应用程序,注意窗口现在在右下角有一个角落调整大小控件。这里重要的是按钮会随着窗口大小的变化而自动调整大小,这是由于我们上面实现的方式。在调用 setResizable()
函数时,第一个参数设置窗口是否可调整大小,第二个参数设置这是否通过角落调整大小控件(true
)或允许拖动窗口边框来调整窗口大小(false
)。
子组件可以按比例定位,而不是使用绝对值或偏移值。实现这一点的其中一种方法是通过 setBoundsRelative()
函数。在以下示例中,你将在组件中添加一个滑动控件和一个标签。
class MainContentComponent : public Component
{
public:
MainContentComponent()
: button1 ("Click"),
label1 ("label1", "Info")
{
slider1.setRange (0.0, 100.0);
addAndMakeVisible (&button1);
addAndMakeVisible (&slider1);
addAndMakeVisible (&label1);
setSize (200, 100);
}
void resized()
{
button1.setBoundsRelative (0.05, 0.05, 0.90, 0.25);
slider1.setBoundsRelative (0.05, 0.35, 0.90, 0.25);
label1.setBoundsRelative (0.05, 0.65, 0.90, 0.25);
}
private:
TextButton button1;
Slider slider1;
Label label1;
};
在这种情况下,每个子组件的宽度是父组件宽度的 90%,并且从左边开始定位在父组件宽度的 5%。每个子组件的高度是父组件高度的 25%,三个组件从上到下分布,按钮从顶部开始距离父组件高度的 5%。构建并运行应用程序,注意窗口自动且平滑地调整大小,更新子组件的大小和位置。窗口应类似于以下截图。在下一节中,你将拦截并响应用户交互:
小贴士
下载示例代码
您可以从您在 www.packtpub.com
的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support
并注册以直接将文件通过电子邮件发送给您。
响应用户交互和变化
创建一个名为 Chapter02_02
的新 Introjucer 项目,包含一个基本窗口;这次保留所有自动生成的文件。现在,我们将上一节中的代码拆分为 MainComponent.h
和 MainComponent.cpp
文件。MainComponent.h
文件应如下所示:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
void resized();
private:
TextButton button1;
Slider slider1;
Label label1;
};
#endif
MainComponent.cpp
文件应如下所示:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
: button1 ("Click")
{
slider1.setRange (0.0, 100.0);
addAndMakeVisible (&button1);
addAndMakeVisible (&slider1);
addAndMakeVisible (&label1);
setSize (200, 100);
}
void MainContentComponent::resized()
{
button1.setBoundsRelative (0.05, 0.05, 0.90, 0.25);
slider1.setBoundsRelative (0.05, 0.35, 0.90, 0.25);
label1.setBoundsRelative (0.05, 0.65, 0.90, 0.25);
}
广播器和监听器
虽然滑块类已经包含一个显示滑块值的文本框,但检查这种通信如何在 JUCE 中工作将是有用的。在下一个示例中,我们将:
-
从滑块中移除文本框
-
使滑块的值出现在标签中
-
通过点击按钮使滑块能够归零
为了实现这一点,JUCE 在整个库中广泛使用 观察者 模式,以使对象能够进行通信。特别是,Component
类及其子类使用它来通知您的代码当用户界面项被点击、内容发生变化等情况。在 JUCE 中,这些通常被称为 监听器(观察者)和 广播器(观察者的主题)。JUCE 还大量使用多重继承。在 JUCE 中,多重继承特别有用的一处是通过使用广播器和监听器系统。通常,支持广播其状态变化的 JUCE 类有一个嵌套类称为 Listener
。因此,Slider
类有 Slider::Listener
类,而 Label
类有 Label::Listener
类。(这些通常通过具有类似名称的类来表示,以帮助支持旧 IDE,例如,SliderListener
和 LabelListener
是等效的。)TextButton
类实际上是更通用的 Button
类的子类;因此,其监听器类是 Button::Listener
。每个这些监听器类都将包含至少一个 纯虚函数 的声明。这将要求我们的派生类实现这些函数。监听器类可能包含其他常规虚函数,这意味着它们可以可选实现。要实现这些函数,首先在 MainComponent.h
文件中将按钮和滑块的监听器类作为 MainContentComponent
类的公共基类添加,如下所示:
class MainContentComponent : public Component,
public Button::Listener,
public Slider::Listener
{
...
我们这里的每个用户界面监听器都需要我们实现一个函数来响应其变化。这些是 buttonClicked()
和 sliderValueChanged()
函数。将这些函数添加到我们的类声明中的 public
部分:
...
void buttonClicked (Button* button);
void sliderValueChanged (Slider* slider);
...
用于 MainComponent.cpp
文件的完整列表如下所示:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
: button1 ("Zero Slider"),
slider1 (Slider::LinearHorizontal, Slider::NoTextBox)
{
slider1.setRange (0.0, 100.0);
slider1.addListener (this);
button1.addListener (this);
slider1.setValue (100.0, sendNotification);
addAndMakeVisible (&button1);
addAndMakeVisible (&slider1);
addAndMakeVisible (&label1);
setSize (200, 100);
}
void MainContentComponent::resized()
{
button1.setBoundsRelative (0.05, 0.05, 0.90, 0.25);
slider1.setBoundsRelative (0.05, 0.35, 0.90, 0.25);
label1.setBoundsRelative (0.05, 0.65, 0.90, 0.25);
}
void MainContentComponent::buttonClicked (Button* button)
{
if (&button1 == button)
slider1.setValue (0.0, sendNotification);
}
void MainContentComponent::sliderValueChanged(Slider* slider)
{
if (&slider1 == slider) {
label1.setText (String (slider1.getValue()),
sendNotification);
}
}
使用addListener()
函数添加监听器的两次调用,传递this
指针(指向我们的MainContentComponent
实例的指针)。这会将我们的MainContentComponent
实例分别作为监听器添加到滑块和按钮。
尽管每种类型的组件只有一个实例,但前面的示例展示了在可能存在许多类似组件(如按钮组或滑块)的情况下,检查哪个组件广播了更改的推荐方法。这种技术是检查监听函数收到的指针值,并判断它是否与某个成员变量的地址匹配。在此处编码风格上有一点需要注意。你可能更喜欢将if()
语句的参数交换过来,如下所示:
if
(button == &button1)
...
然而,本书中使用的样式是为了在错误地将"==
"运算符误写为单个"=
"字符时产生故意的编译器错误。这应该有助于避免由这种错误引入的 bug。
存储某种类型值(如滑块和标签)的组件当然可以以编程方式设置其状态。在这种情况下,你可以控制其监听器是否通知更改(你也可以自定义这是同步还是异步传输)。这是sendNotification
值(一个枚举常量)在调用Slider::setValue()
和Label::setText()
函数(如前面的代码片段所示)中的目的。此外,你应该注意到在构造函数中对Slider::setValue()
函数的调用是在类注册为监听器之后进行的。这确保了所有组件从开始就配置正确,同时最大限度地减少了代码的重复。此代码使用String
类将文本传递给标签,将文本转换为数值,反之亦然。String
类将在下一章中更详细地探讨,但到目前为止,我们将限制String
类的使用仅限于这些基本操作。通过在初始化列表中使用滑块样式和文本框样式初始化滑块,从滑块中移除文本框。在这种情况下,初始化器slider1 (Slider::LinearHorizontal, Slider::NoTextBox)
指定了一个水平滑块,并且不应附加文本框。
最后,如果我们想将滑块的值设置为特定值,我们可以使标签可编辑,并将输入到标签中的任何更改传输到滑块。创建一个新的 Introjucer 项目,并将其命名为Chapter02_03
。在头文件中将Label::Listener
类添加到我们的MainContentComponent
类的基类中:
class MainContentComponent : public Component,
public Button::Listener,
public Slider::Listener,
public Label::Listener
{
...
在头文件中添加响应标签变化的Label::Listener
函数:
...
void labelTextChanged (Label* label);
...
更新MainComponent.cpp
文件中的构造函数以进一步配置标签:
MainContentComponent::MainContentComponent()
: button1 ("Zero Slider"),
slider1 (Slider::LinearHorizontal, Slider::NoTextBox)
{
slider1.setRange (0.0, 100.0);
label1.setEditable (true);
slider1.addListener (this);
button1.addListener (this);
label1.addListener (this);
slider1.setValue (100.0, sendNotification);
addAndMakeVisible (&button1);
addAndMakeVisible (&slider1);
addAndMakeVisible (&label1);
setSize (200, 100);
}
在这里,标签被设置为单次点击可编辑,并且我们的类将自己注册为标签的监听器。最后,将 labelTextChanged()
函数的实现添加到 MainComponent.cpp
文件中:
void MainContentComponent::labelTextChanged (Label* label)
{
if (&label1 == label) {
slider1.setValue (label1.getText().getDoubleValue(),
sendNotification);
}
}
构建并运行应用程序以测试此功能。存在一些问题:
-
滑块正确地剪辑了输入到标签中的超出滑块范围的值,但如果这些值超出范围,标签中的文本仍然保留
-
标签允许输入非数值字符(尽管这些字符被有用地解析为零)
过滤数据输入
上述提到的问题之一是直接的,那就是将滑块的值转换回文本,并使用这个文本来设置标签内容。这次我们使用 dontSendNotification
值,因为我们想避免无限循环,其中每个组件都会广播一个消息,导致变化,进而导致消息被广播,如此循环:
if (&label1 == label)
{
slider1.setValue (label1.getText().getDoubleValue(),
sendNotification);
label1.setText (String (slider1.getValue()),
dontSendNotification);
}
第二个问题需要一个过滤器来只允许某些字符。在这里,你需要访问标签的内部 TextEditor
对象。为此,你可以通过从 Label
类继承并实现 editorShown()
虚拟函数来创建一个自定义的标签类。将这个小的类添加到 MainComponent.h
文件中,在 MainContentComponent
类声明之上(虽然为了在应用程序中的多个组件中重用这个类,可能将此代码放在一个单独的文件中会更好):
class NumericalLabel : public Label
{
public:
void editorShown (TextEditor* editor)
{
editor->setInputRestrictions (0, "-0123456789.");
}
};
因为文本编辑器即将显示,这个功能是通过标签调用的,在那个时刻你可以使用文本编辑器的 setInputRestrictions()
函数来设置文本编辑器的输入限制。这两个参数是:长度和允许的字符。零长度表示没有长度限制,在这种情况下允许的字符包括所有数字、负号和点号。(实际上,你可以省略负号以禁止负数,如果你想只允许整数,可以省略点号。)要使用这个类代替内置的 Label
类,只需在我们的 MainContentComponent
类的成员变量列表中替换这个类名,如下所示,高亮显示:
...
private:
TextButton button1;
Slider slider1;
NumericalLabel label1;
...
希望到这一点,你能够看出 JUCE 类提供了一系列有用的核心功能,同时允许相对容易地进行自定义。
使用其他组件类型
除了已经看到的滑块和按钮之外,还有很多其他的内置组件类型和变体。在前一节中,我们使用了默认的水平滑块,但Slider
类非常灵活,正如 JUCE 演示应用程序的 Widget 演示页面所示。滑块可以采用旋转式控制,具有最小和最大范围,并且可以扭曲数值轨迹以采用非线性行为。同样,按钮可以采用不同的样式,例如切换按钮、使用图像的按钮等。以下示例说明了更改两个滑块样式的切换类型按钮。创建一个新的 Introjucer 项目,命名为Chapter02_04
,并使用以下代码:
-
MainComponent.h:
#ifndef __MAINCOMPONENT_H__ #define __MAINCOMPONENT_H__ #include "../JuceLibraryCode/JuceHeader.h" class MainContentComponent : public Component, public Button::Listener { public: MainContentComponent(); void resized(); void buttonClicked (Button* button); private: Slider slider1; Slider slider2; ToggleButton toggle1; }; #endif
-
MainComponent.cpp:
#include "MainComponent.h" MainContentComponent::MainContentComponent() : slider1 (Slider::LinearHorizontal, Slider::TextBoxLeft), slider2 (Slider::LinearHorizontal, Slider::TextBoxLeft), toggle1 ("Slider style: Linear Bar") { slider1.setColour (Slider::thumbColourId, Colours::red); toggle1.addListener (this); addAndMakeVisible (&slider1); addAndMakeVisible (&slider2); addAndMakeVisible (&toggle1); setSize (400, 200); } void MainContentComponent::resized() { slider1.setBounds (10, 10, getWidth() - 20, 20); slider2.setBounds (10, 40, getWidth() - 20, 20); toggle1.setBounds (10, 70, getWidth() - 20, 20); } void MainContentComponent::buttonClicked (Button* button) { if (&toggle1 == button) { if (toggle1.getToggleState()) { slider1.setSliderStyle (Slider::LinearBar); slider2.setSliderStyle (Slider::LinearBar); } else { slider1.setSliderStyle (Slider::LinearHorizontal); slider2.setSliderStyle (Slider::LinearHorizontal); } } }
此示例使用ToggleButton
对象,并在buttonClicked()
函数中使用getToggleState()
函数检查其切换状态。尚未讨论的一个明显的自定义选项是更改内置组件内部各种元素的颜色。这将在下一节中介绍。
指定颜色
JUCE 中的颜色由Colour
和Colours
类处理(注意这两个类名的英国拼写):
-
Colour
类存储一个 32 位颜色,具有 8 位 alpha、红色、绿色和蓝色值(ARGB)。一个Colour
对象可以从其他格式初始化(例如,使用浮点值或HSV格式的值)。 -
Colour
类包括从现有颜色创建新颜色的许多实用工具,例如,通过修改 alpha 通道、仅更改亮度或找到合适的对比颜色。 -
Colours
类是一组静态Colour
实例的集合(例如,Colour::red
,Colour::cyan
)。这些基于超文本标记语言(HTML)标准中的颜色命名方案。
例如,以下代码片段说明了创建相同“红色”颜色的几种不同方法:
Colour red1 = Colours::red; // using Colours
Colour red2 = Colour (0xffff0000); // using hexadecimal ARGB
Colour red3 = Colour (255, 0, 0); // using 8-bit RGB values
Colour red4 = Colour::fromFloatRGBA (1.f, 0.f, 0.f, 1.f); // float
Colour red5 = Colour::fromHSV (0.f, 1.f, 1.f, 1.f); // HSV
组件类使用 ID 系统来引用它们用于不同目的的各种颜色(背景、边框、文本等)。要使用这些颜色来更改组件的外观,请使用Component::setColour()
函数:
void setColour (int colourId, Colour newColour);
例如,要更改滑块的拇指颜色(即可拖动的部分),ID 是Slider::thumbColourId
常量(这也改变了当滑块样式设置为Slider::LinearBar
常量时表示滑块值的填充颜色)。您可以在Chapter02_04
项目中通过在构造函数中添加以下突出显示的行来测试此功能:
MainContentComponent::MainContentComponent()
: slider1 (Slider::LinearHorizontal, Slider::TextBoxLeft),
slider2 (Slider::LinearHorizontal, Slider::TextBoxLeft),
toggle1 ("Slider style: Linear Bar")
{
slider1.setColour (Slider::thumbColourId, Colours::red);
slider2.setColour (Slider::thumbColourId, Colours::red);
toggle1.addListener (this);
addAndMakeVisible (&slider1);
addAndMakeVisible (&slider2);
addAndMakeVisible (&toggle1);
setSize (400, 200);
}
以下截图显示了此应用程序的最终外观,显示了两种类型的滑块:
组件颜色 ID
许多内置组件定义了自己的颜色 ID 常量;最有用的是:
-
Slider::backgroundColourId
-
Slider::thumbColourId
-
Slider::trackColourId
-
Slider::rotarySliderFillColourId
-
Slider::rotarySliderOutlineColourId
-
Slider::textBoxTextColourId
-
Slider::textBoxBackgroundColourId
-
Slider::textBoxHighlightColourId
-
Slider::textBoxOutlineColourId
-
Label::backgroundColourId
-
Label::textColourId
-
Label::outlineColourId
-
ToggleButton::textColourId
-
TextButton::buttonColourId
-
TextButton::buttonOnColourId
-
TextButton::textColourOffId
-
TextButton::textColourOnId
这些枚举常量在每个它们被使用的类中定义。对于每种组件类型,还有很多其他的。
使用 LookAndFeel 类设置颜色
如果你有很多控件并且想要为它们设置统一的颜色,那么在组件层次结构中的其他某个点设置颜色可能更方便。这是 JUCE LookAndFeel
类的一个目的。这在第一章中简要提到,安装 JUCE 和 Introjucer 应用程序,其中可以通过使用不同的外观和感觉来选择各种小部件的不同样式。如果这要在整个应用程序中进行全局更改,那么最佳位置可能是在初始化代码中放置此更改。为了尝试这样做,从你的项目中删除以下两行代码,这些代码是在上一步中添加的:
slider1.setColour (Slider::thumbColourId, Colours::red);
slider2.setColour (Slider::thumbColourId, Colours::red);
导航到Main.cpp
文件。现在将以下行添加到initialise()
函数中(再次注意英国拼写)。
void initialise (const String& commandLine)
{
LookAndFeel& lnf = LookAndFeel::getDefaultLookAndFeel();
lnf.setColour (Slider::thumbColourId, Colours::red);
mainWindow = new MainWindow();
}
应该很明显,此时可以配置一个扩展的颜色列表来定制应用程序的外观。另一种技术,同样使用LookAndFeel
类,是从默认的LookAndFeel
类继承并更新这个派生类中的颜色。为组件设置特定的外观和感觉会影响其层次结构中的所有子组件。因此,这种方法将允许你在应用程序的不同部分有选择地设置颜色。以下是一个使用此方法的解决方案示例,其中重要的部分被突出显示:
主组件头文件:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component,
public Button::Listener
{
public:
MainContentComponent();
void resized();
void buttonClicked (Button* button);
class AltLookAndFeel : public LookAndFeel
{
public:
AltLookAndFeel()
{
setColour (Slider::thumbColourId, Colours::red);
}
};
private:
Slider slider1;
Slider slider2;
ToggleButton toggle1;
AltLookAndFeel altLookAndFeel;
};
#endif
在MainComponent.cpp
文件中,只需更新构造函数:
MainContentComponent::MainContentComponent()
: slider1 (Slider::LinearHorizontal, Slider::TextBoxLeft),
slider2 (Slider::LinearHorizontal, Slider::TextBoxLeft),
toggle1 ("Slider style: Linear Bar")
{
setLookAndFeel (&altLookAndFeel);
toggle1.addListener (this);
addAndMakeVisible (&slider1);
addAndMakeVisible (&slider2);
addAndMakeVisible (&toggle1);
setSize (400, 200);
}
在这里,我们创建了一个基于默认LookAndFeel
类的嵌套类AltLookAndFeel
。这是因为我们只需要从MainContentComponent
实例内部引用它。如果AltLookAndFeel
成为一个更广泛的类或者需要被我们编写的其他组件类重用,那么在MainContentComponent
类外部定义这个类可能更合适。
在AltLookAndFeel
构造函数中,我们设置了滑块的拇指颜色。最后,我们在其构造函数中为MainContentComponent
类设置了外观和感觉。显然,使用这少量工具还有许多其他可能的技巧,而且具体的方法很大程度上取决于正在开发的具体应用程序功能。需要注意的是,LookAndFeel
类不仅处理颜色,而且更广泛地允许你配置某些用户界面元素绘制的确切方式。你不仅可以更改滑块的拇指颜色,还可以通过重写LookAndFeel::getSliderThumbRadius()
函数来更改其半径,或者甚至完全更改其形状(通过重写LookAndFeel::drawLinearSliderThumb()
函数)。
使用绘图操作
虽然在可能的情况下使用内置组件是明智的,但有时你可能需要或希望创建一个全新的自定义组件。这可能是为了执行某些特定的绘图任务或独特的用户界面项目。JUCE 也优雅地处理了这一点。
首先,创建一个新的 Introjucer 项目,并将其命名为Chapter02_05
。要在组件中执行绘图任务,你应该重写Component::paint()
函数。将MainComponent.h
文件的内容更改为:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
void paint (Graphics& g);
};
#endif
将MainComponent.cpp
文件的内容更改为:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
setSize (200, 200);
}
void MainContentComponent::paint (Graphics& g)
{
g.fillAll (Colours::cornflowerblue);
}
构建并运行应用程序,以查看结果为蓝色的空窗口。
当组件需要重新绘制自身时,会调用paint()
函数。这可能是因为组件已被调整大小(当然,你可以通过角调整器尝试),或者对无效化显示的特定调用(例如,组件显示值的视觉表示,而这个值不再是当前存储的值)。paint()
函数传递一个对Graphics
对象的引用。正是这个Graphics
对象,你指示它执行你的绘图任务。上述代码中使用的Graphics::fillAll()
函数应该是自解释的:它使用指定的颜色填充整个组件。Graphics
对象可以绘制矩形、椭圆、圆角矩形、线条(以各种样式)、曲线、文本(具有在特定区域内适应或截断文本的多个快捷方式)和图像。
下一个示例说明了使用随机颜色绘制一组随机矩形的操作。将MainComponent.cpp
文件中的paint()
函数更改为:
void MainContentComponent::paint (Graphics& g)
{
Random& r (Random::getSystemRandom());
g.fillAll (Colours::cornflowerblue);
for (int i = 0; i < 20; ++i) {
g.setColour (Colour (r.nextFloat(),
r.nextFloat(),
r.nextFloat(),
r.nextFloat()));
const int width = r.nextInt (getWidth() / 4);
const int height = r.nextInt (getHeight() / 4);
const int left = r.nextInt (getWidth() - width);
const int top = r.nextInt (getHeight() - height);
g.fillRect (left, top, width, height);
}
}
这利用了 JUCE 随机数生成器类的多次调用Random
。这是一个方便的类,允许生成伪随机整数和浮点数。你可以创建自己的Random
对象实例(如果你的应用程序在多个线程中使用随机数,则建议这样做),但在这里我们只是复制一个全局“系统”Random
对象的引用(使用Random::getSystemRandom()
函数)并多次使用它。在这里,我们用蓝色背景填充组件并生成 20 个矩形。颜色是从随机生成的浮点 ARGB 值生成的。调用Graphics::setColour()
函数设置后续绘图命令将使用的当前绘图颜色。通过首先选择宽度和高度(每个都是父组件宽度和高度的 1/4 的最大值)来创建一个随机生成的矩形。然后随机选择矩形的位置;再次使用父组件的宽度和高度,但这次减去随机矩形的宽度和高度,以确保其右下角不在屏幕外。如前所述,每当组件需要重绘时都会调用paint()
函数。这意味着当组件大小调整时,我们将得到一组全新的随机矩形。
将绘图命令更改为fillEllipse()
而不是fillRect()
将绘制一系列椭圆。线条可以以各种方式绘制。如下更改paint()
函数:
void MainContentComponent::paint (Graphics& g)
{
Random& r (Random::getSystemRandom());
g.fillAll (Colours::cornflowerblue);
const float lineThickness = r.nextFloat() * 5.f + 1.f;
for (int i = 0; i < 20; ++i) {
g.setColour (Colour (r.nextFloat(),
r.nextFloat(),
r.nextFloat(),
r.nextFloat()));
const float startX = r.nextFloat() * getWidth();
const float startY = r.nextFloat() * getHeight();
const float endX = r.nextFloat() * getWidth();
const float endY = r.nextFloat() * getHeight();
**g.drawLine (startX, startY,**
**endX, endY,**
**lineThickness);**
}
}
在这里,我们在for()
循环之前选择一个随机的线宽(介于 1 到 6 像素之间),并用于每条线。线的起始和结束位置也是随机生成的。要绘制连续的线,有几种选择,你可以:
-
存储线的最后一个端点并将其用作下一条线的起点;或者
-
使用 JUCE
Path
对象构建一系列线条绘制命令,并在一次遍历中绘制路径。
第一种解决方案可能如下所示:
void MainContentComponent::paint (Graphics& g)
{
Random& r (Random::getSystemRandom());
g.fillAll (Colours::cornflowerblue);
const float lineThickness = r.nextFloat() * 5.f + 1.f;
**float x1 = r.nextFloat() * getWidth();**
**float y1 = r.nextFloat() * getHeight();**
for (int i = 0; i < 20; ++i) {
g.setColour (Colour (r.nextFloat(),
r.nextFloat(),
r.nextFloat(),
r.nextFloat()));
**const float x2 = r.nextFloat() * getWidth();**
**const float y2 = r.nextFloat() * getHeight();**
**g.drawLine (x1, y1, x2, y2, lineThickness);**
**x1 = x2;**
**y1 = y2;**
}
}
第二种选项略有不同;特别是,构成路径的每条线都必须是相同的颜色:
void MainContentComponent::paint (Graphics& g)
{
Random& r (Random::getSystemRandom());
g.fillAll (Colours::cornflowerblue);
**Path path;**
**path.startNewSubPath (r.nextFloat() * getWidth(),**
**r.nextFloat() * getHeight());**
for (int i = 0; i < 20; ++i) {
**path.lineTo (r.nextFloat() * getWidth(),**
**r.nextFloat() * getHeight());**
}
**g.setColour (Colour (r.nextFloat(),**
**r.nextFloat(),**
**r.nextFloat(),**
**r.nextFloat()));**
****const float lineThickness = r.nextFloat() * 5.f + 1.f;**
**g.strokePath (path, PathStrokeType (lineThickness));**
}**
在这里,路径是在for()
循环之前创建的,循环的每次迭代都会向路径添加一个线段。这两种线条绘制方法显然适用于不同的应用。路径绘制技术高度可定制,特别是:
-
线段的角点可以使用
PathStrokeType
类进行自定义(例如,使角略微圆润)。 -
线条不一定是直的:它们可以是贝塞尔曲线。
-
路径可能包括其他基本形状,如矩形、椭圆、星星、箭头等。
除了这些线绘制命令之外,还有专门用于绘制水平和垂直线(即非对角线)的加速函数。这些是Graphics::drawVerticalLine()
和Graphics::drawHorizontalLine()
函数。
拦截鼠标活动
为了帮助您的组件响应用户的鼠标交互,Component
类有六个重要的回调函数,您可以重写它们:
-
mouseEnter()
: 当鼠标指针进入此组件的边界且鼠标按钮处于抬起状态时调用。** -
mouseMove()
: 当鼠标指针在此组件的边界内移动且鼠标按钮处于抬起状态时调用。mouseEnter()
回调总是先被接收到。** -
mouseDown()
: 当鼠标指针在此组件上方按下一个或多个鼠标按钮时调用。在调用mouseEnter()
回调之前,总会先接收到一个回调,并且很可能还会接收到一个或多个mouseMove()
回调。 -
mouseDrag()
: 当鼠标指针在mouseDown()
回调后在此组件上移动时调用。鼠标指针的位置可能位于组件的边界之外。 -
mouseUp()
: 当在mouseDown()
回调后释放鼠标按钮时调用(此时鼠标指针不一定在组件上)。 -
mouseExit()
: 当鼠标指针在鼠标按钮抬起状态下离开此组件的边界,并且在用户点击此组件后(即使鼠标指针在一段时间前已经离开了此组件的边界)接收到mouseUp()
回调时调用。**
在这些情况下,回调函数会传递一个指向MouseEvent
对象的引用,该对象可以提供有关鼠标当前状态的信息(事件发生时鼠标的位置、事件发生的时间、键盘上的哪些修改键被按下、哪些鼠标按钮被按下等等)。实际上,尽管这些类和函数名称指的是“鼠标”,但此系统可以处理多点触控事件,并且MouseEvent
对象可以询问在这种情况下涉及的是哪个“手指”(例如,在 iOS 平台上)。
为了实验这些回调函数,创建一个新的 Introjucer 项目,并将其命名为Chapter02_06
。为此项目使用以下代码。
MainComponent.h
文件声明了具有其各种成员函数和数据的类:
**#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
void paint (Graphics& g);
void mouseEnter (const MouseEvent& event);
void mouseMove (const MouseEvent& event);
void mouseDown (const MouseEvent& event);
void mouseDrag (const MouseEvent& event);
void mouseUp (const MouseEvent& event);
void mouseExit (const MouseEvent& event);
void handleMouse (const MouseEvent& event);
private:
String text;
int x, y;
};
#endif**
MainComponent.cpp
文件应包含以下代码。首先,添加构造函数和paint()
函数。paint()
函数在鼠标位置绘制一个黄色圆圈,并显示当前鼠标交互阶段的文本:
**#include "MainComponent.h"
MainContentComponent::MainContentComponent()
: x (0), y (0)
{
setSize (200, 200);
}
void MainContentComponent::paint (Graphics& g)
{
g.fillAll (Colours::cornflowerblue);
g.setColour (Colours::yellowgreen);
g.setFont (Font (24));
g.drawText (text, 0, 0, getWidth(), getHeight(),
Justification::centred, false);
g.setColour (Colours::yellow);
const float radius = 10.f;
g.fillEllipse (x - radius, y - radius,
radius * 2.f, radius * 2.f);
}**
然后添加鼠标事件回调和以下描述的我们的 handleMouse()
函数。我们根据我们的组件存储鼠标回调的坐标,并根据回调类型(鼠标按下、释放、移动等)存储一个 String
对象。由于每种情况下坐标的存储都是相同的,我们使用 handleMouse()
函数,该函数将 MouseEvent
对象的坐标存储在我们的类成员变量 x
和 y
中,并将此 MouseEvent
对象从回调中传递。为了确保组件重新绘制,我们必须调用 Component::repaint()
函数。
**void MainContentComponent::mouseEnter (const MouseEvent& event)
{
text = "mouse enter";
handleMouse (event);
}
void MainContentComponent::mouseMove (const MouseEvent& event)
{
text = "mouse move";
handleMouse (event);
}
void MainContentComponent::mouseDown (const MouseEvent& event)
{
text = "mouse down";
handleMouse (event);
}
void MainContentComponent::mouseDrag (const MouseEvent& event)
{
text = "mouse drag";
handleMouse (event);
}
void MainContentComponent::mouseUp (const MouseEvent& event)
{
text = "mouse up";
handleMouse (event);
}
void MainContentComponent::mouseExit (const MouseEvent& event)
{
text = "mouse exit";
handleMouse (event);
}
void MainContentComponent::handleMouse (const MouseEvent& event)
{
x = event.x;
y = event.y;
repaint();
}**
如图所示,结果是位于我们的鼠标指针下的黄色圆圈和窗口中心的一个文本消息,该消息提供有关最近接收到的鼠标事件类型的反馈:
****# 配置复杂的组件布局
JUCE 使创建自定义组件变得简单,无论是通过组合几个内置组件,还是通过提供一种与指针设备交互的有效方法,并结合一系列基本绘图命令。除此之外,Introjucer 应用程序还提供了一个图形编辑器,用于布局自定义组件。然后它会自动生成重建此界面所需的应用程序代码。像之前一样创建一个新的 Introjucer 项目,包含一个基本窗口,并将其命名为 Chapter02_07
。
切换到 文件 面板,在层次结构中的 源 文件夹上右键单击(在 Mac 上,按 control 并单击),然后从上下文菜单中选择 添加新 GUI 组件…,如图所示:
您将被要求命名头文件,该文件也命名了相应的 .cpp
文件。将头文件命名为 CustomComponent.h
。当您选择以这种方式创建的 .cpp
文件时,您将获得几种编辑文件的方式。特别是您可以添加子组件,添加绘图命令,或者您可以直接编辑代码。选择如图所示的 CustomComponent.cpp
文件:
在 子组件 面板中,您可以在网格上右键单击以添加几种内置组件类型之一。添加几个按钮和滑块。选择任何一个组件时,都可以使用窗口右侧的属性进行编辑。这里特别有用的是能够设置关于组件相对于彼此和父组件位置复杂规则的能力。以下截图显示了此选项的一些示例:
由于 Introjucer 应用程序生成 C++ 代码,应该很清楚这些选项可以通过编程方式明确获得。对于某些任务,尤其是复杂的 GUI,使用 GUI 编辑器可能更方便。这也是发现各种组件类中可用的功能和启用/控制这些功能的相应代码的有用方式。
在在你的 IDE 中打开项目之前,选择 类 面板(使用位于 子组件 选项卡左侧的选项卡),并将 类名 从 NewComponent
更改为 CustomComponent
(以匹配代码的文件名)。保存 Introjucer 项目并打开其 IDE 项目。你需要对 MainContentComponent
类进行仅少数几个小的修改,才能将此自动生成的代码加载进去。按照以下方式更改 MainComponent.h
文件:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
#include "CustomComponent.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
private:
CustomComponent custom;
};
#endif
然后,将 MainComponent.cpp
文件更改为:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
addAndMakeVisible (&custom);
setSize (custom.getWidth(), custom.getHeight());
}
这将分配一个 CustomComponent
对象,并使其填充 MainContentComponent
对象的边界。构建并运行应用程序,你应该在 Introjucer 应用程序的 GUI 编辑器中看到你设计的任何用户界面。Introjucer 应用程序对这些自动生成的 GUI 控件的源文件进行特殊控制。查看 CustomComponent.h
和 CustomComponent.cpp
文件。你将看到一些在本章早期部分出现过的代码(一个主要区别是,Introjucer 应用程序生成代码以动态分配子组件类,而不是像我们在这里所做的那样使用静态分配)。在编辑这些自动生成的 GUI 文件中的代码时,你必须非常小心,因为将项目重新加载到 Introjucer 应用程序可能会覆盖一些你的更改(这不会在常规代码文件中发生)。Introjucer 应用程序使用特殊标记的开头和结尾注释来识别你可以进行修改的区域。例如,这是一个典型的自动生成组件构造函数的结尾:
...
//[UserPreSize]
//[/UserPreSize]
setSize (600, 400);
//[Constructor] You can add your own custom stuff here..
//[/Constructor]
}
你可以在 [UserPreSize]
开头标签和 [UserPreSize]
结束标签之间,以及 [Constructor]
开头标签和 [Constructor]
结束标签之间进行修改和添加代码。实际上,你可以在这些开头和结束标签之间进行编辑,但不能在其他任何地方。这样做可能会在下次将 Introjucer 项目保存到磁盘时删除你的更改。这适用于你添加另一个构建目标、添加另一个 GUI 组件、将其他文件添加到 Introjucer 项目中,以及你在 Introjucer 应用程序中明确保存项目的情况。
其他组件类型
JUCE 包含用于特定任务的广泛其他组件类型。其中许多将很熟悉,因为许多操作系统和其他 GUI 框架中都提供了类似的控件。特别是:
-
按钮: 有几种按钮类型,包括可以使用图像文件和其他形状创建的按钮(例如,
ImageButton
和ShapeButton
类);还有一个ToolbarButton
类,可以用来创建工具栏。 -
菜单: 有一个
PopupMenu
类(用于发布命令)和一个ComboBox
类(用于选择项目)。 -
布局: 有各种类用于组织其他组件,包括一个
TabbedComponent
类(用于创建标签页),一个ViewPort
类(用于创建可滚动内容),一个TableListBox
类(用于创建表格),以及一个TreeView
类(用于将内容组织成层次结构)。 -
文件浏览器: 有多种方式显示和访问文件目录结构,包括
FileChooser
、FileNameComponent
和FileTreeComponent
类。 -
文本编辑器: 有一个通用的
TextEditor
类,以及一个CodeEditorComponent
用于显示和编辑代码。
这些组件的大部分源代码可以在juce/modules/juce_gui_basics
中找到,一些额外的类可以在juce/modules/juce_gui_extra
中找到。所有类都在在线文档中有文档说明。所有类的字母顺序列表可以在这里找到:
www.juce.com/api/annotated.html
概述
到本章结束时,你应该熟悉在 JUCE 中通过编程和通过 Introjucer 应用程序构建用户界面的原则。本章向您展示了如何创建和使用 JUCE 的内置组件,如何构建自定义组件,以及如何在屏幕上执行基本的绘图操作。你应该阅读本章介绍的所有类的在线文档。你还应该检查本书的代码包,其中包含本章开发的每个示例。此代码包还包括每个示例的更多内联注释。下一章将涵盖一系列非 GUI 类,尽管其中许多对于管理用户界面功能的一些元素将很有用。****
第三章。基本数据结构
JUCE 包含了一系列重要的数据结构,其中许多可以被视为标准库类的一些替代品。本章介绍了 JUCE 开发所必需的类。在本章中,我们将涵盖以下主题:
-
理解数值类型
-
使用
String
类指定和操作文本字符串 -
测量和显示时间
-
使用
File
类以跨平台方式指定文件路径(包括对用户主目录、桌面和文档位置的访问) -
使用动态分配的数组:
Array
类 -
使用智能指针类
到本章结束时,你将能够创建和操作 JUCE 的基本类中的数据。
理解数值类型
一些基本数据类型(如 char
、int
、long
等)的字长在不同的平台、编译器和 CPU 架构中是不同的。一个很好的例子是 long
类型。在 Mac OS X 的 Xcode 中,当编译 32 位代码时,long
是 32 位宽,而当编译 64 位代码时,long
是 64 位宽。在 Windows 的 Microsoft Visual Studio 中,long
总是 32 位宽。(同样适用于无符号版本。)JUCE 定义了一些原始类型来帮助编写平台无关的代码。许多这些类型都有熟悉的名字,并且可能与你的代码中使用的其他库和框架中使用的名字相同。这些类型在 juce
命名空间中定义;因此,如果需要,可以使用 juce::
前缀来消除歧义。这些原始类型包括:int8
(8 位有符号整数)、uint8
(8 位无符号整数)、int16
(16 位有符号整数)、uint16
(16 位无符号整数)、int32
(32 位有符号整数)、uint32
(32 位无符号整数)、int64
(64 位有符号整数)、uint64
(64 位无符号整数)、pointer_sized_int
(与平台上的指针具有相同字长的有符号整数)、pointer_sized_uint
(与平台上的指针具有相同字长的无符号整数),以及 juce_wchar
(32 位 Unicode 字符类型)。
在许多情况下,内置类型是足够的。例如,JUCE 在内部使用 int
数据类型用于许多目的,但前面的类型在字长至关重要时可用。此外,JUCE 没有为 char
、float
或 double
定义特殊的数据类型。两种浮点类型都假定符合 IEEE 754 标准,并且假定 float
数据类型是 32 位宽,double
数据类型是 64 位宽。
在此方面,一个最终的实用工具解决了代码中编写 64 位字面量在不同编译器中存在差异的问题。如果需要,可以使用 literal64bit()
宏来编写这样的字面量:
int64 big = literal64bit (0x1234567890);
JUCE 还声明了一些基本的模板类型,用于定义某些几何形状;Component
类特别使用这些类型。一些有用的例子是 Point<ValueType>
、Line<ValueType>
和 Rectangle<ValueType>
。
指定和操作文本字符串
在 JUCE 中,文本通常使用String
类进行操作。在许多方面,这个类可以被视为 C++标准库std::string
类的替代品。我们已经在早期章节中使用了String
类进行基本操作。例如,在第二章中,构建用户界面,字符串被用来设置TextButton
对象上显示的文本,并用来存储在鼠标活动响应中显示的动态变化的字符串。尽管这些例子相当简单,但它们利用了String
类的力量,使得对用户来说设置和操作字符串变得简单直接。
实现这一点的第一种方式是通过使用引用计数的对象来存储字符串。也就是说,当创建一个字符串时,在幕后 JUCE 为该字符串分配了一些内存,存储了该字符串,并返回一个指向该分配内存的String
对象。这个字符串的直拷贝(即没有任何修改)仅仅是新的String
对象,它们指向相同的共享内存。这有助于保持代码效率,允许在函数之间通过值传递String
对象,而无需在过程中复制大量内存的开销。
为了说明一些这些特性,我们首先将使用控制台,而不是图形用户界面(GUI)应用程序。创建一个新的名为Chapter03_01
的 Introjucer 项目;将项目类型更改为控制台应用程序,并在自动生成文件菜单中仅选择创建 Main.cpp 文件。保存项目并将其打开到你的集成开发环境(IDE)中。
将日志消息发布到控制台
要将消息发布到控制台窗口,最好使用 JUCE 的Logger
类。日志可以设置为记录到文本文件,但默认行为是将日志消息发送到控制台。以下是一个简单的“Hello world!”项目,它使用 JUCE String
对象和Logger
类:
#include "../JuceLibraryCode/JuceHeader.h"
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
String message ("Hello world!");
log->writeToLog (message);
return 0;
}
main()
函数中的第一行代码存储了对当前日志记录器的指针,这样我们就可以在后面的例子中多次重用它。第二行从字面量 C 字符串"Hello world!"
创建一个 JUCE String
对象,第三行使用其writeToLog()
函数将此字符串发送到日志记录器。构建并运行此应用程序,控制台窗口应该看起来像以下这样:
JUCE v2.1.2
Hello world!
JUCE 会自动报告第一行;如果你使用的是来自 GIT 仓库的 JUCE 的较新版本,这可能会不同。随后是来自你应用程序的任何日志消息。
字符串操作
虽然这个例子比使用标准 C 字符串的等效例子更复杂,但 JUCE 的String
类的强大功能是通过字符串的存储和处理来实现的。例如,为了连接字符串,+
操作符被重载用于此目的:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
String hello ("Hello");
String space (" ");
String world ("world!");
String message = hello + space + world;
log->writeToLog (message);
return 0;
}
在这里,从字面量构造了"Hello"
、中间的空格和"world!"
等单独的字符串,然后通过连接这三个字符串来构造最终的message
字符串。流操作符<<
也可以用于此目的,以获得类似的结果:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
String hello ("Hello");
String space (" ");
String world ("world!");
String message;
message << hello;
message << space;
message << world;
log->writeToLog (message);
return 0;
}
流操作符将表达式的右侧连接到左侧,就地完成。实际上,使用这个简单的例子,当应用于字符串时,<<
操作符等同于+=
操作符。为了说明这一点,将代码中所有<<
实例替换为+=
。
主要区别在于<<
操作符可以更方便地链入更长的表达式,而不需要额外的括号(这是由于 C++中<<
和+=
操作符优先级的差异)。因此,如果需要,可以像使用+
操作符一样,在一行内完成连接:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
String message;
message << "Hello" << " " << "world!";
log->writeToLog (message);
return 0;
}
要使用+=
达到相同的结果,需要在表达式的每一部分都使用繁琐的括号:(((message += "Hello") += " ") += "world!")
。
JUCE 中字符串内部引用计数的实现方式意味着你很少需要担心意外的副作用。例如,以下列表的工作方式可能正如你从阅读代码中预期的那样:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
String string1 ("Hello");
String string2 = string1;
string1 << " world!";
log->writeToLog ("string1: " + string1);
log->writeToLog ("string2: " + string2);
return 0;
}
这会产生以下输出:
string1: Hello world!
string2: Hello
将其分解为步骤,我们可以看到发生了什么:
-
String string1 ("Hello");
:string1
变量使用字面量字符串初始化。 -
String string2 = string1;
:string2
变量使用string1
初始化;它们现在在幕后指向完全相同的数据。 -
string1 << " world!";
:string1
变量附加了另一个字面量字符串。此时string1
指向一个包含连接字符串的新内存块。 -
log->writeToLog ("string1: " + string1);
: 这条日志记录了string1
,显示了连接后的字符串Hello world!
。 -
log->writeToLog ("string2: " + string2);
: 这条日志记录了string2
;这表明string1
仍然指向初始字符串Hello
。
String
类的一个非常有用的功能是其数值转换能力。通常,你可以将数值类型传递给String
构造函数,生成的String
对象将表示该数值。例如:
String intString (1234); // string will be "1234"
String floatString (1.25f); // string will be "1.25"
String doubleString (2.5); // string will be "2.5"
其他有用的功能包括转换为大写和小写。字符串也可以使用==
操作符进行比较。
测量和显示时间
JUCE 的 Time
类提供了一种跨平台的方法,以人类可读的方式指定、测量和格式化日期和时间信息。内部,Time
类以相对于 1970 年 1 月 1 日午夜毫秒为单位存储一个值。要创建表示当前时间的 Time
对象,使用 Time::getCurrentTime()
,如下所示:
Time now = Time::getCurrentTime();
要绕过创建 Time
对象,可以直接以 64 位值的形式访问毫秒计数器:
int64 now = Time::currentTimeMillis();
Time
类还提供了访问自系统启动以来的 32 位毫秒计数器的功能,用于测量时间:
uint32 now = Time::getMillisecondCounter();
关于 Time::getMillisecondCounter()
的重要点是,它是独立于系统时间的,并且不会受到用户更改时间、由于国家夏令时变化等原因的系统时间更改的影响。
显示和格式化时间信息
显示时间信息很简单;以下示例从操作系统获取当前时间,将其格式化为字符串,并输出到控制台:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
Time time (Time::getCurrentTime());
bool includeDate = true;
bool includeTime = true;
bool includeSeconds = true;
bool use24HourClock = true;
String timeStr (time.toString (includeDate, includeTime,
includeSeconds, use24HourClock));
log->writeToLog ("the time is: " + timeStr);
return 0;
}
这说明了 Time::toString()
函数可用的四个选项标志。控制台上的输出将类似于以下内容:
the time is: 7 Jul 2013 15:05:55
对于更全面的选择,Time::formatted()
函数允许用户使用特殊的格式字符串(使用与标准 C strftime()
函数等效的系统)指定格式。或者,你可以获取日期和时间信息的各个部分(日、月、时、分、时区等),并将它们组合成字符串。例如,可以使用以下方式实现相同的前置格式:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
Time time (Time::getCurrentTime());
String timeStr;
bool threeLetterMonthName = true;
timeStr << time.getDayOfMonth() << " ";
timeStr << time.getMonthName (threeLetterMonthName) << " ";
timeStr << time.getYear() << " ";
timeStr << time.getHours() << ":";
timeStr << time.getMinutes() << ":";
timeStr << time.getSeconds();
log->writeToLog ("the time is: " + timeStr);
return 0;
}
操作时间数据
Time
对象也可以被操作(借助 RelativeTime
类的帮助)并与其他 Time
对象进行比较。以下示例展示了基于当前时间创建三个时间值,使用一小时偏移量:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
Time time (Time::getCurrentTime());
RelativeTime oneHour (RelativeTime::hours (1));
Time oneHourAgo (time - oneHour);
Time inOneHour (time + oneHour);
Time inTwoHours (inOneHour + oneHour);
log->writeToLog ("the time is:" +
time.toString (true, true, true, true));
log->writeToLog ("one hour ago was:" +
oneHourAgo.toString (true, true, true, true));
log->writeToLog ("in one hour it will be:" +
inOneHour.toString (true, true, true, true));
log->writeToLog ("in two hours it will be:" +
inTwoHours.toString (true, true, true, true));
return 0;
}
这个输出的结果应该类似于这样:
the time is: 7 Jul 2013 15:42:27
one hour ago was: 7 Jul 2013 14:42:27
in one hour it will be: 7 Jul 2013 16:42:27
in two hours it will be: 7 Jul 2013 17:42:27
要比较两个 Time
对象,可以使用标准比较运算符。例如,你可以等待特定的时间,如下所示:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
Time now (Time::getCurrentTime());
Time trigger (now + RelativeTime (5.0));
log->writeToLog ("the time is now: " +
now.toString (true, true, true, true));
while (Time::getCurrentTime() < trigger) {
Thread::sleep (10);
log->writeToLog ("waiting...");
}
log->writeToLog ("the time has reached: " +
trigger.toString (true, true, true, true));
return 0;
}
这里有两个需要注意的地方是:
-
传递给
RelativeTime
构造函数的值以秒为单位(所有其他时间值都需要使用前面显示的静态函数,例如小时、分钟等)。 -
Thread::sleep()
函数的调用使用毫秒值,这将使调用线程休眠。Thread
类将在 第五章,有用的工具 中进一步探讨。
测量时间
从 Time::getCurrentTime()
函数返回的时间值对于大多数用途应该是准确的,但如前所述,当前时间 可以通过用户修改系统时间而改变。以下是一个等效的示例,使用 Time::getMillisecondCounter()
,它不受此类变化的影响:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
uint32 now = Time::getMillisecondCounter();
uint32 trigger = now + 5000;
log->writeToLog ("the time is now: " +
String (now) + "ms");
while (Time::getMillisecondCounter() < trigger) {
Thread::sleep (10);
log->writeToLog ("waiting...");
}
log->writeToLog ("the time has reached: " +
String (trigger) + "ms");
return 0;
}
Time::getCurrentTime()
和Time::getMillisecondCounter()
函数具有相似的精度,在大多数平台上都在几毫秒之内。然而,Time
类还提供了一个更高分辨率的计数器,返回双精度(64 位)浮点值。这个函数是Time::getMillisecondCounterHiRes()
,与Time::getMillisecondCounter()
函数返回的值一样,也是相对于系统启动的。一个应用这个函数的例子是测量某些代码片段执行所需的时间,如下面的示例所示:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
double start = Time::getMillisecondCounterHiRes();
log->writeToLog ("the time is now: " +
String (start) + "ms");
float value = 0.f;
const int N = 10000;
for (int i = 0; i < N; ++i)
value += 0.1f;
double duration = Time::getMillisecondCounterHiRes() - start;
log->writeToLog ("the time taken to perform " + String (N) +
" additions was: " + String (duration) + "ms");
return 0;
}
这通过轮询更高分辨率的计数器,执行大量浮点数加法,然后再次轮询更高分辨率的计数器来确定这两个时间点之间的持续时间。输出应该类似于以下内容:
the time is now: 267150354ms
the time taken to perform 10000 additions was: 0.0649539828ms
当然,这里的结果取决于编译器和运行时系统中的优化设置。
指定文件路径
JUCE 通过File
类提供了一种相对跨平台的方式来指定和操作文件路径。特别是,这提供了一种访问用户系统上各种特殊目录的方法,例如桌面
目录、他们的用户文档
目录、应用程序首选项目录等等。File
类还提供了访问文件信息的功能(例如,创建日期、修改日期、文件大小)以及读写文件内容的基本机制(尽管对于大型或复杂文件,其他技术可能更合适)。在以下示例中,一个字符串被写入磁盘上的文本文件(使用File::replaceWithText()
函数),然后被读入第二个字符串(使用File::loadFileAsString()
函数),并在控制台中显示:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
String text ("The quick brown fox jumps over the lazy dog.");
File file ("./chapter03_01_test.txt");
file.replaceWithText (text);
String fileText = file.loadFileAsString();
log->writeToLog ("fileText: " + fileText);
return 0;
}
在这种情况下,File
对象被初始化为路径./chapter03_01_test.txt
。需要注意的是,此时该文件可能不存在,并且在第一次运行时,它将在调用File::replaceWithText()
函数之前不存在(在后续运行中,该文件将存在,但在那个点将被覆盖)。路径前面的./
字符序列是一个常见的惯用语,指定路径的其余部分应该是相对于当前目录(或当前工作目录)。在这个简单的情况下,当前工作目录很可能是可执行文件所在的目录。以下截图显示了在 Mac 平台上相对于 Introjucer 项目的这个位置:
这不是一个可靠的方法;然而,如果工作目录正好是你想要保存文件的地方,它将会工作。
访问各种特殊目录位置
使用File
类的一个特殊位置会更精确,如下所示:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
String text ("The quick brown fox jumps over the lazy dog.");
File exe (File::getSpecialLocation(
File::currentExecutableFile));
File exeDir (exe.getParentDirectory());
File file (exeDir.getChildFile ("chapter03_01_test.txt"));
file.replaceWithText (text);
String fileText = file.loadFileAsString();
log->writeToLog ("fileText: " + fileText);
return 0;
}
为了使代码清晰,访问此目录中的文件位置的步骤被拆分在多行中。在这里,你可以看到获取当前可执行文件位置、然后是其父目录,然后为相对于此目录的文本文件创建文件引用的代码。大部分这段代码可以通过使用函数调用链在单逻辑行中压缩:
...
File file (File::getSpecialLocation(
File::currentExecutableFile)
.getParentDirectory()
.getChildFile ("chapter03_01_test.txt"));
...
由于此代码中某些标识符的长度和本书的页面宽度,这段代码仍然占据了四行物理代码。尽管如此,这说明了你可以如何使用这些函数调用来满足你的需求和代码布局偏好。
获取有关文件的各种信息
File
类可以提供有关文件的有用信息。一个重要的测试是文件是否存在;这可以通过使用File::exists()
来确定。如果文件确实存在,则可以获得更多信息,例如其创建日期、修改日期和大小。以下示例中展示了这些信息:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
File file (File::getSpecialLocation(File::currentExecutableFile)
.getParentDirectory()
.getChildFile("chapter03_01_test.txt"));
bool fileExists = file.exists();
if (!fileExists) {
log->writeToLog ("file " +
file.getFileName() +
" does not exist");
return -1;
}
Time creationTime = file.getCreationTime();
Time modTime = file.getLastModificationTime();
int64 size = file.getSize();
log->writeToLog ("file " +
file.getFileName() + " info:");
log->writeToLog ("created: " +
creationTime.toString(true, true, true, true));
log->writeToLog ("modified:" +
modTime.toString(true, true, true, true));
log->writeToLog ("size:" +
String(size) + " bytes");
return 0;
}
假设你运行了所有前面的示例,文件应该存在于你的系统上,信息将在控制台以类似以下方式报告:
file chapter03_01_test.txt info:
created: 8 Jul 2013 17:08:25
modified: 8 Jul 2013 17:08:25
size: 44 bytes
其他特殊位置
除了File::currentExecutableFile
,JUCE 还知道的其他特殊位置包括:
-
File::userHomeDirectory
-
File::userDocumentsDirectory
-
File::userDesktopDirectory
-
File::userApplicationDataDirectory
-
File::commonApplicationDataDirectory
-
File::tempDirectory
-
File::currentExecutableFile
-
File::currentApplicationFile
-
File::invokedExecutableFile
-
File::hostApplicationPath
-
File::globalApplicationsDirectory
-
File::userMusicDirectory
-
File::userMoviesDirectory
-
File::userPicturesDirectory
每个这些名称都相当直观。在某些情况下,这些特殊位置在某些平台上不适用。例如,iOS 平台上没有所谓的Desktop
。
导航目录结构
最终,一个File
对象在用户的系统上解析为一个绝对路径。如果需要,可以使用File::getFullPathName()
函数来获取:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
File file (File::getSpecialLocation(
File::currentExecutableFile)
.getParentDirectory()
.getChildFile ("chapter03_01_test.txt"));
log->writeToLog ("file path: " + file.getFullPathName());
return 0;
}
此外,传递给File::getChildFile()
的相对路径可以包含一个或多个使用双点表示法(即,“..
”字符序列)引用父目录的引用。在下面的示例中,我们创建了一个简单的目录结构,如代码列表后面的截图所示:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
File root (File::getSpecialLocation (File::userDesktopDirectory)
.getChildFile ("Chapter03_01_tests"));
File dir1 (root.getChildFile ("1"));
File dir2 (root.getChildFile ("2"));
File dir1a (dir1.getChildFile ("a"));
File dir2b (dir2.getChildFile ("b"));
Result result (Result::ok());
result = dir1a.createDirectory();
if (!result.wasOk()) {
log->writeToLog ("Creating dir 1/a failed");
return -1;
}
result = dir2b.createDirectory();
if (!result.wasOk()) {
log->writeToLog ("Creating dir 2/b failed");
return -1;
}
File rel = dir1a.getChildFile ("../../2/b");
log->writeToLog ("root: " + root.getFullPathName());
log->writeToLog ("dir1: " + dir1.getRelativePathFrom (root));
log->writeToLog ("dir2: " + dir2.getRelativePathFrom (root));
log->writeToLog ("dir1a: " + dir1a.getRelativePathFrom (root));
log->writeToLog ("dir2b: " + dir2b.getRelativePathFrom (root));
log->writeToLog ("rel: " + rel.getRelativePathFrom (root));
return 0;
}
这总共创建了五个目录,只使用了两次 File::createDirectory()
函数调用。由于这取决于用户在此目录中创建文件的权限,该函数返回一个 Result
对象。该对象包含一个状态来指示函数是否成功(我们通过 Result::wasOk()
函数进行检查),如果需要,还可以获取有关任何错误的更多信息。每次调用 File::createDirectory()
函数都确保如果需要,它将创建任何中间目录。因此,在第一次调用时,它创建了根目录、目录 1
和目录 1/a
。在第二次调用时,根目录已经存在,因此它只需要创建目录 2
和 2/a
。
控制台输出应该是这样的:
root: /Users/martinrobinson/Desktop/Chapter03_01_tests
dir1: 1
dir2: 2
dir1a: 1/a
dir2b: 2/b
rel: 2/b
当然,第一行将根据您的系统而有所不同,但剩余的五行应该是相同的。这些路径是相对于我们使用 File::getRelativePathFrom()
函数创建的目录结构根目录显示的。注意,最后一行显示 rel
对象指向与 dir2b
对象相同的目录,但我们通过使用函数调用 dir1a.getChildFile("../../2/b")
相对于 dir1a
对象创建了此 rel
对象。也就是说,我们在目录结构中向上导航两级,然后访问下面的目录。
File
类还包括检查文件是否存在的功能,在文件系统中移动和复制文件(包括将文件移动到 垃圾桶 或 回收站),以及在特定平台上创建合法的文件名(例如,避免冒号和斜杠字符)。
使用动态分配的数组
虽然大多数 JUCE 对象的实例可以存储在常规 C++ 数组中,但 JUCE 提供了一些更强大的数组,与 C++ 标准库类(如 std::vector
)有些相似。JUCE 的 Array
类提供了许多功能;这些数组可以是:
-
动态大小;可以在任何索引处添加、删除和插入项目
-
使用自定义比较器进行排序
-
搜索特定内容
Array
类是一个模板类;其主模板参数 ElementType
必须满足某些标准。Array
类在调整大小和插入元素时通过复制内存来移动其内容,这可能会与某些类型的对象造成问题。作为 ElementType
模板参数传递的类必须同时具有复制构造函数和赋值运算符。特别是,Array
类与原始类型和一些常用的 JUCE 类(例如 File
和 Time
类)配合得很好。在以下示例中,我们创建了一个整数数组,向其中添加了五个项目,并遍历数组,将内容发送到控制台:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
Array<int> array;
for (int i = 0; i < 5; ++i)
array.add (i * 1000);
for (int i = 0; i < array.size(); ++i) {
int value = array[i];
log->writeToLog ("array[" + String (i) + "]= " + String (value));
}
return 0;
}
这应该会产生以下输出:
array[0]= 0
array[1]= 1000
array[2]= 2000
array[3]= 3000
array[4]= 4000
注意到 JUCE 的 Array
类支持 C++ 索引下标操作符 []
。即使数组索引超出范围,它也会始终返回一个有效值(与内置数组不同)。进行此检查涉及一些开销;因此,您可以通过使用 Array::getUnchecked()
函数来避免边界检查,但您必须确保索引在范围内,否则您的应用程序可能会崩溃。第二个 for()
循环可以重写如下以使用此替代函数,因为我们已经检查了索引将在范围内:
...
for (int i = 0; i < array.size(); ++i) {
int value = array.getUnchecked (i);
log->writeToLog("array[" + String (i) + "] = " +
String (value));
}
...
在目录中查找文件
JUCE 库使用 Array
对象来完成许多目的。例如,File
类可以使用 File::findChildFiles()
函数将包含的子文件和目录列表填充到一个 File
对象数组中。以下示例应将用户 Documents
目录中的文件和目录列表输出到控制台:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
File file =
File::getSpecialLocation (File::userDocumentsDirectory);
Array<File> childFiles;
bool searchRecursively = false;
file.findChildFiles (childFiles,
File::findFilesAndDirectories,
searchRecursively);
for (int i = 0; i < childFiles.size(); ++i)
log->writeToLog (childFiles[i].getFullPathName());
return 0;
}
在这里,File::findChildFiles()
函数被传递了一个 File
对象数组,它应该添加搜索结果。它还被指示使用值 File::findFilesAndDirectories
(其他选项是 File::findDirectories
和 File::findFiles
值)来查找文件和目录。最后,它被指示不要递归搜索。
字符串标记化
虽然可以使用 Array<String>
来存储 JUCE String
对象的数组,但有一个专门的 StringArray
类,在将数组操作应用于字符串数据时提供了额外的功能。例如,可以使用 String::addTokens()
函数将字符串标记化(即将原始字符串中的空白字符分割成更小的字符串),或者使用 String::addLines()
函数将其分割成表示文本行的字符串(基于原始字符串中找到的换行符序列)。以下示例将一个字符串标记化,然后遍历生成的 StringArray
对象,将其内容输出到控制台:
int main (int argc, char* argv[])
{
Logger *log = Logger::getCurrentLogger();
StringArray strings;
bool preserveQuoted = true;
strings.addTokens("one two three four five six",
preserveQuoted);
for (int i = 0; i < strings.size(); ++i) {
log->writeToLog ("strings[" + String (i) + "]=" +
strings[i]);
}
return 0;
}
组件数组
由类似控件(如按钮和滑块)组成的用户界面可以使用数组有效地管理。然而,JUCE 的 Component
类及其子类不符合在 JUCE Array
对象中作为对象(即按值)存储的标准。这些必须存储为指向这些对象的指针数组。为了说明这一点,我们需要一个新的 Introjucer 项目,其中包含一个基本的窗口,如 第二章 中使用的,构建用户界面。创建一个新的 Introjucer 项目,例如,命名为 Chapter03_02
,并在您的 IDE 中打开它。在 Main.cpp
中 MainWindow
构造函数的末尾添加以下行:
setResizable (true, true);
在 MainComponent.h
文件中修改代码如下:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
~MainContentComponent();
void resized();
private:
Array<TextButton*> buttons;
};
#endif
注意,这里的 Array
对象是一个指向 TextButton
对象的指针数组(即 TextButton*
)。在 MainComponent.cpp
文件中修改代码如下:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
for (int i = 0; i < 10; ++i)
{
String buttonName;
buttonName << "Button " << String (i);
TextButton* button = new TextButton (buttonName);
buttons.add (button);
addAndMakeVisible (button);
}
setSize (500, 400);
}
MainContentComponent::~MainContentComponent()
{
}
void MainContentComponent::resized()
{
Rectangle<int> rect (10, 10, getWidth() - 20, getHeight() - 20);
int buttonHeight = rect.getHeight() / buttons.size();
for (int i = 0; i < buttons.size(); ++i) {
buttons[i]->setBounds (rect.getX(),
i * buttonHeight + rect.getY(),
rect.getWidth(),
buttonHeight);
}
}
在这里,我们创建了 10 个按钮,使用for()
循环将它们添加到数组中,并基于循环计数器命名按钮。按钮使用new
运算符分配(而不是在第二章中使用的静态分配,即构建用户界面),并且这些指针被存储在数组中。(注意,在调用Component::addAndMakeVisible()
函数时不需要&
运算符,因为值已经是指针。)在resized()
函数中,我们使用Rectangle<int>
对象创建一个矩形,该矩形从MainContentComponent
对象的外边距矩形向内缩进 10 像素。按钮位于这个较小的矩形内。每个按钮的高度通过将矩形的高度除以按钮数组中的按钮数量来计算。然后for()
循环根据其在数组中的索引定位每个按钮。构建并运行应用程序;其窗口应显示 10 个按钮,排列成单列。
前面的代码有一个主要缺陷。使用new
运算符分配的按钮从未被删除。代码应该可以正常运行,尽管当应用程序退出时,你将得到一个断言失败。控制台中的消息可能类似于:
*** Leaked objects detected: 10 instance(s) of class TextButton
JUCE Assertion failure in juce_LeakedObjectDetector.h:95
为了解决这个问题,我们可以在MainComponent
析构函数中删除按钮,如下所示:
MainContentComponent::~MainContentComponent()
{
for (int i = 0; i < buttons.size(); ++i)
delete buttons[i];
}
然而,在编写复杂代码时很容易忘记执行此类操作。
使用OwnedArray
类
JUCE 提供了一个针对指针类型定制的Array
类的有用替代品:OwnedArray
类。OwnedArray
类始终存储指针,因此模板参数中不应包含*
字符。一旦指针被添加到OwnedArray
对象中,它将接管指针的所有权,并在必要时(例如,当OwnedArray
对象本身被销毁时)负责删除它。在MainComponent.h
文件中修改声明,如下所示:
...
private:
OwnedArray<TextButton> buttons;
};
你还应该从MainComponent.cpp
文件中的析构函数中删除代码,因为删除对象多次同样有问题:
...
MainContentComponent::~MainContentComponent()
{
}
...
构建并运行应用程序,注意应用程序现在将无问题退出。
这种技术可以扩展到使用广播者和听众。像之前一样创建一个新的基于 GUI 的 Introjucer 项目,并将其命名为Chapter03_03
。将MainComponent.h
文件修改为:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component,
public Button::Listener
{
public:
MainContentComponent();
void resized();
void buttonClicked (Button* button);
private:
OwnedArray<Button> buttons;
Label label;
};
#endif
这次我们使用OwnedArray<Button>
对象而不是OwnedArray<TextButton>
对象。这仅仅避免了在数组中搜索指针时需要将我们的按钮指针强制转换为不同类型的需求,就像我们在以下代码中所做的那样。注意,在这里我们还添加了一个Label
对象到我们的组件中,使我们的组件成为按钮监听器,并且我们不需要析构函数。将MainComponent.cpp
文件修改为:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
for (int i = 0; i < 10; ++i) {
String buttonName;
buttonName << "Button " << String (i);
TextButton* button = new TextButton (buttonName);
button->addListener (this);
buttons.add (button);
addAndMakeVisible (button);
}
addAndMakeVisible (&label);
label.setJustificationType (Justification::centred);
label.setText ("no buttons clicked", dontSendNotification);
setSize (500, 400);
}
void MainContentComponent::resized()
{
Rectangle<int> rect (10, 10,
getWidth() / 2 - 20, getHeight() - 20);
int buttonHeight = rect.getHeight() / buttons.size();
for (int i = 0; i < buttons.size(); ++i) {
buttons[i]->setBounds (rect.getX(),
i * buttonHeight + rect.getY(),
rect.getWidth(),
buttonHeight);
}
label.setBounds (rect.getRight(),
rect.getY(),
getWidth() - rect.getWidth() - 10,
20);
}
void MainContentComponent::buttonClicked (Button* button)
{
String labelText;
nt buttonIndex = buttons.indexOf (button);
labelText << "Button clicked: " << String (buttonIndex);
label.setText (labelText, dontSendNotification);
}
在这里,我们在构造函数中添加了标签,将按钮组的宽度减小到只占用组件的左侧一半,并将标签定位在右侧的上部。在按钮监听器回调中,我们可以使用OwnedArray::indexOf()
函数来获取按钮的索引,以搜索指针(顺便说一句,Array
类也有一个indexOf()
函数用于搜索项)。构建并运行应用程序,注意我们的标签报告了哪个按钮被点击。当然,这个代码优雅的地方在于,我们只需在构造函数中更改for()
循环中的值,就可以更改创建的按钮数量;其他所有事情都会自动完成。
其他控件组
这种方法可以应用于其他控件组。以下示例创建了一个滑块和标签的控件组,并确保每个对应组件都使用适当的值进行更新。创建一个新的基于 GUI 的 Introjucer 项目,并将其命名为Chapter03_04
。将MainComponent.h
文件更改为:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component,
public Slider::Listener,
public Label::Listener
{
public:
MainContentComponent();
void resized();
void sliderValueChanged (Slider* slider);
void labelTextChanged (Label* label);
private:
OwnedArray<Slider> sliders;
OwnedArray<Label> labels;
};
#endif
在这里,我们有滑块和标签的数组,我们的组件既是标签监听器也是滑块监听器。现在,更新MainComponent.cpp
文件以包含包含指令、构造函数和resized()
函数:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
for (int i = 0; i < 10; ++i) {
String indexString (i);
String sliderName ("slider" + indexString);
Slider* slider = new Slider (sliderName);
slider->setTextBoxStyle (Slider::NoTextBox, false, 0, 0);
slider->addListener (this);
sliders.add (slider);
addAndMakeVisible (slider);
String labelName ("label" + indexString);
Label* label = new Label (labelName,
String (slider->getValue()));
label->setEditable (true);
label->addListener (this);
labels.add (label);
addAndMakeVisible (label);
}
setSize (500, 400);
}
void MainContentComponent::resized()
{
Rectangle<int> slidersRect (10, 10,
getWidth() / 2 - 20,
getHeight() - 20);
Rectangle<int> labelsRect (slidersRect.getRight(), 10,
getWidth() / 2 - 20,
getHeight() - 20);
int cellHeight = slidersRect.getHeight() / sliders.size();
for (int i = 0; i < sliders.size(); ++i) {
sliders[i]->setBounds (slidersRect.getX(),
i * cellHeight + slidersRect.getY(),
slidersRect.getWidth(),
cellHeight);
labels[i]->setBounds (labelsRect.getX(),
i * cellHeight + labelsRect.getY(),
labelsRect.getWidth(),
cellHeight);
}
}
在这里,我们使用for()
循环创建组件并将它们添加到相应的数组中。在resized()
函数中,我们创建了两个辅助矩形,一个用于滑块组,一个用于标签组。这些矩形分别定位在主组件的左侧和右侧一半。
在监听器回调函数中,广播组件的索引在其数组中查找,并使用此索引设置其他对应组件的值。将这些监听器回调函数添加到MainComponent.cpp
文件中:
void MainContentComponent::sliderValueChanged (Slider* slider)
{
int index = sliders.indexOf (slider);
labels[index]->setText (String (slider->getValue()),
sendNotification);
}
void MainContentComponent::labelTextChanged (Label* label)
{
int index = labels.indexOf (label);
sliders[index]->setValue (label->getText().getDoubleValue());
}
在这里,我们使用String
类来执行数值转换。移动一些滑块后,应用程序窗口应类似于以下截图:
希望这些示例说明了将 JUCE 数组类与其他 JUCE 类结合使用的强大之处,以编写优雅、易读且功能强大的代码。
使用智能指针类
OwnedArray
类可以被视为智能指针的管理器,从它管理的对象的生命周期来看。JUCE 包括一系列其他智能指针类型,以帮助解决在编写使用指针的代码时遇到的一些常见问题。特别是,这些有助于避免内存和其他资源的误管理。
可能最简单的智能指针是由ScopedPointer
类实现的。它管理单个指针,并在不再需要时删除它所指向的对象。这可能以两种方式发生:
-
当
ScopedPointer
对象本身被销毁时 -
当一个新的指针被分配给
ScopedPointer
对象时
ScopedPointer
类的一个用途是作为存储 Component
对象(或其子类之一)的替代方法。实际上,在 Introjucer 应用程序的图形编辑器中添加子组件会将组件以 ScopedPointer
对象的形式添加到代码中,这与下面的示例类似。创建一个新的 Introjucer 项目,命名为 Chapter03_05
。以下示例实现了与 Chapter02_02
项目相同的结果,但使用 ScopedPointer
对象来管理组件,而不是静态分配它们。将 MainComponent.h
文件修改如下:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component,
public Button::Listener,
public Slider::Listener
{
public:
MainContentComponent();
void resized();
void buttonClicked (Button* button);
void sliderValueChanged (Slider* slider);
private:
ScopedPointer<Button> button1;
ScopedPointer<Slider> slider1;
ScopedPointer<Label> label1;
};
#endif
注意,我们使用 ScopedPointer<Button>
对象而不是 ScopedPointer<TextButton>
对象,原因与之前我们更倾向于使用 OwnedArray<Button>
对象而不是 OwnedArray<TextButton>
对象的原因相同。将 MainComponent.cpp
文件修改如下:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
button1 = new TextButton ("Zero Slider");
slider1 = new Slider (Slider::LinearHorizontal,
Slider::NoTextBox);
label1 = new Label();
slider1->setRange (0.0, 100.0);
slider1->addListener (this);
button1->addListener (this);
slider1->setValue (100.0, sendNotification);
addAndMakeVisible (button1);
addAndMakeVisible (slider1);
addAndMakeVisible (label1);
setSize (200, 100);
}
void MainContentComponent::resized()
{
button1->setBoundsRelative (0.05, 0.05, 0.90, 0.25);
slider1->setBoundsRelative (0.05, 0.35, 0.90, 0.25);
label1->setBoundsRelative (0.05, 0.65, 0.90, 0.25);
}
void MainContentComponent::buttonClicked (Button* button)
{
if (button1 == button)
slider1->setValue (0.0, sendNotification);
}
void MainContentComponent::sliderValueChanged (Slider* slider)
{
if (slider1 == slider)
label1->setText (String (slider1->getValue()),
sendNotification);
}
这里的主要变化是使用 ->
操作符(ScopedPointer
类重载以返回其包含的指针)而不是 .
操作符。组件都是显式使用 new
操作符分配的,但除此之外,代码几乎与 Chapter02_02
项目相同。
JUCE 中其他有用的内存管理类包括:
-
ReferenceCountedObjectPtr<ReferenceCountedObjectClass>
:这允许您编写类,使得实例可以像String
对象一样在代码中传递。生命周期由维护其自己的计数器的对象管理,该计数器统计代码中对对象的引用数量。这在多线程应用程序和生成图或树结构时特别有用。ReferenceCountedObjectClass
模板参数需要继承自ReferenceCountedObject
类。 -
MemoryBlock
:这管理一块可调整大小的内存,是管理原始内存的推荐方法(例如,而不是使用标准的malloc()
和free()
函数)。 -
HeapBlock<ElementType>
:类似于MemoryBlock
类(实际上,MemoryBlock
对象包含一个HeapBlock<char>
对象),但这是一个智能指针类型,并支持->
操作符。由于它是一个模板类,它也指向特定类型的对象或对象。
摘要
本章概述了 JUCE 中的一些核心类,这些类为构建 JUCE 应用程序提供了基础,并为构建符合 JUCE 风格的应用程序提供了框架。这些类为本书的其余部分提供了进一步的基础。这些类中包含的功能远不止这里概述的。再次强调,您必须查看本章中引入的每个类的 JUCE 类文档。许多这些类在 JUCE 示例应用程序和 Introjucer 应用程序的代码中被大量使用。这些也应该对进一步阅读很有帮助。下一章将介绍用于处理文件(特别是媒体文件,如图像和声音文件)的类。
第四章. 使用媒体文件
JUCE 提供了自己的文件读写类以及许多针对特定媒体格式的辅助类。本章介绍了这些类的主要示例。在本章中,我们将涵盖以下主题:
-
使用简单的输入输出流
-
读写图像文件
-
播放音频文件
-
使用二进制构建工具将二进制文件转换为源代码
到本章结束时,你将能够使用 JUCE 操作一系列媒体文件。
使用简单的输入输出流
在第三章中,我们介绍了 JUCE 的File
类,该类用于以跨平台的方式指定文件路径。此外,File
类还包含一些方便的函数,用于以数据块或文本字符串的形式读写文件。在许多情况下,这些函数是足够的,但在其他情况下,对输入输出流的原始访问可能更有用。
读写文本文件
首先,在 Introjucer 应用程序中创建一个控制台应用程序项目,并将其命名为Chapter04_01
。在这个简单的例子中,我们将声明两个函数,一个用于将文本写入文件——writeFile()
,另一个用于读取文件内容——readFile()
。这些函数都传递了以我们第三章中介绍的方式创建的相同文件路径引用。将Main.cpp
文件的内容替换为以下内容,其中我们声明了文件读写函数,并定义了一个main()
函数:
#include "../JuceLibraryCode/JuceHeader.h"
void writeFile (File const& file);
void readFile (File const& file);
int main (int argc, char* argv[])
{
File file (File::getSpecialLocation(File::currentExecutableFile)
.getParentDirectory()
.getChildFile ("chapter04_01_test.txt"));
writeFile (file);
readFile (file);
return 0;
}
然后,添加writeFile()
函数的定义:
void writeFile (File const& file)
{
Logger *log = Logger::getCurrentLogger();
FileOutputStream stream (file);
if (!stream.openedOk()) {
log->writeToLog ("failed to open stream");
return;
}
stream.setPosition (0);
stream.truncate();
String text ("The quick brown fox jumps over the lazy dog.");
bool asUTF16 = false;
bool byteOrderMark = false;
stream.writeText (text, asUTF16, byteOrderMark);
}
在这里,我们创建了一个FileOutputStream
对象,将其传递给指向文件路径的File
对象。FileOutputStream
类继承自表示将数据写入流的一般概念的基类OutputStream
。还可以有其他类型的输出流,例如用于以流式方式将数据写入计算机内存区域的MemoryOutputStream
类。FileOutputStream
类在构造时的默认行为是将流的写入位置定位在文件的末尾(如果文件已存在),或者(如果不存在)创建一个空文件。对FileOutputStream::setPosition()
和FileOutputStream::truncate()
函数的调用在每次写入之前实际上清空了文件。当然,在实际应用中,你可能不希望每次都这样做。对FileOutputStream::writeText()
函数的调用几乎等同于File::appendText()
函数,尽管File::appendText()
函数的输出控制标志在 Unicode UTF16 格式下是隐式的,但需要显式指定FileOutputStream::writeText()
函数的标志。在这里,我们通过将两个标志都设置为false
以 UTF8 格式写入数据。
小贴士
UFT8 格式可能最方便,因为我们正在写入的是纯 ASCII 文本,它与 UTF8 编码兼容。
最后,添加readFile()
函数的定义:
void readFile (File const& file)
{
Logger *log = Logger::getCurrentLogger();
FileInputStream stream (file);
if (!stream.openedOk()) {
log->writeToLog ("failed to open stream");
return;
}
log->writeToLog ("fileText: " +stream.readEntireStreamAsString());
}
在这里,我们尝试将整个流读取到一个String
中,并将其记录到日志中。我们使用一个继承自更通用的InputStream
类的FileInputStream
对象。在writeFile()
和readFile()
函数中,我们在继续之前检查是否成功打开了流。此外,当流超出作用域时,流对象会优雅地关闭流。
读取和写入二进制文件
输出和输入流也可以用于二进制数据,并且比File
类便利函数提供更强大的功能。在这里,您可以写入原始数值数据,并选择多字节数据类型的字节序。
在 Introjucer 应用程序中创建一个新的控制台应用程序,并将其命名为Chapter04_02
。以下示例将int
、float
和double
数据类型写入文件,然后读取这些数据,并将结果记录到日志中。将Main.cpp
文件的内容替换为以下代码:
#include "../JuceLibraryCode/JuceHeader.h"
void writeFile (File const& file);
void readFile (File const& file);
int main (int argc, char* argv[])
{
File file (File::getSpecialLocation(File::currentExecutableFile)
.getParentDirectory()
.getChildFile ("chapter04_02_test.bin"));
writeFile (file);
readFile (file);
return 0;
}
void writeFile (File const& file)
{
Logger *log = Logger::getCurrentLogger();
FileOutputStream stream (file);
if (!stream.openedOk()) {
log->writeToLog ("failed to open stream");
return;
}
stream.setPosition (0);
stream.truncate();
stream.writeInt (1234);
stream.writeFloat (3.142);
stream.writeDouble (0.000000001);
}
void readFile (File const& file)
{
Logger *log = Logger::getCurrentLogger();
FileInputStream stream (file);
if (!stream.openedOk()) {
log->writeToLog ("failed to open stream");
return;
}
log->writeToLog("readInt: " + String (stream.readInt()));
log->writeToLog("readFloat: " + String (stream.readFloat()));
log->writeToLog("readDouble: " + String (stream.readDouble()));
}
OutputStream
和InputStream
类及其相应的子类支持使用writeInt()
、writeFloat()
、readInt()
、readFloat()
等函数写入和读取各种内置类型。这些函数的版本使用小端字节序写入这些多字节类型。对于需要大端存储的文件格式,有等效的函数writeIntBigEndian()
、writeFloatBigEndian()
、readIntBigEndian()
、readFloatBigEndian()
等。
JUCE 流类很有用,但相当底层。对于许多用途,JUCE 已经包括用于读取和写入特定文件类型的高级类。当然,这些类建立在流类之上,但除非您处理自定义数据格式,否则使用内置功能处理图像、音频和其他格式(如可扩展标记语言(XML)和JavaScript 对象表示法(JSON))可能更合理。
读取和写入图像文件
JUCE 包括内置支持读取和写入 GIF、PNG 和 JPEG 图像文件。JUCE 还包括自己的Image
类来存储位图图像。以下示例说明如何显示一个本地文件浏览器以选择图像文件,加载图像文件,并在ImageComponent
对象中显示它。在 Introjucer 应用程序中创建一个新的 GUI 项目,并命名为Chapter04_03
。在Main.cpp
文件中,将其设置为可调整大小的,就像我们在前面的章节中所做的那样。然后,您应该将MainComponent.h
文件修改为包含以下内容:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component,
public Button::Listener
{
public:
MainContentComponent();
void resized();
void buttonClicked (Button* button);
private:
TextButton readFileButton;
ImageComponent imageComponent;
Image image;
};
#endif
将MainComponent.cpp
修改为包含以下内容:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
: readFileButton ("Read Image File...")
{
addAndMakeVisible (&readFileButton);
addAndMakeVisible (&imageComponent);
readFileButton.addListener (this);
setSize (500, 400);
}
void MainContentComponent::resized()
{
int buttonHeight = 20;
int margin = 10;
readFileButton.setBounds(margin, margin,
getWidth() – margin * 2, buttonHeight);
imageComponent.setBounds(margin, margin + buttonHeight + margin,
getWidth() – margin * 2,
getHeight() – buttonHeight – margin * 3);
}
void MainContentComponent::buttonClicked (Button* button)
{
if (&readFileButton == button)
{
FileChooser chooser ("Choose an image file to display...");
if (chooser.browseForFileToOpen()) {
image = ImageFileFormat::loadFrom (chooser.getResult());
if (image.isValid())
imageComponent.setImage (image);
}
}
}
在这里,我们创建一个 FileChooser
对象以响应用户点击 读取图像文件… 按钮。这会弹出一个原生对话框,允许用户选择文件。我们使用 ImageFileFormat::loadFrom()
函数尝试将文件作为图像加载。因为我们没有限制文件选择器中显示或启用的文件类型,用户可能没有选择有效的图像文件。我们检查图像的有效性,如果有效,我们将加载的图像传递给 ImageComponent
对象以进行显示。ImageComponent
类有各种选项来控制图像的位置和缩放方式,这取决于原始图像大小和组件矩形的比较。这些可以通过使用 ImageComponent::setImagePlacement()
函数来控制。以下截图显示了读取图像文件的程序:
Image
类类似于 String
类,因为它在内部使用引用计数的对象,这样几个 Image
对象可以共享相同的数据。
操作图像数据
在下一个示例中,我们将添加一个滑块来控制显示图像的亮度,并添加一个按钮将此处理后的图像作为 PNG 文件写入。更改 MainComponent.h
文件的内容,以下代码列表中突出显示了更改:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component,
public Button::Listener,
public Slider::Listener
{
public:
MainContentComponent();
void resized();
void buttonClicked (Button* button);
void sliderValueChanged (Slider* slider);
private:
TextButton readFileButton;
ImageComponent imageComponent;
Slider brightnessSlider;
TextButton writeFileButton;
Image origImage, procImage;
};
#endif
现在用包含指令和构造函数替换 MainComponent.cpp
文件:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
: readFileButton ("Read Image File..."),
writeFileButton ("Write Image File...")
{
brightnessSlider.setRange (0.0, 10.0);
addAndMakeVisible (&readFileButton);
addAndMakeVisible (&imageComponent);
addAndMakeVisible (&brightnessSlider);
addAndMakeVisible (&writeFileButton);
readFileButton.addListener (this);
writeFileButton.addListener (this);
brightnessSlider.addListener (this);
setSize (500, 400);
}
添加 resized()
函数以定位组件:
void MainContentComponent::resized()
{
int controlHeight = 20;
int margin = 10;
int width = getWidth() - margin * 2;
readFileButton.setBounds
(margin, margin, width, controlHeight);
imageComponent.setBounds
(margin, readFileButton.getBottom() + margin, width,
getHeight() - (controlHeight + margin) * 3 - margin * 2);
brightnessSlider.setBounds
(margin, imageComponent.getBottom() + margin,
width, controlHeight);
writeFileButton.setBounds
(margin, brightnessSlider.getBottom() + margin,
width, controlHeight);
}
添加响应按钮交互的 buttonClicked()
函数:
void MainContentComponent::buttonClicked (Button* button)
{
if (&readFileButton == button) {
FileChooser chooser ("Choose an image file to display...");
if (chooser.browseForFileToOpen()) {
origImage = ImageFileFormat::loadFrom (chooser.getResult());
if (origImage.isValid()) {
procImage = origImage.createCopy();
imageComponent.setImage (procImage);
}
}
} else if (&writeFileButton == button) {
if (procImage.isValid()) {
FileChooser chooser ("Write processed image to file...");
if (chooser.browseForFileToSave (true)) {
FileOutputStream stream (chooser.getResult());
PNGImageFormat pngImageFormat;
pngImageFormat.writeImageToStream (procImage, stream);
}
}
}
}
最后,添加响应滑块交互的 sliderValueChanged()
函数:
void MainContentComponent::sliderValueChanged (Slider* slider)
{
if (&brightnessSlider == slider) {
if (origImage.isValid() &&
procImage.isValid()) {
const float amount = (float)brightnessSlider.getValue();
if (amount == 0.f) {
procImage = origImage.createCopy();
} else {
for (int v = 0; v < origImage.getHeight(); ++v) {
for (int h = 0; h < origImage.getWidth(); ++h) {
Colour col = origImage.getPixelAt (h, v);
if (amount > 0.f)
procImage.setPixelAt (h, v, col.brighter (amount));
else if (amount < 0.f)
procImage.setPixelAt (h, v, col.darker (-amount));
}
}
}
imageComponent.repaint();
}
}
}
在这里,我们保留原始图像和经过处理的副本。每次滑块改变时,通过遍历每个像素,图像都会用新的亮度更新。当点击 写入图像文件… 按钮时,我们创建一个 FileChooser
对象,并使用 FileChooser::browseForFileToSave()
函数将其呈现给用户,而不是像读取文件时那样使用 FileChooser::browseForFileToOpen()
函数。然后使用 PNGImageFormat
类将处理后的图像作为文件流写入所选文件。这里的图像处理可以显著优化,但这超出了本书的范围。
播放音频文件
JUCE 提供了一套复杂的类来处理音频。这包括:声音文件读写工具、与原生音频硬件接口、音频数据转换函数,以及为一系列知名宿主应用程序创建音频插件的跨平台框架。涵盖所有这些方面超出了本书的范围,但本节中的示例将概述播放声音文件和与音频硬件通信的原则。除了展示 JUCE 的音频功能外,本节我们还将使用 Introjucer 应用程序创建 GUI 并自动生成代码的其他一些方面。
创建用于控制音频文件播放的 GUI
创建一个名为Chapter04_04
的新 GUI 应用程序 Introjucer 项目,选择创建基本窗口的选项。在 Introjucer 应用程序中,选择配置面板,并在层次结构中选择模块。
对于这个项目,我们需要juce_audio_utils
模块(其中包含用于配置音频设备硬件的特殊Component
类);因此,打开此模块。尽管我们创建了一个基本窗口和一个基本组件,但我们将以与第二章末尾的相同方式使用 Introjucer 应用程序创建 GUI,即构建用户界面。
导航到文件面板,在层次结构中右键单击(在 Mac 上,按control键并单击)源文件夹,然后从上下文菜单中选择添加新 GUI 组件…。
当被要求时,将头文件命名为MediaPlayer.h
并点击保存。在文件层次结构中,选择MediaPlayer.cpp
文件。首先选择类面板,并将类名从NewComponent
更改为MediaPlayer
。我们在这个基本项目中需要四个按钮:一个用于打开音频文件的按钮、一个播放按钮、一个停止按钮和一个音频设备设置按钮。选择子组件面板,并通过右键单击以访问上下文菜单,向编辑器添加四个TextButton
组件。将按钮均匀地分布在编辑器顶部附近,并按照以下表格中的说明配置每个按钮:
目的 | 成员名称 | 名称 | 文本 | 背景(正常) |
---|---|---|---|---|
打开文件 | openButton |
open |
打开… |
默认 |
播放/暂停文件 | playButton |
play |
播放 |
绿色 |
停止播放 | stopButton |
stop |
停止 |
红色 |
配置音频 | settingsButton |
settings |
音频设置… |
默认 |
按照以下截图所示排列按钮:
对于每个按钮,访问模式弹出菜单以设置宽度,并选择从父宽度减去。如果调整窗口大小,这将保持按钮的右侧与窗口右侧的距离相同。在 Introjucer 项目中还有更多自定义设置要做,但在此阶段,请确保在打开本机 IDE 项目之前已保存MediaPlayer.h
文件、MediaPlayer.cpp
文件和 Introjucer 项目。
小贴士
确保在 Introjucer 应用程序中已保存所有这些文件;否则,当项目在 IDE 中打开时,文件可能无法在文件系统中正确更新。
在集成开发环境(IDE)中,我们需要替换MainContentComponent
类的代码,以便在其中放置一个MediaPlayer
对象。按照以下方式修改MainComponent.h
文件:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
#include "MediaPlayer.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
void resized();
private:
MediaPlayer player;
};
#endif
然后,修改MainComponent.cpp
文件:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
addAndMakeVisible (&player);
setSize (player.getWidth(),player.getHeight());
}
void MainContentComponent::resized()
{
player.setBounds (0, 0, getWidth(), getHeight());
}
最后,在Main.cpp
文件中使窗口可调整大小(正如我们在第二章的添加子组件部分所做的那样),构建并运行项目以检查窗口是否按预期显示。
添加音频文件播放支持
退出应用程序并返回到Introjucer
项目。在文件面板层次结构中选择MediaPlayer.cpp
文件,并选择其类面板。父类设置已经包含public Component
。我们将监听来自两个成员对象的状态变化,这两个成员对象是ChangeBroadcaster
对象。为此,我们需要我们的MediaPlayer
类继承自ChangeListener
类。修改父类设置,使其读取如下:
public Component, public ChangeListener
再次保存MediaPlayer.h
文件、MediaPlayer.cpp
文件和 Introjucer 项目,并将其打开到您的 IDE 中。注意在MediaPlayer.h
文件中,父类已经被更新以反映这一变化。为了方便起见,我们将添加一些枚举常量来反映我们的MediaPlayer
对象的当前播放状态,以及一个函数来集中更改此状态(这将反过来更新各种对象的状态,例如按钮上显示的文本)。ChangeListener
类还有一个纯虚函数,我们需要添加。将以下代码添加到MediaPlayer.h
文件的[UserMethods]
部分:
//[UserMethods]-- You can add your own custom methods...
enum TransportState {
Stopped,
Starting,
Playing,
Pausing,
Paused,
Stopping
};
void changeState (TransportState newState);
void changeListenerCallback (ChangeBroadcaster* source);
//[/UserMethods]
我们还需要一些额外的成员变量来支持我们的音频播放。将这些添加到[UserVariables]
部分:
//[UserVariables] -- You can add your own custom variables...
AudioDeviceManager deviceManager;
AudioFormatManager formatManager;
ScopedPointer<AudioFormatReaderSource> readerSource;
AudioTransportSource transportSource;
AudioSourcePlayer sourcePlayer;
TransportState state;
//[/UserVariables]
AudioDeviceManager
对象将管理应用程序与音频硬件之间的接口。AudioFormatManager
对象将协助创建一个对象,该对象将读取和解码音频文件中的音频数据。此对象将存储在 ScopedPointer<AudioFormatReaderSource>
对象中。AudioTransportSource
对象将控制音频文件的播放并执行可能需要的任何采样率转换(如果音频文件的采样率与音频硬件采样率不同)。AudioSourcePlayer
对象将从 AudioTransportSource
对象流式传输音频到 AudioDeviceManager
对象。state
变量将存储我们的枚举常量之一,以反映 MediaPlayer
对象当前的播放状态。
现在向 MediaPlayer.cpp
文件添加一些代码。在构造函数的 [Constructor]
部分中,添加以下两行:
playButton->setEnabled (false);
stopButton->setEnabled (false);
这将使 播放 和 停止 按钮最初处于禁用状态(并变为灰色)。稍后,一旦加载了有效的文件,我们将启用 播放 按钮,并根据文件是否正在播放更改每个按钮的状态和按钮上显示的文本。在此 [Constructor]
部分中,您还应按照以下方式初始化 AudioFormatManager
:
formatManager.registerBasicFormats();
这允许 AudioFormatManager
对象检测不同的音频文件格式并创建适当的文件读取器对象。我们还需要将 AudioSourcePlayer
、AudioTransportSource
和 AudioDeviceManager
对象连接在一起,并初始化 AudioDeviceManager
对象。为此,将以下行添加到 [Constructor]
部分中:
sourcePlayer.setSource (&transportSource);
deviceManager.addAudioCallback (&sourcePlayer);
deviceManager.initialise (0, 2, nullptr, true);
第一行将 AudioTransportSource
对象连接到 AudioSourcePlayer
对象。第二行将 AudioSourcePlayer
对象连接到 AudioDeviceManager
对象。最后一行使用以下内容初始化 AudioDeviceManager
对象:
-
所需的音频输入通道数(在本例中为
0
)。 -
所需的音频输出通道数(在本例中为
2
,用于立体声输出)。 -
AudioDeviceManager
对象的可选 "保存状态"(nullptr
从头开始初始化)。 -
如果保存状态失败是否打开默认设备。由于我们未使用保存状态,此参数无关紧要,但无论如何将其设置为
true
是有用的。
在 [Constructor]
部分添加最后三行以配置我们的 MediaPlayer
对象作为 AudioDeviceManager
和 AudioTransportSource
对象的监听器,并将当前状态设置为 Stopped
:
deviceManager.addChangeListener (this);
transportSource.addChangeListener (this);
state = Stopped;
在 buttonClicked()
函数中,我们需要在各个部分添加一些代码。在 [UserButtonCode_openButton]
部分中,添加:
//[UserButtonCode_openButton] -- add your button handler...
FileChooser chooser ("Select a Wave file to play...",
File::nonexistent,
"*.wav");
if (chooser.browseForFileToOpen()) {
File file (chooser.getResult());
readerSource = new AudioFormatReaderSource(formatManager.createReaderFor (file), true);
transportSource.setSource (readerSource);
playButton->setEnabled (true);
}
//[/UserButtonCode_openButton]
当点击 openButton
按钮时,这将创建一个 FileChooser
对象,允许用户使用平台的本地界面选择文件。允许选择的文件类型通过通配符 *.wav
限制,以允许仅选择具有 .wav
文件扩展名的文件。
如果用户实际选择了文件(而不是取消操作),代码可以调用 FileChooser::getResult()
函数来检索所选文件的引用。然后,该文件传递给 AudioFormatManager
对象以创建文件读取器对象,该对象随后传递以创建一个 AudioFormatReaderSource
对象,该对象将管理和拥有此文件读取器对象。最后,AudioFormatReaderSource
对象连接到 AudioTransportSource
对象,并启用 Play 按钮。
playButton
和 stopButton
对象的处理程序将根据当前传输状态调用我们的 changeState()
函数。我们将在稍后定义 changeState()
函数,届时其目的将变得清晰。
在 [UserButtonCode_playButton]
部分,添加以下代码:
//[UserButtonCode_playButton] -- add your button handler...
if ((Stopped == state) || (Paused == state))
changeState (Starting);
else if (Playing == state)
changeState (Pausing);
//[/UserButtonCode_playButton]
如果当前状态是 Stopped
或 Paused
,则将状态更改为 Starting
,如果当前状态是 Playing
,则将状态更改为 Pausing
。这是为了有一个具有播放和暂停功能的按钮。
在 [UserButtonCode_stopButton]
部分,添加以下代码:
//[UserButtonCode_stopButton] -- add your button handler...
if (Paused == state)
changeState (Stopped);
else
changeState (Stopping);
//[/UserButtonCode_stopButton]
如果当前状态是 Paused
,则将状态设置为 Stopped
,在其他情况下设置为 Stopping
。我们将在稍后添加 changeState()
函数,其中这些状态变化将更新各种对象。
在 [UserButtonCode_settingsButton]
部分,添加以下代码:
//[UserButtonCode_settingsButton] -- add your button handler...
bool showMidiInputOptions = false;
bool showMidiOutputSelector = false;
bool showChannelsAsStereoPairs = true;
bool hideAdvancedOptions = false;
AudioDeviceSelectorComponent settings (deviceManager,
0, 0, 1, 2,
showMidiInputOptions,
showMidiOutputSelector,
showChannelsAsStereoPairs,
hideAdvancedOptions);
settings.setSize (500, 400);
DialogWindow::showModalDialog(String ("Audio Settings"),
&settings,
TopLevelWindow::getTopLevelWindow (0),
Colours::white,
true); //[/UserButtonCode_settingsButton]
这提供了一个有用的界面来配置音频设备设置。
我们需要添加 changeListenerCallback()
函数以响应 AudioDeviceManager
和 AudioTransportSource
对象的变化。将以下内容添加到 MediaPlayer.cpp
文件的 [MiscUserCode]
部分:
//[MiscUserCode] You can add your own definitions...
void MediaPlayer::changeListenerCallback (ChangeBroadcaster* src)
{
if (&deviceManager == src) {
AudioDeviceManager::AudioDeviceSetup setup;
deviceManager.getAudioDeviceSetup (setup);
if (setup.outputChannels.isZero())
sourcePlayer.setSource (nullptr);
else
sourcePlayer.setSource (&transportSource);
} else if (&transportSource == src) {
if (transportSource.isPlaying()) {
changeState (Playing);
} else {
if ((Stopping == state) || (Playing == state))
changeState (Stopped);
else if (Pausing == state)
changeState (Paused);
}
}
}
//[/MiscUserCode]
如果我们的 MediaPlayer
对象收到 AudioDeviceManager
对象发生某种变化的消息,我们需要检查这种变化不是禁用所有音频输出通道,通过从设备管理器获取设置信息来做到这一点。如果输出通道的数量为零,我们将通过将源设置为 nullptr
来断开我们的 AudioSourcePlayer
对象与 AudioTransportSource
对象的连接(否则我们的应用程序可能会崩溃)。如果输出通道的数量再次变为非零,我们将重新连接这些对象。
如果我们的AudioTransportSource
对象已更改,这可能是其播放状态的变化。重要的是要注意请求传输开始或停止与这种变化实际发生之间的区别。这就是为什么我们为所有其他状态(包括过渡状态)创建了枚举常量。根据我们的state
变量的当前值和AudioTransportSource
对象的状态,我们再次调用changeState()
函数。
最后,将重要的changeState()
函数添加到MediaPlayer.cpp
文件的[MiscUserCode]
部分,该函数处理所有这些状态变化:
void MediaPlayer::changeState (TransportState newState)
{
if (state != newState) {
state = newState;
switch (state) {
case Stopped:
playButton->setButtonText ("Play");
stopButton->setButtonText ("Stop");
stopButton->setEnabled (false);
transportSource.setPosition (0.0);
break;
case Starting:
transportSource.start();
break;
case Playing:
playButton->setButtonText ("Pause");
stopButton->setButtonText ("Stop");
stopButton->setEnabled (true);
break;
case Pausing:
transportSource.stop();
break;
case Paused:
playButton->setButtonText ("Resume");
stopButton->setButtonText ("Return to Zero");
break;
case Stopping:
transportSource.stop();
break;
}
}
}
在检查newState
值与state
变量的当前值不同之后,我们将state
变量更新为新值。然后,我们执行循环中这个特定点的适当操作。这些总结如下:
-
在
停止
状态中,按钮配置为播放和停止标签,停止按钮被禁用,并且传输定位到音频文件的开头。 -
在
启动
状态中,AudioTransportSource
对象被告知开始。一旦AudioTransportSource
对象实际上开始播放,系统将处于播放
状态。在这里,我们更新playButton
按钮以显示文本暂停,确保stopButton
按钮显示文本停止,并启用停止按钮。 -
如果点击了暂停按钮,状态变为
暂停中
,并告知传输停止。一旦传输实际上停止,状态变为暂停
,playButton
按钮更新以显示文本继续,stopButton
按钮更新以显示返回零。 -
如果点击了停止按钮,状态变为
停止中
,并告知传输停止。一旦传输实际上停止,状态变为停止
(如第一点所述)。 -
如果点击了返回零按钮,状态将直接变为
停止
(再次,如前所述)。 -
当音频文件到达文件末尾时,状态也会变为
停止
。
构建并运行应用程序。点击打开...按钮后,你应该能够选择一个.wav
音频文件,使用相应的按钮播放、暂停、继续和停止音频文件,并使用音频设置…按钮配置音频设备。音频设置窗口允许你选择输入和输出设备、采样率和硬件缓冲区大小。它还提供了一个测试按钮,可以通过选定的输出设备播放一个音调。
使用二进制构建工具
在编写跨平台应用程序时,一个问题是需要在应用程序内部使用二进制文件的打包。JUCE 包含一个名为 Binary Builder 的工具,该工具可以将二进制文件转换为源代码,然后编译到应用程序的代码中。这确保了文件在所有平台上表现一致,而不是依赖于运行时机器的特定性。尽管 Binary Builder 作为独立项目(在 juce/extras/binarybuilder
中)提供,但其功能在 Introjucer 应用程序的 GUI 组件编辑器中可用。
使用 Introjucer 应用程序嵌入图像文件
创建一个名为 Chapter04_05
的新 Introjucer 项目,包含一个基本窗口。像之前一样添加一个新的 GUI 组件;这次将其命名为 EmbeddedImage
(记得也要更改其 类 面板中的名称)。在其 子组件 面板中,在画布上右键单击并选择 新建通用组件,并将其调整大小以填充画布并边缘留有少量边框。将 成员名称 和 名称 更改为 image
,并将 类 更改为 ImageComponent
。在 资源 面板中,选择 添加新资源… 并选择一个图像文件以添加。这将创建一个资源,它是转换为代码的二进制文件。它将在该组件内部根据原始文件名获得一个变量名,并作为静态变量存储。例如,名为 sample.png
的文件将被命名为 sample_png
。此外,还会创建一个存储该资源大小的静态变量,并将其名称附加 Size
,例如,sample_pngSize
。保存项目并将其打开到您的 IDE 中。像之前一样更新 MainComponent
文件的內容。按照以下方式更改 MainComponent.h
文件:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
#include "EmbeddedImage.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
void resized();
private:
EmbeddedImage embeddedImage;
};
#endif
然后将 MainComponent.cpp
文件更改为:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
addAndMakeVisible (&embeddedImage);
setSize (embeddedImage.getWidth(),embeddedImage.getHeight());
}
void MainContentComponent::resized()
{
embeddedImage.setBounds (0, 0, getWidth(), getHeight());
}
最后,在 EmbeddedImage.cpp
文件中注意文件末尾的大数组,这是转换为代码的图像文件。在 [构造函数]
部分中,添加以下两行(尽管您可能需要使用与 sample_png
、sample_pngSize
不同的名称,具体取决于您之前添加的文件资源):
//[Constructor] You can add your own custom stuff here...
MemoryInputStream stream (sample_png, sample_pngSize, false);
image->setImage (ImageFileFormat::loadFrom (stream));
//[/Constructor]
这创建了一个从我们的资源创建的内存流,提供了数据指针和数据大小(最后的 false
参数告诉内存流不要复制数据)。然后我们像之前一样使用 ImageFileFormat
类加载图像。构建并运行应用程序,图像应该显示在应用程序的窗口中。
摘要
本章介绍了处理 JUCE 中文件的各种技术,特别是针对图像和音频文件。鼓励您探索在线 JUCE 文档,它提供了关于这里介绍的大多数可能性的更多细节。我们还介绍了 Binary Builder 工具,该工具提供了一种将媒体文件转换为适用于跨平台使用的源代码的方法。鼓励您阅读本章中介绍的所有类的在线 JUCE 文档。本章仅提供了一个入门介绍;还有许多其他选项和替代方法,可能适合不同的环境。JUCE 文档将带您了解这些方法,并指向相关的类和函数。下一章将介绍 JUCE 中可用于创建跨平台应用程序的一些有用工具。
第五章。有用的实用工具
除了前几章中介绍的基本类之外,JUCE 还包括一系列用于解决应用程序开发中常见问题的类。在本章中,我们将涵盖以下主题:
-
使用
Value
、var
和ValueTree
类 -
实现撤销管理
-
添加 XML 支持
-
理解 JUCE 如何处理多线程
-
存储应用程序属性
-
添加菜单栏控件
到本章结束时,你将了解 JUCE 提供的一些额外有用实用工具。
使用动态类型对象
JUCE 的 Value
、var
和 ValueTree
类是应用程序数据存储和处理的有价值工具。var
类(简称 variant)旨在存储一系列原始数据类型,包括整数、浮点数和字符串(JUCE String
对象)。它还可以递归,即一个 var
实例可以包含一个 var
实例的数组(一个 JUCE Array<var>
对象)。这样,var
类类似于许多脚本语言(如 JavaScript)支持的动态类型。var
对象还可以持有任何类型的 ReferenceCounterObject
对象或二进制数据的块。以下都是有效的初始化方式:
var anInt = 1;
var aDouble = 1.2345;
var aString = "Hello world!";
使用 Value
类
Value
类旨在持有 var
对象的共享实例(通过将 var
存储在引用计数包装器中)。Value
对象可以使用 Value::Listener
函数和 第二章 中关于 GUI 类使用的监听器和广播系统所涵盖的相同技术附加监听器。实际上,Value
对象被各种 Component
子类用于存储任何值,例如 Label
对象中的文本、Slider
对象的位置等。例如,以下代码片段说明了使用 Value
对象设置 Label
对象的值和 Slider
对象的值的方法:
// Slider
Slider slider;
slider.getValueObject().setValue (10.0);
// instead of:
// slider.setValue (10);
// Label
Label label;
label.getTextValue().setValue ("Hello");
// instead of:
// label.setText ("Hello", sendNotification);
Value
对象也是共享值的一种方式,因为它们可以指向相同的基本数据。这在同一值以不同方式在 GUI 中显示时非常有用,尤其是在复杂和详细的 GUI 显示中。创建一个名为 Chapter05_01
的新 Introjucer 项目,包含一个基本窗口,并将 MainComponent.h
文件替换为以下内容:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
void resized();
private:
Value value;
Slider slider;
Label label;
};
#endif // __MAINCOMPONENT_H__
在这里,我们将 Value
、Slider
和 Label
对象存储在我们的类中。将 MainComponent.cpp
文件替换为以下内容:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
: value (1.0),
slider (Slider::LinearHorizontal, Slider::NoTextBox)
{
label.setEditable (true);
slider.getValueObject().referTo (value);
label.getTextValue().referTo (value);
addAndMakeVisible (&slider);
addAndMakeVisible (&label);
setSize (500, 400);
}
void MainContentComponent::resized()
{
slider.setBounds (10, 10, getWidth() - 20, 20);
label.setBounds (10, 40, getWidth() - 20, 20);
}
在这里,我们将我们的 Value
对象初始化为 1 的值,然后使用 Value::referTo()
函数配置 Slider
和 Label
对象的值,使它们都引用这个相同的底层 Value
对象。构建并运行应用程序,注意无论更改哪一个,滑块和标签都会保持相同的值更新。这一切都无需我们配置自己的监听器,因为 JUCE 内部处理所有这些。
结构化分层数据
显然,在大多数应用程序中,数据模型要复杂得多,通常是分层的。ValueTree
类旨在通过相对轻量级且强大的实现来反映这一点。一个 ValueTree
对象持有名为 var
的命名对象作为属性构成的树结构,这意味着树中的节点可以是几乎任何数据类型。以下示例说明了如何将数据存储在 ValueTree
对象中,以及一些使 ValueTree
类对 JUCE 应用程序开发极具价值的特性。
创建一个名为 Chapter05_02
的新 Introjucer 项目,并包含一个基本窗口。以前类似的方式添加一个名为 EntryForm
的 GUI 组件。首先,导航到 EntryForm.cpp
文件的 Graphics 面板,并将背景颜色更改为灰色。现在我们将添加一个表单样式的页面,我们可以在此页面中输入一个人的姓名、年龄和地址。在 Subcomponents 面板中添加六个 Label
对象,作为以下内容的标签:First name、Last name、Age、Line 1、Line 2 和 Line 3。
现在添加六个无内容(即空文本)的 Label
对象,并将它们放置在之前步骤中添加的每个标签旁边。将这些标签的背景设置为白色,而不是透明,并将它们的 编辑 属性设置为 单击编辑。给这些标签以下 成员名称 和 名称 值:firstNameField
、lastNameField
、ageField
、line1Field
、line2Field
和 line3Field
。
最后,通过在 Subcomponents 编辑器中右键单击(在 Mac 上,按 control 并单击)访问上下文菜单,添加一个 Group Box。将此位置设置为围绕与 Line 1、Line 2、Line 3 及其输入字段相关的标签。现在它应该看起来类似于以下截图:
现在保存项目并在您的 IDE 中打开它。像之前一样,将 EntryForm
对象放入 MainContentComponent
对象中,通过修改 MainComponent.h
文件以包含以下内容:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
#include "EntryForm.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
void resized();
private:
EntryForm form;
};
#endif
将 MainComponent.cpp
文件修改为包含以下内容:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
addAndMakeVisible (&form);
setSize (form.getWidth(), form.getHeight());
}
void MainContentComponent::resized()
{
form.setBounds (0, 0, getWidth(), getHeight());
}
现在我们需要向 EntryForm
类添加一些自定义代码,以便使其将数据存储在 ValueTree
对象中。首先,在 EntryForm.h
文件中的 [UserVariables]
部分添加一些变量,如下所示:
//[UserVariables] -- You can add your own custom variables..
ValueTree personData;
static const Identifier personId;
static const Identifier firstNameId;
static const Identifier lastNameId;
static const Identifier ageId;
static const Identifier addressId;
static const Identifier line1Id;
static const Identifier line2Id;
static const Identifier line3Id;
//[/UserVariables]
在这里,我们有一个 ValueTree
对象将存储数据,以及几个静态的 Identifier
对象(我们将在稍后的 EntryForm
类中初始化它们),它们作为 ValueTree
对象结构和其属性的名称。Identifier
对象实际上是一种特殊的 String
对象类型,它只包含有限字符集,因此它将在其他上下文中有效(例如,变量名、XML)。
提示
在应用程序启动时创建这些 Identifier
对象比每次需要时创建它们更有效率。
在 EntryForm.cpp
文件中,将以下代码添加到 [杂项用户代码]
部分以初始化 Identifier
对象:
//[MiscUserCode] You can add your own definitions ...
const Identifier EntryForm::personId = "person";
const Identifier EntryForm::firstNameId = "firstName";
const Identifier EntryForm::lastNameId = "lastName";
const Identifier EntryForm::ageId = "age";
const Identifier EntryForm::addressId = "address";
const Identifier EntryForm::line1Id = "line1";
const Identifier EntryForm::line2Id = "line2";
const Identifier EntryForm::line3Id = "line3";
//[/MiscUserCode]
在构造函数中,我们需要初始化 ValueTree
对象,因此将以下代码添加到 [构造函数]
部分:
//[Constructor] You can add your own custom stuff here..
personData = ValueTree (personId);
personData.setProperty (firstNameId, String::empty, nullptr);
personData.setProperty (lastNameId, String::empty, nullptr);
personData.setProperty (ageId, String::empty, nullptr);
ValueTree addressData = ValueTree (addressId);
addressData.setProperty (line1Id, String::empty, nullptr);
addressData.setProperty (line2Id, String::empty, nullptr);
addressData.setProperty (line3Id, String::empty, nullptr);
personData.addChild (addressData, -1, nullptr);
//[/Constructor]
在这里,我们创建了一个名为 person
的类型的主 ValueTree
对象,并为第一个名字、姓氏和年龄添加了三个属性。(我们的 EntryForm
组件的图形布局显示了此顶级 personData
对象中值的层次关系。)每个情况下的 nullptr
参数表示我们不希望进行撤销管理;我们将在本章后面讨论这一点。然后,我们创建另一个名为 address
的类型的 ValueTree
对象。然后,我们将地址的三个行作为属性添加,并将其作为 子节点 添加到主 ValueTree
对象中。正是通过这种方式,我们使用多个 ValueTree
对象创建了树结构。调用 ValueTree::addChild()
函数的第二个参数表示我们想要添加子节点的索引。在这种情况下传入的 -1
参数表示我们只想将其添加到节点列表的末尾(但无论如何我们只有一个;因此,这个值并不重要)。
最后,我们需要在标签更改时更新 ValueTree
对象。在适当的部分添加以下代码:
//[UserLabelCode_firstNameField] -- add your label text handling..
personData.setProperty (firstNameId,
labelThatHasChanged->getText(), nullptr);
//[/UserLabelCode_firstNameField]
...
//[UserLabelCode_lastNameField] -- add your label text handling..
personData.setProperty (lastNameId,
labelThatHasChanged->getText(), nullptr);
//[/UserLabelCode_lastNameField]
...
//[UserLabelCode_ageField] -- add your label text handling..
personData.setProperty (ageId,
labelThatHasChanged->getText(), nullptr);
//[/UserLabelCode_ageField]
...
//[UserLabelCode_line1Field] -- add your label text handling..
ValueTree addressData (personData.getChildWithName (addressId));
addressData.setProperty (line1Id,
labelThatHasChanged->getText(), nullptr);
//[/UserLabelCode_line1Field]
...
//[UserLabelCode_line2Field] -- add your label text handling..
ValueTree addressData (personData.getChildWithName (addressId));
addressData.setProperty (line2Id,
labelThatHasChanged->getText(), nullptr);
//[/UserLabelCode_line2Field]
...
//[UserLabelCode_line3Field] -- add your label text handling..
ValueTree addressData (personData.getChildWithName (addressId));
addressData.setProperty (line3Id,
labelThatHasChanged->getText(), nullptr);
//[/UserLabelCode_line3Field]
构建、运行应用程序并确认您可以编辑表单字段的内 容。您可能还会注意到,JUCE 自动实现了 焦点顺序,这样您可以使用 Tab 键在字段之间移动。然而,这目前并不太有用,因为我们没有对数据进行任何操作。在下一节中,我们将添加撤销管理,这将开始展示 ValueTree
类的强大功能。
使用撤销管理
JUCE 包含一个 UndoManager
类来帮助管理撤销和重做操作。这可以独立使用,但如果应用程序的数据存储在 ValueTree
对象中,则几乎可以自动工作。为了说明这一点,我们需要对迄今为止开发的项目进行一些修改。首先,在 Introjucer 项目中进行一些更改。添加一个标签为 撤销 的 TextButton
子组件,并将其 名称 和 成员名称 更改为 undoButton
。在 类 面板中,将 ValueTree::Listener
类添加到 父类 属性中,使其读取:
public Component, public ValueTree::Listener
保存所有文件和项目,然后在你的集成开发环境(IDE)中打开它。将以下代码添加到MainComponent.h
文件的[UserMethods]
部分中,针对ValueTree::Listener
类:
//[UserMethods] -- You can add your own custom methods ...
void valueTreePropertyChanged (ValueTree& tree,
const Identifier& property);
void valueTreeChildAdded (ValueTree& parentTree,
ValueTree& child) { }
void valueTreeChildRemoved (ValueTree& parentTree,
ValueTree& child) { }
void valueTreeChildOrderChanged (ValueTree& tree) { }
void valueTreeParentChanged (ValueTree& tree) { }
void valueTreeRedirected (ValueTree& tree) { }
//[/UserMethods]
按照以下方式将UndoManager
对象添加到[UserVariables]
部分:
//[UserVariables] -- You can add your own custom variables...
UndoManager undoManager;
...
在EntryForm.cpp
文件中,将以下代码添加到[MiscUserCode]
部分,用于ValueTree::Listener
函数(注意,我们只需要这些函数中的一个,因为我们已经在先前的头文件中添加了空函数):
void EntryForm::valueTreePropertyChanged
(ValueTree& tree, const Identifier& property)
{
if (property == firstNameId) {
firstNameField->setText (tree.getProperty (property),
dontSendNotification);
} else if (property == lastNameId) {
lastNameField->setText (tree.getProperty (property),
dontSendNotification);
} else if (property == ageId) {
ageField->setText (tree.getProperty (property),
dontSendNotification);
} else if (property == line1Id) {
line1Field->setText (tree.getProperty (property),
dontSendNotification);
} else if (property == line2Id) {
line2Field->setText (tree.getProperty (property),
dontSendNotification);
} else if (property == line3Id) {
line3Field->setText (tree.getProperty (property),
dontSendNotification);
}
}
通过将以下代码添加到[Constructor]
部分的末尾,将我们的EntryForm
对象作为监听器添加到主值树中:
...
personData.addListener (this);
//[/Constructor]
每次我们调用ValueTree::setProperty()
函数时,都需要传递我们的UndoManager
对象的指针。找到使用ValueTree::setProperty()
的每一行代码,并将nullptr
参数更改为&undoManager
,例如:
//[UserLabelCode_firstNameField] -- add your label text handling..
personData.setProperty (firstNameId,
labelThatHasChanged->getText(),
&undoManager);
//[/UserLabelCode_firstNameField]
不要使用简单的查找和替换,因为代码中还有其他与ValueTree
对象和UndoManager
对象代码无关的nullptr
使用。在我们的应用程序中,当我们想要撤销更改时,我们需要告诉UndoManager
对象一个事务包含什么。在某些情况下,将每个小更改视为一个事务可能是合适的。在其他情况下,将小更改组合成一个单一的事务可能对用户更有用(例如,在特定时间限制内发生的变化,或对同一对象的多次更改)。我们将把EntryForm::labelTextChanged()
函数中的每个更改都作为一个事务,因此将以下代码添加到[UserlabelTextChanged_Pre]
部分:
//[UserlabelTextChanged_Pre]
undoManager.beginNewTransaction();
//[/UserlabelTextChanged_Pre]
最后,在[UserButtonCode_undoButton]
部分执行撤销操作,添加以下代码:
//[UserButtonCode_undoButton] -- add your button handler..
undoManager.undo();
//[/UserButtonCode_undoButton]
这行代码告诉UndoManager
对象撤销最后一个事务。添加重做支持同样简单。构建并运行应用程序,注意你现在可以使用撤销按钮撤销数据输入表单的更改。ValueTree
类还支持通过二进制或 XML 格式进行序列化和反序列化;这将在下一节中概述。
添加 XML 支持
JUCE 包含了一系列对 XML 解析和存储的支持。你可能已经注意到,Introjucer 应用程序使用 XML 格式在自动生成的某些文件末尾存储元数据(例如,在我们的 EntryForm.cpp
文件中)。特别是,一个 ValueTree
对象可以被序列化为 XML,并且相同的 XML 可以反序列化回一个 ValueTree
对象(尽管在没有进行一些自己的解析的情况下,你不能将任何任意的 XML 转换为一个 ValueTree
对象)。为了给我们的项目添加打开和保存功能,首先我们需要在 Introjucer 项目中添加一个 Open… 和一个 Save… 按钮。分别将这些按钮命名为 name 和 member name openButton
和 saveButton
。然后,在代码中我们需要执行到和从 XML 的转换。在 [UserButtonCode_saveButton]
部分添加以下代码,向用户展示文件选择器并将 ValueTree
对象的数据保存到 XML 文件中:
//[UserButtonCode_saveButton] -- add your button handler...
FileChooser chooser ("Save person data",
File::nonexistent,
"*.xml");
if (chooser.browseForFileToSave (true)) {
File file (chooser.getResult());
if (file.existsAsFile())
file.moveToTrash();
FileOutputStream stream (file);
ScopedPointer<XmlElement> xml = personData.createXml();
xml->writeToStream (stream, String::empty);
}
//[/UserButtonCode_saveButton]
在 [UserButtonCode_openButton]
部分添加以下代码,将 XML 文件读回到 ValueTree
对象中:
//[UserButtonCode_openButton] -- add your button handler...
FileChooser chooser ("Open person data",
File::nonexistent,
"*.xml");
if (chooser.browseForFileToOpen()) {
Logger* log = Logger::getCurrentLogger();
File file (chooser.getResult());
XmlDocument xmlDoc (file);
ScopedPointer<XmlElement> xml = xmlDoc.getDocumentElement();
if (xml == nullptr) {
log->writeToLog ("XML error");
return;
}
ValueTree newPerson (ValueTree::fromXml (*xml));
if (newPerson.getType() != personId) {
log->writeToLog ("Invalid person XML");
return;
}
undoManager.beginNewTransaction();
personData.copyPropertiesFrom (newPerson, &undoManager);
ValueTree newAddress (newPerson.getChildWithName (addressId));
ValueTree addressData (personData.getChildWithName (addressId));
addressData.copyPropertiesFrom (newAddress, &undoManager);
}
//[/UserButtonCode_openButton]
在这里,我们将选定的文件作为 XML 文档加载,并访问其文档元素。我们对 XML 执行两次检查,并在必要时向日志报告错误:
-
我们检查 XML 元素是否成功访问(即,没有返回
nullptr
)。如果失败,文件可能不是一个有效的 XML 文件。 -
我们将 XML 加载到一个
ValueTree
对象中,然后检查这个ValueTree
对象的类型,以确保它是我们预期的person
数据。
一旦加载的 ValueTree
对象成功检查,我们就将其属性复制到存储的 ValueTree
对象中,作为一个单独的 UndoManager
对象事务。
构建并运行应用程序,检查保存、打开以及所有撤销行为是否按预期工作。以下截图显示了应用程序窗口应该如何显示:
代码为截图中的数据生成的 XML 文件将类似于以下这样:
<?xml version="1.0" encoding="UTF-8"?>
<person firstName="Joe" lastName="Bloggs" age="25">
<address line1="1 The Lines" line2="Loop" line3="Codeland"/>
</person>
当然,在实际应用中,我们还会在菜单栏中添加打开、保存和撤销等命令(或者用它们来代替),但在这里我们为了简单起见使用了按钮。(关于添加菜单栏控制的内容将在本章末尾介绍。)
这里展示的 XmlDocument
和 XmlElement
类提供了独立于 ValueTree
对象的广泛功能,用于解析和创建 XML 文档。
理解 JUCE 如何处理多线程
JUCE 通过其Thread
类提供了一个跨平台的操作系统线程接口。还有一些类可以帮助同步线程间的通信,特别是CriticalSection
类、WaitableEvent
类和Atomic
模板类(例如,Atomic<int>
)。编写多线程应用程序本质上具有挑战性,本书的范围超出了作为介绍的范围。然而,JUCE 确实使编写多线程应用程序的过程变得容易一些。实现这一点的其中一种方式是提供所有平台上一致的接口。如果执行某些可能导致一些常见问题(例如死锁和竞态条件)的操作,JUCE 也会引发断言。以下是一个基本演示;我们将创建一个简单的线程,该线程增加一个计数器并在 GUI 中显示这个计数器。创建一个新的名为Chapter05_03
的 Introjucer 项目,包含一个基本窗口。在你的 IDE 中打开项目,并将MainComponent.h
文件更改为包含以下内容:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component,
public Button::Listener,
public Thread
{
public:
MainContentComponent();
~MainContentComponent();
void resized();
void buttonClicked (Button* button);
void run();
private:
TextButton startThreadButton;
TextButton stopThreadButton;
Label counterLabel;
int counter;
};
#endif // __MAINCOMPONENT_H__
注意,我们的类继承自Thread
类,这要求我们实现Thread::run()
纯虚函数(它作为我们线程的入口点)。现在将MainComponent.cpp
文件中的代码替换为以下内容:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
: Thread ("Counter Thread"),
startThreadButton ("Start Thread"),
stopThreadButton ("Stop Thread"),
counter (0)
{
addAndMakeVisible (&startThreadButton);
addAndMakeVisible (&stopThreadButton);
addAndMakeVisible (&counterLabel);
startThreadButton.addListener (this);
stopThreadButton.addListener (this);
setSize (500, 400);
}
MainContentComponent::~MainContentComponent()
{
stopThread (3000);
}
void MainContentComponent::resized()
{
startThreadButton.setBounds (10, 10, getWidth() - 20, 20);
stopThreadButton.setBounds (10, 40, getWidth() - 20, 20);
counterLabel.setBounds (10, 70, getWidth() - 20, 20);
}
void MainContentComponent::buttonClicked (Button* button)
{
if (&startThreadButton == button)
startThread();
else if (&stopThreadButton == button)
stopThread (3000);
}
在这里需要注意的主要事项是,我们必须通过将一个String
对象传递给Thread
类的构造函数来为我们的线程提供一个名称。在buttonClicked()
函数中,我们分别使用Start Thread和Stop Thread按钮启动和停止我们的线程。传递给Thread::stopThread()
函数的3000
值是一个超时时间(以毫秒为单位),在此之后,线程将被强制终止(除非出现错误,否则这种情况很少发生)。我们还需要实现Thread::run()
函数,这是线程执行其工作的地方。这正是许多问题发生的地方。特别是,你不能从除了 JUCE 消息线程之外的任何地方直接更新 GUI 对象。这个消息线程是主线程,你的应用程序的initialise()
和shutdown()
函数(以及你应用程序的大部分构建和销毁)都是在这个线程上被调用的,你的 GUI 监听器回调函数、鼠标事件等都是在该线程上报告的。实际上,它是“主”线程(在许多情况下,它可能是可执行文件的主线程)。这就是为什么在响应用户与其他 GUI 对象的交互时更新 GUI 对象是安全的。将以下代码添加到MainComponent.cpp
文件的末尾。当你点击Start Thread按钮时,它应该立即失败:
void MainContentComponent::run()
{
while (!threadShouldExit()) {
counterLabel.setText (String (counter++),
dontSendNotification);
}
}
在 调试 构建中,你的代码应该在断言处停止。查看引发断言的 JUCE 代码,你会看到一个注释,告诉你除非使用 MessageManagerLock
对象,否则无法执行此操作。(在 发布 构建中,它可能简单地崩溃或导致应用程序行为异常。)要正确使用 MessageManagerLock
对象,请按以下方式更改 run()
函数:
void MainContentComponent::run()
{
while (!threadShouldExit()) {
const MessageManagerLock lock (Thread::getCurrentThread());
if (lock.lockWasGained()) {
counterLabel.setText (String (counter++),
dontSendNotification);
}
}
}
在这里,我们创建一个 MessageManagerLock
对象,传递给它当前线程的指针(即这个线程)。如果 MessageManagerLock::lockWasGained()
函数返回 true
,则可以安全地操作 GUI 对象。当 MessageManagerLock
对象超出作用域(当我们再次绕过 while()
循环时)时,线程会释放锁。此代码还显示了 Thread::run()
函数的典型结构;即一个 while()
循环,该循环检查调用 Thread::threadShouldExit()
函数的结果,并且除非线程被告知退出,否则会继续循环。
存储应用程序属性
在这个最后的例子中,我们将实现一个简单的应用程序,该应用程序将其状态存储在运行时平台的标准位置上的属性文件(即设置或首选项)中。首先创建一个名为 Chapter05_04
的新 Introjucer 项目,包含一个基本窗口。将 MainComponent.h
文件更改为包含以下代码:
#ifndef __MAINCOMPONENT_H__
#define __MAINCOMPONENT_H__
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component
{
public:
MainContentComponent();
~MainContentComponent();
void resized();
private:
Label label;
Slider slider;
ApplicationProperties appProperties;
};
#endif // __MAINCOMPONENT_H__
在这里,我们有一个标签和一个滑块;这些将代表我们的简单应用程序属性。显然,在一个完全开发的应用程序中,属性将在一个单独的窗口或面板中展示,但原理是相同的。
提示
ApplicationProperties
类是一个辅助类,用于管理应用程序属性,将它们保存到用户系统上的适当位置。
将 MainComponent.cpp
文件的内容更改为:
#include "MainComponent.h"
MainContentComponent::MainContentComponent()
{
label.setEditable(true);
addAndMakeVisible(&label);
addAndMakeVisible(&slider);
setSize (500, 400);
PropertiesFile::Options options;
options.applicationName = ProjectInfo::projectName;
options.folderName = ProjectInfo::projectName;
options.filenameSuffix = "settings";
options.osxLibrarySubFolder = "Application Support";
appProperties.setStorageParameters (options);
PropertiesFile* props = appProperties.getUserSettings();
label.setText (props->getValue ("label", "<empty>"),
dontSendNotification);
slider.setValue (props->getDoubleValue ("slider", 0.0));
}
MainContentComponent::~MainContentComponent()
{
PropertiesFile* props = appProperties.getUserSettings();
props->setValue ("label", label.getText());
props->setValue ("slider", slider.getValue());
}
void MainContentComponent::resized()
{
label.setBounds (10, 10, getWidth() - 20, 20);
slider.setBounds (10, 40, getWidth() - 20, 20);
}
在这里,我们通过传递我们希望的应用程序名称和文件夹名称(使用 Introjucer 应用程序将在 ProjectInfo::projectName
常量中生成的名称)来配置 ApplicationProperties
对象。我们提供一个文件后缀(例如,settings
,xml
)。为了支持 Mac OS X,建议您设置 PropertiesFile::Options::osxLibrarySubFolder
选项,因为苹果更改了他们关于存储应用程序首选项的建议。这之前在 Library/Preferences
中,但现在苹果建议开发者使用 Library/Application Support
。这是为了向后兼容;所有新的应用程序都应该将其设置为 Application Support
。对于其他平台,此设置无害。在通过 ApplicationProperties::setStorageParameters()
函数传递选项之前配置这些选项是很重要的。实际上,ApplicatonProperties
类维护两组属性,一组用于所有用户,一组用于当前用户。在这个例子中,我们只为当前用户创建属性。
当应用程序启动时,它会尝试访问属性的值并适当地设置标签和滑块。首先,我们使用 ApplicationProperties::getUserSettings()
函数访问用户设置的 PropertiesFile
对象。我们将返回的 PropertiesFile
对象的指针存储在一个普通指针中,因为它由 ApplicationProperties
对象拥有,我们只需要临时使用它。(在这种情况下将其存储在 ScopedPointer
对象中可能会导致崩溃,因为 ScopedPointer
对象最终会尝试删除它实际上不应该拥有的对象,因为它已经有了一个所有者。)然后我们使用 PropertiesFile::getValue()
函数获取文本值,以及 PropertiesFile::getDoubleValue()
函数获取双精度值(如果需要,还有 PropertiesFile::getIntValue()
和 PropertiesFile::getBoolValue()
函数)。当然,应用程序第一次启动时,这些属性将是空的。每个属性访问器都允许你提供一个默认值,如果指定的属性不存在。在这里,我们将 <empty>
作为标签内容的默认值,将 0.0
作为滑块的默认值。当应用程序关闭时(在这种情况下我们知道这是在调用 MainContentComponent
析构函数时发生的)我们将属性的值设置为标签和滑块的当前状态。这意味着当我们关闭应用程序并重新打开它时,滑块和标签应该看起来在启动之间保持了它们的状态。构建并运行应用程序并测试这一点。由 ApplicationProperties
对象生成的文件应该看起来如下:
<?xml version="1.0" encoding="UTF-8"?>
<PROPERTIES>
<VALUE name="label" val="hello"/>
<VALUE name="slider" val="1.62303665"/>
</PROPERTIES>
在 Mac OS X 上,这应该在以下位置:
**~/Library/Application Support/Chapter05_04**
在 Windows 上,这应该在以下位置:
**C:\\Documents and Settings\USERNAME\Application Data\Chapter05_04**
其中 USERNAME
是当前登录用户的名称。
# 添加菜单栏控件
如你所见,JUCE 提供了使用 Introjucer 应用程序和 JUCE 示例应用程序在 第一章 中安装 JUCE 和 Introjucer 应用程序的方法。这些菜单栏可以在所有平台上使用 JUCE 的 MenuBarComponent
类在窗口内,或者在 Mac OS X 顶部作为原生菜单栏。为了演示这一点,我们将向 Chapter05_04
项目添加一些特殊命令来以各种方式重置标签和滑块。
在 JUCE 中构建菜单栏的第一个要求是创建一个 菜单栏模型,通过创建 MenuBarModel
类的子类来实现。首先,在 MainComponent.h
文件中将 MenuBarModel
类作为 MainContentComponent
类的基类添加,如下所示:
...
class MainContentComponent : public Component,
public MenuBarModel
{
...
MenuBarModel
类有三个纯虚函数,将用于填充菜单栏。要添加这些函数,请将以下三行添加到 MainComponent.h
文件的 public
部分:
StringArray getMenuBarNames();
PopupMenu getMenuForIndex (int index, const String& name);
void menuItemSelected (int menuID, int index);
getMenuBarNames()
函数应返回一个菜单名称数组,这些名称将出现在菜单栏上。getMenuForIndex()
函数用于在用户点击菜单栏名称时创建实际菜单。这应该返回一个针对给定菜单的PopupMenu
对象(可以使用菜单索引或其名称来确定)。每个菜单项都应该有一个唯一的 ID 值,用于在选中时识别菜单项。这将在稍后描述。当用户从菜单中选择特定的菜单项时,将调用menuItemSelected()
函数。这里提供了所选菜单项的 ID 值(以及如果真的需要,该菜单项所在的菜单索引)。为了方便,我们应该将这些 ID 作为枚举常量添加。将以下代码添加到MainComponent.h
文件public
部分的末尾:
...
enum MenuIDs {
LabelClear = 1000,
SliderMin,
SliderMax
};
...
注意,第一个项目被赋予值为1000
;这是因为 ID 值为0
(否则是默认值)不是一个有效的 ID。我们还需要存储MenuBarComponent
对象。按照以下突出显示的代码添加代码:
...
private:
Label label;
Slider slider;
MenuBarComponent menuBar;
ApplicationProperties appProperties;
};
在MainComponent.cpp
文件中,更新MainContentComponent
类的构造函数,如下所示:
...
MainContentComponent::MainContentComponent()
: menuBar (this)
{
addAndMakeVisible (&menuBar);
label.setEditable (true);
...
在初始化列表中,我们将this
指针传递给MenuBarComponent
对象。这是为了告诉MenuBarComponent
对象使用哪个MenuBarModel
。更新MainComponent.cpp
文件中的resized()
函数,以如下方式定位组件:
void MainContentComponent::resized()
{
menuBar.setBounds (0, 0, getWidth(), 20);
label.setBounds (10, 30, getWidth() - 20, 20);
slider.setBounds (10, 60, getWidth() - 20, 20);
}
这将菜单栏放置在窗口顶部,填充整个窗口宽度。现在我们将通过实现MenuBarModel
类的虚拟函数来添加菜单栏功能。将以下代码添加到MainComponent.cpp
文件中:
StringArray MainContentComponent::getMenuBarNames()
{
const char* menuNames[] = { "Label", "Slider", 0 };
return StringArray (menuNames);
}
这通过返回一个包含名称的StringArray
对象来创建顶级菜单名称。这里我们将有两个菜单,一个用于控制标签,另一个用于控制滑块。接下来,将以下代码添加到MainComponent.cpp
文件中:
PopupMenu MainContentComponent::getMenuForIndex
(int index, const String& name)
{
PopupMenu menu;
if (name == "Label")
{
menu.addItem (LabelClear, "Clear");
} else if (name == "Slider") {
menu.addItem (SliderMin, "Set to minimum");
menu.addItem (SliderMax, "Set to maximum");
}
return menu;
}
这通过检查菜单名称来确定应该填充哪个菜单。标签菜单将被填充一个单独的项目,用于清除标签内容。滑块菜单将被填充两个项目:一个用于将滑块设置为最小值,另一个用于将滑块设置为最大值。请注意,这是使用之前创建的枚举常量之一的地方。最后,将以下代码添加到MainComponent.cpp
文件中:
void MainContentComponent::menuItemSelected (int menuID,
int index)
{
switch (menuID) {
case LabelClear:
label.setText (String::empty, dontSendNotification);
break;
case SliderMin:
slider.setValue (slider.getMinimum());
break;
case SliderMax:
slider.setValue (slider.getMaximum());
break;
}
}
在这里,我们检查用户选择了哪个菜单 ID,并相应地采取行动。构建并运行应用程序以检查此功能。代码包中提供了一个额外的示例项目Chapter05_04b
,说明了如何修改此示例以在 Mac OS X 平台上使用原生菜单栏。实现菜单栏的更复杂技术是使用 JUCE 的ApplicationCommandManager
类,该类被 JUCE 演示应用程序和 Introjucer 应用程序代码用于显示其菜单、从按钮发出命令等。有关此类的完整指南,请参阅 JUCE 文档。
摘要
本章介绍了 JUCE 应用开发中一系列额外的有用工具。这包括使用ValueTree
类和相关类来结构化和存储应用程序数据和属性,以及添加撤销管理。本章还探讨了 JUCE 中的多线程支持,并介绍了一个用于向 JUCE 应用程序添加菜单栏控制的最终用户界面组件。这些只是冰山一角。很难找到一个难以集成到您自己的代码中的 JUCE 类。鼓励您探索 JUCE 文档,以找到更多支持您开发的类。本书中介绍的 JUCE 代码和类应该让您对 JUCE 代码的惯用法有了深入了解。这应该会使发现和使用新的 JUCE 类相对简单。**