qt6.5仿QQ音乐实现本地音乐播放器

一、项目简介

项目名称:本地音乐播放器
环境和使用技术:QtCreator6.5.3、mingw_64、QtDesigner界面设计、基于QWidget项目布局、QSS样式设置、自定义控件、信号槽机制、媒体播放技术、简单动画效果、文件操作
项目说明:实现在本地对音乐进行管理和播放的音乐播放器,,该项目可分为四个部分:界面布局、音乐管理、播放控制、持久化操作
项目源代码:https://github.com/lx467/-lx-LXMusic-

二、界面的说明

界面上的空间比较多,归结为两部分可以分为head区,和body区。

head区域从左往右依次为:图标、搜索框、更换皮肤按钮、最小化 & 最大化 & 退出按钮。
在这里插入图片描述

body区域分为左侧种类选择区域和右侧Page展示区。

在这里插入图片描述

Body左侧区域有两部分组成:在线音乐 和 我的音乐 ,两部分内部的控件种类是相同的

在这里插入图片描述

  • 说明区域,实际为QLabel
  • ⾃定义控件(按钮的扩展):图片+文本+动画
  • 同上,⾃定义控件(按钮的扩展):图片+文本+动画
  • 同上,⾃定义控件(按钮的扩展):图片+文本+动画

说明区域,实际为QLabel

Body右侧区域由:Page区、播放进度、播放控制区三部分构成。

在这里插入图片描述

  • Page区:歌曲信息页面,点击 < 或 > 具有轮播图效果
  • ② 播放进度:当前歌曲播放进度说明,支持seek功能,与播放控制区时间、以及LRC歌词是同步的
  • ③ 播放控制区域:显示歌曲图片&名称&歌手、 播放模式 & 下⼀曲 & 播放暂停 & 上⼀曲 & 音量调节和静音 & 添加本地⾳乐 当前播放时间 / 歌曲总时长 & 弹出歌词窗⼝按钮

【Page区说明】

当点击body左侧不同按钮时,Page区域会显示不同的页面
在这里插入图片描述

Body右侧目前支持的4个页面结构,整体的布局是相同的,唯独Page区域显示的内容稍有区别。 推荐页面具有类似轮播图的动态效果:
在这里插入图片描述

整个页面内容可以分为上下两组:今日为你推荐、你的歌曲补给站。两组的布局实际是相同的,元素说明:

  • 上方显示1行,内部有4个推荐元素;下方显示2行,每行有4个推荐元素

  • 左右两侧⼀个按钮,点击后推荐内容会更换下一批,不停点击会循环推荐

  • 当鼠标悬停在推荐元素上时,推荐元素会向上移动,当鼠标离开时,又回到原位置

  • 当鼠标悬停在推荐元素上时,同时会出现小手图标,说明该推荐元素具有点击功能

该页面中内容也为自定义元素,后序页面实现时具体分析。

三、界面的开发

3.1 创建工程

创建⼀个基于QWidget的工程,选中生成form选项,将来界⾯部分主要使用QDesigner来设计

在这里插入图片描述

3.2 主界面布局设置

基于Widget局部 ,QT系统提供4种布局管理器:

  1. QVBoxLayout:垂直布局

  2. QHBoxLayout:水平布局

  3. QGridLayout:栅格布局

  4. QFormLayout:表单布局
    在这里插入图片描述

由于⼀个widget中只能包含上述布局管理器中的⼀种,所以直接使⽤布局管理器来布局不是很灵活;而⼀个widget中可以包含多个widget,在widget中的控件可以进行水平、垂直、栅格、表单等布局操作,非常灵活。

在这里插入图片描述

因此本项⽬基于Widget来进⾏布局

窗⼝主框架设计 【主窗⼝的布局】

选中LXMusic,在弹出的属性中找到geometry属性,将窗口宽度修改为:1040,高度修改为700

在这里插入图片描述

从控件区拖拽⼀个Widget到窗口区域,objectName修改为:background,选中LXMusic,然后点击垂直布局,background就填充满了整个窗口

整个窗口由head和body上下两部分组成。

直接拖两个Widget放到设计区,双击将名字修改为head和body;

修改背景颜色方便查看效果,head背景色修改为green,body背景色修改为pink;

background-color:green;
background-color:pink;

head在上,body在下,然后选中background对象,点击垂直布局

head和body平分了整个background,并且head和body的margin有间隔。再次选中background对象,右侧属性部分下滑找到Layout,将Margni和Space修改为0

在这里插入图片描述

修完完成后,head和body之间的间隔就没有了。

但是head占区域过大,选中head对象,将head的minimumSize和maxmumSize属性的高度都调整为80,这样head的大小就固定了
在这里插入图片描述

head内部设计

  1. head内部由两部分构成,headLeft区域显示图标,headRight区域为搜索框和功能按钮区域。 拖两个widget到head中,将objectName修改为headLeft和headRight
  2. 然后选中head对象,点击水平布局(垂直布局左侧就是水平布局),它们就会分布在两侧。
  3. 继续选中head对象,下滑找到Layout属性,将Margin和Spacing全部设置为0;
  4. 选中headLeft对象,将minimumSize和maximumSize的宽度修改为200,就能看到head的初步效果

【headLeft】

拖⼀个QLabel控件放置headLeft内,将QLabel的objectName修改为logo,text属性修改为空;然后选中headLeft,点击⽔平布局,此时QLabel就会填充满headLeft。同样需要选中headLeft,下滑找到Layout属性,将Margin和spacing全部设置为0.

【headRight】

headRight内部也是由两部分构成:搜索框和按钮区域 拖拽两个widget到headRight,修改objectName为SearchBox和SettingBox,将SearchBox的minimumSize和maximumSize的宽度修改为300

选中headRight,然后点击水平布局,并将headRight的Margin和Spacing修改为0

【searchBox】

拖⼀个QLineEdit进去,并将其objectName修改为searchEdit然后选中searchBox点击水平布局

【settingBox】

拖拽⼀个按钮PushButton到SettingBox,按钮的minimumSize和maximumSize的宽度和⾼度都修改为30,然后⿏标选中,按着ctrl键+⿏标拖拽,复制3个出来摆放好,依次将四个按钮的objectName从左往右修改为:skinButton、maxButton、minButton、quitButton,并将按钮的text属性也修改为空,将来设置图片。

在控件区域找到Spacers,找到Horizontal Spacer控件,拖拽到SettingBox区域
在这里插入图片描述

选中SettingBox,点击水平布局,并将SettingBox的Margin和Spacing修改为0
在这里插入图片描述

Body部分布局

  1. 整个body部分是由bodyLeft和bodyRight两部分组成。
  2. 拖两个Widget到Body中,将objectName修改为bodyLeft和bodyRight
  3. 选中body,点击水平布局,将bodyLeft的minimumSize和maxmumSize的宽度修改为200
  4. 选中Body,将body的Margin和Spacing修改为0

bodyLeft内部布局

  1. 拖拽⼀个Widget到bodyLeft,将objectName修改为leftBox,背景颜⾊修改为:backgroundcolor:pink;
  2. 拖拽Vertical Spacer到bodyLeft
  3. 选中leftBox,将minmumSize和maxmumSize的⾼度修改为400
  4. 选中bodyLeft,点击垂直布局,并将bodyLeft的Margin和Spacing修改为0

leftBox内部布局

  1. 拖拽两个Widget到leftBox中,将objectName依次修改为:onlineMusic和myMusic
  2. 选中leftBox,点击垂直布局,然后将Margin和Spacing设置为0
  3. onlineMusic 和 myMusic内部的元素都是相同的,由⼀个QLabel和三个Widget构成,后期Widget会替换为⾃定义按钮,此处先用Widget占位。因此分别向onlineMusic和myMusic内部拖拽⼀个QLabel和三个QWidget,并选中 onlineMusic 和 myMusic点击垂直布局,然后将Margin和Spacing设置为0

在这里插入图片描述

bodyRight布局

bodyRight由层叠窗口、进度滑竿、播放控制区三部分组成

  1. 拖拽层叠窗口控件Stacked Widget,就在Widget控件上方到bodyRight中
  2. 拖拽Widget到bodyRight,将objectName修改为processBar,将minimumSize和maximumSize的⾼度修改为30,背景颜色修改为绿色
  3. 拖拽Widget到bodyRight,将objectName修改为controlBox,将minmumSize高度修改为60
  4. 选中bodyRight,点击垂直布局,然后将bodyRight的Margin和Spacing修改为0

在这里插入图片描述

stackedWidget内部增加页面

  1. stackedWidget默认会提供两个页面,还需添加四个页面。
  2. 在对象区域选中stackedWidget控件,然后右键单击弹出菜单中选择添加页:

在这里插入图片描述

以类似的方式添加添加4个页面,并修改每个页面的objectName如下:

在这里插入图片描述

总共六个页面,每个页面都有自己的索引,所以是从0开始的,将来切换页面时就是通过索引来切换的。选中stackedWidget,然后右键单击,弹出菜单中选择:改变页顺序,在弹出的窗⼝中就能看到每个页面的索引

在这里插入图片描述

ControlBox内部布局

  1. 该区域内部由三部分组成:歌曲信息部分、播放控制部分、时间显示

  2. 拖拽三个Widget到ControlBox中,将ObjectName依次修改为play1、play2、play3

  3. 选中ControlBox,点击⽔平布局,将ControlBox的Margin和Spacing修改为0

play1内部:

  1. 拖拽3个QLabel,放置歌曲图片、歌手名和歌曲名字,调整好位置,将QLabel的objectName修改为: musicCoverLabel、musicNameLabel、musicSingerLabel
  2. 然后选中play1,点击栅格布局

play2内部:

  1. 从左到右依次摆放6个按钮,按钮的minimumSize和maxmumSize均修改为30*30,将objectName从左往右依次修改为:playModeButton、playUpButton、PlayButton、playDownButton、soundButton、addLocalButton;
  2. 然后选中play2,点击水平布局,并将play_2的Margin和Spacing修改为0

play3内部:

  1. 拖四个QLabel和⼀个按钮,调整大小位置,从左往右QLabel的objectName依次修改为:labelNull、currentTimeLabel、lineLabel、totalTimeLabel,按钮的objectName修改为lyricButton,按钮的maxmumSize的宽度和⾼度修改为30*30;
  2. 选中play3,点击水平布局,并将play2的Margin和Spacing修改为0

在这里插入图片描述

3.3 界面的美化

主窗口设定

仔细观察发现主窗⼝是没有标题栏,因此在窗⼝创建前,就需要设置下窗口的格式

QWidget::setWindowFlag(...): //设置窗⼝格式,⽐如创建⽆边框的窗⼝

由于窗口中控件比较多,这些控件将来都需要初始化,如果将所有代码放在LXMusic的构造函数中实现,将来会造成构造函数非常臃肿,因此在LXMusic类中添加initUI()方法来完成界面初始化工作

// LXMusic.h ⽂件中添加:
void initUI()// 添加完成后,光标放在函数名字上按 alt + Enter 组合键完成⽅法定义
// LXMusic.cpp 头⽂件中完成定义
void QQMusic::initUI()
{
	// 设置⽆边框窗⼝,即窗⼝将来⽆标题栏
	setWindowFlag(Qt::WindowType::FramelessWindowHint);
}

添加完成后一定要在LXMusic的构造函数中调用initUI()函数,否则设置不会生效。

运行后,发现有以下两个问题:

  • 窗口无标题栏,找不到关闭按钮,导致窗口无法关闭窗⼝
  • 无法拖拽窗口

可以先将光标放在任务栏中当前应⽤程序图标上,弹出的框中选择关闭,后序会实现关闭功能
在这里插入图片描述

解决界面无法关闭问题

让quitButton按钮关联一个槽函数,当点击这个按钮的时候,调用程序的退出接口即可

//LXMusic.h文件中
private slots:
    void on_quitButton_clicked();
//LXMusic.cpp文件中
void LXMusic::on_quitButton_clicked()
{
    this->close();
}
//注意:如果直接使用代码实现,需要使用connect绑定槽函数,如果使用ui创建的函数,那么不需要我们自己绑定

解决主界面无法拖动问题

主界面无法拖动,此时只需要处理下鼠标单击(mousePressEvent)和鼠标移动(mouseMoveEvent)事件即可

鼠标左键按下时,记录下窗口左上角和鼠标的相对位置

鼠标移动时,会产生新的位置,保持鼠标和窗⼝左上角相对位置不变,通过move修改窗口的左上角坐标即可

// LXMusic.h中添加
protected:
    // 重写QWidget类的⿏标单击和⿏标滚轮事件
    void mousePressEvent(QMouseEvent *event)override;
    void mouseMoveEvent(QMouseEvent* event)override;
    // 记录光标相对于窗⼝标题栏的相对距离
    QPoint dragPosition;
-------------------------------------------------------------
// LXMusic.cpp中添加
void LXMusic::mousePressEvent(QMouseEvent* event){
    // 拦截⿏标左键单击事件
    if(event->button() == Qt::LeftButton){
        dragPosition = event->globalPosition().toPoint() - geometry().topLeft();
        event->accept();
	}
	QWidget::mousePressEvent(event);

}

void LXMusic::mouseMoveEvent(QMouseEvent* event){
    // 拦截⿏标左键单击事件
    if(event->buttons() == Qt::LeftButton){
        move(event->globalPosition().toPoint() - dragPosition);
        event->accept();
    }
	QWidget::mouseMoveEvent(event);
}
// event->globalPositon().toPoint():⿏标按下事件发⽣时,光标相对于屏幕左上⻆位置
// frameGeometry().topLeft(): ⿏标按下事件发⽣时,窗⼝左上⻆位置
// geometry(): 不包括边框及顶部标题区的范围
// frameGeometry(): 包括边框及顶部标题区的范围
// event->globalPositon().toPoint() - frameGeometry().topLeft() 即为:
// ⿏标按下时,窗⼝左上⻆和光标之间的距离差
// 想要窗⼝⿏标按下时窗⼝移动,只需要在mouseMoveEvent中,让光标和窗⼝左上⻆保持相同的位置差
// 获取⿏标相对于屏幕左上⻆的全局坐标

给窗口周围设置阴影部分

再仔细观察,窗口周围是有阴影效果的,窗口四周黑色部分就是阴影

在这里插入图片描述

给窗口添加阴影需要用到 QGraphicsDropShadowEffect 类,具体步骤如下:

  1. 创建 QGraphicsDropShadowEffect 类对象

  2. 设置阴影的属性。⽐如:设置阴影的偏移、颜⾊、圆⻆等

  3. 将阴影设置到具体对象上

在 initUI() 函数中添加如下代码:

//设置窗口背景透明
this->setAttribute(Qt::WA_TranslucentBackground);

//设置窗口阴影部分
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0,0);//设置阴影偏移量
shadowEffect->setColor(Qt::black);//设置阴影颜色
shadowEffect->setBlurRadius(10);//设置阴影半径

this->setGraphicsEffect(shadowEffect);

注意:给窗口设置阴影效果时,需要将窗口标题栏无边框,背景设置为透明

添加图片资源

添加⼀个qrc文件,将图⽚资源拷贝到工程目录下,并添加到工程中

在这里插入图片描述

将之前布局时所有按钮的背景全色全部清除掉,按照下面的风格重新设定。

head部分处理

不满意下面的设定,可以通过颜色查看器自行更改设定

控件QSS美化
headLeft#headLeft{background-color:#F0F0F0;/设置背景颜⾊为浅灰⾊/}
headRight#headRight{background-color:#F5F5F5; /设置背景颜⾊为亮灰⾊/}
logo#logo{border-radius:0px;background-image: url(:/images/Logo.png);background-repeat:no-repeat;border:none; background-position:center center;}
lineEdit#lineEdit{ background-color: #E3E3E3; /设置背景颜⾊/border-radius: 17px; /设置四个⻆的圆⻆/padding-left: 17px; /内部⽂字到边的距离/}
settingBox/* 类型选择器 /QPushButton {border-radius:0px; /设置按钮的边框圆⻆为 0 像素,实现直⻆边缘/background-repeat:no-repeat; / 背景图⽚不重复平铺*/border: none; /⽆边框/background-position:center center; /背景图⽚放置在按钮的中⼼位置/}/悬停状态/QPushButton:hover {background-color: rgba(230,0,0,0.5); /设置背景颜⾊为半透明的红⾊/}
skinbackground-image: url(:/images/skin.png);
maxbackground-image: url(:/images/max.png);
minbackground-image: url(:/images/min.png);
quitbackground-image: url(:/images/quit.png);
bodyLeft#bodyLeft{background-color:#F0F0F0;/设置背景颜⾊为浅灰⾊/}
bodyRight#bodyRight{background-color:#F5F5F5; /设置背景颜⾊为亮灰⾊/}

播放控制区处理

祛除play1、play2、play3的页面布局时设置的临时背景色。 将按钮上的文字全部去除,然后重新添加样式和图片

控件QSS美化
play2QPushButton{border: none; /* 去除边框 */ }/悬停状态/QPushButton:hover {/设置背景颜⾊为半透明的红⾊/background-color: rgba(220,220,220,0.5); }
playMode#playMode{ background-image: url(:/images/shuffle_2.png);}
playPrev#playPrev{ background-image: url(:/images/up.png);}
play#play{ background-image: url(:/images/play3.png);}
playNext#playNext{ background-image: url(:/images/down.png);}
volume#volume{ background-image: url(:/images/volumn.png);}
addLocal#addLocal{ background-image: url(:/images/add.png);}
lrcWord#lrcWord{border: none; /* 去除边框 */ background-repeat:no-repeat; background-position:center center;}QPushButton:hover {background-color: rgba(220,220,220,0.5); }

