文章目录
前言
完整代码:音乐播放器完整代码
一、项目介绍
本项目主要涉及 Qt 界面设计、文件与目录操作、图像处理与绘制、多媒体播放功能、Qt 信号与槽等的应用,通过扫描指定目录下的 MP3 歌曲文件以及对应的 LRC 歌词文件,将歌曲添加到播放列表显示,进行歌曲的操作,包括曲目切换、循环播放、播放/暂停等操作,同时通过图像处理技术,实现专辑封面的加载、缩放以及添加圆角效果等展示功能,增强界面交互性与视觉体验。
界面展示:
二、项目基础知识
1. QPixmap 类
QPixmap
类是一种离屏图像表示形式,可用作绘图设备。
与部件配合显示:QPixmap
是为在屏幕上高效显示图像而设计的。它可以很方便地与 QLabel
或 QAbstractButton
的子类(如 QPushButton
和 QToolButton
)一起使用来展示图像。例如,对于 QLabel
,你可以通过设置其 pixmap
属性来显示图像;对于 QAbstractButton
,可利用其 icon
属性(本质上也是基于 QPixmap
)来显示图标。
绘图设备角色:作为一种绘图设备,QPixmap
能够接收 QPainter
的绘制操作。这意味着你可以使用 QPainter
在 QPixmap
对象上绘制各种图形、线条、文字等。
隐式数据共享:QPixmap
类使用隐式数据共享机制。这一特性使得它可以按值传递,在传递过程中如果数据没有被修改,多个 QPixmap
对象可以共享同一份图像数据。这不仅节省了内存空间,还提高了数据传递的效率。例如,在一个函数中传递 QPixmap
参数时,只要没有对其进行修改,就不会产生数据的复制开销。
数据存储与系统交互:其像素数据由底层窗口系统管理,这使得它能够很好地与操作系统的图形处理功能相结合。例如,在不同的操作系统(如 Windows
、Linux
、macOS
)下,QPixmap
能够利用系统的图形库来高效地存储和显示图像。
获取和变换信息:提供了一系列函数来获取有关 pixmap
的各种信息,如尺寸、深度等。同时,还有一些函数可以对 pixmap
进行变换,比如缩放、旋转、裁剪等操作。
与 QImage
转换:可以和 QImage
相互转换。通常可以先用 QImage
加载图像文件,对图像数据进行操作(如修改像素值、调整颜色等),然后再转换为 QPixmap
进行屏幕显示。
2. QDir 类
QDir
类用于提供对目录结构及其内容的访问。
QDir
可用于操作路径名称、获取有关路径和文件的信息,以及操作底层文件系统。它还能用于访问 Qt 的资源系统。
Qt 使用 “/” 作为通用的目录分隔符,就如同在统一资源定位符(URL)中 “/” 被用作路径分隔符那样。如果始终使用 “/” 作为目录分隔符,Qt 会将你的路径转换为符合底层操作系统要求的格式。
QDir
可以使用相对路径或绝对路径指向一个文件。绝对路径以目录分隔符开头(在 Windows 系统下,可选择地在前面加上驱动器标识)。相对文件名以目录名或文件名开头,它指定的路径是相对于当前目录而言的。
绝对路径示例如下:
QDir("/home/user/Documents")
QDir("C:/Documents and Settings")
在 Windows 系统中,当使用上面第二个示例去访问文件时,它将会被转换为 “C:\Documents and Settings”。
相对路径示例如下:
QDir("images/landscape.png")
可以使用isRelative()
或isAbsolute()
函数来检查一个QDir
使用的是相对路径还是绝对路径。调用makeAbsolute()
函数可以将一个相对的QDir
转换为绝对路径形式。
3. QMediaPlayer 类
QMediaPlayer
类用于播放媒体资源。
多种媒体格式支持:QMediaPlayer
是一个高级别的媒体播放类,能够播放多种类型的媒体内容,包括但不限于歌曲(如 MP3、WAV 等音频格式)、电影(如 MP4、AVI 等视频格式)以及网络电台等,它提供了一个统一的接口来处理不同类型的媒体。
本地和网络媒体播放:可以播放本地文件系统中的媒体文件,通过QUrl::fromLocalFile
将本地文件路径转换为QUrl
对象后传递给setMedia
函数来指定播放内容。同时,也能够播放网络媒体资源,直接将网络媒体的QUrl
(如网络视频或网络电台的链接)传递给setMedia
函数即可。
与播放列表结合(QMediaPlaylist
):可以与 QMediaPlaylist
类协同工作,实现播放列表功能。QMediaPlaylist
用于管理多个媒体资源,通过addMedia
函数向播放列表中添加媒体文件,然后将播放列表关联到 QMediaPlayer
(使用setPlaylist
函数),这样 QMediaPlayer
就可以按照播放列表的顺序来播放媒体内容。
视频渲染(QVideoWidget
):在播放视频内容时,与 QVideoWidget
配合使用来实现视频的渲染和显示。QVideoWidget
作为视频输出部件,通过setVideoOutput
函数将其与 QMediaPlayer
关联起来,使得视频内容能够在指定的 QVideoWidget
部件上显示。
播放控制操作:提供了基本的播放控制方法,如play
、pause
、stop
等,用于启动、暂停或者停止媒体播放。
播放状态信号通知:利用信号与槽机制,QMediaPlayer
会发出各种信号来通知应用程序播放状态的变化。例如,positionChanged
信号在播放位置改变时发出,stateChanged
信号在播放状态(如播放、暂停、停止)改变时发出。
元数据访问信号通知:作为QMediaObject
的子类,还可以访问当前播放媒体的元数据。通过metaData
函数以及预定义的元数据键来获取诸如媒体标题、艺术家名字、专辑封面等信息,并且在元数据可用改变时也会发出相应信号,方便开发者根据这些信息来丰富用户界面的展示内容,如在音乐播放器中显示歌曲的标题和艺术家信息。
4. QMediaPlaylist 类
QMediaPlaylist
类用于提供一个待播放的媒体内容列表。
QMediaPlaylist
类旨在与其他媒体对象(例如QMediaPlayer
)配合使用。它能够在服务本身具备播放列表功能时访问该功能,如果没有,则提供基于本地内存的播放列表实现方式。
以下是一段示例代码及相关解释:
// 创建一个QMediaPlaylist对象实例
playlist = new QMediaPlaylist;
// 向播放列表中添加媒体内容,这里添加的是三个网络视频文件的URL地址
playlist->addMedia(QUrl("http://example.com/movie1.mp4"));
playlist->addMedia(QUrl("http://example.com/movie2.mp4"));
playlist->addMedia(QUrl("http://example.com/movie3.mp4"));
// 设置当前播放项的索引为1,意味着将从列表中的第二个媒体内容(索引从0开始计数)开始播放
playlist->setCurrentIndex(1);
// 创建一个QMediaPlayer对象实例
player = new QMediaPlayer;
// 将前面创建的播放列表关联到QMediaPlayer上,这样播放器就可以按照播放列表中的顺序来播放媒体内容了
player->setPlaylist(playlist);
// 创建一个QVideoWidget对象实例,用于视频渲染显示
videoWidget = new QVideoWidget;
// 将QVideoWidget设置为QMediaPlayer的视频输出部件,以便视频能在该部件上显示出来
player->setVideoOutput(videoWidget);
// 显示QVideoWidget部件,使其在界面上可见
videoWidget->show();
// 启动播放操作,此时QMediaPlayer会按照QMediaPlaylist所设定的顺序播放媒体内容,并通过QVideoWidget进行视频渲染展示
player->play();
三、代码框架
首先在 QT 项目 .pro
文件中添加多媒体模块:
QT += core gui multimedia
1. 媒体播放器初始化
初始化媒体播放器,遍历文件夹中的歌曲,显示在界面上。
代码示例:
/* 媒体信息结构体 */
struct MediaObjectInfo {
// 用于保存歌曲文件名
QString fileName;
// 用于保存歌曲文件路径
QString filePath;
// 歌词文件路径
QString lyricsPath;
};
private:
/* 媒体播放器,用于播放音乐 */
QMediaPlayer *musicPlayer;
/* 媒体列表 */
QMediaPlaylist *mediaPlaylist;
/* 媒体信息存储 */
QVector<MediaObjectInfo> mediaObjectInfo;
/* 媒体播放器类初始化 */
void mediaPlayerInit();
/* 扫描歌曲 */
void scanSongs();
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
/* 媒体播放器初始化 */
mediaPlayerInit();
/* 扫描歌曲 */
scanSongs();
}
// 初始化媒体播放器
void Widget::mediaPlayerInit()
{
musicPlayer = new QMediaPlayer(this);
mediaPlaylist = new QMediaPlaylist(this);
/* 确保列表是空的 */
mediaPlaylist->clear();
/* 设置音乐播放器的列表为mediaPlaylist */
musicPlayer->setPlaylist(mediaPlaylist);
/* 设置播放模式,Loop是列循环 */
mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
}
// 扫描歌曲目录,将歌曲添加到媒体列表中,并为有歌词的歌曲保存歌词路径
void Widget::scanSongs()
{
// 歌曲目录
QDir dir(QCoreApplication::applicationDirPath()
+ "/myMusic");
// 获取歌曲目录绝对路径
QDir dirbsolutePath(dir.absolutePath());
qDebug() << dirbsolutePath.path() <<endl;
/* 如果目录存在 */
if (dirbsolutePath.exists())
{
/* 定义过滤器 */
QStringList filter;
/* 包含所有.mp3后缀的文件 */
filter << "*.mp3";
/* 获取该目录下的所有文件 */
QFileInfoList files =
dirbsolutePath.entryInfoList(filter, QDir::Files);
// 歌词映射:歌曲文件名 -> 歌词文件完整路径
QHash<QString, QString> songToLyricsMap;
// 添加歌词文件的搜索
filter.clear(); // 清空过滤器
filter << "*.lrc";
QFileInfoList lrcFiles = dirbsolutePath.entryInfoList(filter, QDir::Files);
for (const QFileInfo &lrcFileInfo : lrcFiles) {
QString baseName = lrcFileInfo.completeBaseName(); // 完整的基础文件名,不含扩展名
songToLyricsMap.insert(baseName, lrcFileInfo.absoluteFilePath());
}
/* 遍历 */
for (int i = 0; i < files.count(); i++) {
MediaObjectInfo info;
QString baseName = files.at(i).completeBaseName();
QString fileName = baseName;
info.fileName = fileName + "\n"
+ fileName.split("-").at(1);
info.filePath = QString::fromUtf8(files.at(i)
.filePath()
.toUtf8()
.data());
// 获取歌词路径
QString lyricsPath = songToLyricsMap.value(baseName);
/* 媒体列表添加歌曲 */
if (mediaPlaylist->addMedia(
QUrl::fromLocalFile(info.filePath))) {
// 如果歌词文件存在,则保存其路径
if (!lyricsPath.isEmpty()) {
info.lyricsPath = lyricsPath;
}
/* 添加到容器数组里储存 */
mediaObjectInfo.append(info);
/* 添加歌曲名字至列表 */
ui->listWidget->addItem(info.fileName);
} else {
qDebug()<<
mediaPlaylist->errorString()
.toUtf8().data()
<< endl;
qDebug()<< " Error number:"
<< mediaPlaylist->error()
<< endl;
}
}
}
else
qDebug() << "dir is no exists" <<endl;
}
编译运行结果:
2. 点击列表歌曲播放
代码示例:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
/* 媒体信号槽连接 */
/* 1. 列表项点击信号,播放和切换歌曲 */
connect(ui->listWidget, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(listWidgetCliked(QListWidgetItem*)));
}
// 列表项点击事件处理
void Widget::on_listWidget_itemClicked(QListWidgetItem *item)
{
musicPlayer->stop();
mediaPlaylist->setCurrentIndex(ui->listWidget->row(item));
emit ui->btn_play->clicked(true);
}
// 播放/暂停按钮点击事件处理
void Widget::on_btn_play_clicked(bool checked)
{
int state = musicPlayer->state();
switch (state) {
case QMediaPlayer::StoppedState:
/* 媒体播放 */
musicPlayer->play();
break;
case QMediaPlayer::PlayingState:
/* 媒体暂停 */
musicPlayer->pause();
break;
case QMediaPlayer::PausedState:
musicPlayer->play();
break;
}
}
编译运行结果:
3. 通过播放状态修改图标
代码示例:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
/* 媒体信号槽连接 */
/* 2. 状态改变信号,修改图标 */
connect(musicPlayer, SIGNAL(stateChanged(QMediaPlayer::State)), this, SLOT(mediaPlayerStateChanged(QMediaPlayer::State)));
}
// 媒体播放器状态改变时的槽函数
void Widget::mediaPlayerStateChanged(QMediaPlayer::State state)
{
switch (state) {
case QMediaPlayer::StoppedState: // 停止
ui->btn_play->setChecked(false);
break;
case QMediaPlayer::PlayingState: // 播放
ui->btn_play->setChecked(true);
break;
case QMediaPlayer::PausedState: // 暂停
ui->btn_play->setChecked(false);
break;
}
}
编译运行结果:点击播放歌曲时,图标从暂停变为播放
4. 上下首歌曲切换
代码示例:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
/* 媒体信号槽连接 */
/* 3. 媒体播放列表当前索引改变信号(上一首下一首图标显示) */
connect(mediaPlaylist, &QMediaPlaylist::currentIndexChanged, this, &Widget::mediaPlaylistCurrentIndexChanged);
}
// 下一曲按钮点击事件处理
void Widget::on_btn_next_clicked()
{
musicPlayer->stop();
int count = mediaPlaylist->mediaCount();
if (0 == count) {
qDebug() << "mediaPlaylist->mediaCount = 0" << endl;
return;
}
/* 列表下一个 */
mediaPlaylist->next();
emit ui->btn_play->clicked(true);
}
// 媒体播放列表当前索引改变时的槽函数
void Widget::mediaPlaylistCurrentIndexChanged(int index)
{
if (-1 == index)
return;
/* 设置列表正在播放的项 */
ui->listWidget->setCurrentRow(index);
}
编译运行结果:
5. 显示播放进度
通过水平滑动条设置时间和播放进度。
代码示例:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
/* 媒体信号槽连接 */
/* 4. 媒体播放时长改变信号(显示歌曲时长) */
connect(musicPlayer, SIGNAL(durationChanged(qint64)), this, SLOT(musicPlayerDurationChanged(qint64)));
/* 5. 媒体播放位置改变信号 */
connect(musicPlayer, SIGNAL(positionChanged(qint64)), this, SLOT(mediaPlayerPositionChanged(qint64)));
}
// 媒体播放时长改变时的槽函数
void Widget::musicPlayerDurationChanged(qint64 duration)
{
// 设置歌曲播放时间范围 duration / 1000 :毫秒转换为秒
ui->horizontalSlider->setRange(0, duration / 1000);
int second = duration / 1000; // 获取秒
int minute = second / 60; // 获取分
second %= 60;
QString mediaDuration;
mediaDuration.clear();
// 分、秒小于10则前面补0,否则正常显示
if (minute >= 10)
mediaDuration = QString::number(minute, 10);
else
mediaDuration = "0" + QString::number(minute, 10);
if (second >= 10)
mediaDuration = mediaDuration
+ ":" + QString::number(second, 10);
else
mediaDuration = mediaDuration
+ ":0" + QString::number(second, 10);
/* 显示媒体总长度时间 */
ui->label_3->setText(mediaDuration);
}
// 水平滑块释放事件处理
void Widget::on_horizontalSlider_sliderReleased()
{
/* 设置媒体播放的位置 */
musicPlayer->setPosition(ui->horizontalSlider->value() * 1000);
}
// 媒体播放位置改变时的槽函数 拖动进度条
void Widget::mediaPlayerPositionChanged(qint64 position)
{
// 判断滑动条是否被按下
if (!ui->horizontalSlider->isSliderDown())
ui->horizontalSlider->setValue(position/1000);
int second = position / 1000;
int minute = second / 60;
second %= 60;
QString mediaPosition;
mediaPosition.clear();
if (minute >= 10)
mediaPosition = QString::number(minute, 10);
else
mediaPosition = "0" + QString::number(minute, 10);
if (second >= 10)
mediaPosition = mediaPosition
+ ":" + QString::number(second, 10);
else
mediaPosition = mediaPosition
+ ":0" + QString::number(second, 10);
/* 显示现在播放的时间 */
ui->label_2->setText(mediaPosition);
}
编译运行结果:
6. 显示歌词
设置歌词显示居中,字体大小为13。
根据播放位置显示歌词
代码示例:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
/* 媒体信号槽连接 */
/* 6. 根据播放位置显示歌词 */
connect(musicPlayer, &QMediaPlayer::positionChanged, this, &Widget::updateLyrics);
}
// 媒体播放列表当前索引改变时的槽函数
void Widget::mediaPlaylistCurrentIndexChanged(int index)
{
if (-1 == index)
return;
/* 设置列表正在播放的项 */
ui->listWidget->setCurrentRow(index);
// 检查索引有效性
if (index >= 0 && index < mediaObjectInfo.size()) {
// 更新当前歌词路径
currentLyricsPath = mediaObjectInfo.at(index).lyricsPath;
qDebug() << currentLyricsPath << endl;
// 如果需要,你可以在这里调用更新歌词显示的函数
// updateLyrics();
} else {
// 如果索引无效,清除当前歌词路径r
currentLyricsPath.clear();
qDebug() << "currentLyricsPath.clear" << endl;
}
}
// 根据播放进度更新歌词显示
void Widget::updateLyrics(qint64 currentTime)
{
if (!currentLyricsPath.isEmpty()) {
QFile file(currentLyricsPath);
// 以只读方式打开文本
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QTextStream stream(&file);
stream.setCodec("UTF-8");
//qint64 offset = 4000;
while (!stream.atEnd())
{
QString line = stream.readLine();
// 解析每一句歌词的时间
// 歌词格式通常是如下形式:[00:12.00]歌词内容
QRegExp timeRegex("\\[(\\d+):(\\d+\\.\\d+)\\]");
if (timeRegex.indexIn(line) != -1)
{
// 将分钟数和秒数相加得到歌词播放时间,精确到小数点后两位
qint64 startTime = timeRegex.cap(1).toInt() * 60000 + // 提取分钟数
timeRegex.cap(2).toFloat() * 1000; // 提取秒数
//startTime += offset;
QString currentLyric = line.remove(timeRegex);
// 开始时间小于等于现在播放时间
if (startTime <= currentTime)
{
ui->label_4->setText(currentLyric); // 显示
//ui->label_4->setText("测试"); // 显示
}
else
{
break;
}
}
}
}
}
else
qDebug() << "currentLyricsPath isEmpty " << endl;
}
编译运行结果:
7. 调整声音大小和播放模式
通过垂直滑动条调节声音大小,通过点击按钮改变播放模式为循环播放或者单独循环。
代码示例:
// 垂直滑块释放事件处理
void Widget::on_verticalSlider_sliderReleased()
{
musicPlayer->setVolume(ui->verticalSlider->value());
}
// 循环模式按钮点击事件处理
void Widget::on_btn_loop_clicked(bool checked)
{
if(checked)
{
// 当前项循环
mediaPlaylist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
}
else
{
// 列表循环
mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
}
}
8. 美化 UI 界面
作者认为 QT 的 UI 界面美化远比功能实现复杂,QT 的灵魂在于如何做出一个漂亮的界面。
为什么非要布局,直接自己摆不是更好吗?
刚开始学的时候,我也有这样的想法,因为刚开始接触界面,布局确实不是那么重要,我们做的小项目,组件可能很少,只需要几个按钮,几个文本框就可以实现,那么随着我们的软件越来越庞大,你就得考虑软件大小变化,后期添加组件等问题,所以让组件自动分配空间显得尤为重要。
由于作者能力有限,对界面布局和美化也只知道些皮毛,后续有时间还需继续深入学习, UI 界面美化你们得自己去深入研究。
篇幅有限,最终 UI 界面展示,可自行参考网上资料实现: