【实战项目】简易版的 QQ 音乐:二

 > 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:能自我实现简易版的 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 页面了。

更新步骤:

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

处理:

  1. 当 CommonPage 往界面更新 Music 信息时,也要根据 Music 的 isLike 属性更新对应的图标。因此 ListItemBox 需要根据
  2. 当点击我喜欢按钮之后,要切换 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 上"下拉"按钮时,窗口先以动画的方式下移,动画结束后窗口隐藏。

窗口显示和上移动画:

  1. QQMusic 的 initUi 函数中,创建 lrcPage 对象并将窗口隐藏;给 lrcPage 窗口添加上移动画,动画暂不开启
  2. 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);
    // ...
}

八、结束语

今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值