说明:QPushButton:hover {background-color: rgba(220,220,220,0.5); 表示,当鼠标移到这个控件时,该控件的颜色改变为rgba(220,220,220,0.5);
在这里插入图片描述

3.4 自定义控件BtForm

BtForm界⾯设计

添加⼀个新设计界面,命名为BtForm

在这里插入图片描述

该控件实际由:图片、文字、动画三部分组成。图⽚和文字分别用QLabel展示,动画部分内部实际为4个QLabel

  1. 将BtForm的geometry的宽度和高度修改为200*35。
  2. 拖⼀个Widget到btForm中,objectName修改为BtStyle,将btForm的margin和Spacing设置为0.
  3. 拖2个QLable、1个Widget和一个horizontalSpacer到btStyle中,并将objectName依次修改为btIconLabel、btTextLabel、lineBox ,btIconLabel的minimumSize和maximumSize的宽度设置为30(为了看到效果可将颜色设置为red) btTextLabel的minimumSize和maximumSize的宽度设置为90(为了看到效果可将颜色设置为green) lineBox的minimumSize和maximumSize的宽度设置为30,然后选中btStyle进行水平对齐,并将其margin和Spacing设置为0
  4. 然后往lineBox内部拖4个QLabel,objectName依次修改为line1、line2、line3、line4,minimumSize和 maximumSize的宽度均设置为2,选中lineBox进行水平对齐,并将其margin和Spacing设置为0
    在这里插入图片描述
控件QSS美化
btStyle#btStyle:hover { background:#D8D8D8;}
lineBox.QLabel { background-color:#FFFFFF;}

将bodyLeft内部onlineMusic和MyMusic中的QWidget全部提升为BtForm。具体操作: 选中要提升的控件,比如:Recommend,在弹出的菜单中选择提升为,会出现⼀个新窗口(如下右侧图),在提升的类名称中输⼊要提升为的类型BtForm,然后点击添加,最后选中btform.h点击提升,便可以将Rec由QWidget提升为自定义的BtForm类型

此时运行的话会出现一个问题,BtForm控件都是一样的,这不是我们预期的结果。

解决BtForm控件显示都是一样内容的问题

// btform.h 新增
// 按钮id:该按钮对应的page⻚
int _pageId = 0;
// 设置图标 ⽂字 id
void setIconAndTextAndId(QString btIcon,QString content,int mid);
--------------------------------------------------------------------------------
// btform.cpp新增
void BtForm::setIconAndTextAndId(QString iconURL, QString text,int pageId){
    //修改图标
    ui->btIconLabel->setPixmap(iconURL);
    //修改内容
    ui->btTextLabel->setText(text);
    //设置id
    this->_pageId = pageId;
}

在LXMusic.cpp的initUi()函数中新增:

//设置btform内容
ui->recommend->setIconAndTextAndId(":/images/recommend.png","推荐",0);
ui->musicHall->setIconAndTextAndId(":/images/musicHall.png", "乐馆", 1);
ui->audio->setIconAndTextAndId(":/images/audio.png", "视频", 2);
ui->likeMusic->setIconAndTextAndId(":/images/likeMusic.png", "喜欢", 3);
ui->recentMusic->setIconAndTextAndId(":/images/recentMusic.png", "最近播放", 4);
ui->localMusic->setIconAndTextAndId(":/images/localMusic.png", "本地和下载", 5);

注意:设置id的目的是为了后面点击某个控件的时候,跳转到stackWidget不同的界面中。
在这里插入图片描述

给每个控件设置槽函数,使得点击该控件时,显示相应的效果

按钮响应

重写鼠标mousePressEvent,当按钮按下时:

  1. 按钮颜色发生变化
  2. 给LXMusic类发送click信号
// btform.h 新增
protected:
	//鼠标点击事件
    virtual void mousePressEvent(QMouseEvent*evnet)override;
----------------------------------------------------------------------
// btform.cpp新增
void BtForm::mousePressEvent(QMouseEvent *event)
{
    //避免编译器触发警告
    (void)event;

    //点击修改背景颜色
    ui->BtStyle->setStyleSheet("#BtStyle{background-color:rgb(30, 206, 154);}");

    //当按钮按下的时候,通过发送信号给主界面对象,修改右边bodyRight的Page界面
    //发送信号
    emit btClick(_pageId);
}

LXMusic类处理该信号,内部:实现窗口切换,并清除上次按钮点击留下的样式,因此LXMuisc中需要新增:

// lxmusic.h 新增
// btForm点击槽函数
private slots:
    void onBtformClicked(int pageId);
------------------------------------------------------
//lxmusic.cpp中
//处理BtForm发来的信号的槽函数
void LXMusic::onBtformClicked(int pageId)
{
    //获取btform类型的孩子
    QList<BtForm*> btFormList = this->findChildren<BtForm*>();
    for(auto btForm : btFormList){
        if(btForm->getPageId() != pageId){
            btForm->clearBackground();
        }
    }
    //修改stackPage页面
    ui->stackedWidget->setCurrentIndex(pageId);
}
//绑定BtForm相关控件信号与槽函数
void LXMusic::connectSignalAndSlots()
{
    connect(ui->recommend, &BtForm::btClick, this, &LXMusic::onBtformClicked);
    connect(ui->musicHall, &BtForm::btClick, this, &LXMusic::onBtformClicked);
    connect(ui->audio, &BtForm::btClick, this, &LXMusic::onBtformClicked);
    connect(ui->likeMusic, &BtForm::btClick, this, &LXMusic::onBtformClicked);
    connect(ui->recentMusic, &BtForm::btClick, this, &LXMusic::onBtformClicked);
    connect(ui->localMusic, &BtForm::btClick, this, &LXMusic::onBtformClicked);
}
//在LXMusic::initUi()函数中调用
connectSignalAndSlots();

因为_pageId是私有的,且改变BtForm的颜色需在BtForm中完成,所以需要添加两个函数:

// btform.h 新增
public:
	int getPageId();
    void clearBackground();
---------------------------------------------------
//btform.cpp
//获取当前控件对应的stackWidget页面索引
int BtForm::getPageId()
{
    return _pageId;
}

//清楚点击后留存的颜色
void BtForm::clearBackground()
{
    ui->BtStyle->setStyleSheet("#BtStyle:hover{background:#D8D8D8;}");
}

为了能看到Page切换的效果,可以在stackedWidget的每个page上放⼀个QLabel说明 ,也可以通过QDebug通过控制台输出验证。
在这里插入图片描述

BtFrom上的动画效果

Qt中QPropertyAnimation类可以提供简单的动画效果,允许对QObject获取派⽣类的可读写属性进⾏动画处理,创建平滑、连续的动画效果,比如控件的位置、大小、颜⾊等属性变化,使用时需包含QPropertyAnimation头文件,关键函数说明:

/*
功能:实例化QPropertyAnimation类对象
参数:
target: 给target设置动画效果
propertyName:动画如何变化,⽐如:geometry,让target以矩形的⽅式滑动
parent:该动画实例的⽗对象,即将该对象加到对象树中
*/
QPropertyAnimation(QObject *target,
const QByteArray &propertyName,
QObject *parent = nullptr);
/*
功能: 设置动画持续的时⻓
参数: 单位为毫秒
*/
void setDuration(int msecs);

/*
功能:根据value创建关键帧
参数:
step:值再0~1之间,0表⽰开始,1表⽰停⽌
value:动画的⼀个关键帧,即动画现在的形态,假设是基于geometry,可以设置矩形的范围
*/
void setKeyValueAt(qreal step, const QVariant &value);

/*
功能:设置动画的循环次数
参数:
loopCount:默认值是1,表⽰动画执⾏1次,如果是-1,表⽰⽆限循环
*/
void setLoopCount(int loopCount)/// 槽函数 ///
void pause(); // 暂停动画
void start(QAbstractAnimation::DeletionPolicy policy = KeepWhenStopped); // 开
启动画
void stop(); // 停⽌动画
// 设置动画的起始帧
void setStartValue(const QVariant &value)
// 设置动画的结束帧
void setEndValue(const QVariant &value);
/*
设置动画效果步骤:
1. 创建QPropertyAnimation 对象
2. 设置动画的持续时间
3. 设置动画的关键帧
4. 设置动画的循环次数【⾮必须】,如果未调⽤动画默认执⾏⼀次
5. 开启动画
6. 动画运⾏结束时,会发射finished信号,如果需要进⾏额外处理时,处理该信号即可
*/

lineBox中的lineLabel1、lineLabel2、lineLabel3、lineLabel4添加动画效果,BtForm类中增加如下代码:

//btform.h
public:
	void setLabelAnimation(QPropertyAnimation* animation, QLabel* label,int time, int x, int y);
private:
	//动画对象
    QPropertyAnimation* animationLine1;
    QPropertyAnimation* animationLine2;
    QPropertyAnimation* animationLine3;
    QPropertyAnimation* animationLine4;
------------------------------------------------------------------
//btform.cpp
//设置动画
void BtForm::setLabelAnimation(QPropertyAnimation* animation, QLabel* label,int time, int x, int y)
{
    animation = new QPropertyAnimation(label, "geometry", this);
    animation->setDuration(time);//持续时间
    animation->setKeyValueAt(0,QRect(x,y,2,15));
    animation->setKeyValueAt(0.5,QRect(x,y + 15,2,15));
    animation->setKeyValueAt(1,QRect(x,y,2,15));
    animation->setLoopCount(-1);
    animation->start();
}
//btform.cpp中的构造函数中调用上述函数,初始化动画
//设置动画效果
setLabelAnimation(animationLine1,ui->lineLabel1,1500,4,0);
setLabelAnimation(animationLine2,ui->lineLabel2,1600,10,0);
setLabelAnimation(animationLine3,ui->lineLabel3,1700,16,0);
setLabelAnimation(animationLine4,ui->lineLabel4,1800,22,0);

关于动画显示

动画并不是所有页面都显示,只有当前选中的页面显示,所以默认情况下,动画隐藏。

注:后续会修改

//btform.h中
public:
//显示动画
void showAnimation();
//隐藏动画
void hideAnimation();
------------------------------------------
//btform.cpp中
//btform.cpp中的构造函数中调用上述函数,隐藏动画
//隐藏动画
hideAnimation();
//新增函数
void BtForm::showAnimation()
{
    ui->lineBox->show();
}

void BtForm::hideAnimation()
{
    ui->lineBox->hide();
}
//lxmusic中,修改void LXMusic::onBtformClicked(int pageId)函数
void LXMusic::onBtformClicked(int pageId)
{
    //获取btform类型的孩子
    QList<BtForm*> btFormList = this->findChildren<BtForm*>();
    for(auto btForm : btFormList){
        if(btForm->getPageId() != pageId){
            //不是当前选中的,清除颜色,隐藏动画
            btForm->clearBackground();
            btForm->hideAnimation();
        }
        else{
            //是当前选中,显示动画
            btForm->showAnimation();
        }
    }

    //修改stackPage页面
    ui->stackedWidget->setCurrentIndex(pageId);
}

3.5 推荐页面

推荐页面分析

仔细观察推荐页面,对其进行拆解发现,推荐页面由五部分构成:

图片待写

  1. "推荐"文本提示,即QLabel
  2. "今⽇为你推荐"文本提示,即QLabel
  3. 具体推荐的歌曲内容,点击左右两侧翻页按钮,具有轮番图效果,将光标放到图上,有图⽚上移动画
  4. "你的歌曲补给站"文本提示,即QLabel
  5. 具体显示音乐,和③实际是⼀样的,不同的是③中⾳乐只有一行,⑤中的音乐有两行,因为页面中元素较多,直接摆到⼀个页面太拥挤,从右侧的滚动条可以看出,整个页面中的元素都放置在QScrollArea中。

仔细分析3发现,里面包含了:
在这里插入图片描述

左右各两个按钮,点击之后中间的图片会左右移动,Qt中未提供类似该种组合控件,因此3实际为自定义控件。

3中按钮之间的元素,由图片和底下的文字组成,当光标放在图⽚上会有上移的动画,因此该元素实际也为自定义控件

推荐页面的布局

在stackedWidget中选中推荐页面,objectName为recommendPage的页面,删掉之前添加的QLabel推荐提示(没添加不用)。

  1. 拖拽⼀个QScrollArea到recPage中,geometry的宽度和⾼度修改为 820 和 500,样式将边框去掉。
  2. 拖拽⼀个QLable,objectName修改为recText,显⽰内容修改为推荐,minimumSize和maximumSize的高度均修改为50,Font大小修改为24
  3. 再拖拽⼀个QLable和Widget,QLable的objectName修改为recMusictext,内容修改为"今日为你推荐",minimumSize和maximumSize的⾼度均修改为30,Font大小修改为18;Widget的objectName修改为recMusicBox
  4. 再拖拽⼀个QLabel和Widget,QLabel的objectName修改为supplyMusicText,内容修改为"你的⾳乐补给站",minimumSize和maximumSize的⾼度均修改为30,Font大小修改为18;Widget的objectName修改为supplyMusicBox。
  5. 最后选中QScrollArea,点击垂直布局。

这样整个recommendPage基本就布局完成
在这里插入图片描述
美化scollArea滚动条

#scrollArea{
	border:none;
}
/* 设置垂直滚动条整体样式 */
QScrollBar:vertical {
    border: none;               /* 无边框 */
    background: #F0F0F0;        /* 滚动条背景色 */
    width: 5px;               /* 垂直滚动条宽度 */
    margin: 0px 0px 0px 0px;  /* 外边距 */
}

/* 垂直滚动条手柄 */
QScrollBar::handle:vertical {
    background: #D8D8D8;       /* 手柄颜色 */
    border-radius: 3px;        /* 圆角半径 */
}

/* 垂直滚动条手柄悬停效果 */
QScrollBar::handle:vertical:hover {
    background: #D3D3D3;       /* 悬停时颜色加深 */
}

/* 垂直滚动条向上/向下按钮 */
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
    border: none;
    background: none;          /* 隐藏上下按钮 */
    height: 0px;               /* 设置高度为0隐藏按钮 */
}

/* 垂直滚动条页面区域(手柄上下空白区域) */
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
    background: none;          /* 页面区域透明 */
}

自定义RecBox

RecBox界⾯布局

  1. 新添加设计师界面,命名为RecBox。geometry的宽⾼修改为:685*440。
  2. 添加三个Widget,objectName依次修改为leftPage、musicContent、rightPage; leftPage 和 rightPage的minimumSize和maximumSize修改宽为30,然后选中RecBox点击⽔平布局。将RecBox的margin和Spacing修改为0
  3. 在upPage和downPage中各拖⼀个按钮,upPage中按钮objectName修改为btUp,minimumSize的⾼度修改为220;downPage中按钮objectName修改为btDown,minimumSize的⾼度修改为220;然后选中upPage和downPage点击⽔平布局。将upPagedownPage和的margin和Spacing修改为0。
  4. 在musicContent中拖两个Widget,objectName依次修改为recListUp和recListDown,然后选中musicContent点击垂直布局,将musicContent的margin和Spacing修改为0。(为了看清楚效果可临时将recListUp背景色设置为:background-color:green; 将recListDown背景色设置为:background-color:red;)
  5. 在recListUp和recListDown中分别拖两个⽔平布局器,依次命名为recListUpHLayout和recListDownHLayout,选中recListUp和recListDown点击⽔平布局,将margin和Spacing修改为0

按钮添加如下QSS美化:

名称qss样式
btUpQPushButton { background-repeat:no-repeat; border:none; background-image : url(:/images/up_page.png); background-position:center center; }QPushButton:hover {background-color: #1ECD97; }
btDownQPushButton { background-repeat:no-repeat; border:none; background-image : url(:/images/down_page.png); background-position:center center; } QPushButton:hover {background-color: #1ECD97; }

将LXMusic主界⾯中recommendPage页面中的recMusicBox和supplyMusicBox提升为RecBox,就能看到如下效果:
在这里插入图片描述

自定义recBoxItem

RecBoxItem界面布局

添加⼀个Designer 界面,命名为RecBoxItem,geometry的宽和⾼设置为:150 * 200。

  1. 拖拽⼀个Widget到RecBoxItem中,objectName修改为musicImageBox,minimumSize和maximumSize的高度均修改为150;
  2. 拖拽⼀个QLabel到Widget中,objectName修改为recBoxItemText,⽂本设置为"推荐-001",QLabel的alignment属性设置为水平、垂直居中。
  3. 拖拽⼀个QLabel到musicImageBox中,objectName修改为recMusicImage, geometry设置为:[(0, 0), 150*150]
  4. 拖拽⼀个QPushButton到musicImageBox中,objectName修改为recMusicBtn,删除掉文本内容。在属性中找到cursor,点击选择⼩⼿图标
#recMusicBtn
{
	border:none;
}

RecBoxItem类中添加动画效果

在RecBoxItem类中拦截鼠标进入和离开事件,在进⼊时让图片上移,在离开时让图片下移回到原位。

// RecBoxItem.h 新增
protected:
	virtual bool eventFilter(QObject *watched, QEvent *event)override;

// RecBoxItem.cpp 新增
#include <QPropertyAnimation>

//拦截鼠标进入和离开时的事件处理
bool RecBoxItem::eventFilter(QObject *watch, QEvent *event)
{
    if(watch == ui->musicImageBox){
        int imageWidth = ui->musicImageBox->width();
        int imageHeight = ui->musicImageBox->height();

        if(event->type() == QEvent::Enter){
            QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox,"geometry");
            animation->setDuration(100);
            animation->setStartValue(QRect(0,10,imageWidth,imageHeight));
            animation->setEndValue(QRect(0,0,imageWidth,imageHeight));
            animation->start();

            //动画结束会发送finished信号,拦截该信号并销毁animation对象
            connect(animation, &QPropertyAnimation::finished, this, [=](){
                delete animation;
            });
            return true;
        }else if(event->type() == QEvent::Leave){
            QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox,"geometry");
            animation->setDuration(150);
            animation->setStartValue(QRect(0,0,imageWidth,imageHeight));
            animation->setEndValue(QRect(0,10,imageWidth,imageHeight));
            animation->start();

            //动画结束会发送finished信号,拦截该信号并销毁animation对象
            connect(animation, &QPropertyAnimation::finished, this, [=](){
                delete animation;
            });
            return true;
        }

    }
    //其他事件交给父类处理
    return QObject::eventFilter(watch, event);
}

注意:不要忘记事件拦截器安装,否则时间拦截不到,因此需要在构造函数中添加:

ui->musicImageBox->installEventFilter(this);

该类中还需要添加设置推荐文本和图片的方法,将来需要在外部来设置每个RecBoxItem的文本和图片:

// RecBoxItem.h 新增
void setText(const QString& text);
void setImage(const QString& imagePath);
// RecBoxItem.cpp 新增
void RecBoxItem::setText(const QString& text)
{
	ui->recBoxItemText->setText(text);
}
void RecBoxItem::setImage(const QString& imagePath)
{
	QString imageStyle = "border-image:url("+imagePath+");";
    ui->recMusicImage->setStyleSheet(imageStyle);
}

RecBox添加RecBoxItem

每个RecBoxItem都有对应的图⽚和推荐文本,在往RecBox中添加RecBoxItem前需要先将图片路径和对应文本准备好。由于图片和文本具有对应关系,可以以键值对方式来进行组织,以下实现的时采用QT内置的QJsonObject对象管理图片路径和文本内容:

QJsonObject类:
头⽂件: <QJsonObject>
// 功能: 插⼊<key, value>键值对,如果key已经存在,则⽤value更新与key对应的value
// 返回值:返回指向新插⼊项的键值对
QJsonObject::iterator insert(const QString &key, const QJsonValue &value);
// 功能:获取与key对应的value
// 返回值:返回的value⽤QJsonValue对象组织
QJsonValue QJsonObject::value(const QString &key) const
QJsonArray类
作⽤:管理的是QJsonValue对象
头⽂件:<QJsonArray>
该类重载了[]运算符,可以通过下标⽅式获取管理的QJsonValue对象
QJsonValue operator[](int i) const
QJsonValueRef operator[](int i)
// 往QJsonArray中插⼊⼀个QJsonValue对象
void append(const QJsonValue &value)

QJsonValue类
// 单参构造⽅法,将QJsonObject对象转换为QJsonValue对象
QJsonValue(const QJsonObject &o)
// 将内部管理的数据转化成QJsonObject返回
QJsonObject toObject() const
// 将内部管理的数据转化成QString返回
QString toString() const

图⽚路径和对应文本的准备⼯作,应该在LXMusic类中处理好,RecBoxItem只负责设置,因此该准备工作需要在LXMusic类中进行,故LXMusic中需要添加如下代码:

// LXMusic.h 新增
// 参数num:RecBox中图⽚个数
QJsonArray RandomPicture();
// LXMusic.cpp中新增
//将图片路径打乱
QJsonArray LXMusic::randomPticture()
{
    //设置随机数种子
    srand(time(0));
    //添加推荐图片名称
    QVector<QString> imageName;
    for(int i = 1; i <= 24; ++i){
        imageName << "00" + QString::number(i) + ".jpg";
    }
    //打乱顺序
    std::random_shuffle(imageName.begin(), imageName.end());

    QJsonArray objArray;
    int n = imageName.size();
    for(int i = 0; i < n; ++i){
        QJsonObject obj;
        obj.insert("imagePath", ":/image/" + imageName[i]);

        // arg(i, 3, 10, QCchar('0'))
        // i:要放⼊%1位置的数据
        // 3: 三位数
        // 10:表⽰⼗进制数
        // QChar('0'):数字不够三位,前⾯⽤字符'0'填充
        QString strText = QString("推荐-%1").arg(i + 1, 3, 10, QChar('0'));
        obj.insert("text", strText);
        objArray.append(obj);
    }

    return objArray;
}

recBox中添加元素

由于recPage页面中有两个RecBox控件,上面的RecBox为一行四列,下方的RecBox为2行四列,因此在RecBox类中增加以下成员变量:

// RecBox.h 新增
#include <QJsonArray>
public:
void initRecBoxUi(QJsonArray data, int row);
void createRecBoxItem();
private:
int row; // 记录当前RecBox实际总⾏数
int col; // 记录当前RecBox实际每⾏有⼏个元素
QJsonArray imageList; // 保存界⾯上的图⽚, ⾥⾯实际为key、value键值对

RecBox的构造函数中,将row和col默认设置为1和4,count需要具体来计算

RecBox::RecBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::RecBox),
row(1),
col(4)
{
ui->setupUi(this);
}
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
    // 如果是两⾏,说明当前RecBox是主界⾯上的supplyMusicBox
    if(2 == row)
    {
        this->row = row;
        this->col = 8;
    }
    else
    {
        // 否则:只有⼀⾏,为主界⾯上recMusicBox
        ui->recBoxBottom->hide();
	}
    // 图⽚保存起来
    imageList = data;
    //向recListUpHlyout添加元素
    createRecBoxItem();
}    
    
void RecBox::createRecBoxItem()
{
    for(int i = 0; i < _col; ++i){
        RecBoxItem* item = new RecBoxItem();
        QJsonObject obj = _imageList[i].toObject();
        item->setText(obj.value("text").toString());
        item->setImage(obj.value("imagePath").toString());
		
        
        if(i >= _col / 2 && _row == 2){
            //添加八个元素
            ui->recListDownHLayout->addWidget(item);
        }
        else{
            //添加四个元素
            ui->recListUpHLayout->addWidget(item);
        }
    }
}

效果:
在这里插入图片描述

RecBox中btUp和btDown按钮clicked处理

添加槽函数

选中recbox.ui⽂件,分别选中btUp和btDown,右键单击弹出菜单选择转到槽,选中clicked确定, btUp和btDown的槽函数就添加好了

imageList中图⽚分组

假设imageList中有24张图片路径和推荐文本信息,如果将信息分组:

  • 如果是recMusicBox,将元素按照col分组,即每4个元素为⼀组,可分为6组;如果是supplyMuscBox,将元素按照col分组,即每8个元素为⼀组,可分为3组。

  • RecBox类中添加currentIndex和count整形成员变量,currentIndex记录当前显⽰组,count记录总的信息组数。当点击btUp时,currentIndex–,显⽰前⼀组,如果currentIndex⼩于0时,将其设置为count-1;当点击btDown按钮时,currentIndex++显⽰下⼀组,当currentIndex为count时,将count设置为0.

这样就实现了轮番显示效果

// recbox.h 中新增
int currentIndex; // 标记当先显⽰第⼏组图⽚和推荐信息
int count; // 标记imageList中元素按照col分组总数

//recbox.cpp
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
    if(row == 2){
        _row = row;
        _col = 8;
    }
    else{
        //隐藏RecBoxDownHlyout
        ui->recListDown->hide();
    }
    _imageList = data;

    //当前在第几组,默认0组
    _currentIndex = 0;
    _count = ceil(data.size() / _col); //算出有多少组

    createRecBoxItem();
}

void RecBox::on_btUp_clicked()
{
    _currentIndex--;
    if(_currentIndex < 0)_currentIndex = 0;

    createRecBoxItem();
}


void RecBox::on_btDown_clicked()
{
    _currentIndex++;
    if(_currentIndex == _count)_currentIndex = 0;

    createRecBoxItem();
}

void RecBox::createRecBoxItem()
{
    //移除之前存在的旧元素
    QList<RecBoxItem*> recUpList = ui->recListUp->findChildren<RecBoxItem*>();
    for(auto e : recUpList){
        ui->recListUpHLayout->removeWidget(e);
        delete e;
    }

    QList<RecBoxItem*> recDownList = ui->recListDown->findChildren<RecBoxItem*>();
    for(auto e : recDownList){
        ui->recListDownHLayout->removeWidget(e);
        delete e;
    }

    int index = 0;
    for(int i = _currentIndex * _col; i < _col + _currentIndex * _col; ++i){
        RecBoxItem* item = new RecBoxItem();
        QJsonObject obj = _imageList[i].toObject();
        item->setText(obj.value("text").toString());
        item->setImage(obj.value("imagePath").toString());



        if(index >= _col / 2 && _row == 2){
            ui->recListDownHLayout->addWidget(item);
        }
        else{
            ui->recListUpHLayout->addWidget(item);
        }
        ++index;
    }
}

效果图:
在这里插入图片描述

这样实现每次启动程序,推荐页面推荐的元素都是一样的,所以可以在lxmusic.h文件中的LXMusic类的构造函数中加上随机数种子:

srand(time(0));

3.6 自定义CommonPage

CommonPage页面布局

  1. 新增加⼀个设计界面,objectName修改为CommonPage,geometry的宽高修改为800*500

  2. 拖拽⼀个QLabel、两个Widget和⼀个List View控件到CommonPage中,并将List View变形为QLIstWidget,objectName从上往下依次修改为pageTittle、musicPlayBox、listLabelBox、pageMusicList,然后选中CommonPage点击垂直布局,将CommonPage的margin和Spacing修改为0

  3. pageTittle的minimumSize和maximumSize的⾼度修改为30。

  4. musicPlayBox的minimumSize和maximumSize的⾼度修改为150。

  5. listLabelBox的minimumSize和maximumSize的⾼度修改为40

  6. musicPlayBox中拖拽⼀个QLabel,objectName修改为musicImageLabel,minimumSize和maximumSize的宽度修改为150 ,拖拽⼀个Widget,objectName修改为playAll,minimumSize和maximumSize的宽度修改为120,在其内部拖拽 ⼀个PushButton和Vertical Space(即垂直弹簧),将按钮的objectName修改为playAllBtn,minimumSize和 maximumSize的宽和⾼修改为100*30,文本内容修改为"播放全部",然后选中playAll点击垂直布局,拖拽⼀个Horizontal Spacer到CommonPage中,放在playAll之后。然后选中musicPlayBox,点击水平布局,将margin和spacing设置为0.

  7. listLabelBox中拖拽三个QLabel,内容依次修改为:歌曲名称、歌手名称、专辑名称,objectName从左往右依次修改为:musicNameLabel、musicSingerLabel、musicAlbumLabel然后选中musicPlayBox,点击水平布局,将margin和spacing设置为0

界面美化

//给下面控件设置qss样式
//musicAlbumLabel,musicNameLabel,musicSingerLabel
QLabel {
    text-align: left;     /* 文字左对齐 */
    padding-left: 40px;  /* 文字向右偏移 10px */
    padding-right: 0;    /* 右侧无内边距 */
	font-size: 10px;      /* 设置字体大小为 16px */
    font-weight: bold;    /* 设置字体加粗 */
}
//pageTittle
QLabel {
    text-align: left;     /* 文字左对齐 */
    padding-left: 25px;  /* 文字向右偏移 10px */
    padding-right: 0;    /* 右侧无内边距 */
	font-size: 30px;      /* 设置字体大小为 16px */
    font-weight: bold;    /* 设置字体加粗 */
}
//playAllBtn
#playAllBtn
{
background-color:#E3E3E3;
border-radius:10px;
}
#playAllBtn:hover
{
background-color:#1ECD97;
}
//pageMusicList
#pageMusicList{border:none;}

CommonPage界⾯设置和显⽰

CommonPage页面是我喜欢、本地下载、最近播放三个界面的共同类型,因此该类需要提供设置: pageTittle 和 musicImageLabel的公共方法,将来在程序启动时完成三个界面信息的设置,因此CommonPage类需要添加⼀个public的setCommonPageUI函数

// commonpage.h 中新增
public:
void setCommonPageUi(const QString &title, const QString &image);
// commonpage.cpp 中新增
void CommonPage::setCommonPageUi(const QString &title, const QString &image)
{
    //设置标题
    ui->pageTittle->setText(title);

    //设置图片
    ui->musicImageLabel->setPixmap(QPixmap(image));

    //是图片自动缩放
    ui->musicImageLabel->setScaledContents(true);
}

界面设置的函数需要在程序启动时就完成好配置,即需要在LXMusic的initUi()函数中调⽤完成设置:

3.7 自定义ListItemBox

ListItemBox页面分析

CommonPage页面创建好之后,等音乐加载到程序之后,就可以将音乐信息往CommonPage的pageMusicList中显⽰了

图片代谢:

上图每行都是QListWidget中的⼀个元素,每个元素中包含多个控件:

  1. 收藏图标,即QLabel
  2. 歌曲名称,即QLabel
  3. VIP和SQ,VIP即收费会员专享,SQ为无损⾳乐,也是两个QLabel
  4. 歌手名称,即QLabel
  5. 音乐专辑名称,即QLabel

此处,需要将上述所有QLabel组合在⼀起,作为⼀个独立的控件,添加到QListWidget中,因此该控件也需要自定义

ListItemBox页面布局

在这里插入图片描述

添加⼀个设计师界面,objectName为ListItemBox,geometry的宽度和高度修改为800*45

  1. 拖三个Widget到ListItemBox中,objectName从左往右依次修改为musicNameBox、musicSingerBox、 musicAlbumBox,将musicNameBox的minimumSize和maximumSize的宽修改为380,将musicSingerBox的minimumSize和maximumSize的宽修改为200,然后选中ListItemBox,点击水平布局,将ListItemBox的margin和spacing修改为0
  2. musicNameBox: 拖拽⼀个QPushButton到musicNameBox中,objectName修改为likeBtn,minimumSize和maximumSize的宽高修改为25*25。 拖⼀个QLabel到musicNameBox中,objectName修改为musicNameLabel,minimumSize和的宽修改为130. 拖⼀个QLabel到musicNameBox中,objectName修改为VIPLabel,minimumSize和maximumSize的宽修改为30,maximumSize高度修改为15,文本内容修改为VIP。 拖⼀个QLabel到musicNameBox中,objectName修改为SQLabel,minimumSize和maximumSize的宽修改为25,maximumSize高度修改为15,文本内容修改为SQ。 拖拽⼀个水平弹簧控件到musicNameBox中,将上述控件撑到musicNameBox的左侧 选中musicNameBox,点击水平布局,将musicNameBox的margin和spacing修改为0
  3. 拖拽⼀个QLabel到musicSingerBox中,objectName修改为musicSingerLabel,然后选中musicSingerBox点击水平布局,将musicSingerBox的margin和spacing修改为0
  4. 拖拽⼀个QLabel到albumBox中,objectName修改为albumNameLabel,然后选中albumBox点击水平布局,将musicSingerBox的margin和spacing修改为0

界面美化

#likeBtn
{
border:none;
}

#VIPLabel
{
border:1px solid #1ECD96;
color:#1ECD96;
border-radius:2px;
}

#SQLabel
{
border:1px solid #FF6600;
color:#FF6600;
border-radius:2px;
}

ListItemBox显⽰测试

ListItemBox将来要添加到CommonPage页面中的QListWidget中,因此在CommonPage类的初始化方法中添加如下代码:

void CommonPage::setCommonPageUi(const QString &title, const QString &image)
{
    //设置标题
    ui->pageTittle->setText(title);

    //设置图片
    ui->musicImageLabel->setPixmap(QPixmap(image));

    //是图片自动缩放
    ui->musicImageLabel->setScaledContents(true);

    // 测试
    ListItemBox* listItemBox = new ListItemBox(this);
    QListWidgetItem* listWidgetItem = new QListWidgetItem(ui->pageMusicList);
    listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(), 45));
    ui->pageMusicList->setItemWidget(listWidgetItem, listItemBox);
}

⽀持hover效果

ListItemBox添加到CommonPage中的QListWidget之后,自带hover效果,但是背景颜色和界面不太搭配,此处重新实现hover效果,此处重写enterEvent和leaveEvent来实现hover效果

//listitembox.h
protected:
    virtual void enterEvent(QEnterEvent* event)override;
    virtual void leaveEvent(QEvent*event)override;
//listitembox.cpp
//鼠标离开,取消背景色
void ListItemBox::leaveEvent(QEvent *event)
{
    (void)event;
    setStyleSheet("");
}

//鼠标进入,修改背景色
void ListItemBox::enterEvent(QEnterEvent *event)
{
    (void)event;
    setStyleSheet("background-color:#EFEFEF");

}

3.8 ⾃定义MusicSlider

由于QT内置的Horizontal Slider(⽔平滑竿)不是很好看,该控件也采用自定义。 该控件比较简单,实际就是两个QFrame嵌套起来的

  1. 添加⼀个设计师界面,objectName修改为MusicSlider,geometry修改为800*20。 *
  2. 拖拽⼀个QFrame,objectName修改为inLine,geometry修改为[(0,8), 8004]。
  3. 拖拽⼀个QFrame,objectName修改为outLine,geometry修改为[(0,8), 400*4]。
  4. 选中MusicSlider,点击水平布局。

inLine和outLine的样式设置如下:

#inLine
{
background-color:#EBEEF5;
}
#outLine
{
background-color:#1ECC94;
}

打开LXMusic.ui,选中progressBar清除之前样式,将progressBar提升为MusicSlider,运行程序就能看到效果

3.9 自定义volumeTool

VolumeTool控件分析

音量调节控件本来也可以使⽤Qt内置的垂直滑杆来代替,只是垂直滑杆不好看,因此也自定义

在这里插入图片描述

  1. 内部为类似MusicSlider控件+小圆球,圆球实际为⼀个QPushButton
  2. 音量大小文本显示,实际为QLabel
  3. QPushButton,点击之后在静音和取消静音切换
  4. ⼀个倒三角,Qt未提供三⻆控件,该控件需要手动绘制,用来提示是播放控制区那个按钮按下的

VolumeTool界⾯布局

  1. 生成⼀个QT设计师界面,objectName命名为VolumeTool,geometry的宽⾼修改为100*350。
  2. 拖拽⼀个Widget到VolumeTool中,objectName修改为volumeWidget,geometry修改为:[(10,10), 80*300]
  3. *拖拽⼀个QPushButton到volumeWidget,objectName修改为silenceBtn,mimimumSize和maximumSize的宽高修改为80,45
  4. 拖拽⼀个QLabel到volumeWidget,objectName修改为volumeRatio,mimimumSize和maximumSize的高修改为30,QLabel的alignment属性修改为水平和垂直居中。
  5. 拖拽⼀个QWidget到volumeWidget,objectName修改为sliderBox。geometry修改为:[(0,0), 80*225]
  6. sliderBox内部: 拖拽⼀个QFrame,objectName修改为inSlider,geometry修改为[(38, 25), 4,180]。拖拽⼀个QFrame,objectName修改为outSlider,geometry修改为[(38, 25), 4,180]。 拖拽⼀个QPushButton,objectName修改为sliderBtn,geometry修改为[(33, 20), 14,14], mimimumSize和maximumSize的宽高14*14

美化界面

#volumeWidget{
background-color:#ffffff;
border-radius:5px;
}

#silenceBtn
{
border:none;
}
#silenceBtn:hover{
background-color:#F0F0F0;
}

#inSlider
{
background-color:#ECECEC;
}

#outSlider
{
background-color:#1ECC94;
}

#sliderBtn
{
background-color:#1ECC94;
border-radius:7px;
}

注意:静音底下的空缺用来绘制三角

界⾯设置

该控件属于弹出窗口,即点击了主界面的音量调节按钮后,才需要弹出该界面,点击其他位置该界面自动隐藏。因此在窗口创建时,需要设置窗口为无边框以及为弹出窗口

//volumetool.cpp中
//在构造函数中添加如下内容:
VolumeTool::VolumeTool(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::VolumeTool)
{
    ui->setupUi(this);

    // 在windows上,设置透明效果后,窗⼝需要加上Qt::FramelessWindowHint格式
    // 否则没有控件位置的背景是⿊⾊的
    // 由于默认窗⼝有阴影,因此还需要将窗⼝的原有的阴影去掉,窗⼝需要加上Qt::NoDropShadowWindowHint
    this->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);

    //将窗口设为透明
    this->setAttribute(Qt::WA_TranslucentBackground);

    //给窗口设置自定义阴影
    QGraphicsDropShadowEffect* se = new QGraphicsDropShadowEffect(this);
    se->setBlurRadius(10);
    se->setOffset(0,0);
    se->setColor("#64646464");
    this->setGraphicsEffect(se);

    //设置图片
    ui->silenceBtn->setIcon(QIcon(":/images/sound.png"));
    // ⾳量的默认⼤⼩是20
    ui->outSlider->setGeometry(ui->outSlider->x(), 180 - 36 - 25, ui->outSlider->width(), 20);
    ui->sliderBtn->move(ui->sliderBtn->x(), ui->outSlider->y() - ui->sliderBtn->height()/2);
    ui->volumeRatio->setText("20%");

}

音量调节属于主界面上元素,因此在LXMusic类中需要添加VolumeTool的对象,在initUi中new该类的对象。

主界面中音量调节按钮添加clicked槽函数

//lxmusic.h文件中
private:
	VolumeTool* _volumetool;
//lxmusic.cpp中
//构造函数中创建VolumeTooll类对象
_volumetool = new VolumeTool();

//点击volumeButton后调用的槽函数
void LXMusic::on_volumeButton_clicked()
{
    //获取volumeButton左上角的坐标
    QPoint point = ui->volumeButton->mapToGlobal(QPoint(0,0));

    //计算volume窗⼝的左上⻆位置
    // 让该窗⼝显⽰在⿏标点击的正上⽅
    // ⿏标位置:减去窗⼝宽度的⼀半,以及⾼度恰巧就是窗⼝的左上⻆
    QPoint volumeLeftTop = point - QPoint(_volumetool->width()/2, _volumetool->height());

    // 微调窗⼝位置
    volumeLeftTop.setY(volumeLeftTop.y()+30);
    volumeLeftTop.setX(volumeLeftTop.x()+15);

    // 3. 移动窗⼝位置
    _volumetool->move(volumeLeftTop);

    _volumetool->show();
}

绘制三⻆

由于Qt中并未给出三角控件,因此三角需要手动绘制,故在VolumeTool类中重写paintEvent事件函数

// volumetool.h中新增
virtual void paintEvent(QPaintEvent* event)override;
// volumetool.cpp中新增
void VolumeTool::paintEvent(QPaintEvent *event)
{
    (void)event;
    // 1. 创建绘图对象
    QPainter painter(this);
    // 2. 设置抗锯⻮
    painter.setRenderHint(QPainter::Antialiasing, true);
    // 3. 设置画笔
    // 没有画笔时:画出来的图形就没有边框和轮廓线
    painter.setPen(Qt::NoPen);
    // 4. 设置画刷颜⾊
    painter.setBrush(QBrush(Qt::white));
    // 创建⼀个三⻆形
    QPolygon polygon;
    polygon.append(QPoint(30, 320));
    polygon.append(QPoint(70, 320));
    polygon.append(QPoint(50, 340));
    // 绘制三⻆形
    painter.drawPolygon(polygon);
}

3.10 更换主窗口图标

更换窗口图标,在主界面显示时,在标题栏显示设置的图标

//lxmusic.cpp
void LXMusic::initUi(){
    ........
//更换主窗口图标
this->setWindowIcon(QIcon(":/images/logo.png"));
    ............
}

3.11 处理最小化和最大化按钮

由于窗口中控件并非全部基于Widget布局,有些控件的位置是计算死得,窗口最大化时有些控件可能无法适配尺寸,因此禁止窗口最大化

//lxmusic.h
private slots:
 void on_minButton_clicked();
 void on_skinButton_clicked();
//lxmusic.cpp
//最小化
void LXMusic::on_minButton_clicked()
{
    showMinimized();
}
//换肤图标
void LXMusic::on_skinButton_clicked()
{
    QMessageBox::information(this, "温馨提⽰", "正在加班紧急⽀持中...");
}
void LXMusic::initUi(){
    ........
	//禁用最大化图标
    ui->maxButton->setEnabled(false);
    ............
}

四、音乐管理

界⾯处理好之后,现在就需要将⾳乐⽂件加载到程序然后显示在界面上,待后续播放操作

4.1 音乐加载

lxmusic类中给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)
    
// 设置打开对话框的类型
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();

当点击加号的时候,弹出选择文件的窗口:

void LXMusic::on_addLocalButton_clicked()
{
    //创建QFileDialog类对象
    QFileDialog fileDialog(this);
    fileDialog.setWindowTitle("添加本地音乐");

    //设置为打开窗口
    fileDialog.setAcceptMode(QFileDialog::AcceptOpen);

    //设置对话框模式
    //设置该窗口为打开可选择一个或多个文件的窗口
    fileDialog.setFileMode(QFileDialog::ExistingFiles);

    //设置窗口过滤器
    QStringList mimeList;
    mimeList << "application/octet-stream";
    fileDialog.setMimeTypeFilters(mimeList);

    //设置对话框默认的打开路径,设置⽬录为当前⼯程所在⽬录
    QDir dir(QDir::currentPath());
    dir.cdUp();
    dir.cdUp();
    QString path = dir.path() + "/music";
    fileDialog.setDirectory(path);

    if(QFileDialog::Accepted == fileDialog.exec()){
        //获取btform类型的孩子
        QList<BtForm*> btFormList = this->findChildren<BtForm*>();
        for(auto btForm : btFormList){
            if(btForm->getPageId() != 5){
                btForm->clearBackground();
                btForm->hideAnimation();
            }
            else{
                btForm->showAnimation();
                btForm->setBackground();
            }
        }

        //修改stackPage页面
        ui->stackedWidget->setCurrentIndex(5);

        // 获取对话框的返回值
        QList<QUrl> urls = fileDialog.selectedUrls();
        //获取到的音乐后续处理
    }
}

4.2 Music类

将来添加到播放器中的音乐比较多,可借助⼀个类对所有的音乐进行管理。添加新C++类与添加设计师界⾯类似

每首音乐文件,将来需要获取其内部的歌曲名称、歌手、音乐专辑、歌曲时长等信息,因此在MusicList类中,将所有的歌曲文件以Music对象方式管理起来

//music.h
public:
    Music();

public:
    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.cpp
Music::Music():_isLike(false),_isHistory(false){}

Music::Music(const QUrl &url)
    :_isLike(false)
    ,_isHistory(false)
    ,_musicUrl(url)
{
    _musicId = QUuid::createUuid().toString();
}

void Music::setIsLike(bool isLike)
{
    _isLike = isLike;
}

void Music::setIsHistory(bool isHistory)
{
    _isHistory = isHistory;
}

void Music::setMusicName(const QString &musicName)
{
    _musicName = musicName;
}

void Music::setSingerName(const QString &singerName)
{
    _singerName = singerName;
}

void Music::setAlbumName(const QString &albumName)
{
    _albumName = albumName;
}

void Music::setDuration(const qint64 duration)
{
    _duration = duration;
}

void Music::setMusicUrl(const QUrl &url)
{
    _musicUrl = url;
}

void Music::setMusicId(const QString &musicId)
{
    _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 musicList类的实现

LXMusic中,通过QFileDialog将⼀组音乐文件的url获取到之后,可以交给MusicList类来管理。 但是LXMusic加载的二进制文件不一定全部都是音乐文件,因此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格式的歌曲文件
  • audio/mp3:表示mp3格式的歌曲文件

上述歌曲文件格式,Qt的QMediaPlayer类都是支持的

//musiclist.h
typedef typename QVector<Music>::iterator iterator;
public:
    MusicList();

public:
    void addMusicByUrl(const QList<QUrl>&url);
	iterator begin();
    iterator end();


private:
    QVector<Music> _musicList;
//musiclist.cpp
MusicList::MusicList() {}
//让musiclist类能够支持范围for的使用
iterator MusicList::begin()
{
    return _musicList.begin();
}

iterator MusicList::end()
{
    return _musicList.end();
}

//通过音乐路径构造music对象,然后添加到musiclist中管理
void MusicList::addMusicByUrl(const QList<QUrl> &url)
{
    for(const auto& e : url){
        QMimeDatabase mimedata;
        QMimeType mime = mimedata.mimeTypeForFile(e.toLocalFile());
        if(mime.name() != "audio/mpeg" && mime.name() != "audio/flac" && mime.name() != "audio/mp3")
        {
            continue;
        }

        // 如果是⾳乐⽂件,加⼊歌曲列表
        _musicList.push_back(e);
    }
}


4.4 解析音乐文件元数据

对于每首歌曲,将来在界面上需要显示出:歌曲名称、歌手、专辑名称,在播放时还需要拿到歌曲总时长,因此在构造音乐对象时,就需要将上述信息解析出来。歌曲元数据解析,需要⽤到 QMediaPlayer ,该类也是用来进行歌曲播放的类,后续在播放音乐位置详细介绍

QMediaPlayer类中的setSource()函数

// 功能:设置要播放的媒体源,媒体数据从中读取
// media: 要播放的媒体内容,⽐如⼀个视频或⾳频⽂件,该类提供了⼀个QUrl格式的单参构造
void setSource(const QUrl& url);

注意:该函数执行后立即返回,不会等待媒体加载完成,也不检查错误,如果在媒体加载时发生错误,会触发mediaStatusChanged和error信号 ,由于加载媒体文件需要时间,可以通过QMediaPlayer::metaDataChanged信号检测媒体数据是否就绪,媒体元数据加载成功之后,可以通过metaData函数获取指定的媒体数据

//获取音乐名称
_musicName = player.metaData().value(QMediaMetaData::Title).toString();

本⽂需要获取媒体的:标题、作者、专辑、持续时长,以下是需要遵守的传入名称

valye(传入的参数)description(说明)type(返回类型)
QMediaMetaData::Title媒体的标题QString
QMediaMetaData::Author媒体的作者QStringList
QMediaMetaData::AlbumTitle媒体所属专辑名称QString
QMediaMetaData::Duration媒体的播放时长qint64

注意:有些媒体中媒体数据可能不全,即有些媒体数据获取不到,比如盗版歌曲

使用QMediaPlayer媒体播放类时,需要在LXMusic.pro项目工程文件中添加媒体模块multimedia ,该模块主要用来播放各种音频视频文件等.

QT       += core gui multimedia

添加完成之后,重新将项⽬构建⼀下,否则Qt create可能识别不过来

对音乐文件进行解析:

// music.h 中新增
#include<QUrl>
#include<QMediaPlayer>
#include <QMediaMetaData>
private:
    void parseMediaMetaData();
// music.cpp 中新增
//解析元媒体的数据信息,保存到music类中
void Music::parseMediaMetaData()
{

    QMediaPlayer player;
    //qDebug() << "nihao";

    // 媒体元数据解析需要时间,只有等待解析完成之后,才能提取⾳乐信息
    // 当元数据加载完成以后,会发送metadataChanged信号,此时就可以处理我们需要的内容了
    // 连接元数据变化信号
    QObject::connect(&player, &QMediaPlayer::metaDataChanged, [&]() {
        // 提取元数据
        _musicName = player.metaData().value(QMediaMetaData::Title).toString();
        _singerName = player.metaData().value(QMediaMetaData::Author).toString();
        _albumName = player.metaData().value(QMediaMetaData::AlbumTitle).toString();
        _duration = player.duration();

        QString fileName =  _musicUrl.toString();
        int i = fileName.size() - 1;
        for(; i >= 0; --i){
            if(fileName[i] == '/')break;
        }

        int index = fileName.indexOf("-");
        if(_musicName.isEmpty())
        {
            if(index != -1)
            {
                // "动物世界 - 薛之谦.mp3"
                _musicName = fileName.mid(i + 1, index - i - 1).trimmed();
            }
            else
            {
                // "动物世界.mp3"
                _musicName = fileName.mid(i + 1, fileName.indexOf('.') - i - 1).trimmed();
            }
        }
        // 作者为空
        if(_singerName.isEmpty())
        {
            if(index != -1)
            {
                _singerName = fileName.mid(index+1, fileName.indexOf('.')-index-1).trimmed();
            }
            else
            {
                _singerName = "未知歌⼿";
            }
        }
        if(_albumName.isEmpty()){_albumName = "未知专辑";}

        // 打印结果
        qDebug() << "歌曲名称:" << (_musicName.isEmpty() ? "未知" : _musicName);
        qDebug() << "歌手:" << (_singerName.isEmpty() ? "未知" : _singerName);
        qDebug() << "专辑:" << (_albumName.isEmpty() ? "未知" : _albumName);
        qDebug() << "时长:" << _duration << "毫秒";
    });
    
    // 解析时候需要读取歌曲数据,读取歌曲⽂件需要⽤到QMediaPlayer类
    //加载歌曲文件
    player.setSource(_musicUrl);
}

4.5 音乐分类

QQMusic中,有三个显示歌曲信息的页面:

  • likePage:管理和显示点击小心心后收藏的歌曲

  • localPage:管理和显示本地加载的歌曲

  • recentPage:管理和显示历史播放过的歌曲

这三个页面的类型都是CommonPage,每个页面应该维护自己页面中的歌曲。因此CommonPage类中需要新增:

// commonpage.h中新增
// 区分不同page⻚⾯
enum PageType{
    LIKEPAGE,
    RECENTPAGE,
    LOCALPAGE
};
// 新增成员函数
public:
void setMusicListType(PageType pageType);
private:
//歌单列表
QVector<QString> musicOfPage; //存放音乐的数组
PageType _pageType;//标记属于哪个页面

//commonpage.cpp
void CommonPage::setMusicListType(PageType pageType)
{
    _pageType = pageType;
}

//lxmusic.cpp
void initUi()
{
    .......
//初始化commandpage界面
    ui->likeMusicPage->setCommonPageUi("我喜欢的音乐",":/images/likemusic.jpg");
    ui->likeMusicPage->setMusicListType(PageType::LIKEPAGE);
    ui->recentMusicPage->setCommonPageUi("最近播放",":/images/recentmusic.jpg");
    ui->recentMusicPage->setMusicListType(PageType::RECENTPAGE);
    ui->localMusicPage->setCommonPageUi("本地下载",":/images/localmusic.jpg");
    ui->localMusicPage->setMusicListType(PageType::LOCALPAGE);
}

QQMusic中,点击addLocal(本地加载)按钮后,会通过其musicList成员变量,将music添加到musicList中管理,在添加过程中,每个歌曲会对应⼀个Music对象,Music对象在构造时,会完成歌曲文件的加载,顺便完成歌曲名称、作者、专辑名称等元数据的解析。⼀切准备就绪之后,每个CommonPage页面,通过LXMusic的musicList分离出自己页面的歌曲,保存在musicOfPage中

//commonPage.h
void addMusicToMusicPage(MusicList& musicList);
//commonPage.cpp
//将musicList中的音乐分别保存在对应的Page中
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
    //删除存在的旧数据
    _musicOfPage.clear();
    ui->pageMusicList->clear();

    for(auto& music : musicList){
        switch(_pageType){
            case PageType::LIKEPAGE:
            {
                if(music.getIsLike()){
                    _musicOfPage.push_back(music.getMusicId());
                }
                break;
            }
            case PageType::RECENTPAGE:
            {
                if(music.getIsHistory()){
                    _musicOfPage.push_back(music.getMusicId());
                }
                break;
            }
            case PageType::LOCALPAGE:
            {
                _musicOfPage.push_back(music.getMusicId());
                break;
            }
            default:
            {
                qDebug() << "功能暂未支持";
            }

        }
    }
}

4.6 Music数据保存并显示到界面中

通过QFileDialog将⾳乐从本地磁盘加载到程序中后,拿到的是所有音乐文件的QUrl,而在程序中需要的是经过元数据解析之后的Music对象,并且Music对象需要管理起来,此时就可以采用MusicList类对解析之后的Music对象进行管理,LXMusic类中只需要保存MusicList的对象,就可以让lxmusic.ui界⾯中CommonPage对象完成Music信息往界面更新

//lxmusic.h
void LXMusic::on_addLocalButton_clicked()
{
    .....
    //将音乐保存到musiclist中
    _musicList.addMusicByUrl(urls);

    //将添加的音乐显示在localPage中
    ui->localMusicPage->refreshPage(_musicList);
}

refreshPage函数的实现步骤:

  1. 调用addMusicIdPageFromMusicList函数,从musicList中添加当前页面的歌曲
  2. 遍历musicListOfPage,拿到每首音乐后先检查其是否在,存在则添加
  3. 界面上需要更新每首歌曲的:歌曲名称、作者、专辑名称,而commonPage中只保存了歌曲的musicId,因此需要在MusicList中增加通过musicID查找Music对象的方法
// commonpage.h中新增
void refreshPage(MusicList& musicList);
//commonPage.cpp中新增
void CommonPage::refreshPage(MusicList &musicList)
{
    addMusicToMusicPage(musicList);

    for(auto& musicId : _musicOfPage){
        auto it = musicList.findMusicById(musicId);
        if(it == musicList.end())continue;

        //初始化ListItemBox对象
        ListItemBox * listItemBox = new ListItemBox(ui->pageMusicList);
        listItemBox->setMusicName(it->getMusicName());
        listItemBox->setSingerName(it->getSingerName());
        listItemBox->setAblumName(it->getAlbumName());

        //设置QListWigetItem对象的宽和高
        QListWidgetItem* item = new QListWidgetItem(ui->pageMusicList);
        item->setSizeHint(QSize(ui->pageMusicList->width(),45));

        //添加到QListWiget中
        ui->pageMusicList->setItemWidget(item,listItemBox);
    }

    // 更新完成后刷新下界⾯
    repaint();
}

