简介:本文详细介绍了如何使用Qt库中的 QAudioInput
模块实现音频输入和波形显示功能,涵盖音频格式配置、音频数据处理、波形绘制、实时更新、以及波形的放大、缩小和移动控制。通过该教程,开发者可以学习到如何将多媒体功能与图形界面紧密结合,创建具有交互性的应用程序。
1. Qt音频输入模块QAudioInput的使用
音频处理是多媒体应用中的一个重要环节,尤其是在开发需要音频输入功能的软件时。Qt框架提供的 QAudioInput
类是实现音频输入功能的关键组件之一,允许程序从音频输入设备(如麦克风)捕获音频流,并将其转换为可用于进一步处理的数据。
在使用 QAudioInput
之前,首先需要创建一个 QAudioFormat
对象来设置音频流的参数,这包括采样率、采样大小、通道数以及编码格式等。这一步骤是音频输入模块工作的基础,因为不同的音频格式可能需要不同的处理方法。
一旦音频格式设置完成并且 QAudioInput
被正确初始化,就可以开始音频数据的捕获了。捕获到的原始音频数据通常为PCM(脉冲编码调制)格式,这种格式需要经过进一步的处理才能用于播放或分析。例如,可以通过 QIODevice
接口读取数据并进行编码转换,或者将其送入音频处理算法中进行实时分析。
此外,为了给用户提供直观的音频输入反馈,可以将捕获到的音频数据转换成可视化波形。在Qt中, QGraphicsView
和 QPainter
是两种常用的图形视图和绘图工具,它们可以帮助开发者将音频数据以波形的形式呈现出来,从而完成整个音频输入模块的开发。
接下来,我们将深入探讨 QAudioInput
模块的使用,以及如何与 QAudioFormat
进行有效结合,为后续的音频处理和可视化奠定基础。
2. 音频格式QAudioFormat的设置与调整
2.1 音频格式基础
音频信号的数字化需要经过采样、量化、编码等步骤,而实现这些步骤需要我们对音频格式有一个基本的理解。在Qt框架中, QAudioFormat
类为我们提供了一种便捷的方式来设置和查询音频格式。
2.1.1 音频数据编码方式选择
音频数据可以采用多种编码方式进行编码,每种编码方式都有其特点和适用场景。常见的音频编码方式有PCM(脉冲编码调制)、MP3、AAC等。选择合适的编码方式是音频处理中的第一步。
QAudioFormat format;
format.setSampleFormat(QAudioFormat::Int16); // 例如,设置音频数据格式为16位整型PCM
上面的代码块中,我们通过 setSampleFormat
方法设置了音频数据的样本格式为 QAudioFormat::Int16
,即16位整型PCM编码。这是因为PCM编码是最基础的音频格式,无损并且易于处理,适合用于分析和实时处理。
2.1.2 样本率与通道数的配置
样本率(Sample Rate)和通道数(Channel Count)是音频格式的两个重要参数,样本率决定了音频的采样频率,而通道数则决定了音频的声场宽度。
format.setSampleRate(44100); // 设置样本率为44.1kHz
format.setChannelCount(2); // 设置为双声道立体声
这里,我们设置样本率为44.1kHz,这是CD质量音频的标准样本率,能够确保声音的质量与音乐的原始音质接近。双声道立体声设置则能够提供空间感较强的听觉体验。
2.2 高级音频格式属性
在处理高级音频格式时,我们还需要考虑一些额外的属性,比如音频位深度、音频缓冲大小等。这些属性对于音频的质量和性能都有着重要的影响。
2.2.1 音频位深度的设置
音频位深度表示每个样本可以使用的位数,它影响着音频信号的动态范围。位深度越高,音频信号的动态范围越大,音质也就越好。
format.setSampleSize(16); // 设置位深度为16位
在这个例子中,我们设置了位深度为16位,这意味着每个音频样本可以有65536(2^16)个不同的值,足以满足专业级别音频处理的要求。
2.2.2 音频缓冲大小与时间
音频缓冲大小影响了音频数据传输的性能和延迟。太小的缓冲可能导致断断续续的音频播放,而太大的缓冲则会导致较大的延迟。音频缓冲时间是缓冲大小的另一种表示方式,它通常以毫秒为单位。
format.setBufferDuration(5000); // 设置缓冲时间为5000毫秒
这里设置缓冲时间为5000毫秒,意味着系统会预加载5秒的音频数据到缓冲区中,以避免播放中断。缓冲时间的选择需要根据实际应用场景来权衡性能和延迟。
以上是 QAudioFormat
基础和高级属性设置的介绍。通过以上设置,我们可以配置音频的数字化参数,为之后的音频处理打下基础。在下一节中,我们将深入探讨如何结合 QIODevice
进行音频数据的处理。
3. 音频数据处理与QIODevice的结合应用
3.1 QIODevice在音频输入中的角色
3.1.1 QIODevice接口与音频流的关系
QIODevice是Qt框架中用于处理输入输出设备的抽象基类,它为音频流的读写提供了统一的接口。通过继承QIODevice,可以实现对不同音频源的数据读取和写入操作。音频输入设备(如麦克风)通过封装成QAudioInput对象,再通过QIODevice接口进行数据的读取。这个过程实质上是对音频设备捕获的数据进行缓冲,QAudioInput在内部使用QBuffer,QFile或其他类型的QIODevice进行数据的存储与管理。
代码块3.1展示如何初始化一个QAudioInput对象并创建一个QBuffer来使用内存作为音频数据的临时存储:
QAudioFormat format;
format.setSampleRate(44100); // 设置采样率
format.setChannelCount(2); // 设置通道数
format.setSampleSize(16); // 设置样本大小为16位
format.setCodec("audio/pcm"); // 设置音频格式为PCM
format.setByteOrder(QAudioFormat::LittleEndian); // 设置字节序
format.setSampleType(QAudioFormat::SignedInt); // 设置样本类型
QAudioInput audioInput(format); // 初始化音频输入对象
QBuffer buffer; // 创建QBuffer对象
buffer.open(QIODevice::ReadWrite); // 打开缓冲区进行读写
audioInput.start(&buffer); // 开始录音,音频数据存储到buffer中
在上述代码中, format
对象定义了音频数据的格式。 QAudioInput
对象被初始化,并且开始将音频数据输入到内存中, buffer
对象被用于存储这些数据。
3.1.2 数据缓冲与异步读写
QIODevice还提供了异步读写功能,这对于处理音频数据尤为重要,因为它允许应用程序在不阻塞主线程的情况下处理数据。在音频数据处理中,可以利用 read
和 write
方法的重载版本,这些版本允许以块的方式异步读写数据,这样就无需等待整个音频流读取完毕再进行处理。
3.2 音频数据的编码转换
3.2.1 PCM数据的读取与存储
脉冲编码调制(PCM)数据是音频处理的基础格式,它代表了数字音频信号的最直接表达方式。要处理音频数据,首先需要从QIODevice中读取PCM格式的音频数据。下面的代码展示了如何从内存缓冲区中异步读取PCM数据:
QByteArray data;
while (!buffer.atEnd() && audioInput.state() == QAudio::ActiveState) {
data = buffer.read(1024); // 异步读取1024字节的数据块
// 处理读取到的PCM数据...
}
在上述代码块中,循环继续读取数据直到文件结束或音频输入状态不再是激活状态。
3.2.2 编解码器的选择与使用
对于原始PCM数据,可能需要转换到其他格式,以便于存储或传输。Qt提供了一系列编解码器,允许开发者轻松地在不同的音频格式之间转换。例如,可以使用 QAudioDecoder
来解码压缩过的音频文件,或者使用 QAudioEncoder
来将PCM数据编码为压缩格式。
示例代码3.2展示了如何使用QAudioEncoder将PCM数据编码为MP3格式:
QAudioEncoderSettings settings;
settings.setCodec("audio/mp3"); // 设置输出格式为MP3
settings.setQuality(QMultimedia::HighQuality); // 设置音质
QAudioEncoder encoder;
encoder.setAudioSettings(settings); // 应用设置
encoder.start(); // 开始编码过程
// 将PCM数据送入编码器进行处理
for (int i = 0; i < data.size(); i += 1024) {
QByteArray chunk = data.mid(i, 1024);
encoder.encode(chunk); // 编码数据块
}
encoder.stop(); // 停止编码器
QByteArray encodedData = encoder.result(); // 获取编码后的数据
在上面的代码中, QAudioEncoderSettings
对象被配置为输出MP3格式,然后开始编码过程。每次循环中,一部分PCM数据被送入 encode
方法进行处理,直到所有数据被编码完成。
4. ```
第四章:实时波形显示与QGraphicsView集成
实时波形显示是音频处理软件中一个十分重要的功能,它能够让用户直观地看到声音的波形变化,对于音频分析、编辑与调试都有很大的帮助。本章将深入探讨如何将QAudioInput捕获的音频数据实时显示在QGraphicsView中。
4.1 波形数据可视化基础
4.1.1 波形数据的提取与映射
在Qt中,波形数据的提取通常涉及到QAudioInput和QIODevice。QAudioInput负责从音频设备读取原始的音频数据,而QIODevice提供了读取和写入数据的接口。通过将QAudioInput与QIODevice结合,我们能够以缓冲区的方式读取到音频数据。
波形数据映射到图形界面上,首先需要理解波形数据的基本结构。音频波形数据通常由一系列的采样点组成,每一个点都代表了一个时间点上的声音强度。要将这些采样点映射到QGraphicsView中,我们需要进行坐标的转换和缩放。
4.1.2 使用QGraphicsView绘制波形
QGraphicsView是Qt中用于绘制和显示图形的部件,它与QGraphicsScene和QGraphicsItem协同工作。在实时波形显示中,我们将波形数据封装成QGraphicsItem,然后添加到QGraphicsScene中,并最终显示在QGraphicsView上。
绘制波形涉及到QPainter的使用,我们需要定义波形的绘制逻辑,设置正确的线条样式和颜色。此外,为了能够适应不同的音频数据格式和采样率,绘制过程中的坐标转换和缩放处理尤为重要。
// 示例代码:绘制波形的QGraphicsItem
class WaveformItem : public QGraphicsItem {
public:
WaveformItem(QList<QPointF>& points, QGraphicsItem* parent = nullptr);
QRectF boundingRect() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
private:
QList<QPointF> points; // 存储波形点的列表
};
WaveformItem::WaveformItem(QList<QPointF>& points, QGraphicsItem* parent) : QGraphicsItem(parent) {
this->points = points;
}
QRectF WaveformItem::boundingRect() const {
return QRectF(0, 0, 100, 100); // 根据波形数据设定合适的边界框
}
void WaveformItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
QPen pen(Qt::blue); // 设置波形颜色
painter->setPen(pen);
for (int i = 1; i < points.size(); ++i) {
painter->drawLine(points[i - 1], points[i]); // 绘制线条连接各个点
}
}
上面的示例代码定义了一个 WaveformItem
类,它继承自 QGraphicsItem
。在构造函数中,我们接受波形点的列表,并在 paint
函数中进行绘制。这个过程涉及到坐标的转换、线条的绘制、颜色的设置等。
4.2 高级波形显示技术
4.2.1 波形的颜色与样式定制
为了使波形显示更加友好和直观,我们可以自定义波形的颜色和样式。例如,可以使用渐变色来表示声音的强度,或者在波形上叠加不同的样式来区分不同的音频通道。
QLinearGradient是一个用来创建线性渐变的类,我们可以利用它来设置波形的渐变效果。首先需要创建一个QLinearGradient对象,然后为它添加不同的颜色节点,最后将这个渐变应用到QPainter中。
QLinearGradient gradient(QPointF(0, 0), QPointF(0, height()));
gradient.setColorAt(0.0, Qt::blue);
gradient.setColorAt(1.0, Qt::red);
painter->setBrush(gradient);
painter->drawRect(boundingRect());
4.2.2 多通道波形的同步显示问题
对于立体声或多声道的音频文件,我们通常需要同时显示多个通道的波形。这要求我们在绘制波形时,能够区分不同的通道,并确保它们同步显示。
我们可以为每个通道创建一个 WaveformItem
对象,并将它们添加到QGraphicsScene中。为了同步显示,我们需要协调每个通道波形的绘制时间点,确保它们同时被更新。
// 示例代码:为多通道音频创建波形项
QList<WaveformItem*> waveformItems;
for (int channel = 0; channel < numChannels; ++channel) {
// 提取对应通道的波形数据点
QList<QPointF> channelPoints = extractChannelPoints(audioData, channel);
WaveformItem* item = new WaveformItem(channelPoints);
scene->addItem(item);
waveformItems.append(item);
}
在上述代码中, extractChannelPoints
是一个假设的函数,用来根据通道索引提取对应通道的波形数据点。我们为每个通道创建了一个 WaveformItem
实例,并将它们添加到场景中。
4.2.3 实现多通道波形的同步显示
为了实现多通道波形的同步显示,我们需要在一个统一的更新机制下对所有通道波形进行绘制。这通常涉及到定时器(如QTimer)的使用,通过定时器周期性地触发波形的更新,以保证所有波形项在同一时刻更新其显示内容。
QTimer* updateTimer = new QTimer(this);
connect(updateTimer, &QTimer::timeout, this, [this, &waveformItems]() {
for (auto& item : waveformItems) {
// 更新每个通道波形数据点
QList<QPointF> updatedPoints = extractUpdatedPoints(item->data());
item->setData(updatedPoints);
}
scene->update(); // 通知场景更新
});
updateTimer->start(50); // 设置每50ms更新一次
在上述代码中,我们创建了一个 QTimer
对象,设置它每50毫秒触发一次。在定时器的超时事件中,我们更新所有通道波形的数据点,并通知场景进行更新,从而实现多通道波形的实时同步显示。
5. 波形绘制技术的深入探讨
5.1 QPainter在波形绘制中的应用
5.1.1 QPainter的高级绘图技巧
QPainter
是Qt用于绘制2D图形的一个核心类。在波形绘制中, QPainter
能够提供高级绘图技巧,包括颜色混合、渐变填充、抗锯齿处理等,以生成高质量的视觉效果。为了实现波形的高质量绘制,掌握 QPainter
的一些关键功能是必不可少的。
颜色混合和渐变填充
使用 QPainter
可以实现颜色之间的混合,从而创建出丰富的视觉效果。例如,通过颜色混合,可以在波形的高点和低点使用不同的颜色,以提高波形的可读性。渐变填充是另一种高级绘图技巧,可以用在波形背景或波形本身上,为视觉界面增加深度和立体感。
QLinearGradient gradient(QPoint(0, 0), QPoint(0, 100));
gradient.setColorAt(0.0, Qt::blue);
gradient.setColorAt(1.0, Qt::red);
painter->setBrush(gradient);
painter->drawRect(0, 0, width(), height());
上面的代码段创建了一个从蓝色到红色的线性渐变,然后用它填充了矩形区域。渐变的方向、颜色和位置都可以根据实际需求进行调整,以适应不同的设计风格。
抗锯齿处理
在绘制波形时,抗锯齿是一个非常重要的考虑因素,因为它可以平滑曲线边缘,减少视觉上的锯齿状效果。 QPainter
提供了一系列抗锯齿设置,如 QPainter::Antialiasing
,可以用来改善绘制质量。
painter->setRenderHint(QPainter::Antialiasing, true);
在启用抗锯齿后, QPainter
会自动优化曲线和直线边缘的绘制,使波形看起来更平滑。
5.1.2 绘图优化与抗锯齿处理
优化绘图是一个复杂但必要的过程,它涉及到减少不必要的绘图操作,以及使用更有效的绘图方法。在波形绘制的上下文中,优化措施可以提高程序性能,并确保用户界面的响应速度。
减少绘图调用
减少绘图调用的数量是优化绘图性能的关键步骤。在绘制波形时,可以采取以下措施:
- 利用脏区域更新(Dirty Region Updates):仅重绘发生变化的屏幕区域,而不是整个波形区域。
- 批量绘制:如果多个波形元素需要被绘制,可以将它们合并为一次绘制调用,而不是多次单独调用。
QPainter painter(this);
// 假设有一个波形点列表 points
QPolygonF polygon;
for (const auto& point : points) {
polygon << mapFromParent(point);
}
painter.drawPolygon(polygon);
以上示例中,所有波形点在绘图之前被添加到一个多边形中,然后一次性绘制出来。
使用高效的数据结构
在处理大量波形数据时,选择合适的数据结构可以显著提升性能。例如,使用 QPolygonF
而不是多个 QPointF
可以减少绘图调用,并且 QPolygonF
在内部优化了绘图操作。
抗锯齿的性能影响
虽然抗锯齿可以改善视觉质量,但它也对绘图性能有所影响。在高分辨率的屏幕上绘制高复杂度的波形时,过高的抗锯齿级别可能会导致性能瓶颈。可以通过实验来找到合适的抗锯齿级别,以达到性能和视觉效果的平衡。
painter->setRenderHint(QPainter::Antialiasing, antialiasingLevel);
在这里, antialiasingLevel
应该是一个根据具体需求调整的参数。
5.2 自定义QGraphicsItem实现专业化波形
5.2.1 创建自定义波形图形类
为了实现更专业的波形显示,自定义 QGraphicsItem
是一个有效的方法。 QGraphicsItem
提供了高度的灵活性和控制能力,使得开发者能够创建专门的图形元素。通过继承 QGraphicsItem
类并实现必要的方法,可以设计出适合特定波形绘制需求的自定义图形类。
实现绘图方法
自定义波形图形类需要至少实现 QGraphicsItem
的两个关键方法: boundingRect
和 paint
。 boundingRect
方法用于指定图形项的边界矩形,而 paint
方法则负责在边界内绘制波形图形。
class WaveformGraphicsItem : public QGraphicsItem
{
public:
WaveformGraphicsItem(QGraphicsItem* parent = nullptr) : QGraphicsItem(parent) {}
QRectF boundingRect() const override {
// 返回波形图形的边界矩形
}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override {
// 在此方法中实现波形绘制
}
};
在 boundingRect
方法中,返回值应该准确地描述图形项的外部边界,这对于优化绘制性能和事件处理非常关键。 paint
方法则是波形绘制的核心,通过 QPainter
对象在指定区域内绘制波形。
自定义事件处理
除了绘图,自定义图形类还可以处理鼠标和键盘事件。这些事件可以用来处理用户交互,比如点击波形来放大显示细节,或者通过拖动来移动波形。
void WaveformGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent *event) override {
// 在这里处理鼠标事件,例如记录鼠标点击的位置
}
通过重写 mousePressEvent
方法,可以根据用户的输入来实现不同的交互行为。
5.2.2 事件处理与动画效果集成
事件处理和动画效果的集成能够为波形显示带来额外的动态特性,增强用户体验。通过将自定义波形图形与 QGraphicsScene
中的事件处理相结合,可以实现例如鼠标悬停高亮波形,或者平滑动画显示波形变化等交互功能。
鼠标悬停波形高亮
当用户将鼠标悬停在波形上时,高亮显示该部分波形可以突出当前查看区域。实现这个效果需要处理鼠标悬停事件,并更新波形的视觉表现。
void WaveformGraphicsItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) override {
// 高亮波形的代码逻辑
}
在 hoverEnterEvent
方法中,可以临时改变波形的颜色或透明度,或者在波形上添加额外的视觉元素,如高亮框或箭头,来指示鼠标悬停的位置。
波形动画效果
为了在波形显示中加入动态视觉效果,可以使用Qt的动画框架。 QPropertyAnimation
可以用来创建动画效果,如波形平滑移动或淡入淡出等。
QPropertyAnimation animation(this, "opacity");
animation.setDuration(2000); // 设置动画持续时间为2秒
animation.setStartValue(1.0);
animation.setEndValue(0.0);
animation.start();
上面的代码示例演示了如何通过改变图形元素的透明度来创建淡出效果。在自定义波形图形中,可以将类似的动画效果应用到波形移动或缩放等操作中,以提供流畅的用户体验。
通过将事件处理和动画效果与自定义波形图形集成,开发者能够为专业音频应用提供更丰富的视觉和交互特性,满足专业用户的需求。
6. 波形的交互操作与实时更新
在前几章节中,我们已经探讨了音频数据的采集、格式化和波形数据的绘制。在这一章中,我们将深入讨论如何让用户与波形视图进行交互,以及如何实现波形的实时更新。这包括键盘和鼠标事件的处理、波形视图的放大、缩小和移动,以及使用QTimer进行波形数据的定时更新。
6.1 波形视图的交互操作
在波形视图中,提供用户交互操作是提升用户体验的重要手段。对于图形用户界面,通常需要处理键盘和鼠标事件来响应用户的操作。
6.1.1 键盘事件的处理与响应
键盘事件的处理使得用户能够通过快捷键来控制波形视图。例如,我们可能会允许用户使用箭头键来微调视图位置,或者使用”Z”和”X”键来进行放大和缩小操作。在Qt中,这可以通过重写QWidget类中的 keyPressEvent(QKeyEvent *event)
方法来实现。
void WaveformWidget::keyPressEvent(QKeyEvent *event) {
switch (event->key()) {
case Qt::Key_Left:
// 处理向左移动事件
break;
case Qt::Key_Right:
// 处理向右移动事件
break;
case Qt::Key_Up:
// 处理放大事件
break;
case Qt::Key_Down:
// 处理缩小事件
break;
default:
QWidget::keyPressEvent(event);
}
}
6.1.2 鼠标事件的处理与响应
鼠标事件处理通常涉及到鼠标移动、点击和双击等操作。在波形视图中,我们可能需要处理鼠标滚轮事件来放大或缩小波形,或者通过拖拽来移动波形视图。
void WaveformWidget::mousePressEvent(QMouseEvent *event) {
// 处理鼠标点击事件,记录当前位置用于拖拽
}
void WaveformWidget::mouseMoveEvent(QMouseEvent *event) {
// 处理鼠标移动事件,计算偏移并更新视图
}
void WaveformWidget::mouseReleaseEvent(QMouseEvent *event) {
// 处理鼠标释放事件
}
6.2 实现波形的放大、缩小与移动
波形的放大、缩小与移动是波形视图中非常重要的交互特性。以下是如何实现这些操作的基本思路。
6.2.1 交互式控制的逻辑实现
放大、缩小和移动操作涉及到对波形视图中数据点的重新映射。放大操作意味着更多的数据点将映射到相同的屏幕空间中,而缩小操作则相反。
void WaveformWidget::zoomIn() {
// 调整视图缩放比例,放大波形
}
void WaveformWidget::zoomOut() {
// 调整视图缩放比例,缩小波形
}
void WaveformWidget::panLeft() {
// 向左移动视图,显示更早的数据
}
void WaveformWidget::panRight() {
// 向右移动视图,显示更晚的数据
}
6.2.2 平滑动画与视图更新同步
为了给用户更加流畅的体验,我们需要在用户进行放大、缩小或移动操作时,使用平滑动画来更新视图。这可以通过Qt的动画框架来实现。
QPropertyAnimation *animation = new QPropertyAnimation(this, "scrollPosition");
animation->setDuration(300);
animation->setStartValue(currentScrollPosition);
animation->setEndValue(newScrollPosition);
animation->start(QAbstractAnimation::DeleteWhenStopped);
6.3 使用QTimer进行波形的实时更新
为了保持波形视图的数据是最新的,我们通常需要定时从音频源获取新的数据。这可以通过使用QTimer来实现。
6.3.1 QTimer的基本使用与配置
QTimer可以定时触发信号,我们可以在信号的槽函数中处理音频数据的更新。
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &WaveformWidget::updateWaveform);
timer->start(10); // 设置定时器每10毫秒触发一次
6.3.2 实时数据处理与刷新机制
在定时器触发的槽函数中,我们需要处理实时的音频数据,并更新波形视图。
void WaveformWidget::updateWaveform() {
// 从音频源获取新的数据
// 更新波形数据模型
// 刷新波形视图
}
在这一章节中,我们学习了如何处理用户输入、实现波形的交互式控制和实时更新。这些技术是开发专业音频分析软件不可或缺的一部分,能够提供给用户丰富和直观的操作体验。在下一章节中,我们将探索如何进一步优化波形绘制性能和交互体验。
简介:本文详细介绍了如何使用Qt库中的 QAudioInput
模块实现音频输入和波形显示功能,涵盖音频格式配置、音频数据处理、波形绘制、实时更新、以及波形的放大、缩小和移动控制。通过该教程,开发者可以学习到如何将多媒体功能与图形界面紧密结合,创建具有交互性的应用程序。