【基于Qt5仿音乐播放器(三)】------音乐管理
- [1. 音乐加载](#1. 音乐加载)
- [2. Music类](#2. Music类)
- [3. MusicList类](#3. MusicList类)
- [4. 音乐的分类](#4. 音乐的分类)
- [5. 将歌曲加载到CommonPage中](#5. 将歌曲加载到CommonPage中)
- [6. 音乐收藏](#6. 音乐收藏)
1. 音乐加载
在我们的音乐播放器界面中有一个添加歌曲的按钮,点击添加按钮,会弹出一个文件对话框,然后我们进行选择歌曲所在的文件路径,将所需要添加的歌曲进行添加即可。所以我们选中添加按钮,为其添加槽函数
cpp
void Widget::on_addLocal_clicked()
{
QFileDialog fileDialog(this);
// 设置文件的标题
fileDialog.setWindowTitle("添加本地音乐");
// 设置对话框的模式
fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
// 设置文件的打开模式
fileDialog.setFileMode(QFileDialog::ExistingFiles);
// 设置MIme格式的过滤器,筛选出歌曲
QStringList mimeTypeFilters;
mimeTypeFilters << "application/octet-stream";// will show "All files (*)"
fileDialog.setMimeTypeFilters(mimeTypeFilters);
// 设置文件打开路径
QDir dir(QDir::currentPath());
QString path = dir.path() + "/musics";
fileDialog.setDirectory(path);
if (fileDialog.exec() == QDialog::Accepted){
// 设置page页面为localPage
ui->stackedWidget->setCurrentIndex(4);
// 获取所有歌曲的路径
QList<QUrl> urls = fileDialog.selectedUrls();
}
}
2. Music类
当我们把歌曲进行添加后,需要对歌曲进行描述,一首歌曲的信息有很多,包括歌曲名称,歌手名称,歌曲专辑,歌曲播放时长,当我们点击界面上的收藏按钮时,音乐会被标记位喜欢,播放之后需要标记为历史记录。所以这这个时候需要定义一个单独的类来描述一首歌曲。

cpp
//music.h中添加
#include <QString>
#include <QUrl>
class Music
{
public:
Music();
Music(const QUrl& url);
void setIsLike(bool isLike);
void setIsHistory(bool isHistory);
void setMusicName(const QString& musicName);
void setMusicSinger(const QString& musicSinger);
void setMusicAlbum(const QString& musicAlbum);
void setMusicId(const QString& musicId);
void setMusicUrl(const QUrl& url);
void setMusicDuration(quint64 duration);
bool getIsLike();
bool getIsHistory();
QString getMusicName();
QString getMusicSinger();
QString getMusicAlbum();
QString getMusicId();
QUrl getMusicUrl();
private:
bool isLike;
bool isHistory;
QString musicName;
QString musicSinger;
QString musicAlbum;
QUrl musicUrl;
QString musicId; // 音乐唯一标识
quint64 duration;
};
//music.cpp中添加
Music::Music():isLike(false),
isHistory(false)
{
setMusicId(QUuid::createUuid().toString());
}
Music::Music(const QUrl &url)
:isLike(false),
isHistory(false),
musicUrl(url)
{
musicId = QUuid::createUuid().toString();
}
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::setMusicSinger(const QString& musicSinger){
this->musicSinger = musicSinger;
}
void Music::setMusicAlbum(const QString& musicAlbum){
this->musicAlbum = musicAlbum;
}
void Music::setMusicId(const QString& musicId){
this->musicId = musicId;
}
void Music::setMusicUrl(const QUrl& url){
this->musicUrl = url;
}
void Music::setMusicDuration(quint64 duration)
{
this->duration = duration;
}
bool Music::getIsLike(){
return isLike;
}
bool Music::getIsHistory(){
return isHistory;
}
QString Music::getMusicName(){
return musicName;
}
QString Music::getMusicSinger(){
return musicSinger;
}
QString Music::getMusicAlbum(){
return musicAlbum;
}
QString Music::getMusicId(){
return musicId;
}
QUrl Music::getMusicUrl(){
return musicUrl;
}
为了保证Music对象的唯⼀性,给每个Music对象设置⼀个UUID。UUID,即通用唯⼀识别码(Universally Unique Identifier),确保在分布式系统中每个元素都有唯⼀的标识。UUID由一组32位数的16进制数字组成,形式为8-4-4-4-12的32个字符,比如:"230a2267-e36b47d4-a785-658834481234"在Music对象查找和更新时,可以已通过对比UUID,来保证Music对象的唯⼀性。Qt中QUuid类中可生成UUID。
【解析音乐文件元数据】
歌曲元数据解析,需要用到 QMediaPlayer ,该类也是用来进行歌曲播放的类,后续在播放音乐位置详细介绍。
cpp
// 功能:设置要播放的媒体源,媒体数据从中读取
// media: 要播放的媒体内容,⽐如⼀个视频或⾳频⽂件,该类提供了⼀个QUrl格式的单参构造
void setMedia(const QMediaContent &media, QIODevice *stream = nullptr)
注意:该函数执行后不会立刻返回,不会等媒体加载完成,也不检查错误,如果在加载没媒体时出错,会触发mediaStatusChanged和error信号。由于加载媒体文件需要时间,可以通过QMediaObject类中的isMetaDataAvailable()方法检测媒体数据是否可用QMediaObject类是QMediaPlayer类的基类。
cpp
// 检测媒体源是否有效,如果是有效的返回true,否则返回false
bool isMetaDataAvailable() const;
媒体元数据加载成功之后,可以通过QMediaObject类的metaData函数获取指定的媒体数据:
cpp
// 返回要获取的媒体数据key的值
QVariant QMediaObject::metaData(const QString &key) const
cpp
// music.cpp中添加
Music::Music(const QUrl &url)
:isLike(false),
isHistory(false),
musicUrl(url)
{
musicId = QUuid::createUuid().toString();
parseMediaData();
}
void Music::parseMediaData()
{
QMediaPlayer player;
player.setMedia(musicUrl);
// 加载歌曲元数据
while (!player.isMetaDataAvailable()){
// 将循环事件交给窗口执行
QCoreApplication::processEvents();
}
if (player.isMetaDataAvailable()){
musicName = player.metaData("Title").toString();
musicSinger = player.metaData("Author").toString();
musicAlbum = player.metaData("AlbumTitle").toString();
duration = player.duration();
// "2002年的第⼀场雪 - ⼑郎.mp3"
// letting go.mp3"
QString fileName = musicUrl.fileName();
int begin = 0;
int end = fileName.indexOf('-');
if (musicName.isEmpty()){
if (end != -1){
musicName = fileName.mid(begin, end).trimmed();
}else{
musicName = fileName.mid(begin, fileName.indexOf('.')).trimmed();
}
}
if (musicSinger.isEmpty()){
if (end != -1){
musicSinger = fileName.mid(end + 1, fileName.indexOf('.') - 1).trimmed();
}else {
musicSinger = "未知歌手";
}
}
if (musicAlbum.isEmpty()){
musicAlbum = "未知专辑";
}
qDebug() << musicName << " " << musicSinger << " " << musicAlbum << " " << duration;
}
}
3. MusicList类
按照创建Music类的方式创建MusicList类。因为我们添加的歌曲不止一首,而是很多首,而这些歌曲的管理方式都是一样的,所以还需要创建一个MusicList类来对Music进行管理
WIdget中,通过QFileDialog将一组音乐文件的url获取到之后,可以交给MusicList类来管理。但是WIdget加载的⼆进制文件不⼀定全部都是音乐文件,因此MusicList类中需要对文件的MIME类型再次检测,以筛选出真正的音乐文件。QMimeDatabase类是Qt中主要用于处理的MIME类型,经常用到:
- 文件类型识别
- 文件过滤
- 多媒体文件处理
- 文件导入导出
- 文件管理器
该类中的mimeTypeForFile函数可用于获取给定文件的MIME类型
cpp
// musiclist.h 中新增
#include <QVector>
// Music类是⾃定义的C++类,描述歌曲相关信息
QVector<Music> musicList;
// 将widget⻚⾯中读取到的⾳乐⽂件,检测是⾳乐⽂件后添加到musicList中
void addMusicByUrl(const QList<QUrl>& urls);
// musiclist.cpp中新增
void MusicList::addMusicByUrl(const QList<QUrl> &urls)
{
for (auto musicUrl : urls){
musicPaths.insert(musicPath);
// 对添加过来的文件再次过滤,只有是歌曲文件的可以进行添加进来
QMimeDatabase db;
QMimeType mime = db.mimeTypeForFile(musicUrl.toLocalFile());
if (mime.inherits("audio/mpeg") || mime.inherits("audio/flac")) {
Music music(musicUrl);
qDebug() << "添加歌曲:" << music.getMusicUrl().toLocalFile();
musicList.push_back(music); // 添加歌曲
}
}
}
【Music数据保存】
通过QFileDialog将音乐从本地磁盘加载到程序中后,拿到的是所有音乐文件的QUrl,而在程序中需要经过数据解析之后的Music对象,并且Music对象需要管理起来,此时就可以采用MusicList类对解析之后的Music对象进行管理,WIdget类中只需要保存MusicList的对象,就可以让widget.ui界面中CommonPage对象完成Music信息网界面更新。
cpp
// widget.h 新增
#include "musiclist.h"
MusicList musicList;
// widget.cpp 新增
void Widget::on_addLocal_clicked()
{
// ....
if (fileDialog.exec() == QDialog::Accepted){
// ....
musicList.addMusicByUrl(urls);
}
4. 音乐的分类
widget中,有三个显示歌曲信息的页面:
- likePage:管理和显示收藏的歌曲
- localPage:管理和显示本地加载的歌曲
- recentPage:管理和显示历史播放过的歌曲
这三个页面的类型都是CommonPage,每个页面应该维护自己的页面中的歌曲。因此CommonPage类中需要新增一个枚举列项,用于区分是哪一种类型的页面:
cpp
// CommonPage.h中添加
// 描述该页面的类型
enum PageType{
LIKE_PAGE,
LOCAL_PAGE,
HISTORY_PAGE
};
class CommonPage : public QWidget
{
Q_OBJECT
public:
// ....
void setPageType(PageType pageType);
private:
PageType pageType;
// 用于存放当前页面所包含的歌曲,使用musicId标记唯一歌曲
QVector<QString> musicListOfPage;
};
// CommonPage.cpp中添加
void CommonPage::setPageType(PageType pageType)
{
this->pageType = pageType;
}
// widget.cpp中添加
void initUi()
{
// ...
// 设置CommonPage的信息
ui->likePage->setPageType(PageType::LIKE_PAGE);
ui->likePage->setCommonPageUI("我喜欢", ":/images/ilikebg.png");
ui->localPage->setPageType(PageType::LOCAL_PAGE);
ui->localPage->setCommonPageUI("本地⾳乐", ":/images/localbg.png");
ui->recentPage->setPageType(PageType::HISTORY_PAGE);
ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");
}
widget中,点击addLocal(本地加载)按钮后,会通过其musicList成员变量,将music添加到musicList中管理,在添加过程中,每个歌曲会对应⼀个Music对象,Music对象在构造时,会完成歌曲文件的加载,顺便完成歌曲名称、作者、专辑名称等元数据的解析。⼀切准备就绪之后,每个CommonPage页面,通过widget的musicList分离出自己的歌曲,保存在musicListOfPage中。
cpp
// commonpage.h中新增:
#include "musiclist.h"
// 添加当前页面的歌曲
void addMusicToPage(MusicList &musicList);
private:
// 用于存放当前页面所包含的歌曲,使用musicId标记唯一歌曲
QVector<QString> musicListOfPage;
// commonpage.cpp中新增:
void CommonPage::addMusicToPage(MusicList &musicList)
{
// 刷新页面之前,需要将原来的内容清空
musicListOfPage.clear();
for (auto music : musicList){
switch(pageType)
{
case LIKE_PAGE:
if (music.getIsLike()){
musicListOfPage.push_back(music.getMusicId());
}
break;
case LOCAL_PAGE:
musicListOfPage.push_back(music.getMusicId());
break;
case HISTORY_PAGE:
if (music.getIsHistory()){
musicListOfPage.push_back(music.getMusicId());
}
break;
default:
qDebug() << "错误歌曲类型";
break;
}
}
}
由于musicList所属类,并不能直接⽀持范围for,因此需要在MusicList类中新增:
cpp
// 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();
}
5. 将歌曲加载到CommonPage中
- 调用addMusicToPage函数,从musicList中添加当前页面的歌曲
- 遍历musicListOfPage,拿到每首音乐后先检查其是否在,存在则添加。
- 界面上需要更新每首歌曲的:歌曲名称、作者、专辑名称,而commonPage中只保存了歌曲的musicId,因此需要在MusicList中增加通过musicID查找Music对象的方法。
cpp
// commonpage.h中新增
void reFresh(MusicList& musicList);
// commonpage.cpp 中新增:
void CommonPage::reFresh(MusicList &musicList)
{
// 刷新页面之前,需要将原来的内容清空,注意已经刷新到页面上的和vector中的是两个东西
ui->pageMusicList->clear();
addMusicToPage(musicList);
// 将对应歌曲添加到page上
for(auto musicId : musicListOfPage){
auto it = musicList.findMusicById(musicId);
if (it == musicList.end()){
continue;
}
ListItemBox *item = new ListItemBox(this);
item->setMusicName(it->getMusicName());
item->setMusicSinger(it->getMusicSinger());
item->setMusicAlbum(it->getMusicAlbum());
item->setLikeIcon(it->getIsLike());
QListWidgetItem *listWidgetItem = new QListWidgetItem(ui->pageMusicList);
listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(), 45));
ui->pageMusicList->setItemWidget(listWidgetItem, item);
}
//往界⾯更新元素的操作,没有引起窗体的重绘,导致不能实时显⽰出来,因此添加完元素之后,需要触发重绘事件,将元素及时绘制出来。
repaint();
}
// musiclist.h中新增
iterator findMusicById(const QString& musicId);
// musiclist.cpp中新增
iterator MusicList::findMusicById(const QString &musicId)
{
for (auto it = musicList.begin(); it != musicList.end(); ++it){
if (it->getMusicId() == musicId){
return it;
}
}
return end();
}
将歌曲名称、作者、专辑名称、喜欢图片等往ListItemBox界⾯中更新时,需要ListItemBox提供对应的set方法,因此需要在ListItemBox类中新增:
cpp
// ListItemBox.h中添加
public:
void setMusicName(const QString& musicName);
void setMusicSinger(const QString& musicSinger);
void setMusicAlbum(const QString& musicAlbum);
void setLikeIcon(bool like);
// ListItemBox.cpp中添加
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false)
{
ui->setupUi(this);
}
void ListItemBox::setMusicName(const QString &musicName)
{
ui->musicNameLabel->setText(musicName);
}
void ListItemBox::setMusicSinger(const QString &musicSinger)
{
ui->musicSingerLabel->setText(musicSinger);
}
void ListItemBox::setMusicAlbum(const QString &musicAlbum)
{
ui->musicAlbumLabel->setText(musicAlbum);
}
void ListItemBox::setLikeIcon(bool like)
{
this->isLike = like;
if (isLike){
ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
}else{
ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
}
}
最后这些操作都是在widget界面进行操作的,所以最后都是由widget进行调用的
cpp
void Widget::on_addLocal_clicked()
{
// ...
if (fileDialog.exec() == QDialog::Accepted){
// .....
// 将歌曲刷新到界面上
ui->localPabe->reFresh(musicList);
}
}
6. 音乐收藏
【我喜欢图标处理】
当CommonPage往界面更新Music信息时,也要根据Music的isLike属性更新对应的图标。因此ListItemBox需要根据当点击我喜欢按钮之后,要切换ListItemBox中的爱心 。因此ListItemBox中添加设置bool类型isLike成员变量,以及setIsLike函数,在CommonPage添加Music信息到界⾯时,要能够设置爱心图片。
cpp
// listItemBox.h 中新增
bool isLike;
void setLikeIcon(bool isLike);
// listItemBox.cp 中新增
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false)
{
ui->setupUi(this);
}
void ListItemBox::setLikeIcon(bool like)
{
this->isLike = like;
if (isLike){
ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
}else{
ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
}
}
【点击喜欢按钮处理】
当喜欢某首歌曲时,可以点击界面上红色爱心收藏该⾸歌曲。我喜欢按钮中应该有以下操作:
- 更新爱心图标
- 更新Music的我喜欢属性,但ListItemBox并没有歌曲数据,所以只能发射信号,让其父元素CommonPage来处理
cpp
// listItemBox.h 中新增
private slots:
void on_likeBtn_clicked();
signals:
void setIsLike(bool isLike);
// listItemBox.cpp 中新增
void ListItemBox::on_likeBtn_clicked() // 使用右击widget空间中的添加槽函数,不需要使用Connect即可
{
isLike = !isLike;
setLikeIcon(isLike);
emit setIsLike(isLike);
}
CommonPage在往QListWidget中添加元素时,会创建⼀个个ListItemBox对象,每个对象将来都可能会发射setIsLike信号,因此在将ListItemBox添加完之后,CommonPage应该关联先该信号,将需要更新的的Music信息以及是否喜欢,同步给widget。
cpp
// commonpage.h中新增
signals:
void updataMusicLike(bool isLike, const QString &musicId);
// commonpage.cpp中新增
void CommonPage::reFresh(MusicList &musicList)
{
// ...
for(auto musicId : musicListOfPage){
// ...
// 接收listItemBox发射的信号,并进行转发
connect(item, &ListItemBox::setIsLike, this, [=](bool isLike){
emit updataMusicLike(isLike, it->getMusicId());
});
}
//往界⾯更新元素的操作,没有引起窗体的重绘,导致不能实时显⽰出来,因此添加完元素之后,需要触发重绘事件,将元素及时绘制出来。
repaint();
}
widget收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、recentPage更新其界⾯的我喜欢歌曲信息。
cpp
// widget.h中添加
void onUpdataMusicLike(bool isLike, const QString &musicId);
// widget.cpp中添加
void Widget::onConnectionSignalAndSlots()
{
// .....
// 点击喜欢按钮进行收藏
connect(ui->likePage, &CommonPage::updataMusicLike, this, &Widget::onUpdataMusicLike);
connect(ui->localPabe, &CommonPage::updataMusicLike, this, &Widget::onUpdataMusicLike);
connect(ui->recentPage, &CommonPage::updataMusicLike, this, &Widget::onUpdataMusicLike);
}
void Widget::onUpdataMusicLike(bool isLike, const QString &musicId)
{
auto it = musicList.findMusicById(musicId);
if (it != musicList.end()){
it->setIsLike(isLike);
}
ui->likePage->reFresh(musicList);
ui->localPabe->reFresh(musicList);
ui->recentPage->reFresh(musicList);
}