//listitembox.h
public:
    void setMusicName(const QString& MusicName);
    void setSingerName(const QString& SingerName);
    void setAblumName(const QString& AblumName);
void ListItemBox::setMusicName(const QString &MusicName)
{
    ui->musicNameLabel->setText(MusicName);
}

void ListItemBox::setSingerName(const QString &SingerName)
{
    ui->musicSingerLabel->setText(SingerName);
}

void ListItemBox::setAblumName(const QString &AblumName)
{
    ui->albumNameLabel->setText(AblumName);
}

//musiclist.h
iterator findMusicById(QString& musicId);
//musiclist.cpp
iterator MusicList::findMusicById(QString &musicId)
{
    iterator it = _musicList.begin();
    while(it != _musicList.end()){
        if(it->getMusicId() == musicId){
            break;
        }
        ++it;
    }
    return it;
}

4.7 优化QListWidget

移除QListWidget的水平滚动条

//commonpage.cpp
CommonPage::CommonPage(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::CommonPage)
{
    ui->setupUi(this);

    // 不要⽔平滚动条
    ui->pageMusicList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}

美化垂直滚动条

#pageMusicList{
	border:none;
}
/* 设置垂直滚动条整体样式 */
QScrollBar:vertical {
    border: none;               /* 无边框 */
    background: #F0F0F0;        /* 滚动条背景色 */
    width: 5px;               /* 垂直滚动条宽度 */
    margin: 0px 0px 0px 0px;  /* 外边距 */
}

/* 垂直滚动条手柄 */
QScrollBar::handle:vertical {
    background: #D8D8D8;       /* 手柄颜色 */
    border-radius: 3px;        /* 圆角半径 */
}

/* 垂直滚动条手柄悬停效果 */
QScrollBar::handle:vertical:hover {
    background: #D3D3D3;       /* 悬停时颜色加深 */
}

/* 垂直滚动条向上/向下按钮 */
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
    border: none;
    background: none;          /* 隐藏上下按钮 */
    height: 0px;               /* 设置高度为0隐藏按钮 */
}

/* 垂直滚动条页面区域(手柄上下空白区域) */
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
    background: none;          /* 页面区域透明 */
}

4.8 音乐收藏

  1. 更新小心心图标

  2. 更新Music的我喜欢属性,但ListItemBox并没有歌曲数据,所以只能发射信号,让其父元素CommonPage来处理

当CommonPage往界面更新Music信息时,也要根据Music的isLike属性更新对应的图标。因此ListItemBox需要根据 当点击我喜欢按钮之后,要切换ListItemBox中的小心心。因此ListItemBox中添加设置bool类型isLike成员变量,以及setIsLike函数,在CommonPage添加Music信息到界⾯时,要能够设置小心心图片

//listitembox.h
private slots:
    void onLikeBtnCliked();
public:
	void setIcon(bool isLike);
signals:
    void setIsLike(bool isLike);
//listitembox.cpp
void ListItemBox::onLikeBtnCliked()
{
    //设置为相反的状态
    _isLike = !_isLike;
    
    //发送信号
    emit setIsLike(_isLike);
}
//设置图标
void ListItemBox::setIcon(bool isLike)
{
    _isLike = isLike;
    if(isLike){
        ui->likeBtn->setIcon(QIcon(":/images/love2.png"));

    }
    else{
        ui->likeBtn->setIcon(QIcon(":/images/love.png"));
    }
}
//构造函数中添加
connect(ui->likeBtn, &QPushButton::clicked,this,&ListItemBox::onLikeBtnCliked);

CommonPage在往QListWidget中添加元素时,会创建⼀个个ListItemBox对象,每个对象将来都可能会发射setIsLike信号,因此在将ListItemBox添加完之后,CommonPage应该关联先该信号,将需要更新的的Music信息以及是否喜欢,同步给LXMusic

//commonpage.h
signals:
    void updateLikeMusic(bool isLike, QString musicId);
//commonpage.cpp
void CommonPage::refreshPage(MusicList &musicList)
{
    .....
    //初始化ListItemBox对象
    ListItemBox * listItemBox = new ListItemBox(ui->pageMusicList);
    listItemBox->setMusicName(it->getMusicName());
    listItemBox->setSingerName(it->getSingerName());
    listItemBox->setAblumName(it->getAlbumName());
    listItemBox->setIcon(it->getIsLike());

    //绑定信号与槽
    connect(listItemBox, &ListItemBox::setIsLike, this, [=](bool isLike){
        emit updateLikeMusic(isLike, it->getMusicId());
    });
    .......
}

LXMusic收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、recentPage更新其界面的我喜欢歌曲信息

//lxmusic.h
private slots:
void onUpdateLikeMusic(bool isLike, QString musicId);
//lxmusic.cpp
void LXMusic::onUpdateLikeMusic(bool isLike, QString musicId)
{
    qDebug() << "22";
    // 1. 找到该⾸歌曲,并更新对应Music对象信息
    auto it = _musicList.findMusicById(musicId);
    if(it != _musicList.end())
    {
        it->setIsLike(isLike);
    }

    // 2. 通知三个⻚⾯更新⾃⼰的数据
    ui->likeMusicPage->refreshPage(_musicList);
    ui->localMusicPage->refreshPage(_musicList);
    ui->recentMusicPage->refreshPage(_musicList);
}

void LXMusic::connectSignalAndSlots()
{
    ....
    connect(ui->likeMusicPage,&CommonPage::updateLikeMusic, this,&LXMusic::onUpdateLikeMusic);
    connect(ui->localMusicPage,&CommonPage::updateLikeMusic, this,&LXMusic::onUpdateLikeMusic);
    connect(ui->recentMusicPage,&CommonPage::updateLikeMusic, this,&LXMusic::onUpdateLikeMusic);
    ....
}

五、音乐播放控制

歌曲已经添加到程序并完成解析,解析的信息也更新到界面了,所有前置⼯作基本完成,接下来重点处理音乐播放,歌曲播放需要用到Qt提供的QMediaPlayer类和QMediaPlaylist类

QMediaPlayer类

QMediaPlayer是Qt框架中用于支持各种音频和视频的播放,流媒体的播放,各种播放模式(单曲播放、列表播放、循环播放等),各种播放模式(播放、暂停、停止等),信号槽机制可以让用户在播放状态改变时进行所需控制,使用时需要包含 #include 头⽂件,并且需要在.pro项目文件中添加媒体库,即: QT += multimedia ,将 multimedia 模块导入到工程中,就可以使⽤该模块中提供的媒体播放控制的相关类,比如:QMediaPlayer、QMediaPlayList等

常用属性:

// 部分常⽤属性
const qint64 duration; // 保存媒体的总播放时间,单位为毫秒
const QMediaContent currentMedia; // 当前正在播放媒体的媒体内容
const QString error; // 最近⼀次错误信息
int volume; // 保存⾳量⼤⼩,范围在0~100之间
const bool audioAvailable; // ⾳频是否可⽤,audioAvailableChanged
信号⽤于监听其状态
QMediaPlaylist* playtlist; // 播放列表

常用函数:

qint64 duration() const; // 获取当前媒体的总时间
qint64 position() const; // 获取当前媒体的播放位置
int volume() const; // 获取播放⾳量⼤⼩
bool isMuted() const; // 检测是否静⾳
State state() const; // 获取当前媒体的播放状态
QMediaContent currentMedia() const; // 获取当前正在播放的媒体内容
QString errorString() const // 获取最近的⼀次错误

常用槽函数:

void pause(); // 播放媒体
void play(); // 暂停媒体
void stop(); // 停⽌播放媒体
void setMuted(bool muted); // 设置是否静⾳,true为静⾳,false为⾮静⾳
void setVolume(int volume); // 设置播放⾳量,volume取值范围在0~100之间
void setPosition(qint64 position); // 设置播放位置,position为要播放的时间,单位毫秒
// 设置播放列表,若播放多个媒体需要设置,默认为空
void setPlaylist(QMediaPlaylist *playlist);
// 设置媒体源
void setMedia(const QMediaContent &media, QIODevice *stream = nullptr);

常用信号:

void stateChanged(QMediaPlayer::State state); // 播放状态改变时发射该信号
void durationChanged(qint64 duration); // 播放时⻓改变时发射该状态
void positionChanged(qint64 position); // 播放位置改变时发射该状态
void volumeChanged(int volume); // ⾳量改变时发射该信号
void metaDataChanged(bool available); // 源数据改变发出

QMediaPlaylist类

在qt6中,QMediaPlaylist类被废弃了,无法在使用,此时需要自定义一个QMediaPlaylist类来对音乐进行相关的管理:

//QMediaPlaylist.h
#ifndef QMEDIAPLAYLIST_H
#define QMEDIAPLAYLIST_H

#include <QMediaPlayer>
#include <QUrl>
#include <QList>
#include <QRandomGenerator>
#include <QDebug>

// 播放模式枚举
enum class PlaybackMode {
    Sequential,  // 顺序播放
    Loop,        // 循环播放
    Random,      // 随机播放
    CurrentItemLoop // 单曲循环
};

// 自定义播放列表类
class QMediaPlaylist : public QObject {
    Q_OBJECT
public:
    explicit QMediaPlaylist(QObject *parent = nullptr);

    // 添加媒体文件
    void addMedia(const QUrl &url);

    // 设置播放模式
    void setPlaybackMode(PlaybackMode mode);

    // 获取下一个媒体文件
    QUrl next();

    // 获取上一个媒体文件
    QUrl previous();

    // 获取当前媒体文件
    QUrl currentMedia() const;

    void setCurrentIndex(int index);
    int getCurrentIndex() const;
    PlaybackMode playbackMode()const;
    void clear();
    void sendIndexAndMediaChanged(int index, QUrl url);

signals:
    // 播放模式改变信号
    void playbackModeChanged(PlaybackMode mode);
    // 当前索引改变信号
    void currentIndexChanged(int position);
    // 当前媒体改变信号
    void currentMediaChanged(const QUrl &content);

private:
    QList<QUrl> mediaList; // 媒体文件列表
    int currentIndex;      // 当前媒体索引
    PlaybackMode mode;     // 当前播放模式
};

#endif // QMEDIAPLAYLIST_H

//QMediaPlaylist.cpp
#include "qmediaplaylist.h"

QMediaPlaylist::QMediaPlaylist(QObject *parent)
    :QObject(parent)
    , currentIndex(-1)
    , mode(PlaybackMode::Loop)
{}

// 添加媒体文件
void QMediaPlaylist::addMedia(const QUrl &url) {
    if(currentIndex < 0){
        currentIndex = 0;
    }
    mediaList.append(url);
}

// 设置播放模式
void QMediaPlaylist::setPlaybackMode(PlaybackMode mode) {
    if (this->mode != mode) {
        this->mode = mode;
        emit playbackModeChanged(mode); // 发射播放模式改变信号
    }
}

// 获取下一个媒体文件
QUrl QMediaPlaylist::next() {
    if (mediaList.isEmpty()) return QUrl();

    int previousIndex = currentIndex;
    int index = currentIndex;

    switch (mode) {
    case PlaybackMode::Sequential:
        index = (currentIndex + 1) % mediaList.size();
        if (currentIndex == 0 && previousIndex != -1) {
            // 顺序播放模式下,播完后停止
            return QUrl();
        }
        break;
    case PlaybackMode::Loop:
        index = (currentIndex + 1) % mediaList.size();
        break;
    case PlaybackMode::Random:
        index = QRandomGenerator::global()->bounded(mediaList.size());
        break;
    case PlaybackMode::CurrentItemLoop:
        // 保持 currentIndex 不变
        index = currentIndex;
        break;
    }

    if (index != previousIndex) {
        setCurrentIndex(index);
    }

    return mediaList.at(currentIndex);
}

// 获取上一个媒体文件
QUrl QMediaPlaylist::previous() {
    if (mediaList.isEmpty()) return QUrl();

    int previousIndex = currentIndex;
    int index = currentIndex;

    switch (mode) {
    case PlaybackMode::Sequential:
    case PlaybackMode::Loop:
        index = (currentIndex - 1 + mediaList.size()) % mediaList.size();
        break;
    case PlaybackMode::Random:
        index = QRandomGenerator::global()->bounded(mediaList.size());
        break;
    case PlaybackMode::CurrentItemLoop:
        // 保持 currentIndex 不变
        index = currentIndex;
        break;
    }

    if (index != previousIndex) {
        setCurrentIndex(index);
    }

    return mediaList.at(currentIndex);
}

// 获取当前媒体文件
QUrl QMediaPlaylist::currentMedia() const {
    if (currentIndex >= 0 && currentIndex < mediaList.size()) {
        return mediaList.at(currentIndex);
    }
    return QUrl();
}

//设置当前播放索引
void QMediaPlaylist::setCurrentIndex(int index) {

    if (index >= mediaList.size()) {
        currentIndex = mediaList.size() - 1;
    } else if (index < 0) {
        currentIndex = 0;
    } else {
        currentIndex = index;
    }
    //qDebug() << currentIndex;

    emit currentIndexChanged(currentIndex); // 发射当前索引改变信号
    //qDebug() << "33333";
    emit currentMediaChanged(mediaList.at(currentIndex)); // 发射当前媒体改变信号
}

//获取当前索引
int QMediaPlaylist::getCurrentIndex()const
{
    //qDebug() << currentIndex;
    return currentIndex;
}

PlaybackMode QMediaPlaylist::playbackMode() const
{
    return mode;
}

void QMediaPlaylist::clear()
{
    mediaList.clear();
    currentIndex  = -1;
}

void QMediaPlaylist::sendIndexAndMediaChanged(int index, QUrl url)
{
    emit currentIndexChanged(index);
    emit currentMediaChanged(url);
}

5.1 音乐播放

播放媒体和播放列表初始化

在播放之前,需要先将QMediaPlayer和QMediaPlaylist初始化好。QQMusic类中需要添加QMediaPlayer和QMediaPlaylist的对象指针,在界⾯初始化时将这两个类的对象创建好

//lxmusic.h
public:
void initPlayer();
private:
QMediaPlayer* _player;
QMediaPlaylist* _playList;
//lxmusic.cpp
void LXMusic::initPlayer()
{
    //创建播放器
    _player = new QMediaPlayer(this);
    //创建播放列表
    _playList = new QMediaPlaylist(this);

    //设置播放模式:默认为循环播放
    _playList->setPlaybackMode(PlaybackMode::Loop);

    //默认音量设置为20
    QAudioOutput *audioOutput = new QAudioOutput(this);
    _player->setAudioOutput(audioOutput);
    _player->setAudioOutput(audioOutput);
    audioOutput->setVolume(0.5);// 设置音量(范围是 0.0 到 1.0)20%
}

播放列表设置

播放之前,先要将歌曲加入用于播放的媒体列表,由于每个CommonPage页面的歌曲不同,因此CommonPage中新增将其页面歌曲添加到播放列表的方法

//commonpage.h
void addMusicToPlayList(MusicList& musicList, QMediaPlaylist* playList);

//commonpage.cpp
void CommonPage::addMusicToPlayList(MusicList &musicList, QMediaPlaylist *playList)
{
    // 根据⾳乐列表中⾳乐所属的⻚⾯,将⾳乐添加到playList中
    for(auto music : musicList){
        switch(_pageType){
            case PageType::LOCALPAGE:{
                playList->addMedia(music.getMusicUrl());
                break;
            }
            case PageType::LIKEPAGE:{
                if(music.getIsLike()){
                    playList->addMedia(music.getMusicUrl());
                }
                break;
            }
            case PageType::RECENTPAGE:{
                if(music.getIsHistory()){
                    playList->addMedia(music.getMusicUrl());
                }
                break;
            }
            default:
                qDebug() << "该页面暂未实现";
                break;
        }
    }
}

播放和暂停

当点击播放和暂停按钮时,播放状态应该在播放和暂停之间切换。播放器的状态如下有:PlayingState()、PausedState()、StoppedState() ,刚开始为停止状态,下面使用isPlaying函数进行判断音乐是否正在播放:

//lxmusic.h
private slots:
void onPlayClicked();
void onPlayStausTochangeIcon();
void finishedAutoplay(QMediaPlayer::PlaybackState state);
private:
bool _isManualPlay = false; //标志位,避免手动操作时调用finishedAutoPlay函数

//lxmusic.cpp
//播放暂停音乐,槽函数
void LXMusic::onPlayClicked()
{
    //正在播放,则暂停,否则播放
    if(_player->isPlaying()){
        _player->pause();
    }
    else{
        _player->play();
    }
}
//播放状态改变,随之改变播放按钮的图标
void LXMusic::onPlayStausTochangeIcon()
{
    if(_player->playbackState() == QMediaPlayer::PlayingState){
        // 播放状态
        ui->playButton->setIcon(QIcon(":/images/play2.png"));
    }
    else{
        // 暂停状态
        ui->playButton->setIcon(QIcon(":/images/play1.png"));
    }
}
//监听播放结束信号,自动播放下一个
void LXMusic::finishedAutoplay(QMediaPlayer::PlaybackState state)
{
    if (_isManualPlay) {
        return; // 如果是手动播放,忽略信号处理
    }
    if (state == QMediaPlayer::StoppedState ) {
        QUrl nextMedia = _playList->next();
        if (!nextMedia.isEmpty()) {
            _player->setSource(nextMedia);
            _player->play();
        } else {
            qDebug() << "Playback finished.";
        }
    }
}
//绑定槽函数
connect(ui->playButton,&QPushButton::clicked, this, &LXMusic::onPlayClicked);
connect(_player,&QMediaPlayer::playbackStateChanged,this,&LXMusic::onPlayStausTochangeIcon);
// 监听播放结束信号,自动播放下一个
connect(_player, &QMediaPlayer::playbackStateChanged, this, &LXMusic::finishedAutoplay);

