JUCE-入门指南-全-

🏷️ bte365 📅 2026-06-11 09:36:48 ✍️ admin 👀 932 ❤️ 443
JUCE-入门指南-全-

JUCE 入门指南(全)

原文:zh.annas-archive.org/md5/9bc26a17fda2743c9170b7661586c2a8

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

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!

新术语和重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,将以如下方式显示:“点击下一个按钮将您带到下一屏幕”。

小贴士

小技巧和窍门如下所示。

读者反馈

我们欢迎读者的反馈。让我们知道您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中获得最大收益的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件到 ,并在邮件主题中提及书名。

如果您在某个主题领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。

客户支持

现在,您已成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从购买中获得最大收益。

下载示例代码

您可以从您在www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表格链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的任何现有勘误列表中。您可以通过选择您的标题从 www.packtpub.com/support 查看任何现有勘误。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 联系我们,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。

问题

如果您在本书的任何方面遇到问题,可以通过 联系我们,我们将尽力解决。

第一章:安装 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 上有完整的文档:

www.juce.com/juce/api/

所有 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部分添加了一个 JUCE TextButton类的实例。此按钮将被静态分配。

按钮在构造函数的初始化列表中使用一个字符串初始化,该字符串设置将在按钮上显示的文本。

将对组件函数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、Line 和 Rectangle

指定和操作文本字符串

在 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 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 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 来存储 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 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 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对象创建一个矩形,该矩形从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 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

🎯 相关推荐

逻辑符号表
365英国上市公司

逻辑符号表

📅 07-19 👀 6881
oppo照相闪光灯怎么开
365英国上市公司

oppo照相闪光灯怎么开

📅 01-29 👀 3789
手机定位软件哪个好? 8款定位软件推荐