> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。> 目标:能自我实现简易版的 QQ 音乐。
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:实战项目_დ旧言~的博客-CSDN博客
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
四、音乐管理
界面处理好之后,现在就需要将音乐文件加载到程序然后显示在界面上,待后续播放操作。
4.1、音乐加载
QQMusic 类中给 addLocal 添加槽函数:
音乐文件在磁盘中,可以借助 QFileDialog 类完成音乐文件加载。QFileDialog 类中函数介绍:
构造函数:
QFileDialog(QWidget *parent = nullptr,
// 指定该对象的⽗对象
const QString &caption = QString(),
// 设置窗⼝标题
const QString &directory = QString(), // 设置默认打开⽬录
const QString &filter = QString())
// 设置过滤器,可以只打开指定后
缀⽂件
默认创建的是打开对话框
///
⽂件过滤器:
筛选所需要格式的⽂件,格式:每组⽂件之间⽤两个分号隔开,同⼀组内不同后缀之间⽤空格隔开
⽐如:打开指定⽂件夹下所有.cpp .h 以及.png的⽂件
QString filter = "代码⽂件(.cpp *.h)";
过滤器可以在构造QFileDialog对象时传⼊,也可以通过setNameFilters函数设置
void setNameFilters(const QStringList &filters);
有些时候⽂件的后缀不⼀定能给全,⽐如图⽚格式:.pnp .bmp .jpg等,有些格式甚⾄没有接触过,
但也属于图⽚⽂件,该种情况下最好使⽤MIME类型过滤
MIME类型(Multipurpose Internet Mail Extensions)是⼀种互联⽹标准,⽤于表⽰⽂档、⽂件或
字节流的
性质和格式。
语法:type/subType
⽐如:text/plain 表⽰⽂本⽂件 application/octet-stream表⽰通⽤的⼆进制数据流的MIME
类型
void setMimeTypeFilters(const QStringList &filters)
⽰例:
QStringList mimeTypeFilters;
mimeTypeFilters << "image/jpeg" // will show "JPEG image (*.jpeg *.jpg *.jpe)
<< "image/png" // will show "PNG image (*.png)"
<< "application/octet-stream"; // will show "All files (*)"
QFileDialog dialog(this);
dialog.setMimeTypeFilters(mimeTypeFilters);
///
//
// 设置打开对话框的类型
QFileDialog::AcceptOpen:表⽰对话框为打开对话框
QFileDialog::AcceptSave:表⽰对话框为保存对话框
void setAcceptMode(QFileDialog::AcceptMode mode);
///
//
// 设置选择⽂件的数量和类型
void setFileMode(QFileDialog::FileMode mode);
QFileDialog::AnyFile
⽤⼾可以选择任何⽂件,甚⾄指定⼀个不存在的⽂件
QFileDialog::ExistingFile
⽤⼾只能选择单个存在的⽂件名称
QFileDialog::Directory
⽤⼾可以选择⼀个⽬录名称
QFileDialog::ExistingFiles ⽤⼾可以选择⼀个或者多个存在的⽂件名称
// 设置⽂件对话框的当前⽬录
void setDirectory(const QString &directory);
// 获取当前⽬录
QDir::currentPath();
打开函数实现如下:
// qqmusic.cpp 中新增
#include <QDir>
#include <QFileDialog>
void QQMusic::on_addLocal_clicked()
{
// 1. 创建⼀个⽂件对话框
QFileDialog fileDialog(this);
fileDialog.setWindowTitle("添加本地⾳乐");
// 2. 创建⼀个打开格式的⽂件对话框
fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
// 3. 设置对话框模式
// 只能选择⽂件,并且⼀次性可以选择多个存在的⽂件
fileDialog.setFileMode(QFileDialog::ExistingFiles);
// 4. 设置对话框的MIME过滤器
QStringList mimeList;
mimeList<<"application/octet-stream";
fileDialog.setMimeTypeFilters(mimeList);
// 5. 设置对话框默认的打开路径,设置⽬录为当前⼯程所在⽬录
QDir dir(QDir::currentPath());
dir.cdUp();
QString musicPath = dir.path()+"/QQMusic/musics/";
fileDialog.setDirectory(musicPath);
// 6. 显⽰对话框,并接收返回值
// 模态对话框, exec内部是死循环处理
if(fileDialog.exec() == QFileDialog::Accepted)
{
// 切换到本地⾳乐界⾯,因为加载完的⾳乐需要在本地⾳乐界⾯显⽰
ui->stackedWidget->setCurrentIndex(4);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
// 拿到歌曲⽂件后,将歌曲⽂件交由musicList进⾏管理
// ...
}
}
4.2、MusicList 类
4.2.1、添加 C++ 类 MusicList
将来添加到播放器中的音乐比较多,可借助一个类对所有的音乐进行管理。添加新 C++ 类与添加设计师界面类似:
4.2.2、歌曲对象存储
概念说明:
每首音乐文件,将来需要获取其内部的歌曲名称、歌手、音乐专辑、歌曲时长等信息,因此在
MusicList 类中,将所有的歌曲文件以 Music 对象方式管理起来。QQMusic 中,通过 QFileDialog将⼀组音乐文件的 url 获取到之后,可以交给 MusicList 类来管理。但是 QQMusic 加载的二进制文件不一定全部都是音乐文件,因此 MusicList 类中需要对文件的 MIME 类型再次检测,以筛选出真正的音乐文件。
QMimeDatabase类是Qt中主要用于处理文件的 MIME 类型,经常用于:
- 文件类型识别
- 文件过滤
- 多媒体文件处理
- 文件导入导出
- 文件管理器
该类中的 mimeTypeForFile 函数可用于获取给定文件的 MIME 类型:
// QMimeDatabase类的mimeTypeForFile⽅法
// 功能:获取fileName⽂件的MIME类型
// fileName:⽂件的名称
// mode: MatchMode为枚举类型,表明如何匹配⽂件的MIME类型
// MatchDefault: 通过⽂件名和⽂件内容来进⾏查询匹配,⽂件名优先于⽂件内容,如果⽂
件扩展名
//
未知,或者匹配多个MIME类型,则使⽤⽂件内容匹配
// MatchExtension: 通过⽂件
// MatchContent:通过⽂件内容来查询匹配
QMimeType mimeTypeForFile(const QString &fileName,
MatchMode mode = MatchDefault) const
// QMimeType类中的name属性,保存了获取到的MIME类型,
// 可以通过该类的name()⽅法以字符串⽅式返回MIME类型
QString name();
// audio/mpeg : 适⽤于mp3格式的⾳频⽂件
// audio/flac : 表⽰⽆损⾳频压缩格式
// audio/wav : 表⽰wav格式的歌曲⽂件
// 上述三种⾳乐格式⽂件,Qt的QMediaPlayer类都是⽀持的
对于歌曲文件:
- audio/mpeg:适用于 mp3 格式的音乐文件
- audio/flac:无损压缩的音频文件,不会破坏任何原有的音频信息
- audio/wav:表示 wav 格式的歌曲文件
上述歌曲文件格式,Qt 的 QMediaPlayer 类都是支持的。
// musiclist.h 中新增
#include <QVector>
QVector<Music> musicList;
// Music类是⾃定义的C++类,描述歌曲相关信息
// 将QQMusic⻚⾯中读取到的⾳乐⽂件,检测是⾳乐⽂件后添加到musicList中
void addMusicByUrl(const QList<QUrl>& urls);
// musiclist.cpp中新增
void MusicList::addMusicByUrl(const QList<QUrl> &urls)
{
for(auto musicUrl : urls)
{
// 由于添加进来的⽂件不⼀定是歌曲⽂件,因此需要再次筛选出⾳乐⽂件
QMimeDatabase db;
QMimeType mime = db.mimeTypeForFile(musicUrl.toLocalFile());
if(mime.name() != "audio/mpeg" && mime.name() != "audio/flac")
{
continue;
}
// 如果是⾳乐⽂件,加⼊歌曲列表
musicList.push_back(musicUrl);
}
}
4.3、Music 类
4.3.1、Music 类介绍
该用来描述⼀个音乐文件,比如:
音乐名称、歌手名称、专辑名称、音乐持续时长,当在界面上点击收藏之后,音乐会被标记为喜欢,播放之后需要标记为历史记录。
因此该类中至少需要以下成员:
// music.h中新增
#include <QUrl>
#include <QString>
class Music
{
public:
Music();
Music(const QUrl& url);
void setIsLike(bool isLike);
void setIsHistory(bool isHistory);
void setMusicName(const QString& musicName);
void setSingerName(const QString& singerName);
void setAlbumName(const QString& albumName);
void setDuration(const qint64 duration);
void setMusicUrl(const QUrl& url);
void setMusicId(const QString& musicId);
bool getIsLike();
bool getIsHistory();
QString getMusicName();
QString getSingerName();
QString getAlbumName();
qint64 getDuration();
QUrl getMusicUrl();
QString getMusicId();
private:
bool isLike;
// 标记⾳乐是否为我喜欢
bool isHistory;
// 标记⾳乐是否播放过
// ⾳乐的基本信息有:歌曲名称、歌⼿名称、专辑名称、总时⻓
QString musicName;
QString singerName;
QString albumName;
qint64 duration;
// ⾳乐的持续时⻓,即播放总的时⻓
// 为了标记歌曲的唯⼀性,给歌曲设置id
// 磁盘上的歌曲⽂件经常删除或者修改位置,导致播放时找不到⽂件,或者重复添加
// 此处⽤musicId来维护播放列表中⾳乐的唯⼀性
QString musicId;
QUrl musicUrl;
// ⾳乐在磁盘中的位置
};
Music::Music()
: isLike(false)
, isHistory(false)
{}
void Music::setIsLike(bool isLike)
{
this->isLike = isLike;
}
void Music::setIsHistory(bool isHistory)
{
this->isHistory = isHistory;
}
void Music::setMusicName(const QString &musicName)
{
this->musicName = musicName;
}
void Music::setSingerName(const QString &singerName)
{
this->singerName = singerName;
}
void Music::setAlbumName(const QString &albumName)
{
this->albumName = albumName;
}
void Music::setDuration(const qint64 duration)
{
this->duration = duration;
}
void Music::setMusicUrl(const QUrl &url)
{
this->musicUrl = url;
}
void Music::setMusicId(const QString &musicId)
{
this->musicId = musicId;
}
bool Music::getIsLike()
{
return isLike;
}
bool Music::getIsHistory()
{
return isHistory;
}
QString Music::getMusicName()
{
return musicName;
}
QString Music::getSingerName()
{
return singerName;
}
QString Music::getAlbumName()
{
return albumName;
}
qint64 Music::getDuration()
{
return duration;
}
QUrl Music::getMusicUrl()
{
return musicUrl;
}
QString Music::getMusicId()
{
return musicId;
}
4.3.2、解析音乐文件元数据
QMediaPlayer 类中的 setMedia() 函数:
// 功能:设置要播放的媒体源,媒体数据从中读取
// media: 要播放的媒体内容,⽐如⼀个视频或⾳频⽂件,该类提供了⼀个QUrl格式的单参构造
void setMedia(const QMediaContent &media, QIODevice *stream = nullptr)
QMediaObject 类是 QMediaPlayer 类的基类:
// 检测媒体源是否有效,如果是有效的返回true,否则返回false
bool isMetaDataAvailable() const;
媒体元数据加载成功之后,可以通过 QMediaObject 类的 metaData 函数获取指定的媒体数据:
// 返回要获取的媒体数据key的值
QVariant QMediaObject::metaData(const QString &key) const
本文需要获取媒体的:标题、作者、专辑、持续时长
注意:有些媒体中媒体数据可能不全,即有些媒体数据获取不到,比如盗版歌曲。
使用 QMediaPlayer 媒体播放类时,需要在 QQMusic.pro 项目工程文件中添加媒体模块multimedia ,该模块主要用来播放各种音频视频文件等,该模块中提供了很多类:
音乐文件的 meta 数据解析如下:
// music.h 中新增
private:
void parseMediaMetaData();
// music.cpp 中新增
#include <QMediaPlayer>
#include <QCoreApplication>
#include <QUuid>
void Music::parseMediaMetaData()
{
// 解析时候需要读取歌曲数据,读取歌曲⽂件需要⽤到QMediaPlayer类
QMediaPlayer player;
player.setMedia(musicUrl);
// 媒体元数据解析需要时间,只有等待解析完成之后,才能提取⾳乐信息,此处循环等待
// 循环等待时:主界⾯消息循环就⽆法处理了,因此需要在等待解析期间,让消息循环继续处理
while(!player.isMetaDataAvailable())
{
QCoreApplication::processEvents();
}
// 解析媒体元数据结束,提取元数据信息
if(player.isMetaDataAvailable())
{
musicName = player.metaData("Title").toString();
singerName = player.metaData("Author").toString();
albumName = player.metaData("AlbumTitle").toString();
duration = player.duration();
if(musicName.isEmpty())
{
musicName = "歌曲未知";
}
if(singerName.isEmpty())
{
singerName = "歌⼿未知";
}
if(albumName.isEmpty())
{
albumName = "专辑名未知";
}
qDebug()<<musicName<<" "<<singerName<<" "<<albumName<<" "<<duration;
}
}
// 该函数需要在Music的构造函数中调⽤,当创建⾳乐对象时,顺便完成歌曲⽂件的加载
Music::Music(const QUrl &url)
: isLike(false)
, isHistory(false)
, musicUrl(url)
{
musicId = QUuid::createUuid().toString();
parseMediaMetaData();
}
4.3.3、Music 数据保存
通过 QFileDialog 将音乐从本地磁盘加载到程序中后,拿到的是所有音乐文件的 QUrl ,而在程序中需要的是经过元数据解析之后的 Music 对象,并且 Music 对象需要管理起来,此时就可以采用 MusicList 类对解析之后的 Music 对象进行管理,QQMusic 类中只需要保存 MusicList 的对象,就可以让 qqMusic.ui 界面中 CommonPage 对象完成 Music 信息往界⾯更新。
// qqmusic.h 新增
#include "musiclist.h"
MusicList musicList;
// qqmusic.cpp
void QQMusic::on_addLocal_clicked()
{
// ....
// 6. 显⽰对话框,并接收返回值
// 模态对话框, exec内部是死循环处理
if(fileDialog.exec() == QFileDialog::Accepted)
{
// 切换到本地⾳乐界⾯,因为加载完的⾳乐需要在本地⾳乐界⾯显⽰
ui->stackedWidget->setCurrentIndex(4);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
// 拿到歌曲⽂件后,将歌曲⽂件交由musicList进⾏管理
musicList.addMusicByUrl(urls);
// 更新到本地⾳乐列表
ui->localPage->reFresh(musicList);
}
}
4.4、音乐分类
QQMusic 中,有三个显示歌曲信息的页面:
- likePage:管理和显示点击小心心后收藏的歌曲
- localPage:管理和显示本地加载的歌曲
- recentPage:管理和显示历史播放过的歌曲
这三个页面的类型都是 CommonPage,每个页面应该维护自己页面中的歌曲。因此 CommonPage 类中需要新增:
// commonpage.h中新增
// 区分不同page⻚⾯
enum PageType
{
LIKE_PAGE,
// 我喜欢⻚⾯
LOCAL_PAGE,
// 本地下载⻚⾯
HISTORY_PAGE
// 最近播放⻚⾯
};
class CommonForm : public QWidget
{
// 新增成员函数
public:
void setMusicListType(PageType pageType);
// 新增成员变量
private:
// 歌单列表
QVector<QString> musicListOfPage;
// 具体某个⻚⾯的⾳乐,将来只需要存储⾳乐的id即可
PageType pageType;
// 标记属于likePage、localPage、
recentPage哪个⻚⾯
};
// commonpage.cpp中新增:
void CommonPage::setMusicListType(PageType pageType)
{
this->pageType = pageType;
}
// qqmusic.cpp中新增:
void initUi()
{
// ...
// 设置CommonPage的信息
ui->likePage->setMusicListType(PageType::LIKE_PAGE);
ui->likePage->setCommonPageUI("我喜欢", ":/images/ilikebg.png");
ui->localPage->setMusicListType(PageType::LOCAL_PAGE);
ui->localPage->setCommonPageUI("本地⾳乐", ":/images/localbg.png");
ui->recentPage->setMusicListType(PageType::HISTORY_PAGE);
ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");
}
CommonPage 页面,通过 QQMusic 的 musicList 分离出自己页面的歌曲,保存 musicListOfPage中:
// commonpage.h中新增:
#include "musiclist.h"
private:
void addMusicToMusicPage(MusicList &musicList);
// commonpage.cpp 中新增:
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
// 将旧内容清空
musicListOfPage.clear();
for(auto& music : musicList)
{
switch(musicListType)
{
case LOCAL_LIST:
musicListOfPage.push_back(music.getMusicId());
break;
case LIKE_LIST:
{
if(music.getIsLike())
{
musicListOfPage.push_back(music.getMusicId());
}
break;
}
case HOSTORY_LIST:
{
if(music.getIsHistory())
{
musicListOfPage.push_back(music.getMusicId());
break;
}
}
default:
break;
}
}
}
由于 musicList 所属类,并不能直接支持范围 for,因此需要在 MusicList 类中新增:
// musiclist.h中新增:
typedef typename QVector<Music>::iterator iterator;
iterator begin();
iterator end();
// musiclist.cpp中新增:
iterator MusicList::begin()
{
return musicList.begin();
}
iterator MusicList::end()
{
return musicList.end();
}
4.5、更新 Music 信息到 ComonPage 界面
歌曲分类完成之后,歌曲信息就可以更新到 CommonPage 页面了。
更新步骤:
- 调用 addMusicIdPageFromMusicList 函数,从 musicList 中添加当前页面的歌曲
- 遍历 musicListOfPage,拿到每首音乐后先检查其是否在,存在则添加。
- 界面上需要更新每首歌歌曲的:歌曲名称、作者、专辑名称,而 commonPage 中只保存了歌曲的 musicId,因此需要在 MusicList 中增加通过 musicID 查找 Music 对象的方法。
// commonpage.h中新增
void reFresh(MusicList& musicList);
// commonpage.cpp 中新增:
void CommonPage::reFresh(MusicList& musicList)
{
// 从musicList中分离出当前⻚⾯的所有⾳乐
addMusicIdPageFromMusicList(musicList);
// 遍历歌单,将歌单中的歌曲显⽰到界⾯
for(auto musicId : musicListOfPage)
{
auto it = musicList.findMusicById(musicId);
if(it == musicList.end())
continue;
ListItemBox* listItemBox = new ListItemBox(ui->pageMusicList);
listItemBox->setMusicName(it->getMusicName());
listItemBox->setSinger(it->getSingerName());
listItemBox->setAlbumName(it->getAlbumName());
listItemBox->setLikeIcon(it->getIsLike());
QListWidgetItem* listWidgetItem = new QListWidgetItem(ui->pageMusicList);
listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(), 45));
ui->pageMusicList->setItemWidget(listWidgetItem, listItemBox);
}
// 更新完成后刷新下界⾯
repaint();
}
// musiclist.h中新增
iterator findMusicById(const QString& musicId);
// musiclist.cpp中新增
iterator MusicList::findMusicById(const QString &musicId)
{
for(iterator it = begin(); it != end(); ++it)
{
if(it->getMusicId() == musicId)
{
return it;
}
}
return end();
}
4.6 CommonPage 显示不足处理
歌曲作者对齐处理:
解析歌曲元数据时,有些歌曲文件中可能不存在歌曲名称、作者、歌曲专辑等,为了界面上显示出歌曲名称,从歌曲文件名中解析出歌曲名称和作者。
显示延迟问题:
在 CommonPage 的 reFresh() 函数中,将 ListItemBox 设置好之后,更新到界面,有时候不会立马显示出来,等鼠标放置 ListWidget 上或者界面刷新的时候,才会显示出来。这是因为往界面更新元素的操作,没有引起窗体的重绘,导致不能实时显示出来,因此添加完元素之后,需要触发重绘事件,将元素及时绘制出来。
// 该⽅法负责将歌曲信息更新到界⾯
void CommonPage::reFresh(MusicList &musicList)
{
// ...
// 该函数最后添加上repaint()函数调⽤
// repaint()会⽴即执⾏paintEvent(),不会等待事件队列的处理
// update()将⼀个paintEvent事件添加到事件队列中,等待稍后执⾏,即不会⽴即执⾏ paintEvent。
repaint();
}
移除掉 QListWidget 的水平滚动条:
一般歌曲名称、作者、专辑名称不会将 ListItemBox 沾满,为了界面好看,可以让 CommonPage中的 QListWidget 控件去除掉水平滚动条。
CommonPage::CommonPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommonPage)
{
ui->setupUi(this);
// 不要⽔平滚动条
ui->pageMusicList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
QListWidget 选中后背景色设置:
QListWidget 中 ListItemBox 选中之后,背景颜色和界面不是很搭,用如下 QSS 代码设置ListItemBox 选中后的背景颜色。
#pageMusicList::item:selected /*::item表⽰⼦控件,即ListItemBox :selected:
表⽰选中*/
{
background-color:#EFEFEF;
}
QListWidget 的垂直滚动条美化:
#pageMusicList::item:selected
{
background-color:#EFEFEF;
}
QScrollBar:vertical
{
border:none;
/*边框去掉*/
width:10px;
/*宽度15像素*/
background-color:#FFFFFF; /*背景颜⾊⽩⾊*/
margin:0px,0px,0px,0px;
/*边距不要*/
}
QScrollBar::handle:vertical
/*设置⽔平滑竿*/
{
width:10px;
background-color:#EFEFEF;
border-radius:5px;
min-height:20px;
}
4.7、音乐收藏
4.7.1、我喜欢图标处理
处理:
- 当 CommonPage 往界面更新 Music 信息时,也要根据 Music 的 isLike 属性更新对应的图标。因此 ListItemBox 需要根据
- 当点击我喜欢按钮之后,要切换 ListItemBox 中的小心心。因此 ListItemBox 中添加设置 bool 类型isLike 成员变量,以及 setIsLike 函数,在 CommonPage 添加 Music 信息到界面时,要能够设置小心心图片。
// listItemBox.h 中新增
bool isLike;
void setLikeMusic(bool isLike);
// listItemBox.cp 中新增
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false)
{
ui->setupUi(this);
}
void ListItemBox::setLikeMusic(bool isLike)
{
this->isLike = isLike;
if(isLike)
{
ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
}
else
{
ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
}
}
4.7.2、点击我喜欢按钮处理
当喜欢某首歌曲时,可以点击界⾯上红色小心心收藏该首歌曲。我喜欢按钮中应该有以下操作:
1. 更新小心心图标
2. 更新 Music 的我喜欢属性,但 ListItemBox 并没有歌曲数据,所以只能发射信号,让其父元素CommonPage 来处理
3. CommonPage 在往 QListWidget 中添加元素时,会创建一个个 ListItemBox 对象,每个对象将来都可能会发射 setLikeMusic 信号,因此在将 ListItemBox 添加完之后,CommonPage 应该关联先该信号,将需要更新的的 Music 信息以及是否喜欢,同步给 QQMusic。
4. QQMusic 收到 CommonPage 发射的 updateLikePage 信号后,通知其上的likePagelocalPage、recentPage更新其界面的我喜欢歌曲信息。
5. 歌曲重复显示问题:当界面上歌曲数据更新之后,CommonPage 往页面上更新其 musicOfPage内容时,musicOfPage 和界面中的 QListWidget 中已经有数据了,需要先将之前的内容清楚掉,否则就会重复。
五、音乐播放控制
歌曲已经添加到程序并完成解析,解析的信息也更新到界面了,所有前置工作基本完成,接下来重点处理音乐播放,歌曲播放需要用到 Qt 提供的 QMediaPlayer 类和 QMediaPlaylist 类。
5.1、QMediaPlayer类
5.1.1、QMediaPlayer 类说明
QMediaPlayer 是 Qt 框架中用于支持各种音频和视频的播放,流媒体的播放,各种播放模式(单曲播放、列表播放、循环播放等),各种播放模式(播放、暂停、停止等),信号槽机制可以让用户在播放状态改变时进行所需控制。
使用时需要包含 #include <QMediaPlayer> 头⽂件,并且需要在 .pro 项目文件中添加媒体库,即: QT += multimedia ,将 multimedia 模块导入到用程中,就可以使用该模块中提供的媒体播放控制的相关类,比如:QMediaPlayer、QMediaPlayList 等。
5.1.2、属性和方法
枚举类型:
常用类型:
常用函数:
常用槽函数:
常用信号:
5.2、QMediaPlaylist 类
5.2.1、QMediaPlaylist 类介绍
QMediaPlaylist 类提供了⼀种灵活而强大的方式管理媒体文件的播放列表。通过结合QMediaplayer,可以实现顺序播放、循环播放随机播放等多种播放模式,提升用户的媒体播放体验。该类提供了以下功能:
- 添加和删除媒体文件
- 播放模式设置(列表播放、随机播放、单曲循环)
- 控制播放列表(开始,停止,上一曲,下一曲)
- 获取和设置当前媒体文件
- 信号槽支持
若播放多个媒体文件,必须使用该类来管理媒体文件,将该列表设置到 player 上,就可实现更加灵活的播放支持。
5.2.2、属性和方法
枚举类型:
常见属性:
常见方法:
常用槽函数:
常用信号:
5.3、歌曲播放
5.3.1、播放媒体和播放列表初始化
在播放之前,需要先将 QMediaPlayer 和 QMediaPlaylis t初始化好。QQMusic 类中需要添加QMediaPlayer 和 QMediaPlaylist 的对象指针,在界面初始化时将这两个类的对象创建好。
#include <QMediaPlayer>
#include <QMediaPlaylist>
// qqmusic.h 新增
public:
void initPlayer();
// 初始化媒体对象
private:
//播放器相关
QMediaPlayer* player;
// 要多⾸歌曲播放,以及更复杂的播放设置,需要给播放器设置媒体列表
QMediaPlaylist* playList;
// qqmusic.cpp 中添加
QQMusic::QQMusic(QWidget *parent)
: QWidget(parent)
, ui(new Ui::QQMusic)
{
ui->setupUi(this);
// 窗⼝控件的初始化⼯作
initUI();
// 初始化播放器
initPlayer();
// 关联所有信号和槽
connectSignalAndSlot();
}
void QQMusic::initPlayer()
{
// 创建播放器
player = new QMediaPlayer(this);
// 创建播放列表
playList = new QMediaPlaylist(this);
// 设置播放模式:默认为循环播放
playList->setPlaybackMode(QMediaPlaylist::Loop);
// 将播放列表设置给播放器
player->setPlaylist(playList);
// 默认⾳量⼤⼩设置为20
player->setVolume(20);
}
5.3.2、播放列表设置
播放之前,先要将歌曲加入用于播放的媒体列表,由于每个 CommonPage 页面的歌曲不同,因此CommonPage中新增将其页面歌曲添加到模仿列表的方法。
// commonpage.h 中新增
#include <QMediaPlaylist>
void addMusicToPlayer(MusicList &musicList, QMediaPlaylist *playList);
// commonpage.cpp 中新增
void CommonPage::addMusicToPlayer(MusicList &musicList, QMediaPlaylist*playList)
{
// 根据⾳乐列表中⾳乐所属的⻚⾯,将⾳乐添加到playList中
for(auto music : musicList)
{
switch(pageType)
{
case LOCAL_PAGE:
{
playList->addMedia(music.getMusicUrl());
break;
}
case LIKE_PAGE:
{
if(music.getIsLike())
{
playList->addMedia(music.getMusicUrl());
}
break;
}
case HISTORY_PAGE:
{
if(music.getIsHistory())
{
playList->addMedia(music.getMusicUrl());
}
break;
}
default:
break;
}
}
}
5.3.3、播放和暂停
当点击播放和暂停按钮时,播放状态应该在播放和暂停之间切换。播放器的状态如下,刚开始为停止状态 QMediaPlayer 的播放状态有:PlayingState()、PausedState()、StoppedState()。
5.3.4、上一曲和下一曲
播放列表中,提供了 previous() 和 next() 函数,通过设置前一个或者下一个歌曲为当前播放源,player 就会播放对应的歌曲。
// qqmusic.h 新增
void onPlayUpCliked();
// 上⼀曲
void onPlayDownCliked();
// 下⼀曲
// qqmusic.cpp 新增
void QQMusic::onPlayUpCliked()
{
playList->previous();
}
void QQMusic::onPlayDownCliked()
{
playList->next();
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 播放控制区的信号和槽函数关联
connect(ui->play, &QPushButton::clicked, this, &QQMusic::onPlayMusic);
connect(ui->playUp, &QPushButton::clicked, this,
&QQMusic::onPlayUpClicked);
connect(ui->playDown, &QPushButton::clicked, this,
&QQMusic::onPlayDownClicked);
}
5.3.5、播放模式设置
媒体列表提供了以下播放模式:
5.3.6、播放所有
播放所有按钮属于 CommonPage 中的按钮,其对应的槽函数添加在 CommonPage 类中,但是
CommonPage 不具有音乐播放的功能,因此当点击播放所有按钮后之后,播放所有的槽函数应该发射出信号,让QQMusic类完成播放。
由于 likePage、localPage、recentPage 三个 CommonPage 页面都有 playAllBtn,因此该信号需要带上 PageType 参数,需要让 QQMusic 在处理该信号时,知道播放哪个页面的歌曲。
// commonpage.h 中新增加
signals:
// 该信号由QQMusic处理--在构函数中捕获
void playAll(PageType pageType);
// commonpage.cpp 中修改
CommonPage::CommonPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommonPage)
{
ui->setupUi(this);
// playAllBtn按钮的信号槽处理
// 当播放按钮点击时,发射playAll信号,播放当前⻚⾯的所有歌曲
// playAll信号交由QQMusic中处理
connect(ui->playAllBtn, &QPushButton::clicked, this, [=](){emit playAll(pageType);});
// ...
}
5.3.7、双击 CommPage 页面 QListWidget 项播放
当 QListWidget 中的项被双击时,会触发 doubleClicked 信号:
5.4、lrc 歌词同步
播放歌曲时,当点击"词"按钮后窗口会慢慢弹出,当点击隐藏按钮后,窗口会慢慢隐藏,且没有题栏。内部显示当前播放歌曲的歌词,以及歌曲名称和作者。当点击下拉按钮时,窗口会隐藏起来
5.4.1、lrc 歌词界面分析
lrcPage 中元素种类比较少,具体分析如下:
5.4.2、lrc歌词界面布局
在qt create 中新创建⼀个 qt 设计师界⾯,命名为 LrcPage,geometry 的宽高修改为:1020 * 680。
5.4.3、LrcPage 显示
在 LrcPage 的构造函数中,将窗口的标题栏去除掉;并给 hideBtn 关联 clicked 信号,当按钮点击时将窗口隐藏。
// lrcPage.cpp 中添加
LyricsPage::LyricsPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::LyricsPage)
{
ui->setupUi(this);
setWindowFlag(Qt::FramelessWindowHint);
connect(ui->hideBtn, &QPushButton::clicked, this, [=]{hide();});
ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));
}
在 QQMusic 中,创建 LrcPage 的指针,并在 initUi() 方法中创建窗口的对象,创建好之后将窗口隐藏起来。
// qqmusic.h 中添加
#include "lrcpage.h"
LrcPage* lrcPage;
void onLrcWordClicked();
// qqmusic.cpp 中添加
void QQMusic::initUI()
{
// ...
// 创建lrc歌词窗⼝
lrcPage = new LrcPage(this);
lrcPage->hide();
}
void QQMusic::onLrcWordClicked()
{
lrcPage->show();
}
void QQMusic::connectSignalAndSlot()
{
// ...
// 显⽰歌词窗⼝
connect(ui->lrcWord, &QPushButton::clicked, this,&QQMusic::onLrcWordClicked);
}
5.4.4、LrcPage 添加动画效果
当点击 QQMusic 中"歌词"按钮时,lrcPage 窗口是以动画效果显示出来的,当点击 lrcPage 上"下拉"按钮时,窗口先以动画的方式下移,动画结束后窗口隐藏。
窗口显示和上移动画:
- QQMusic 的 initUi 函数中,创建 lrcPage 对象并将窗口隐藏;给 lrcPage 窗口添加上移动画,动画暂不开启
- QQMusic 中给"歌词"按钮添加槽函数,当按钮点击时,显示窗口,开启动画。
窗口隐藏和下移动画:
LrcPage 类中,在构造窗口时设置下移动画,给"下拉"按钮添加槽函数,当"下拉按钮"点击时,开启动画;当动画结束时,将窗口隐藏。
六、持久化支持
⽀持播放相关功能之后,每次在验证功能时都需要从磁盘中加载歌曲文件,非常麻烦。而且之前收藏的喜欢歌曲以及播放记录在程序关闭之后就没有了,这一般是无法接受的。因此需要将每次在播放器上进行的操作保留下来,比如:所加载的歌曲、以及歌曲信息;收藏歌曲信息;历史播放等信息保存起来,当下次程序启动时,将保存的信息加载到播放器即可,这样就能将在播放器上的操作记录保留下来了。要永久性保存,最简单的方式就是直接保存到文件,但是保存文件不安全,而且需要自己操作文件比较麻烦,本文采用数据库完成信息的持久保存。
七、边角问题处理
7.1、更换主窗口图标
更换窗口图标,在主界面显示时,在标题栏显示设置的图标:
7.2、处理最大化、最小化按钮
由于窗口中控件并非全部基于 Widget 布局,有些控件的位置是计算死得,窗口最大化时有些控件可能无法适配尺寸,因此禁止窗口最大化。
// qqmusic.h 中新增
void on_skin_clicked();
void on_max_clicked();
void on_min_clicked();
// qqmusic.cpp 中新增
void QQMusic::on_skin_clicked()
{
QMessageBox::information(this, "温馨提⽰", "⼩哥哥正在加班紧急⽀持中...");
}
void QQMusic::on_min_clicked()
{
showMinimized();
}
void QQMusic::initUi()
{
this->setWindowFlag(Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
setWindowIcon(QIcon(":/images/tubiao.png"));
// 设置主窗⼝图标
ui->max->setEnabled(false);
// ...
}
7.3、歌词按钮的样式
#lrcWord
{
border:none;
background-image:url(:/images/ci.png);
background-repeat:no-repeat;
background-position: center center;
}
#lrcWord:hover
{
background-color:rgba(220, 220, 220, 0.5);
}
另外,LrcPage 页面中的按钮,当鼠标放上去时,可以显示向上收拾样式,更容易识别出此处是按钮。具体操作,选中 LrcPage.ui 界面中 hideBtn 按钮,然后在属性页面找到 cursor 属性,然后选择指向收拾。
7.4、CommonPage 中滚动条格式
CommonPage 中 QScorllArea 垂直滚动条的样式不太好看,可以借助 CommonPage 中QListWidget 滚动条样式设置:
QScrollBar:vertical
{
border:none;
width: 10px;
background-color:#F0F0F0;
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:vertical
{
width:10px;
background-color:#E3E3E3;
border-radius:5px;
min-height: 20px;
}
7.5、BtForm 上动画问题
当播放不同页面歌曲时,BtForm 按钮上的跳动动画应该跟随播放页面变化而变化,即那个 page 页面播放,就应该让该页面的对应 BtForm 上的动画显示,其余 BtForm 按钮上的动画隐藏,这样跳动的音符始终就可以标记当前正在播放的页面。
QQmusic 类中 currentPage 标记当前播放页面,QStackedWidget 中提供了通过页面找索引的方法,即 currentPage 可以找到其在层叠窗口中的索引,该索引与 BtForm 中的 pageId 是对应的。因此在 qqMusic 中定义 updateBtFormAnimal 函数,该函数实现原理如下:
- 获取 currentPage 在 stackedWidget 中的索引
- 获取 QQMusic 中所有 BtFrom* 的元素,保存到 btFroms
- 遍历 btFroms,如果那个按钮的 pageId 等于 currentPage 的索引,则显示该按钮的动画,否则隐藏。
注意:
所有修改 currentPage 位置之后都需要调用 updateBtFormAnimal() 函数。
// btform.h 修改
// 添加isShow参数
void BtForm::showAnimal(bool isShow);
// btform.cpp 修改
void BtForm::showAnimal(bool isShow)
{
// 当按钮点击时,根据isShow状态显⽰或隐藏动画
if(isShow)
{
ui->lineBox->show();
}
else
{
ui->lineBox->hide();
}
}
// qqmusc.h 新增
void updateBtformAnimal();
// qqmusic.cpp 新增
void QQMusic::updateBtformAnimal()
{
// 获取currentPage在stackedWidget中的索引
int index = ui->stackedWidget->indexOf(currentPage);
if(-1 == index)
{
qDebug()<<"该⻚⾯不存在";
return;
}
// 获取QQMusci界⾯上所有的btForm
QList<BtForm*> btForms = this->findChildren<BtForm*>();
for(auto btForm : btForms)
{
if(btForm->getPageId() == index)
{
// 将currentPage对⻥竿的btForm找到了
btForm->showAnimal(true);
}
else
{
btForm->showAnimal(false);
}
}
}
void QQMusic::initUi()
{
// ...
// 将localPage设置为当前⻚⾯
ui->stackedWidget->setCurrentIndex(4);
currentPage = ui->localPage;
// 本地下载BtForm动画默认显⽰
ui->local->showAnimal(true);
// ...
}
八、结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。