//初始化playlist,并初始化player
void LXMusic::on_addLocalButton_clicked()
{
    ........
    //将音乐添加到_playlist中,并添加索引为零的音乐到播放器中
     ui->localMusicPage->addMusicToPlayList(_musicList,_playList);
     _player->setSource(_playList->currentMedia());
}

说明:

  • playbackState()获取当前播放状态,当播放状态发生改变的时候,player对象会发出QMediaPlayer::playbackStateChanged的信号,使用槽函数绑定该信号就能知道什么时候改变,然后更改播放按钮的图标

完成上面的步骤就可以实现,第一首音乐的播放和根据设置的播放模式,进行自动播放。

5.2 上一曲和下一曲的切换实现

在定义的播放列表中,提供了previous()和next()函数,通过设置前⼀个或者下⼀个歌曲为当前播放源,player就会播放对应的歌曲

//lxmusic.h
private slots:
void onPlayUpClicked();
void onPlayDownClicked();
//lxmusic.cpp
//切换到上一首歌
void LXMusic::onPlayUpClicked()
{
    _isManualPlay = true;//避免调用finishedAutoplay函数
    _player->setSource(_playList->previous());
    _player->play();
    _isManualPlay = false;
}
//切换到下一首歌
void LXMusic::onPlayDownClicked()
{
    _isManualPlay = true;//避免调用finishedAutoplay函数
    _player->setSource(_playList->next());
    _player->play();
    _isManualPlay = false;
}
//槽函数绑定
connect(ui->playUpButton,&QPushButton::clicked, this, &LXMusic::onPlayUpClicked);
connect(ui->playDownButton,&QPushButton::clicked, this, &LXMusic::onPlayDownClicked);

5.3 播放模式的设置

在在定义的播放列表中,实现了四个自定义模式,在构造函数中默认了播放模式是循环播放:

enum class PlaybackMode {
    Sequential,  // 顺序播放
    Loop,        // 循环播放
    Random,      // 随机播放
    CurrentItemLoop // 单曲循环
};

播放模式切换时会触发 playbackModeChanged 信号,在该信号对应槽函数中,完成图片切换,并在鼠标放在该按钮上时给出提示信息

//lxmusic.h
private slots:
void onPlaybackModeClicked();
void onPlaybackModeChange(PlaybackMode playbackMode);
//lxmusic.cpp
//当点击播放模式按钮的时候,切换播放模式
void LXMusic::onPlaybackModeClicked()
{
    if(_playList->playbackMode() == PlaybackMode::Loop){
        _playList->setPlaybackMode(PlaybackMode::Random);
    }
    else if(_playList->playbackMode() == PlaybackMode::Random){
        _playList->setPlaybackMode(PlaybackMode::Sequential);
    }
    else if(_playList->playbackMode() == PlaybackMode::Sequential)
    {
        _playList->setPlaybackMode(PlaybackMode::CurrentItemLoop);
    }else if(_playList->playbackMode() == PlaybackMode::CurrentItemLoop){
        _playList->setPlaybackMode(PlaybackMode::Loop);
    }
    else{
        qDebug() << "功能暂未支持";
    }
}
//播放模式切换,会自动触发信号,该槽函数与播放模式信号进行绑定
void LXMusic::onPlaybackModeChange(PlaybackMode playbackMode)
{
    if(playbackMode == PlaybackMode::Loop){
        ui->playModeButton->setIcon(QIcon(":/images/loopPlay.png"));
        ui->playModeButton->setToolTip("列表循环");
    }
    else if(playbackMode == PlaybackMode::Random){
        ui->playModeButton->setIcon(QIcon(":/images/randomPlay.png"));
        ui->playModeButton->setToolTip("随机播放");
    }
    else if(playbackMode == PlaybackMode::CurrentItemLoop){
        ui->playModeButton->setIcon(QIcon(":/images/currentItemLoop.png"));
        ui->playModeButton->setToolTip("单曲循环");
    }else if(playbackMode == PlaybackMode::Sequential){
        ui->playModeButton->setIcon(QIcon(":/images/sequentialPlay.png"));
        ui->playModeButton->setToolTip("顺序播放");
    }
    else{
        qDebug() << "功能暂未支持";
    }
}

//绑定信号与槽
//播放模式更换,更改图标
connect(ui->playModeButton,&QPushButton::clicked, this, &LXMusic::onPlaybackModeClicked);
connect(_playList,&QMediaPlaylist::playbackModeChanged, this, &LXMusic::onPlaybackModeChange);

5.4 播放所有按钮的逻辑实现

播放所有按钮属于CommonPage中的按钮,其对应的槽函数添加在CommonPage类中,但是CommonPage不具有音乐播放的功能,因此当点击播放所有按钮后之后,播放所有的槽函数应该发射出信号,让QQMusic类完成播放。由于likePage、localPage、recentPage三个CommonPage页面都有playAllBtn,因此该信号需要带上PageType参数,需要让QQMusic在处理该信号时,知道播放哪个页面的歌曲

//commonpage.h
signals:
void playAll(PageType type);
//commonpage.cpp
//点击播放所有按钮的时候,发送信号给lxmusic
//在构造函数中调用
connect(ui->playAllBtn, &QPushButton::clicked,this, [=](){
        emit CommonPage::playAll(_pageType);});
//lxmusic.h
private slots:
void onPlayAll(PageType type);
public:
void playAllOfPage(CommonPage* page, int index);
//lxmusic.cpp
//实现播放对应页面中的歌曲
void LXMusic::onPlayAll(PageType type)
{
    CommonPage* page = nullptr;
    switch(type){
        case PageType::LIKEPAGE:
        {
            page = ui->likeMusicPage;
            break;
        }
        case PageType::LOCALPAGE:
        {
            page = ui->localMusicPage;
            break;
        }
        case PageType::RECENTPAGE:
        {
            page = ui->recentMusicPage;
            break;
        }
        default:
            qDebug() << "功能暂未支持";
            break;
    }
        playAllOfPage(page,0);
}
//更改播放列表中的歌曲
void LXMusic::playAllOfPage(CommonPage *page, int index)
{
    if(_currentPlayPage != page){
        _currentPlayPage = page;
       // QUrl url = _playList->currentMedia();

        //清空播放列表中的歌
        _playList->clear();

        //将对应页面中存在的歌,加入到播放列表中
        page->addMusicToPlayList(_musicList,_playList);
    }

    //设置当前播放列表的索引
    _playList->setCurrentIndex(index);

    //播放音乐
    _player->setSource(_playList->currentMedia());
    _player->play();

}

//关联CommonPage发出的播放所有信号,实现播放对应页面的歌曲
connect(ui->likeMusicPage,&CommonPage::playAll, this, &LXMusic::onPlayAll);
connect(ui->localMusicPage,&CommonPage::playAll, this, &LXMusic::onPlayAll);
connect(ui->recentMusicPage,&CommonPage::playAll, this, &LXMusic::onPlayAll);

5.5 双击页面中的音乐进行播放

当QListWidget中的项被双击时,会触发 doubleClicked 信号

// QListWidget中的项被双击时触发
// QModelIndex类中的row()函数会返回被点击的QListWidgetItem在QListWidget中索引
void doubleClicked(const QModelIndex &index);

该信号在QListWidget的基类中定义,有⼀个index参数,表示被双击的QListWidgetItem在QListWidget中的索引,该索引刚好与QMediaPlaylist中歌曲的所以⼀致,被双击时直接播放该首歌曲即可

//commonpage.h
signals:
void playMusicbyIndex(CommonPage* page, int index);

//commonpage.cpp
//双击音乐后,发送信号给lxmusic,让其实现播放
    connect(ui->pageMusicList,&QListWidget::doubleClicked, this, [=](const QModelIndex& index){ emit playMusicbyIndex(this, index.row());});

//lxmusic.h
private:
bool _isManualPlay = false; //标志位,避免双击时,调用finishedAutoPlay函数
private slots:
void playMusicByIndex(CommonPage* page, int index);

//lxmuic.cpp
//播放对应页面中,index索引下的歌曲
void LXMusic::playMusicByIndex(CommonPage *page, int index)
{
    playAllOfPage(page, index);
}

//更改下面函数
void LXMusic::playAllOfPage(CommonPage *page, int index)
{
    if(_currentPlayPage != page){
        _currentPlayPage = page;
       // QUrl url = _playList->currentMedia();

        //清空播放列表中的歌
        _playList->clear();

        //将对应页面中存在的歌,加入到播放列表中
        page->addMusicToPlayList(_musicList,_playList);
    }

    //设置当前播放列表的索引
    _playList->setCurrentIndex(index);

	_isManualPlay = true;
    //播放音乐
    _player->setSource(_playList->currentMedia());
    _player->play();

    _isManualPlay = false;
}

//关联CommonPage发出的双击音乐播放信号,实现播放对应页面的歌曲
connect(ui->likeMusicPage,&CommonPage::playMusicbyIndex, this, &LXMusic::playMusicByIndex);
connect(ui->localMusicPage,&CommonPage::playMusicbyIndex, this, &LXMusic::playMusicByIndex);
connect(ui->recentMusicPage,&CommonPage::playMusicbyIndex, this, &LXMusic::playMusicByIndex);

5.6 最近播放同步

当播放歌曲改变时,即播放的媒体源发生了变化,QMediaPlayer会触发metaDataAvailableChanged 信号,QMediaPlaylist也会触发 currentIndexChanged 信号,该信号会带index参数,表示现在是媒体播放列表中的index歌曲被播放,通过index可以获取到recentPage页面中具体播放的歌曲,将该歌曲对应Music对象的isHistoty属性修改为true,然后更新下rencentPage的歌曲列表,播放过的歌曲就添加到历史播放页面中了

问题:获取likePage、localPage、recentPage哪个CommonPage⻚⾯中的歌曲呢?

答案:LXMusic类中维护CommonPage*变量currentPage,记录当前正在播放的CommonPage页面,初始时设置为localPage,当播放的页面发生改变时,修改currentPage为当前正在播放页面,其中点击播放所有按钮以及双击QListWidget中项的时候都回引起currentPage的改变

//lxmusic.h
private:
CommonPage* _currentPage;
//lxmusic.cpp
LXMusic::LXMusic(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::LXMusic)
    ,_currentPage(nullptr)
{
    ui->setupUi(this);

    //初始化界面
    initUi();

    //初始化播放器
    initPlayer();

    //绑定信号和槽函数
    connectSignalAndSlots();
}

void LXMusic::playAllOfPage(CommonPage *page, int index)
{
    _currentPage = page;
    //清空播放列表中的歌
    _playList->clear();

    .............
}

准备工作完成之后,同步最近播放歌曲的逻辑实现如下:

播放索引改变的时候会发送currentIndexChanged信号,在LXMusic中接受该信号,将当前索引对应的歌曲中的isHistory属性设置为true,然后再刷新recentPage界面将_musicList中已经播放的歌曲添加到recentPage页面中即可。

//lxmusic.h
private slots:
void onCurrentIndexChanged(int index);
//lxmusic.cpp
void LXMusic::onCurrentIndexChanged(int index)
{
    QString musicId = _currentPage->getMusicIdByIndex(index);
    if(musicId.isEmpty())return;

    auto it = _musicList.findMusicById(musicId);
    if(it != _musicList.end()){
        // 将该⾳乐设置为历史播放记录
        it->setIsHistory(true);
    }
    //刷新最近播放界面
    ui->recentMusicPage->refreshPage(_musicList);
}
//绑定槽函数
// 播放列表中的索引发⽣改变,此时将播放⾳乐收藏到历史记录中
connect(_playList, &QMediaPlaylist::currentIndexChanged, this,&LXMusic::onCurrentIndexChanged);

//commonpage.h
QString getMusicIdByIndex(int index);
//commonpage.cpp
QString CommonPage::getMusicIdByIndex(int index)
{
    if(index >= _musicOfPage.size()){
        return "";
    }

    return _musicOfPage[index];

}

但这样发现刚开始点击播放音乐按钮的时候,音乐并没有添加到最近播放页面,需进行如下修改:

//lxmusic.h
void setMusicHistoryByUrl(QUrl url);
//lxmusic.cpp
//播放暂停音乐,槽函数
void LXMusic::onPlayClicked()
{
    if(_player->source().isEmpty()){
        QUrl url = _playList->currentMedia();
        qDebug() << url;
        if(!url.isEmpty()){
            setMusicHistoryByUrl(url);
            //刷新最近播放界面
            ui->recentMusicPage->refreshPage(_musicList);
            //设置播放源
            _player->setSource(_playList->currentMedia());
        }
    }
    //正在播放,则暂停,否则播放
    if(_player->isPlaying()){
        _player->pause();
    }
    else{
        _player->play();
    }
}

void LXMusic::on_addLocalButton_clicked()
{
    //这个函数中,将_player->setSource(_playList->currentMedia());去掉
    //不要这里初始化
}

5.7 音量设置

当点击静音按钮时,音量应该在静音和非静音之间进行切换,并且按钮上图标需要同步切换。鼠标在滑竿上点击或拖动滑竿时,应该跟进滑竿的高低比率,设置音量大小,同时修改界面音量比率

静⾳和⾮静⾳切换

给静音按钮参加槽函数onSilenceBtnClicked,并在构造函数中connect按钮的clicked信号,当按钮点击时候,调用setMuted(bool nuted)函数,完成静音和非静音的设置,由于VolumeTool不具备媒体播放控制,因此当静音状态发生改变时,发射设置静音信号,让LXMusic来处理

//volumetool.h
signals:
    void setSilence(bool isMuted);
private:
 	bool _isMuted;
 	int _volumeRatio;
private slots:
    void onSilenceBtnClicked();
//volumetool.cpp
//构造函数中初始化成员变量
VolumeTool::VolumeTool(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::VolumeTool)
    ,_isMuted(false)
    ,_volumeRatio(20){.............}
void VolumeTool::onSilenceBtnClicked()
{
    _isMuted = !_isMuted;
    if(_isMuted){
        ui->silenceBtn->setIcon(QIcon(":/images/volume2.png"));
    }else{
        ui->silenceBtn->setIcon(QIcon(":/images/volume.png"));
    }
    //发送信号
    emit setSilence(_isMuted);

}
//绑定槽函数
connect(ui->silenceBtn, &QPushButton::clicked, this, &VolumeTool::onSilenceBtnClicked);

//lxmusic.h
private slots:
void setMusicSilence(bool isMuted);
//lxmusic.cpp
//设置静音或不静音
void LXMusic::setMusicSilence(bool isMuted)
{
    QAudioOutput* audioOutput = _player->audioOutput();
    audioOutput->setMuted(isMuted);
}
//绑定槽函数
//接受volumetool发来的静音信号
connect(_volumetool, &VolumeTool::setSilence, this, &LXMusic::setMusicSilence);

⿏标按下、滚动以及释放事件处理

当鼠标在滑竿上按下时,需要设置sliderBtn和outLine的位置,当鼠标在滑竿上移动或者鼠标抬起时,需要设置SliderBtnoutLine结束的位置,即改变VolumeTool中滑竿的显示。具体修改播放媒体音量大小操作应该由于QQMusic负责处理,因此当鼠标移动或释放时,需要发射信号让QQMusic知道需要修改播放媒体的音量大小了

//volumetool.h
public:
    void setVolume();
protected:
virtual bool eventFilter(QObject* object, QEvent* event)override;
signals:
void setMusicVolume(int volumeRatio);

//volumetool.cpp
//重写函数,过滤鼠标按下、移动、释放的事件
bool VolumeTool::eventFilter(QObject *object, QEvent *event)
{
    if(object == ui->volumeBox){
        if(event->type() == QEvent::MouseButtonPress){
            setVolume();
        }
        else if(event->type() == QEvent::MouseMove){
            emit setMusicVolume(_volumeRatio);
        }
        else if(event->type() == QEvent::MouseButtonRelease){
            setVolume();
            emit setMusicVolume(_volumeRatio);
        }
        return true;
    }

    return QWidget::eventFilter(object, event);
}
//计算音量大小
void VolumeTool::setVolume()
{
    int height = ui->volumeBox->mapFromGlobal(QCursor().pos()).y();
    height = height < 25 ? 25 : height;
    height = height > 205 ? 205 : height;

    //计算圆圈的位置
    ui->sliderBtn->move(ui->sliderBtn->x(), height - ui->sliderBtn->height() / 2);

    //计算outslider位置
    ui->outSlider->setGeometry(ui->outSlider->x(), height, ui->outSlider->width(), 205 - height);

    //计算音量比率
    _volumeRatio = (int)(ui->outSlider->height() / (float)180 * 100);

    //设置给volumeRatio显示
    ui->volumeRatio->setText(QString::number(_volumeRatio) + "%");

}
//在构造函数中下载过滤器
 ui->volumeBox->installEventFilter(this);

LXMusic类拦截VolumeTool发射的setMusicVolume信号,将音量大小设置为指定值

//lxmusic.h
private slots:
void setMusicVolume(int volumeRatio);
//lxmusic.cpp
//接受volumetool的信号,设置音量
void LXMusic::setMusicVolume(int volumeRatio)
{
    QAudioOutput* audioOutput = _player->audioOutput();
    audioOutput->setVolume(volumeRatio / 100.0);
}
//绑定信号槽
//接受volumetool发来的信号,设置音量大小
connect(_volumetool,&VolumeTool::setMusicVolume, this, &LXMusic::setMusicVolume);

5.8 当前播放时间和总时间更新

  1. 当播放源的持续时长发生改变时,QMediaPlayer会触发durationChanged信号,该信号中提供了将要播放媒体的总时长
  2. 媒体在持续播放过程中,QMediaPlayer会发射positionChanged,该信号带有⼀个qint64类型参数,表示媒体当前持续播放的时间
  3. 因此在LXMusic类中给这两个信号关联槽函数,在槽函数中将信号传递来的参数更新到对应的label中即可
//lxmusic.h
void onDurationChanged(qint64 duration);
void onPositionChanged(qint64 position);
//lxmusic.cpp
//更新总时间
void LXMusic::onDurationChanged(qint64 duration)
{
    ui->totalTimeLabel->setText((QString("%1:%2").arg(duration/1000/60, 2, 10,QChar('0')).arg(duration/1000%60,2,10,QChar('0'))));
}
//更新当前播放时间
void LXMusic::onPositionChanged(qint64 position)
{
    ui->currentTimeLabel->setText((QString("%1:%2").arg(position/1000/60, 2, 10,QChar('0')).arg(position/1000%60,2,10,QChar('0'))));
}
//绑定信号槽
//监听_player总时间的变化
connect(_player, &QMediaPlayer::durationChanged, this, &LXMusic::onDurationChanged);
//监听_player当前播放位置变化
connect(_player,&QMediaPlayer::positionChanged, this, &LXMusic::onPositionChanged);

