【基于Qt5仿音乐播放器(三)】——音乐管理

【基于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类型,经常用到:

  1. 文件类型识别
  2. 文件过滤
  3. 多媒体文件处理
  4. 文件导入导出
  5. 文件管理器

该类中的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中,有三个显示歌曲信息的页面:

  1. likePage:管理和显示收藏的歌曲
  2. localPage:管理和显示本地加载的歌曲
  3. 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中

  1. 调用addMusicToPage函数,从musicList中添加当前页面的歌曲
  2. 遍历musicListOfPage,拿到每首音乐后先检查其是否在,存在则添加。
  3. 界面上需要更新每首歌曲的:歌曲名称、作者、专辑名称,而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"));
    }
}

【点击喜欢按钮处理】

当喜欢某首歌曲时,可以点击界面上红色爱心收藏该⾸歌曲。我喜欢按钮中应该有以下操作:

  1. 更新爱心图标
  2. 更新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);
}
相关推荐
紧跟先前的步伐2 小时前
Go AI Agent 实战 13|大模型流式输出 Streaming 的实现
人工智能·ai·golang·1024程序员节
CRMEB系统商城2 小时前
Redis + 消息队列 + 连接池:CRMEB PRO 性能优化的三驾马车
大数据·数据库·人工智能·小程序·php·1024程序员节
怪我冷i2 小时前
Rust 语言测试的组织结构
1024程序员节
星恒随风2 小时前
Linux网络编程基础(二):彻底理解局域网通信、MAC与IP地址、数据封装、端口号、网络字节序及Socket接口
linux·网络·笔记·学习·tcp/ip·1024程序员节
wanglong37132 小时前
Keil编译后自动重命名HEX文件
单片机·1024程序员节
微三云 - 廖会灵 (私域系统开发)2 小时前
从业务模式到系统架构:拆解社区商业平台「物业2.0」的三个技术改造点
1024程序员节
troy1282 小时前
Skill 详解:从概念到用 AI 写好一个 Skill
1024程序员节
万化冥合2 小时前
如何批量把 PPT 转成视频?
python·1024程序员节
茉莉玫瑰花茶2 小时前
线性代数 [ 序言 ]
线性代数·math·1024程序员节