5.9 进度条的更新

seek功能介绍

播放器的seek功能指,通过时间或位置快速定位到视频或音频流的特定位置,允许用户在播放过程中随时跳转到特定时间点,从而快速找到感兴趣的内容或重新开始播放

在界面上的体现是,当在MusicSlider上点击或者拖拽的时候,会跳转到歌曲的指定位置进行播放,并且歌曲的当前持续播放时间要同步修改

进度条界面显示

进度条功能进度界面展示与音量调节位置类似,拦截鼠标按下、鼠标移动、以及鼠标释放消息即可。在内部捕获到鼠标的位置的横坐标x,将x作为outLine的宽度。即在鼠标按下、移动、释放的时候,修改outLine的宽度即可

//musicSlider.h
protected:
    virtual void mousePressEvent(QMouseEvent* event)override;
    virtual void mouseMoveEvent(QMouseEvent* event)override;
    virtual void mouseReleaseEvent(QMouseEvent* event)override;
//musicSlider.cpp
public:
void moveSlider();
private:
qint64 _currentPosition;//滑块当前位置
qint64 _maxWidth;//滑块的最大宽度

MusicSlider::MusicSlider(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::MusicSlider)
{
    ui->setupUi(this);

     //初始化进度条
    _currentPosition = 0;
    _maxWidth = width();
    moveSlide();
}
//移动进度条
void MusicSlider::moveSlide()
{
    if(_currentPosition < 0){
        _currentPosition = 0;
    }
    if(_currentPosition > _maxWidth){
        _currentPosition = _maxWidth;
    }
    // 根据当前进度设置外部滑动条的位置
    ui->outLine->setMaximumWidth(_currentPosition);
    ui->outLine->setGeometry(0,8, _currentPosition, 4);

}
void MusicSlider::mousePressEvent(QMouseEvent *event)
{
    _currentPosition = event->pos().x();
    moveSlide();
}

void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{
    // QRect rect = QRect(0,0,width(), height());
    // QPoint pos = event->pos();
    // if(!rect.contains(pos)){
    //     return;
    // }

    if(event->buttons() == Qt::LeftButton){
        _currentPosition = event->pos().x();
        moveSlide();
    }
}

void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
    _currentPosition = event->pos().x();
    moveSlide();
	//发送信号,给lxmusic让其更新当前播放时间
    emit setMusicSliderPostion((float)_currentPosition / (float)_maxWidth);
}

进度条同步持续播放时间

当鼠标释放之后,计算出进度条当前位置currentPos和总宽度的maxWidth比率,然后发射信号告诉LxMusic,让player按照该比率更新持续播放时间

// musicslider.h 新增
signals:
void setMusicSliderPosition(float);
//musicslider.cpp
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
    _currentPosition = event->pos().x();
    moveSlide();
	//发送信号,给lxmusic让其更新当前播放时间
    emit setMusicSliderPostion((float)_currentPosition / (float)_maxWidth);
}
//lxmusic.h
void onMusicSliderChanged(float value);
//lxmusic.cpp
//根据进度条位置,更新当前播放时间
void LXMusic::onMusicSliderChanged(float value)
{
    qint64 duration = (qint64)(_player->duration() * value);
    ui->currentTimeLabel->setText((QString("%1:%2").arg(duration/1000/60, 2, 10,QChar('0')).arg(duration/1000%60,2,10,QChar('0'))));
    //设置当前播放时间
    _player->setPosition(duration);
}

//绑定槽函数
//监听进度条的变化,改变当前播放时间
connect(ui->processBar, &MusicSlider::setMusicSliderPostion, this, &LXMusic::onMusicSliderChanged);

持续时间同步进度条

//musicslider.h
public:
void syncSlide(float value);
//musicslider.cpp
//进度条同步当前播放时间
void MusicSlider::syncSlide(float value)
{
    _currentPosition = (qint64)_maxWidth * value;
    moveSlide();
}
//lxmusic.h
private slots:
void onMusicSliderChanged(float value);
//lxmusic.cpp
//更新当前播放时间并设置进度条的位置
void LXMusic::onPositionChanged(qint64 position)
{
    ui->currentTimeLabel->setText((QString("%1:%2").arg(position/1000/60, 2, 10,QChar('0')).arg(position/1000%60,2,10,QChar('0'))));

    //设置进度条的位置
    ui->processBar->syncSlide((float)position / (float)_player->duration());
}

//绑定信号槽
//监听_player当前播放位置变化
connect(_player,&QMediaPlayer::positionChanged, this, &LXMusic::onPositionChanged);

5.10 歌曲名称、歌手和封面图片同步

在进行歌曲切换时候,歌曲名称、歌手以及歌曲的封面图,也需要更新到界面。歌曲名称、歌手可以再Music对象中进行获取,歌曲的封面图可以通过player到歌曲的元数据中获取,获取时需要使用"ThumbnailImage"作为参数,注意有些歌曲可能没有封面图,如果没有设置⼀张默认的封面图。 由于歌曲切换时,player需要将新播放歌曲作为播放源,并解析歌曲文件,如果歌曲文件是有效的才能播放;因此LXMusic类可以给QMediaPlayer发射的 metaDataChanged() 信号关联槽函数,当歌曲更换时,完成信息的更新

//lxmusic.h
private slots:
void onMetaDataChanged();
//lxmusic.cpp
//音乐改变时,更新图片歌名歌手
void LXMusic::onMetaDataChanged()
{
    QString musicName = _player->metaData().value(QMediaMetaData::Title).toString();
    QString singerName = _player->metaData().value(QMediaMetaData::Author).toString();

    if(musicName.isEmpty() || singerName.isEmpty())
    {
        auto it = _musicList.findMusicById(_currentPlayPage->getMusicIdByIndex(_playList->getCurrentIndex()));
        if(it != _musicList.end())
        {
            musicName = it->getMusicName();
            singerName = it->getSingerName();
        }
    }

    //设置歌手名称、歌名
    ui->musicNameLabel->setText(musicName);
    ui->musicSingerLabel->setText(singerName);

    //获取图片
    QVariant coverImage =  _player->metaData().value(QMediaMetaData::ThumbnailImage);
    if(coverImage.isValid()){
        // 获取封⾯图⽚成功
        QImage image = coverImage.value<QImage>();
        // 设置封⾯图⽚
        ui->musicCoverLabel->setPixmap(QPixmap::fromImage(image));
        // 缩放填充到整个Label
        ui->musicCoverLabel->setScaledContents(true);

        _currentPlayPage->setImageLabel(QPixmap::fromImage(image));
    }
    else{
        qDebug() << " 11111";
        ui->musicCoverLabel->setPixmap(QPixmap(":/images/localmusic.jpg"));
        // 缩放填充到整个Label
        ui->musicCoverLabel->setScaledContents(true);
        _currentPlayPage->setImageLabel(QPixmap(":/images/localmusic.jpg"));
    }
}
//绑定信号槽
//监听歌曲的改变,显示歌名歌手和图片
connect(_player,&QMediaPlayer::metaDataChanged, this, &LXMusic::onMetaDataChanged);
//commonpage.h
public:
void setImageLabel(const QPixmap& pixmap);
//commonpage.cpp
void CommonPage::setImageLabel(const QPixmap &pixmap)
{
    ui->musicImageLabel->setPixmap(pixmap);
    ui->musicImageLabel->setScaledContents(true);
}

5.11 歌词同步

播放歌曲时,当点击"词"按钮后窗口会慢慢弹出,当点击隐藏按钮后,窗口会慢慢隐藏,且没有标题栏。内部显示当前播放歌曲的歌词,以及歌曲名称和作者。当点击下拉按钮时,窗口会隐藏起来

lrc歌词界⾯分析

在这里插入图片描述

该页面最上面由两个label和一个按钮组成,下面由七个label组成。

lrc歌词界⾯布局

在qt create中新创建⼀个qt 设计师界面,命名为LrcPage,geometry的宽高修改为:1028*688

  1. 拖⼀个Widget到LrcPage中,objectName修改为bgStyle,选中LrcPage,然后点击垂直布局,并将LrcPage的margin和spacing修改为0;
  2. 拖两个Widget到bgStyle中,objectName从上往下分别修改为lrcTop和lrcContent,lrcTop的minimumSize和maximumSize的高修改为50;然后选中bgStyle点击垂直布局,并将bgStyle的margin和spacing修改为0;
  3. 拖⼀个按钮到lrcTop中,objectName修改为hideBtn,minimumSize和maximumSize的宽和高修改为:30*50;
  4. 拖⼀个Widget到lrcTop中,objectName修改为titleBox 然后选中lrcTop,点击⽔平布局,并将lrcTop的margin和spacing修改为0;
  5. 拖两个QLabel到titleBox中,objectName从上往下修改为musicSinger和musicName,然后选中titleBox,点击垂直布局,并将titleBox的margin和spacing修改为0
  6. 拖六个QLabel到lrcContent中,从上往下将objectName依次修改为:line1、line2、line3、lineCenter、line4、line5、line6,将line1~line6的minimumSize的高度修改为50,font修改为15,将lineCenter的minimumSize高度修改为80,font的大小修改为25;
  7. 拖两个垂直弹簧,一个放在line1上,⼀个放在line6下,将所有的QLabel撑到中间 ,选中lrcContent,然后点击垂直布局,将lrcContent的margin和spacing修改为0

歌词界面优化

#bgStyle{
border-image: url(:/images/bg.png);
}
*{
color : #FFFFFF;
}
#lineCenter
{
color:#1ECE9A;
}
#hideBtn
{
border:none;
}

在这里插入图片描述

LrcPage显示并添加动画效果

在LrcPage的构造函数中,将窗口的标题栏去除掉;并给hideBtn关联clicked信号,当按钮点击时将窗口隐藏

//lxmusic.h
private slots:
void onLrcWordClicked();
private:
LrcPage* _lrcWord;
//lxmusic.cpp
void LXMusic::onLrcWordClicked()
{
    _lrcWord->show();
    _animal->start();
}
void LXMusic::initUi(){
    ......................
    //初始化lrcPage页面
    _lrcWord = new LrcPage(this);
    _lrcWord->hide();

    // lrcPage添加动画效果
    _animal = new QPropertyAnimation(_lrcWord, "geometry", this);
    _animal->setDuration(250);
    _animal->setStartValue(QRect(10, 10+_lrcWord->height(),_lrcWord->width(), _lrcWord->height()));
    _animal->setEndValue(QRect(10, 10, _lrcWord->width(), _lrcWord->height()));
}
//绑定信号槽
//点击歌词按钮时,弹出歌词窗口
connect(ui->lyricButton, &QPushButton::clicked, this, &LXMusic::onLrcWordClicked);
//lrcpage.h
private:
QPropertyAnimation* animation;
//lrcpage.cpp
LrcPage::LrcPage(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::LrcPage)
{

    ui->setupUi(this);
	//设置窗口无边框
    this->setWindowFlag(Qt::FramelessWindowHint);
	//设置按钮图标
    ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));

    animation = new QPropertyAnimation(this, "geometry", this);
    animation->setDuration(250);
    animation->setStartValue(QRect(10, 10, width(), height()));
    animation->setEndValue(QRect(10, 10 + height(), width(), height()));
    //点击按钮显示动画
    connect(ui->hideBtn, &QPushButton::clicked, [=](){
        animation->start();
        //this->hide();
    });

    //动画结束,隐藏窗口
    connect(animation, &QPropertyAnimation::finished, [=](){
            this->hide(); // 隐藏窗口
    });

}

lrc歌词解析和同步

lrc是英文lyric(歌词)的缩写,被用作歌词文件的扩展名。该文件将歌词和歌词出现的时间编辑到⼀起,当歌曲播放的时候,按照歌词文件中的时间依次将歌词显示出来。

  • 标准格式:[分钟:秒.毫秒] 歌词

  • 其他格式:① [分钟:秒] 歌词 ② [分钟:秒:毫秒] 歌词

在这里插入图片描述

每首歌的lrc歌词有多行文本,因此lrc歌词中的每行可以采用结构体管理

//lrcpage.h
struct lyricLine{
    qint64 _time;
    QString _text;
    lyricLine(qint64 time, QString text)
        :_time(time)
        ,_text(text)
    {}
};
private:
QVector<lyricLine> lrcLines; //保存歌词文件中每一行的内容,一行为单位

通过歌曲名找LRC⽂件

在做项目的时候,我将歌词文件和歌曲文件保存在一个文件夹中,因此这里我直接将音乐文件的后缀换成了lrc。

//music.h
public:
QString getLrcFilePath()const;
//music.cpp
QString Music::getLrcFilePath() const
{
   //qDebug() << _musicUrl;
    //QString path = _musicUrl.toString();
    QString path = _musicUrl.toLocalFile();
    //qDebug() << path;
    path.replace(".mp3", ".lrc");
    path.replace(".flac", ".lrc");
    path.replace(".mpga", ".lrc");
    return path;
}

LRC歌词解析

找到lrc歌词文件后,由lrcPage类完成对歌词的解析。解析的⼤概步骤:

  1. 打开歌词文件
  2. 以行为单位,读取歌词文件中的每一行
  3. 按照lrc歌词文件格式,从每行文本中解析出时间和歌词 [00:17.94]那些失眠的人啊 你们还好吗 [0:58.600.00]你像⼀只飞来飞去的蝴蝶
  4. 用<时间,行歌词>构建⼀个LrcLine对象存储到lrcLines中
//lrcpage.h
public:
    bool parseLrc(const QString& lrcPath);
//lrcpage.cpp
bool LrcPage::parseLrc(const QString &lrcPath)
{
    lrcLines.clear();

    QFile file(lrcPath);
    if(!file.open(QFile::ReadOnly)){
        qDebug()<<"打开⽂件:"<<lrcPath;
        return false;
    }

    while(!file.atEnd())
    {
        QString lrcWord = file.readLine(1024);
        // [00:17.94]那些失眠的⼈啊 你们还好吗
        // [0:58.600.00]你像⼀只⻜来⻜去的蝴蝶
        int left = lrcWord.indexOf('[');
        int right = lrcWord.indexOf(']');
        // 解析时间
        qint64 lineTime = 0;
        int start = 0;
        int end = 0;
        QString time = lrcWord.mid(left, right-left+1);
        // 解析分钟
        start = 1;
        end = time.indexOf(':');
        lineTime += lrcWord.mid(start, end-start).toInt()*60*1000;
        // 解析秒
        start = end + 1;
        end = time.indexOf('.', start);
        lineTime += lrcWord.mid(start, end-start).toInt()*1000;
        // 解析毫秒
        start = end+1;
        end = time.indexOf('.', start);
        lineTime += lrcWord.mid(start, end-start).toInt();
        // 解析歌词
        QString word = lrcWord.mid(right+1).trimmed();
        lrcLines.push_back(lyricLine(lineTime, word.trimmed()));
    }

    // for(auto word : lrcLines)
    // {
    //     qDebug()<<word._time<<" "<<word._text;
    // }
    return true;
}

根据歌曲播放位置获取歌词并同步显⽰

当歌曲播放进度改变时候,QMediaPlayer的positionChanged信号会触发,该信号同步播放时间的时候已经在QQMusic类中处理过了,在其槽函数中就能拿到当前歌曲的播放时间,通过播放时间,就能在LrcPage中找到对应行的歌词

  1. 根据时间获取存在lrcLines中的行号,默认以第0行开始
  2. 根据行号开始读取lrcLines中对应的内容
  3. 根据播放时间的改变信号,将歌词内容显示在歌词页面上
//lxmusic.cpp
//更改函数
//更新当前播放时间并设置进度条的位置
void LXMusic::onPositionChanged(qint64 position)
{
    ui->currentTimeLabel->setText((QString("%1:%2").arg(position/1000/60, 2, 10,QChar('0')).arg(position/1000%60,2,10,QChar('0'))));

    //设置进度条的位置
    ui->processBar->syncSlide((float)position / (float)_player->duration());

    if(_playList->getCurrentIndex() >= 0){
        //显示歌词
        _lrcWord->showLrcWord(position);
    }
}
//音乐改变时,更新图片歌名歌手
void LXMusic::onMetaDataChanged()
{
    QString musicName = _player->metaData().value(QMediaMetaData::Title).toString();
    QString singerName = _player->metaData().value(QMediaMetaData::Author).toString();
    QString musicId = _currentPlayPage->getMusicIdByIndex(_playList->getCurrentIndex());
    //qDebug() << "musicId::" << musicId;

    // if(musicName.isEmpty() || singerName.isEmpty())
    // {
        auto it = _musicList.findMusicById(musicId);
        if(it != _musicList.end())
        {
            musicName = it->getMusicName();
            singerName = it->getSingerName();
            //加载歌词
            _lrcWord->parseLrc(it->getLrcFilePath());
        }
    //}

    //设置歌手名称、歌名
    ui->musicNameLabel->setText(musicName);
    ui->musicSingerLabel->setText(singerName);
    _lrcWord->setMusicName(musicName);
    _lrcWord->setSingerName(singerName);
    
    .........................
}
//lrcpage.h
public:
    int getLineLrcWordIndex(qint64 time);
    QString getLineWord(qint64 index);
    void showLrcWord(int time);
    void setMusicName(const QString& name);
    void setSingerName(const QString& name);
//lrcpage.cpp
//根据时间获取行号
int LrcPage::getLineLrcWordIndex(qint64 time)
{
    // 如果歌词是空的,返回-1
    if(lrcLines.isEmpty())
    {
        return -1;
    }
    if(lrcLines[0]._time > time)
    {
        return 0;
    }
    // 通过时间⽐较,获取下标
    for(int i = 1; i < lrcLines.size(); ++i)
    {
        if(time > lrcLines[i-1]._time && time <= lrcLines[i]._time)
        {
            return i-1;
        }
    }
    // 如果没有找到,返回最后⼀⾏
    return lrcLines.size()-1;
}

//根据行号获取内容
QString LrcPage::getLineWord(qint64 index)
{
    if(index < 0 || index >= lrcLines.size()){
        return "";
    }

    return lrcLines[index]._text;
}

//将内容显示到页面上
void LrcPage::showLrcWord(int time)
{
    int index = getLineLrcWordIndex(time);
    if(-1 == index)
    {
        ui->line1->setText("");
        ui->line2->setText("");
        ui->line3->setText("");
        ui->lineCenter->setText("当前歌曲⽆歌词");
        ui->line4->setText("");
        ui->line5->setText("");
        ui->line6->setText("");
    }else
    {
        ui->line1->setText(getLineWord(index-3));
        ui->line2->setText(getLineWord(index-2));
        ui->line3->setText(getLineWord(index-1));
        ui->lineCenter->setText(getLineWord(index));
        ui->line4->setText(getLineWord(index+1));
        ui->line5->setText(getLineWord(index+2));
        ui->line6->setText(getLineWord(index+3));
    }
}

//修改页面上的歌名
void LrcPage::setMusicName(const QString &name)
{
    ui->musicName->setText(name);
}

//修改页面上的歌手名
void LrcPage::setSingerName(const QString &name)
{
    ui->singerName->setText(name);
}

六、数据库支持

数据库初始化

//LXMusic.pro文件中
QT       += core gui multimedia sql
//lxmusic.h
#include<QSqlDatabase>
#include<QMessageBox>
#include<QSqlError>
#include<QSqlQuery>
private:
QSqlDatabase sqlite;
public:
void initSQlite();

//lxmusic.cpp
//初始化数据库
void LXMusic::initSQlite()
{
    //加载驱动
    sqlite = QSqlDatabase::addDatabase("QSQLITE");

    //设置数据库名称
    sqlite.setDatabaseName("LXMusic.db");

    //打开数据库
    if(!sqlite.open()){
        QMessageBox::critical(this, "打开QQMusicDB失败",sqlite.lastError().text());
        return;
    }
    qDebug() << "数据库连接成功";

// 4. 创建数据库表
    QString sql = ("CREATE TABLE IF NOT EXISTS musicInfo(\
            id INTEGER PRIMARY KEY AUTOINCREMENT,\
            musicId varchar(200) UNIQUE,\
            musicName varchar(50),\
            musicSinger varchar(50),\
            albumName varchar(50),\
            duration BIGINT,\
            musicUrl varchar(256),\
            isLike INTEGER,\
            isHistory INTEGER)"
        );
    QSqlQuery query;
    if(!query.exec(sql)){
        QMessageBox::critical(this, "创建数据库表失败",query.lastError().text());
        return;
    }
    qDebug()<<"创建 [musicInfo] 表成功!!!";
}

歌曲信息写⼊数据库

当程序退出的时候,通过musicList获取到所有music对象,然后将music对象写⼊数据库

//musiclist.h
void writeToDB();
//musiclist.cpp
//让数据库中写入数据
void MusicList::writeToDB()
{
    for(const auto& music : _musicList){
        QString musicId = music.getMusicId();
        QSqlQuery query;

        query.prepare("select exists(select 1 from musicInfo where musicId = ?)");
        query.addBindValue(musicId);
        if(!query.exec()){
            qDebug()<<"查询失败: "<<query.lastError().text();
            return;
        }
        if(query.next()){
            bool isExists = query.value(0).toBool();
            if(isExists)
            {
                // musicId的歌曲已经存在
                // 2. 存在:不需要再插⼊musci对象,此时只需要将isLike和isHistory属性进⾏更新
                query.prepare("UPDATE musicInfo SET isLike = ?, isHistory = ? WHERE musicId = ?");
                query.addBindValue(music.getIsLike()? 1 : 0);
                query.addBindValue(music.getIsHistory()? 1 : 0);
                query.addBindValue(musicId);
                if(!query.exec())
                {
                    qDebug()<<"更新失败: "<<query.lastError().text();
                }
                qDebug()<<"更新music信息: "<<music.getMusicName()<<" "<<musicId;
            }
            else
            {
                // 3. 不存在:直接将music的属性信息插⼊数据库
                query.prepare("INSERT INTO musicInfo(musicId, musicName,musicSinger, albumName, musicUrl,\
                duration, isLike, isHistory)VALUES(?,?,?,?,?,?,?,?)");
                query.addBindValue(musicId);
                query.addBindValue(music.getMusicName());
                query.addBindValue(music.getSingerName());
                query.addBindValue(music.getAlbumName());
                query.addBindValue(music.getMusicUrl().toLocalFile());
                query.addBindValue(music.getDuration());
                query.addBindValue(music.getIsLike() ? 1 : 0);
                query.addBindValue(music.getIsHistory()? 1 : 0);

                if(!query.exec())
                {
                    qDebug()<<"插⼊失败: "<<query.lastError().text();
                    return;
                }
                qDebug()<<"插⼊music信息: "<<music.getMusicName()<<" "<<musicId;
            }

    }
    }
}
//lxmusic.cpp
//退出
void LXMusic::on_quitButton_clicked()
{
    //关闭前写入数据库中
    _musicList.writeToDB();
    //qDebug() << "写入完成";
    //关闭数据库
    sqlite.close();
    //关闭程序
    this->close();
}

程序启动时读取数据库恢复歌曲数据

在程序启动时,从数据库中读取到歌曲的信息,将歌曲信息设置到musicList中,然后让likePage、localPage、recentPage将musicList中个歌曲更新到各自页面中。 从数据库读取歌曲数据的操作,应该让MusicList类完成,因为该类管理所有的Music对象

//musiclist.h
void readFromDB();
//musiclist.cpp
void MusicList::readFromDB()
{
    QString sql("SELECT musicId, musicName, musicSinger, albumName,\
    duration, musicUrl, isLike, isHistory FROM musicInfo");
    QSqlQuery query;
    if(!query.exec(sql))
    {
        qDebug()<<"数据库查询失败";
        return;
    }
    qDebug() << "查询完成";
    while(query.next())
    {
        Music music;
        music.setMusicId(query.value(0).toString());
        music.setMusicName(query.value(1).toString());
        music.setSingerName(query.value(2).toString());
        music.setAlbumName(query.value(3).toString());
        music.setDuration(query.value(4).toLongLong());
        music.setMusicUrl(query.value(5).toString());
        music.setIsLike(query.value(6).toBool());
        music.setIsHistory(query.value(7).toBool());
        _musicList.push_back(music);
    }
}

//lxmusic.h
void initMusiclist();
//lxmusic.cpp
void LXMusic::initMusiclist()
{
    //初始化musiclist
    _musicList.readFromDB();
    //将数据更新到各自的页面中
    ui->recentMusicPage->refreshPage(_musicList);
    ui->likeMusicPage->refreshPage(_musicList);
    ui->localMusicPage->refreshPage(_musicList);
    
    //默认将localMusicPage页面中的歌曲设置进playList中
    ui->localMusicPage->addMusicToPlayList(_musicList,_playList);
}

LXMusic::LXMusic(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::LXMusic)
    ,_currentPlayPage(nullptr)
{
    ui->setupUi(this);

    //初始化界面
    initUi();

    //初始化播放器
    initPlayer();

    //绑定信号和槽函数
    connectSignalAndSlots();

    //初始化数据库
    initSQlite();

    //初始化musiclist并同步数据到页面中
    initMusiclist();
}

注意:上面的初始化需要在初始化数据库后面调用,数据库没有建立连接,将不能查询

七、功能查补

7.1 BtForm上动画问题

当播放不同页面歌曲时,BtForm按钮上的跳动动画应该跟随播放页面变化而变化,即那个page页面播放,就应该让该页面的对应BtForm上的动画显示,其余BtForm按钮上的动画隐藏,这样跳动的⾳符始终就可以标记当前正在播放的页面,LXmusic类中currentPage标记当前播放页面,QStackedWidget中提供了通过页面找索引的方法,即currentPage可以找到其在层叠窗口中的索引,该索引与BtForm中的pageId是对应的。因此在LXMusic中定义updateBtFormAnimal函数,该函数实现原理如下:

  1. 通过stackedwidget对象获取到当前播放页面的索引值,然后找到BtForm类型对象的所有孩子,进行判断,如果是当前播放页面就显示动画,如果不是那么就隐藏动画
  2. 绑定播放器的播放状态改变信号,当播放状态改变时,调用该函数进行判断
//lxmusic.h
private slots:
void updateBtFormAnimation()
//lxmusic.cpp
void LXMusic::updateBtFormAnimation()
{
    int index = ui->stackedWidget->indexOf(_currentPlayPage);
    if(-1 == index){
        qDebug() << "该页面不存在";
        return ;
    }

    QList<BtForm*> btlist = findChildren<BtForm*>();
    for(auto& btForm : btlist){
        if(btForm->getPageId() != index){
            btForm->hideAnimation();
        }
        else{
            btForm->showAnimation();
        }
    }
}

//修改函数
void LXMusic::onBtformClicked(int pageId)
{
    //获取btform类型的孩子
    QList<BtForm*> btFormList = this->findChildren<BtForm*>();
    for(auto btForm : btFormList){
        if(btForm->getPageId() != pageId){
            btForm->clearBackground();
        }
        else{
            btForm->setBackground();
        }
    }

    ui->stackedWidget->setCurrentIndex(pageId);


   // qDebug() << pageId;
}

//绑定信号槽
//当播放状态发生改变的时候,更新BtForm上的动画
connect(_player,&QMediaPlayer::playbackStateChanged, this, &LXMusic::updateBtFormAnimation);

7.2 点击BtForm偶尔窗⼝乱移问题

正常情况下,当按钮在非BtForm按钮上点击时,左键长按拖拽窗口才能移动。但是当点击BtForm按钮时,偶尔也能看到窗口移动,估计可能在BtForm上点击时,由于手抖或者其他原因,该次点击时间被qt识别成鼠标移动事件处理了,因此导致窗口移动了。

解决⽅案:在LXMusic类中设置⼀个成员变量isDrag,当鼠标点击时间发生时,如果点在BtForm按钮上则将isDrag设置为false,否则设置为true,此时如果鼠标长按滚动时,再移动窗口

//lxmusic.h
private:
bool _isDrag;//标志位,防止点击BtForm的时候窗口乱移
//lxmusic.cpp,修改函数
void LXMusic::onBtformClicked(int pageId)
{
    //获取btform类型的孩子
    QList<BtForm*> btFormList = this->findChildren<BtForm*>();
    for(auto btForm : btFormList){
        if(btForm->getPageId() != pageId){
            btForm->clearBackground();
        }
        else{
            btForm->setBackground();
        }
    }

    ui->stackedWidget->setCurrentIndex(pageId);

    //设为false防止乱移
    _isDrag = false;


   // qDebug() << pageId;
}

void LXMusic::mousePressEvent(QMouseEvent* event){
    if(event->button() == Qt::LeftButton){
        _isDrag = true;

        _dragPosition = event->globalPosition().toPoint() - geometry().topLeft();
        event->accept();
    }
    QWidget::mousePressEvent(event);

}

void LXMusic::mouseMoveEvent(QMouseEvent* event){
    if(event->buttons() == Qt::LeftButton && _isDrag){
        move(event->globalPosition().toPoint() - _dragPosition);
        event->accept();
    }
    QWidget::mouseMoveEvent(event);
}

7.3 点击添加按钮歌曲重复加载问题

当点击加载按钮对同⼀个目录下歌曲重复加载时,仍能加载到程序中并更新到数据库,正常情况下相同目录中的歌曲只能在播放器中加载一份。出现该问题的原因,是加载时,未对已经存在的歌曲⽂件进行过滤导致,因此在addMusicsByUrl中检测下,如果歌曲已经存在则无需解析,也无需加载。

如何知道歌曲是否已经被加载过了呢?最简单的方式是直接到musicList中查找,但是musicList为线性的QVector,循环遍历效率太低,因此可以借助QSet容器(相当于C++标准库中的unordered_set),将歌曲的路径保存⼀份(同⼀个电脑上,文件路径不可能重复),在进行歌曲加载时,先检测歌曲⽂件是否存在,如果不存在则添加否则不添加

//musiclist.h
#include<QSet>
QSet<QString> _musicPaths;
//musiclist.cpp
//修改函数
//通过url加载文件到歌曲列表
void MusicList::addMusicByUrl(const QList<QUrl> &urls)
{
    for(const auto& url : urls){
        //检查音乐文件是否已经添加过
        QString musicPath = url.toLocalFile();
        if(_musicPaths.contains(musicPath))
            continue;

        //分析文件类型,如果是音乐类型那么就通过url构建music对象,保存到musicList中
        QMimeDatabase mimedata;
        QMimeType mime = mimedata.mimeTypeForFile(url.toLocalFile());
        if(mime.name() != "audio/mpeg" && mime.name() != "audio/mflac" && mime.name() != "audio/mp3")
        {
            continue;
        }

        //如果歌曲还没有被加载过,同时是一个有效的歌曲文件,加载进歌曲列表中
        _musicPaths.insert(musicPath);

        // 如果是⾳乐⽂件,加⼊歌曲列表
        _musicList.push_back(url);
    }

}

//从数据库中读取数据
void MusicList::readFromDB()
{
    QString sql("SELECT musicId, musicName, musicSinger, albumName,\
    duration, musicUrl, isLike, isHistory FROM musicInfo");
    QSqlQuery query;
    if(!query.exec(sql))
    {
        qDebug()<<"数据库查询失败";
        return;
    }
    qDebug() << "查询完成";
    while(query.next())
    {
        //qDebug() << query.value(0).toString();
        Music music;
        music.setMusicId(query.value(0).toString());
        music.setMusicName(query.value(1).toString());
        music.setSingerName(query.value(2).toString());
        music.setAlbumName(query.value(3).toString());
        music.setDuration(query.value(4).toLongLong());
        music.setMusicUrl("file:///" + query.value(5).toString());
        music.setIsLike(query.value(6).toBool());
        music.setIsHistory(query.value(7).toBool());
        _musicList.push_back(music);

        _musicPaths.insert(music.getMusicUrl().toLocalFile());

    }
    // for(auto& url : _musicPaths){
    //     qDebug() << url;
    // }
}
//修改
//music.cpp
QString Music::getLrcFilePath() const
{
   //qDebug() << _musicUrl;
    //QString path = _musicUrl.toString();
    QString path = _musicUrl.toLocalFile();
    //qDebug() << path;
    path.replace(".mp3", ".lrc");
    path.replace(".flac", ".lrc");
    path.replace(".mpga", ".lrc");
    return path;
}

7.4 添加系统托盘

当点击关闭按钮时,不让播放器直接退出,而是将窗口隐藏掉,窗口的图标在系统托盘位置,当在播放器图标上右键单击时,弹出菜单让用户选择是显示窗口还是继续退出

//lxmusic.h
#include <QSystemTrayIcon>
#include <QMenu>
public:
void createTray();
private slots:
void quitLXMusic();
//lxmusic.cpp
//初始化系统托盘
void LXMusic::createTray()
{
    //增加系统托盘
    QSystemTrayIcon* tryIcon = new QSystemTrayIcon(this);
    tryIcon->setIcon(QIcon(":/images/logo.png"));
    //创建托盘菜单
    QMenu* menu = new QMenu(this);
    menu->addAction("还原",this, &QWidget::showNormal);
    menu->addSeparator();
    menu->addAction("退出", this, &LXMusic::quitLXMusic);

    //将托盘菜单添加到托盘图标
    tryIcon->setContextMenu(menu);

    tryIcon->showMessage("提示", "应用程序已最小化到托盘", QSystemTrayIcon::Information, 3000);

    //为托盘图标添加点击事件处理。当左键点击时还原窗口
    connect(tryIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
        if (reason == QSystemTrayIcon::Trigger) { // 左键点击
            this->showNormal(); // 还原窗口
        }
    });

    //显示系统托盘
    tryIcon->show();
}

void LXMusic::quitLXMusic()
{
    qDebug() << "退出按钮被点击";
    //关闭前写入数据库中
    _musicList.writeToDB();
    qDebug() << "写入完成";
    //关闭数据库
    sqlite.close();
    //关闭程序
    QApplication::quit();
}


void LXMusic::on_quitButton_clicked()
{
    // 点击关闭按钮时,程序不退出,隐藏掉
    // ⽤⼾可以从系统托盘位置选择显⽰或者关闭
    hide();
}

7.5 保证程序只运行一次

现在每点击⼀次LXMusic.exe,都会创建⼀个LXMusic的实例,即同⼀个机器上可以运行多份程序实例,多个实例同时运⾏有以下缺陷:

  • 多个实例同时运行可能会导致资源浪费,如内存、CPU效率等
  • 如果应用程序涉及对共享数据的修改,多个程序同时运行可能会导致数据不⼀致问题
  • 若多个实例尝试访问同⼀资源时,如文件、数据库等,可能会导致冲突或错误
  • 另外,用户体验不是很好,多个实例操作时容易混淆因此有时会禁止程序多开,即⼀个应用程序只能运行⼀个实例,也称为单实例应用程序或单例应⽤程序

在Qt中,禁止程序多开的方式有好几种,此处采用共享内存实现。

共享内存是操作系统中的概念,是进程间通信的⼀种机制。由于相同key值的共享内存只能存在⼀份,因此在程序启动时可以检测共享内存是否已经被创建,如果已经创建则说明程序已经在运行,否则程序还没有运行

//mian.cpp
// 创建共享内存
QSharedMemory sharedMem("LXMusic");
// 如果共享内存已经被占⽤,说明已经有实例在运⾏
if (sharedMem.attach()) {
    QMessageBox::information(nullptr, "QQMusic", "QQMusic已经在运⾏...");
    return 0;
}
sharedMem.create(1);

7.6 禁⽌qDebug()输出

要逐个删除程序中qDebug的打印太麻烦,可以再配置文件中通过添加以下语句,禁止qDebug输出,只能禁止release模式下的输出,debug模式下不禁止:

//LXMusic.pro
# ban qDebug output
DEFINES += QT_NO_DEBUG_OUTPUT

八、项目思维导图

在这里插入图片描述
项目到这就结束了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冧轩在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值