目录
[1. 项目演示](#1. 项目演示)
[2.2 界面开发](#2.2 界面开发)
[2.3 界面美化](#2.3 界面美化)
[2.3.2 添加图片资源](#2.3.2 添加图片资源)
[2.3.3 head处理](#2.3.3 head处理)
[2.3.4 播放控制区处理](#2.3.4 播放控制区处理)
[3. 自定义控件](#3. 自定义控件)
[3.1 BtForm](#3.1 BtForm)
[3.1.1 BtForm界面设计](#3.1.1 BtForm界面设计)
[3.1.2 BtForm类中实现](#3.1.2 BtForm类中实现)
[3.2 推荐页面](#3.2 推荐页面)
[3.2.1 推荐页面分析](#3.2.1 推荐页面分析)
[3.2.2 推荐页布局](#3.2.2 推荐页布局)
[3.2.3 自定义recBox](#3.2.3 自定义recBox)
[3.2.4 自定义recBoxItem](#3.2.4 自定义recBoxItem)
[3.2.5 RecBox添加RecBoxItem](#3.2.5 RecBox添加RecBoxItem)
[3.2.6 RecBox中btUp和btDown按钮clicked处理](#3.2.6 RecBox中btUp和btDown按钮clicked处理)
[3.3 自定义CommonPage](#3.3 自定义CommonPage)
[3.3.1 CommonPage页面分析](#3.3.1 CommonPage页面分析)
[3.3.2 CommonPage页面布局](#3.3.2 CommonPage页面布局)
[3.3.3 CommonPage界面设置和显示](#3.3.3 CommonPage界面设置和显示)
[3.4 自定义ListItemBox](#3.4 自定义ListItemBox)
[3.4.1 ListItemBox页面分析](#3.4.1 ListItemBox页面分析)
[3.4.2 ListItemBox页面布局](#3.4.2 ListItemBox页面布局)
[3.4.3 ListltemBox显示测试](#3.4.3 ListltemBox显示测试)
[3.4.4 支持hover效果](#3.4.4 支持hover效果)
[3.5 自定义MusicSlider](#3.5 自定义MusicSlider)
[3.6 自定义VolumeTool](#3.6 自定义VolumeTool)
[3.6.1 VolumeTool控件分析](#3.6.1 VolumeTool控件分析)
[3.6.2 VolumeTool界面布局](#3.6.2 VolumeTool界面布局)
[3.6.3 界面设置](#3.6.3 界面设置)
[3.6.4 界面创建及弹出](#3.6.4 界面创建及弹出)
[3.6.5 绘制三角](#3.6.5 绘制三角)
[4. 音乐管理](#4. 音乐管理)
[4.1 音乐加载](#4.1 音乐加载)
[4.2 MusicList类](#4.2 MusicList类)
[4.2.1 添加C++类MusicList](#4.2.1 添加C++类MusicList)
[4.2.2 歌曲对象存储](#4.2.2 歌曲对象存储)
[4.3 Music类](#4.3 Music类)
[4.3.1 Music类介绍](#4.3.1 Music类介绍)
[4.3.2 解析音乐文件元数据](#4.3.2 解析音乐文件元数据)
[4.4 音乐分类](#4.4 音乐分类)
[4.5 更新Music信息到ComonPage界面](#4.5 更新Music信息到ComonPage界面)
[4.6 CommonPage显示不足处理](#4.6 CommonPage显示不足处理)
[4.7 音乐收藏](#4.7 音乐收藏)
[4.7.1 我喜欢图标处理](#4.7.1 我喜欢图标处理)
[4.7.2 点击我喜欢按钮处理](#4.7.2 点击我喜欢按钮处理)
[5.1 QMediaPlayer类](#5.1 QMediaPlayer类)
[5.1.1 QMediaPlayer类说明](#5.1.1 QMediaPlayer类说明)
[5.1.2 属性和方法](#5.1.2 属性和方法)
[5.2 QMediaPlaylist类](#5.2 QMediaPlaylist类)
[5.2.1 QMediaPlaylist类介绍](#5.2.1 QMediaPlaylist类介绍)
[5.2.2 属性和方法](#5.2.2 属性和方法)
[5.3 歌曲播放](#5.3 歌曲播放)
[5.3.1 播放媒体和播放列表初始化](#5.3.1 播放媒体和播放列表初始化)
[5.3.2 播放列表设置](#5.3.2 播放列表设置)
[5.3.3 播放和暂停](#5.3.3 播放和暂停)
[5.3.4 上一曲和下一曲](#5.3.4 上一曲和下一曲)
[5.3.5 播放模式设置](#5.3.5 播放模式设置)
[5.3.6 播放所有](#5.3.6 播放所有)
[5.3.7 双击CommPage页面QListWidget项播放](#5.3.7 双击CommPage页面QListWidget项播放)
[5.3.8 最近播放同步](#5.3.8 最近播放同步)
[5.3.9 音量设置](#5.3.9 音量设置)
[5.3.10 当前播放时间和总时间更新](#5.3.10 当前播放时间和总时间更新)
[5.3.11 进度条处理[seek功能]](#5.3.11 进度条处理[seek功能])
[5.3.12 歌曲名称、歌手和封面图片同步](#5.3.12 歌曲名称、歌手和封面图片同步)
[5.4 lrc歌词同步](#5.4 lrc歌词同步)
[5.4.1 Irc歌词界面分析](#5.4.1 Irc歌词界面分析)
[5.4.2 Irc歌词界面布局](#5.4.2 Irc歌词界面布局)
[5.4.3 LrcPage显示](#5.4.3 LrcPage显示)
[5.4.4 LrcPage添加动画效果](#5.4.4 LrcPage添加动画效果)
[5.4.5 Irc歌词解析和同步](#5.4.5 Irc歌词解析和同步)
[6. 持久化支持](#6. 持久化支持)
[6.1 SQLite数据库介绍](#6.1 SQLite数据库介绍)
[6.2 QSqlDatabase类介绍](#6.2 QSqlDatabase类介绍)
[6.2.1 数据库连接和关闭](#6.2.1 数据库连接和关闭)
[6.2.2 创建表](#6.2.2 创建表)
[6.2.3 插入数据](#6.2.3 插入数据)
[6.2.4 查询数据](#6.2.4 查询数据)
[6.2.5 更新数据](#6.2.5 更新数据)
[6.2.6 删除数据](#6.2.6 删除数据)
[6.3 MiniMusic中数据库支持](#6.3 MiniMusic中数据库支持)
[6.3.1 数据库初始化](#6.3.1 数据库初始化)
[6.3.2 歌曲信息写入数据库](#6.3.2 歌曲信息写入数据库)
[6.3.3 程序启动时读取数据库恢复歌曲数据](#6.3.3 程序启动时读取数据库恢复歌曲数据)
[7. 边角问题处理](#7. 边角问题处理)
[7.1 更换主窗口图标](#7.1 更换主窗口图标)
[7.2 处理最大化、最小化按钮](#7.2 处理最大化、最小化按钮)
[7.3 歌词按钮的样式](#7.3 歌词按钮的样式)
[7.4 CommonPage中滚动条格式](#7.4 CommonPage中滚动条格式)
[7.5 BtForm上动画问题](#7.5 BtForm上动画问题)
[7.6 点击BtForm偶尔窗口乱移问题](#7.6 点击BtForm偶尔窗口乱移问题)
[7.7 点击添加按钮歌曲重复加载问题](#7.7 点击添加按钮歌曲重复加载问题)
[7.8 添加系统托盘](#7.8 添加系统托盘)
[7.9 保证程序只运行一次](#7.9 保证程序只运行一次)
[7.10 禁止qDebug()输出](#7.10 禁止qDebug()输出)
[7.11 项目打包](#7.11 项目打包)
[7.11.1 为什么要打包](#7.11.1 为什么要打包)
[7.11.2 windeployqt打包工具](#7.11.2 windeployqt打包工具)
[7.11.3 打包步骤](#7.11.3 打包步骤)
[8. 项目总结和面试](#8. 项目总结和面试)
[8.11 项目总结](#8.11 项目总结)
[8.3 项目扩展](#8.3 项目扩展)
1. 项目演示
该项目是基于QT开发的音乐播放软件,界面友好,功能丰富。主要功能如下:
【窗口head部分】
- 点击最小化按钮,窗口最小化
- 点击最大化按钮,窗口无反应(即禁止窗口最大化)
- 点击关闭按钮,程序退出
- 点击皮肤按钮,更换皮肤(该功能暂未支持)
- 搜索框搜索功能(该功能暂未支持)
【窗口body左侧】
- 点击推荐按钮,窗口右侧显示:推荐Page
- 点击电台按钮,窗口右侧显示:电台Page
- 点击音乐馆按钮,窗口右侧显示:音乐馆Page
- 点击我喜欢按钮,窗口右侧显示:收藏的音乐Page
- 点击本地下载按钮,窗口右侧显示:本地音乐Page
- 点击最近播放按钮,窗口右侧显示:最近播放Page
注意:左侧按钮,当光标悬停时会有不同颜色突出显示,当点击时会有绿色显示,并且按钮的
右侧有跳动的竖条。
【窗口右侧】
当窗口左侧不同按钮点击,在窗口右侧会展示不同的页面,本项目暂只支持了本地音乐、喜欢音乐、最近播放音乐的展示。具体功能如下:
- 点击全部播放按钮,播放当前页面列表中所有音乐
- 双击列表中某音乐,播放当前选中音乐
- 点击心心支持收藏
- 支持最近播放过音乐记忆
【播放控制区】
- 支持seek功能,即拖拽到歌曲指定位置播放
- 支持:随机、单曲循环、循环播放
- 支持播放上一曲
- 支持播放下一曲
- 支持播放和暂停
- 支持音量调节和静音
- 支持歌曲总时长显示+当前播放时间显示
- 支持LRC歌词同步显示
2.界面开发
2.1界面简要分析
界面上控件比较多,归类之后主要分为两部分:head区和body区。

head区域从左往右依次为:图标、搜索框、更换皮肤按钮、最小化&最大化&退出按钮。

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

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

①说明区域,实际为QLabel
②自定义控件(按钮的扩展):图片+文本+动画
③同②,自定义控件(按钮的扩展):图片+文本+动画
④同②,自定义控件(按钮的扩展):图片+文本+动画
Body右侧区域由:Page区、播放进度、播放控制区三部分构成。

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

电台
音乐馆
我喜欢

本地下载

最近播放

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

整个页面内容可以分为上下两组:今日为你推荐、你的歌曲补给站。两组的布局实际是相同的,元素说明:
- 上方显示1行,内部有4个推荐元素;下方显示2行,每行有4个推荐元素
- 左右两侧一个按钮,点击后推荐内容会更换下一批,不停点击会循环推荐
- 当鼠标悬停在推荐元素上时,推荐元素会向上移动,当鼠标离开时,又回到原位置
- 当鼠标悬停在推荐元素上时,同时会出现小手图标,说明该推荐元素具有点击功能
该页面中内容也为自定义元素,后序页面实现时具体分析。
我喜欢、本地下载、最近播放类似下图:

这三个Page中布局、控件都是相同的,只是填充的数据不一样。每个Page中包含了多个控件,大致如下:
①QLabel:类型说明
②QLabel:图片显示
③QButton:播放全部按钮
④一组QLabel说明:音乐、歌手、专辑
⑤QListWidget:播放列表
可以通过自定义控件的方式,将①~5的控件集成到一起形成一个新的控件,方便复用,因此这三个Page属于同一个自定义类型的Page。
这六个页面,将来由QStackedWidget控件组织起来,就可以实现点击不同按钮,显示不同页面效果。
【歌词页面】
解析当前正在播放音乐的歌词,同步显示在界面上。

显示内容分为:歌曲信息、歌词部分、左上方收起隐藏按钮。
- 歌曲信息由歌曲名称(QLabel)和歌手名称(QLabel)构成
- 歌词部分展示当前在唱歌词(QLabel)和在唱部分前三行和后三行歌词(QLabel)展示,当前播放歌词突出显示
- 点击收起按钮后,该页面会以动画的方式收起
当歌曲有LRC歌词时,播放时歌词会随播放时间自动调整;歌曲没有LRC歌词时,歌词部分显示空字符。
以上对本项目的界面进行了简单的说明,大家先有个初步了解,接下来利用QTDesigner完成界面的布局。
2.2 界面开发
2.2.1创建工程
创建一个基于QWidget的工程,选中生成form选项,将来界面部分主要使用QDesigner来设计。

2.2.2主界面布局设计
基于Widget局部
QT系统提供4种布局管理器:
- QHBoxLayout:水平布局
- QVBoxLayout:垂直布局
- QGridLayout:栅格布局
- QFormLayout:表单布局

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

因此本项目基于Widget来进行布局。
窗口主框架设计
【主窗口的布局】
①选中MiniMusic,在弹出的属性中找到geometry属性,将窗口宽度修改为:1040,高度修改为700

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

为了看到效果,选中backroound控件,然后右键单击,弹出菜单中选择改变样式表,内部添加:
css
background-color:gray;
点击OK就能看到灰色的背景效果了。
注意:此处的颜色效果仅为方便看到界面效果,等界面框架设计完成后,将所有的颜色清除掉,界面添加特定颜色。
整个窗口由head和body上下两部分组成。
直接拖两个Widget放到设计区,双击将名字修改为head和body;
修改背景颜色方便查看效果,head背景色修改为green,body背景色修改为pink;
css
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内部设计
head内部由两部分构成,headLeft区域显示图标,headRight区域为搜索框和功能按钮区域。
拖两个widget到head中,将objectName修改为headLeft和headRight,背景颜色修改为:
css
headLeft background-color:yellow;
headRight background-color:blue;
然后选中head对象,点击水平布局(垂直布局左侧就是水平布局),就会呈现如下布局:

继续选中head对象,下滑找到Layout属性,将Margin和Spacing全部设置为0;
选中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,背景颜色分别修改为:
css
SearchBox background-color:red;
SettingBox background-color:orange;
选中headRight,然后点击水平布局,并将headRight的Margin和Spacing修改为0,就能看到下面的效果。

【searchBox】
拖一个QLineEdit进去,然后选中searchBox点击水平布局。
【settingBox】
拖拽一个按钮到SettingBox,按钮的minimumSize和maximumSize的宽度和高度都修改为30然后
鼠标选中,按着ctrl键+鼠标拖拽,复制3个出来摆放好,依次将四个按钮的objectName从左往右修改为:skin、max、min、quit,并将按钮的text属性也修改为空,将来设置图片。
在控件区域找到Spacers,找到Horizontal Spacer控件,拖拽到SettingBox区域

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

Body部分布局
整个body部分是由bodyLeft和bodyRight两部分组成。
①拖两个Widget到Body中,将objectName修改为bodyLeft和bodyRight
②将bodyLeft颜色修改为:
css
bodyLeft background-color:#f0f0f0
bodyRight background-color:#f5f5f5
③选中body,点击水平布局,将bodyLeft的minimumSize和maxmumSize的宽度修改为200
④选中Body,将body的Margin和Spacing修改为0

bodyLeft内部布局
①拖拽一个Widget到bodyLeft,将objectName修改为leftBox,背景颜色修改为:background-color:pink;
②拖拽Vertical Spacer到bodyLeft
③选中leftBox,将minmumSize和maxmumSize的高度修改为400
④选中bodyLeft,点击垂直布局,并将bodyLeft的Margin和Spacing修改为0

leftBox内部布局
leftBox内部包含:在线音乐和我的音乐两部分。
①拖拽两个Widget到leftBox中,将objectName依次修改为:onlineMusic和myMusic
②颜色分别修改为:
css
onlineMusic background-color:#f0f0f0
myMusic background-color:#f5f5f5
③选中leftBox,点击垂直布局,然后将Margin和Spacing设置为0
④onlineMusic 和myMusic内部的元素都是相同的,由一个QLabel和三个Widget构成,后期Widget
会替换为自定义按钮,此处先用Widget占位。因此分别向onlineMusic和myMusic内部拖拽一个QLabel和三个QWidget,并选中onlineMusic和myMusic点击垂直布局,然后将Margin和Spacing设置为0

bodyRight布局
bodyRight由层叠窗口、进度滑竿、播放控制区三部分组成。
①拖拽层叠窗口控件Stacked Widget,就在Widget控件上方到bodyRight中
②拖拽Widget到bodyRight,将objectName修改为processBar,将minimumSize和maximumSize的高度修改为30,背景颜色修改为绿色
③拖拽Widget到bodyRight,将objectName修改为controlBox,将minmumSize高度修改为60
④选中bodyRight,点击垂直布局,然后将bodyRight的Margin和Spacing修改为0
⑤为了能看到效果,将processBar颜色修改为:background-color:pink;

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

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

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

ControlBox内部布局
该区域内部由三部分组成:歌曲信息部分、播放控制部分、时间显示
①拖拽三个Widget到ControlBox中,将ObjectName依次修改为play1、play2、play3颜色依次修改为:
css
play1 background-color:#FFFAFA;
play2 background-color:#F8F8FF;
play3 background-color:#FFFAF0;
②选中ControlBox,点击水平布局,将ControlBox的Margin和Spacing修改为0

play1内部:
拖拽3个QLabel,放置歌曲图片、歌手名和歌曲名字,调整好位置,将QLabel的objectName修改为:musicCover、musicName、musicSinger
然后选中playl,点击栅格布局
play2内部:
从左到右依次摆放6个按钮,按钮的minimumSize和maxmumSize均修改为30*30,将objectName从左往右依次修改为:playMode、playUp、Play、playDown、volume、addLocal;
然后选中play2,点击水平布局,并将play_2的Margin和Spacing修改为0
play3内部:
拖四个QLabel和一个按钮,调整大小位置,从左往右QLabel的objectName依次修改为:labelNull、currentTime、line、totalTime,按钮的objectName修改为lrcWord,按钮的maxmumSize的宽度和高度修改为30*30;
选中play3,点击水平布局,并将play2的Margin和Spacing修改为0


2.3 界面美化
2.3.1主窗口设定
仔细观察发现主窗口是没有标题栏,因此在窗口创建前,就需要设置下窗口的格式。
cpp
QWidget::setWindowFlag(...); //设置窗⼝格式,⽐如创建⽆边框的窗⼝
由于窗口中控件比较多,这些控件将来都需要初始化,如果将所有代码放在miniMusic的构造函数中实现,将来会造成构造函数非常臃肿,因此在miniMusic类中添加initUl(方法来完成界面初始化工作。
cpp
// minimusic.h文件中添加
void initUI();
// 添加完成后,光标放在函数名字上按 alt + Enter 组合键完成方法定义
// minimusic.cpp头文件中完成定义
void MiniMusic::initUI()
{
// 设置无边框窗口,即窗口将来无标题栏
setWindowFlag(Qt::WindowType::FramelessWindowHint);
}
添加完成后一定要在mimusic的构造函数中调用initUI()函数,否则设置不会生效。
运行后,发现有以下两个问题:
- 窗口无标题栏,找不到关闭按钮,导致窗口无法关闭
- 窗口无法拖拽
关闭窗口,可以先将光标放在任务栏中当前应用程序图标上,弹出的框中选择关闭,后序会实现关闭功能。

主界面无法拖动,此时只需要处理下鼠标单击(mousePressEvent和鼠标移动(mouseMoveEvent)事
件即可。
鼠标左键按下时,记录下窗口左上角和鼠标的相对位置
鼠标移动时,会产生新的位置,保持鼠标和窗口左上角相对位置不变,通过move修改窗口的左上角坐标即可。
cpp
// minimusic.h中添加
protected:
// 重写QWidget类的鼠标点击和鼠标滚轮时间
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
// 记录光标相对于窗口标题栏的相对距离
QPoint dragPosition;
// minimusic.cpp中添加
void MiniMusic::mousePressEvent(QMouseEvent *event)
{
// 拦截鼠标左键单击事件
if(event->button() == Qt::LeftButton)
{
/*
QPoint point = event->globalPos();// 鼠标按下事件发生时,光标相对于屏幕左上角的位置
QPoint pos = this->frameGeometry().topLeft(); // 鼠标按下事件发生时,窗口左上角位置
QRect poz = geometry(); //不包含边框及顶部标题区的范围
QRect poa = frameGeometry(); // 包含边框及顶部标题区的范围
QPoint cha = event->globalPos() - frameGeometry().topLeft(); // 即为鼠标按下时,窗口左上角和光标之间的距离差
*/
// 想要窗口鼠标按下时窗口移动,只需要在mouseMoveEvent中,让光标和窗口左上角保持相同的位置差
// 获取鼠标相对于屏幕左上角的全局坐标
dragPosition = event->globalPos() - frameGeometry().topLeft();
return;
}
// 剩下的交给系统正常处理
QWidget::mousePressEvent(event);
}
void MiniMusic::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() == Qt::LeftButton)
{
// 更具鼠标移动更新窗口位置
move(event->globalPos() - dragPosition);
return;
}
// 剩下的交给系统正常处理
QWidget::mouseMoveEvent(event);
}
给窗口添加阴影需要用到QGraphicsDropShadowEffect类,具体步骤如下:
- 创建QGraphicsDropShadowEffect类对象
- 设置阴影的属性。比如:设置阴影的偏移、颜色、圆角等
- 将阴影设置到具体对象上
在initUI()函数中添加如下代码:
cpp
// 设置窗口背景透明
this->setAttribute(Qt::WA_TranslucentBackground);
// 给窗口设置阴影效果
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0,0); // 设置阴影偏移
shadowEffect->setColor("#000000"); // 设置阴影颜色:黑色
shadowEffect->setBlurRadius(10); // 设置阴影的模糊半径
this->setGraphicsEffect(shadowEffect);
注意:给窗口设置阴影效果时,需要将窗口标题无边框,背景设置为透明。
2.3.2 添加图片资源
添加一个qrc文件,将图片资源拷贝到工程目录下,并添加到工程中。

将之前布局时所有按钮的背景颜色全部清除掉,按照下面的风格重新设定
2.3.3 head处理
headLeft
css
#headLeft
{
background-color:#F0F0F0;/*背景颜色设置为浅灰色*/
}
headRight
css
#headRight
{
background-color:#F5F5F5;/*设置背景颜色为亮灰色*/
}
logo
css
#logo
{
border-radius:0px;
background-image:url(:/images/Logo.png);
background-repeat:no-repeat;
border:none;
background-position:center center;
}
lineEdit
css
#lineEdit
{
background-color:#E3E3E3; /*设置背景颜色*/
border-radius:17px; /*设置四个角的圆角*/
padding-left:17px; /*内部文字到边的距离*/
}
settingBox
css
/*类型选择器*/
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); /*设置背景颜色为半透明的红色*/
}
skin
css
background-image:url(:/images/skin.png);
max
css
background-image:url(:/images/max.png);
min
css
background-image:url(:/images/min.png);
quit
css
background-image:url(:/images/quit.png);
bodyLeft
css
#bodyLeft
{
background-color:#F0F0F0;/*设置背景颜色为浅灰色*/
}
bodyRight
css
#bodyRight
{
background-color:#F5F5F5; /*设置背景颜色为亮灰色*/
}
2.3.4 播放控制区处理
祛除playl、play2、play3的页面布局时设置的临时背景色。
将按钮上的文字全部去除,然后重新添加样式和图片。
play2
css
QPushButton
{
border:none; /*去除边框*/
}
/*悬停状态*/
QPushButton:hover
{
/*设置背景颜色为半透明的红色*/
background-color:rgba(220,220,220,0.5);
}
playMode
css
#playMode
{
/* 背景图路径 */
background-image:url(:/images/shuffle_2.png);
/* 关键:禁止平铺,只显示一张原图 */
background-repeat: no-repeat;
/* 关键:让图片在控件内居中显示 */
background-position: center center;
}
playUp
css
#playUp
{
background-image:url(:/images/up.png);
background-repeat:no-repeat;
background-position:center center;
}
play
css
#play
{
background-image:url(:/images/play3.png);
background-repeat:no-repeat;
background-position:center center;
}
playDown
css
#playDown
{
background-image:url(:/images/down.png);
background-repeat:no-repeat;
background-position:center center;
}
volume
css
#volume
{
background-image:url(:/images/volumn.png);
background-repeat:no-repeat;
background-position:center center;
}
addLocal
css
#addLocal
{
background-image:url(:/images/add.png);
background-repeat:no-repeat;
background-position:center center;
}
lrcWord
css
#lrcWord
{
background-image:url(:/images/ci.png);
border:none; /*去除边框*/
background-repeat:no-repeat;
background-position:center center;
}
QPushButton:hover
{
/*设置背景颜色为半透明的红色*/
background-color:rgba(220,220,220,0.5);
}
3. 自定义控件
3.1 BtForm
3.1.1 BtForm界面设计
添加一个新设计界面,命名为BtForm。

该控件实际由:图片、文字、动画三部分组成。图片和文字分别用QLabel展示,动画部分内部实际为4个QLabel。
①将BtForm的geometry的宽度和高度修改为200*35。
②拖一个Widget到btForm中,objectName修改为btStyle,将btForm的margin和Spacing设置为0.
③拖2个QLable和1个Widget到btStyle中,并将objectName依次修改为btlcon、btText、lineBoxbtlcon的minimumSize和maximumSize的宽度设置为30(为了看到效果可将颜色设置为red)
btText的minimumSize和maximumSize的宽度设置为9o(为了看到效果可将颜色设置为green)
lineBox的minimumSize和maximumSize的宽度设置为30
然后选中btStyle,并将其margin和Spacing设置为0
④然后往lineBox内部拖4个QLabel,objectName依次修改为linel、line2、line3、line4,minimumSize和maximumSize的宽度均设置为2


btStyle
css
#btStyle:hover
{
background:#D8D8D8;
}
lineBox
css
.QLabel
{
background-color:#FFFFFF;
}
将bodyLeft内部onlineMusic和MyMusic中的QWidget全部提升为BtForm。具体操作:
选中要提升的控件,比如:Rec,在弹出的菜单中选择提升为,会出现一个新窗口(如下右侧图),在提升的类名称中输入要提升为的类型BtForm,然后点击添加,最后选中btform.h点击提升,便可以将Rec由QWidget提升为自定义的BtForm类型。


3.1.2 BtForm类中实现
1.设置按钮上的图片和文字信息,以及该按钮关联的page页面
cpp
// btform.h中新增
// 按钮id:该按钮对应的page页
int id = 0;
// 设置图标 文字 id
void seticon(QString btIcon,QString btText,int mid);
// 在brform.cpp中新增
void btForm::seticon(QString btIcon, QString btText, int mid)
{
// 设置自定义按钮的图片、文字、以及id
ui->btIcon->setPixmap(QPixmap(btIcon));
ui->btText->setText(btText);
id = mid;
}
在miniMusic.cpp的initUI()函数中新增:
cpp
void MiniMusic::initUI()
{
//...
// 设置BodyLeft中6个btForm的信息
ui->rec->seticon(":/images/rec.png","推荐",1);
ui->music->seticon(":/images/music.png","音乐馆",2);
ui->audio->seticon(":/images/radio.png","电台",3);
ui->like->seticon(":/images/like.png","我喜欢",4);
ui->local->seticon(":/images/local.png","本地下载",5);
ui->recent->seticon(":/images/recent.png","最近播放",6);
}
- 按钮响应
重写鼠标mousePressEvent,当按钮按下时:
①按钮颜色发生变化
②给miniMusic类发送click信号
cpp
// btform.h中新增
protected:
// 鼠标点击事件
void mousePressEvent(QMouseEvent *event);
signals:
// btForm按键点击信号
void click(int id);
// btform.cpp新增
void btForm::mousePressEvent(QMouseEvent *event)
{
// 告诉编译器不要触发警告
(void)event;
// 鼠标点击之后,背景变为绿色,文字变为白色
ui->btStyle->setStyleSheet("#btStyle{background:rgb(30,206,154);}*{color:#F6F6F6;}");
// 发送鼠标点击信号
emit click(this->id);
}
③miniMusic类处理该信号,内部:实现窗口切换,并清除上次按钮点击留下的样式,因此miniMuisc中需要新增:
cpp
// minimusic.h 新增
// btForm点击槽函数
void onBtFormClick(int id);
//btForm按钮点击信号处理
void connectSignalAndSlot();
// minimusic.cpp 新增
void MiniMusic::onBtFormClick(int id)
{
// 1.获取当前页面所有btFrom按钮类型的对象
QList<btForm*> buttonList = this->findChildren<btForm*>();
// 2.遍历所有对象,如果不是当前id的按钮,则把之前设置的背景颜色清除掉
foreach(btForm* btitem,buttonList)// 类似for循环
{
if(id != btitem->getId())
{
btitem->clearBg();
}
}
// 3.设置当前栈空间显示页面
ui->stackedWidget->setCurrentIndex(id -1);
}
//btForm按钮点击信号处理 -- 该函数需要在构造函数中调用
void MiniMusic::connectSignalAndSlot()
{
// 自定义的btForm按钮点击信号,当btForm点击后,设置对应的堆叠窗口
connect(ui->rec,&btForm::click,this,&MiniMusic::onBtFormClick);
connect(ui->music,&btForm::click,this,&MiniMusic::onBtFormClick);
connect(ui->audio,&btForm::click,this,&MiniMusic::onBtFormClick);
connect(ui->like,&btForm::click,this,&MiniMusic::onBtFormClick);
connect(ui->local,&btForm::click,this,&MiniMusic::onBtFormClick);
connect(ui->recent,&btForm::click,this,&MiniMusic::onBtFormClick);
}
④ BtForm类中新增
cpp
// btform.h 新增
public:
// 清除上一次按钮点击留下的样式
void clearBg();
// 获取id
int getId();
// btform.cpp 新增
void btForm::clearBg()
{
// 清除上一个按钮点击的背景效果,恢复之前的样式
ui->btStyle->setStyleSheet("#btStyle:hover{background:#D8D8D8;}");
}
int btForm::getId()
{
return id;
}
为了看到Page切换的效果,可以在stackedWinget的每个Page上面放一个QLabel说明。
3.BtFrom上的动画效果
Qt中QPropertyAnimation类可以提供简单的动画效果,允许对QObject获取派生类的可读写属性进行动画处理,创建平滑、连续的动画效果,比如控件的位置、大小、颜色等属性变化,使用时需包含<QPropertyAnimation>。
关键函数说明:
cpp
#include <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信号,如果需要进行额外处理时,处理该信号即可
*/
上述方法是本项目中需要用到的函数,大家可以通过Qt帮助手册了解更多。
lineBox中line1、line2、line3、line4添加动画效果,btForm类中增加如下代码:
cpp
// btform.h新增
// linebox动画起伏效果
QPropertyAnimation *animationLine1;
QPropertyAnimation *animationLine2;
QPropertyAnimation *animationLine3;
QPropertyAnimation *animationLine4;
// 动画函数
void animation(QPropertyAnimation *animationLine,int index);
// btform.cpp的构造函数中新增
btForm::btForm(QWidget *parent) :
QWidget(parent),
ui(new Ui::btForm)
{
ui->setupUi(this);
// 设置line1的动画效果
animationLine1 = new QPropertyAnimation(ui->line1,"geometry",this);
animation(animationLine1,0);
animationLine2 = new QPropertyAnimation(ui->line2,"geometry",this);
animation(animationLine2,7);
animationLine3 = new QPropertyAnimation(ui->line3,"geometry",this);
animation(animationLine3,14);
animationLine4 = new QPropertyAnimation(ui->line4,"geometry",this);
animation(animationLine4,21);
}
void btForm::animation(QPropertyAnimation *animationLine,int index)
{
animationLine->setDuration(1500);
animationLine->setKeyValueAt(0,QRect(index,15,2,0));
animationLine->setKeyValueAt(0.5,QRect(index,0,2,15));
animationLine->setKeyValueAt(1,QRect(index,15,2,0));
animationLine->setLoopCount(-1);
animationLine->start();
}
4.关于动画显示
动画并不是所有页面都显示,只有当前选中的页面显示,所以默认情况下,动画隐藏。默认情况下设置addlocal显示。
cpp
// btform.h 新增
// 显示动画效果
// 显示动画效果
void showAnimal(bool isShow);
// btform.cpp中新增
void btForm::showAnimal(bool isShow)
{
if(isShow)
{
// 显示linebox,设置颜色为绿色
ui->lineBox->show();
}
else
{
// 隐藏
ui->lineBox->hide();
}
}
// miniMusic的initUI中设置默认选中
void MiniMusic::initUI()
{
//...
// 本地下载页面btForm动画默认显示
ui->local->showAnimal(false);
ui->stackedWidget->setCurrentIndex(4);
}
3.2 推荐页面
3.2.1 推荐页面分析
仔细观察推荐页面,对其进行拆解发现,推荐页面由五部分构成:

①"推荐"文本提示,即QLabel
②"今日为你推荐"文本提示,即QLabel
③具体推荐的歌曲内容,点击左右两侧翻页按钮,具有轮番图效果,将光标放到图上,有图片上移动画。
④"你的歌曲补给站"文本提示,即QLabel
⑤具体显示音乐,和③实际是一样的,不同的是③中音乐只有一行,⑤中的音乐有两行因为页面中元素较多,直接摆到一个页面太拥挤,从右侧的滚动条可以看出,整个页面中的元素都放置在QScrollArea中。
仔细分析③发现,里面包含了:

左右各两个按钮,点击之后中间的图片会左右移动,Qt中未提供类似该种组合控件,因此③实际为自定义控件。
③中按钮之间的元素,由图片和底下的文字组成,当光标放在图片上会有上移的动画,因此该元素实际也为自定义控件。

3.2.2 推荐页布局
在stackedWidget中选中推荐页面,objectName为recPage的页面,删掉之前添加的QLabel推荐提示。
①拖拽一个QScrollArea到recPage中,geometry的宽度和高度修改为820和 500,
②拖拽一个QLable,objectName修改为recText,显示内容修改为推荐,minimumSize和maximumSize的高度均修改为50,Font大小修改为24
③再拖拽一个QLable和Widget,QLable的objectName修改为recMusictext,内容修改为"今日为你推荐",minimumSize和maximumSize的高度均修改为30,Font大小修改为18;Widget的objectName修改为recMusicBox
④再拖拽一个QLabel和Widget,QLabel的objectName修改为supplyMusicText,内容修改为"你的音乐补给站",minimumSize和maximumSize的高度均修改为30,Font大小修改为18;Widget的objectName修改为supplyMusicBox。
⑤最后选中QScrollArea,点击垂直布局。
整个recPage基本就布局完成。

3.2.3 自定义recBox
- RecBox界面布局
①新添加设计师界面,命名为RecBox。geometry的宽高修改为:685*440。
②添加三个Widget,objectName依次修改为leftPage、musicContent、rightPage;leftPage和rightPage的minimumSize和maximumSize修改宽为30,然后选中RecBox点击水平布局。将RecBox的margin和Spacing修改为0
③在upPage和downPage中各拖一个按钮,upPage中按钮objectName修改为btUp,minimumSize的高度修改为220;downPage中按钮objectName修改为btDown,minimumSize的高度修改为220;然后选中upPage和downPage点击水平布局。将upPagedownPage和的margin和Spacing修改为0。
③在musicContent中拖两个Widget,objectName依次修改为recListUp和recListDown,然后选中musicContent点击垂直布局,将musicContent的margin和Spacing修改为o。(为了看清楚效果可临时将recListUp背景色设置为:background-color:green;将recListDown背景色设置为:background-color:red;)
在recListUp和recListDown中分别拖两个水平布局器,依次命名为recListUpHLayout和recListDownHLayout,选中recListUp和recListDown点击水平布局,将margin和Spacing修改为0。
按钮添加如下QSS美化:
btUp:
css
QPushButton
{
background-repeat:no-repeat;
border:none;
background-image:url(:/images/up_page.png);
background-position:center center;
}
QPushButton:hover
{
background-color:#1ECD97;
}
btDown:
css
QPushButton
{
background-repeat:no-repeat;
border:none;
background-image:url(:/images/down_page.png);
background-position:center center;
}
QPushButton:hover
{
background-color:#1ECD97;
}

将miniMusic主界面中recPage页面中的recMusicBox和supplyMusicBox提升为RecBox,就能看到如下效果。(去掉recbox中的背景颜色)

3.2.4 自定义recBoxItem
1. RecBoxItem界面布局
添加一个Designer界面,命名为RecBoxltem,geometry的宽和高设置为:150*200。
①拖拽一个Widget到RecBoxltem中,objectName修改为musiclmageBox,minimumSize和maximumSize的高度均修改为150;
②拖拽一个QLabel到Widget中,objectName修改为recBoxltemText,文本设置为"推荐-001",QLabel的alignment属性设置为水平、垂直居中。
③拖拽一个QLabel到musiclmageBox中,objectName修改为recMusiclmage,geometry设置为:[(0, 0),150*150]
④拖拽一个QPushButton到musiclmageBox中,objectName修改为recMusicBtn,删除掉文本内容。在属性中找到cursor,点击选择小手图标
css
#recMusicBtn
{
border:none;
background-color:rgb(0, 255, 0);
}

2. RecBoxItem测试
cpp
// recBox.cpp构造函数中添加如下代码
RecBoxItem* item = new RecBoxItem();
ui->recListUpHLayout->addWidget(item);
上述代码是在RecBox的构造函数中添加一个RecBoxItem的控件。
3. RecBoxItem类中添加动画效果
cpp
// RecBoxItem.h新增
// 事件过滤器
bool eventFilter(QObject *watched,QEvent *event);
// RecBoxItem.cpp新增
#include <QPropertyAnimation>
#include <QDebug>
bool RecBoxItem::eventFilter(QObject *watched, QEvent *event)
{
// 注意recItem上有一个按钮,当鼠标放在按钮上时在开启动画
if(watched == ui->musicImageBox)
{
int ImgWidget = ui->musicImageBox->width();
int ImgHeight = ui->musicImageBox->height();
// 拦截鼠标进入事件
if(event->type() == QEvent::Enter)
{
// 动画
QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox,"geometry");
animation->setDuration(100);
animation->setStartValue(QRect(9,10,ImgWidget,ImgHeight));
animation->setEndValue(QRect(9,0,ImgWidget,ImgHeight));
animation->start();
// 注意:动画结束的时候会触发finished信号,拦截到该信号,销毁animation
connect(animation,&QPropertyAnimation::finished,this,[=](){
delete animation;
qDebug() << "图片上移动画结束";
});
return true;
}
else if(event->type() == QEvent::Leave)
{
// 拦截鼠标离开事件
QPropertyAnimation *animation = new QPropertyAnimation(ui->musicImageBox,"geometry");
animation->setDuration(150);
animation->setStartValue(QRect(9,0,ImgWidget,ImgHeight));
animation->setEndValue(QRect(9,10,ImgWidget,ImgHeight));
animation->start();
// 注意:动画结束的时候会触发finished信号,拦截到该信号,销毁animation
connect(animation,&QPropertyAnimation::finished,this,[=](){
delete animation;
qDebug() << "图片上移动画结束";
});
return true;
}
}
return QObject::eventFilter(watched,event);
}
// 注意:不要忘记事件拦截器安装,否则事件拦截不到,因此需要在构造函数中添加
// 拦截事件处理时,一定要安装事件拦截器
ui->musicImageBox->installEventFilter(this);
该类中还需要添加设置推荐文本和图片的方法,将来需要在外部来设置每个RecBoxltem的文本和图片:
cpp
// 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 imgStyle = "border-image:url" + Imagepath + ");";
ui->recMusicImage->setStyleSheet(imgStyle);
}
3.2.5 RecBox添加RecBoxItem
1. 图片路径和推荐文本准备
每个RecBoxltem都有对应的图片和推荐文本,在往RecBox中添加RecBoxltem前需要先将图片路径和对应文本准备好。由于图片和文本具有对应关系,可以以键值对方式来进行组织,以下实现的时采用QT内置的QJsonObject对象管理图片路径和文本内容。
cpp
// 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);
/*
QJsonArray类
作用:管理的是QJsonValue对象
头文件:<QJsonArray>
该类重载了[]运算符,可以通过下标方式获取管理的QJsonValue对象
*/
QJsonValue operator[] (int i) const;
QJsonValue 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;
图片路径和对应文本的准备工作,应该在miniMusic类中处理好,RecBoxltem只负责设置,因此该准备工作需要在miniMusic类中进行,故miniMusic中需要添加如下代码:
cpp
// miniMusic.h 新增
// 参数num:RecBox中图片个数
// QJsonArray randomPiction();
// miniMusic.cpp中新增
// 设置随机图片[歌曲图片]
QJsonArray MiniMusic::randomPiction()
{
// 推荐文本 + 推荐图片路径
QVector<QString> vecImageName;
vecImageName << "001.png" << "003.png" <<"004.png" << "005.png" << "006.png"
"007.png" << "008.png" <<"009.png" << "010.png" << "011.png"
"012.png" << "013.png" <<"014.png" << "015.png" << "016.png"
"017.png" << "018.png" <<"019.png" << "020.png" << "021.png"
"022.png" << "023.png" <<"024.png" << "025.png" << "026.png"
"027.png" << "028.png" <<"029.png" << "030.png" << "031.png"
"032.png" << "033.png" <<"034.png" << "035.png" << "036.png"
"037.png" << "038.png" <<"039.png" << "040.png";
std::random_shuffle(vecImageName.begin(),vecImageName.end());
// 001.png
// path:":/images/rec/" + vectorImageName[i];
// text: "推荐-001"
QJsonArray objArray;
for(int i = 0;i < vecImageName.size();++i)
{
QJsonObject obj;
obj.insert("path",":/images/rec" + vecImageName[i]);
// arg(i,3,10,QChar('0'))
// i:要放入%1位置的数据
// 3:三位数
// 10:表示十进制数
// QChar('0'):数字不够三位,前面用字符'0'填充
QString strText = QString("推荐-%1").arg(i,3,10,QChar('0'));
obj.insert("text",strText);
objArray.append(obj);
}
return objArray;
}
2. recBox中添加元素
由于recPage页面中有两个RecBox控件,上面的RecBox为一行四列,下方的RecBox为2行四列,因此在RecBox类中增加以下成员变量:
cpp
// RecBox.h 新增
#include <QJsonArray>
public:
// ui界面初始化
void initRecBoxUi(QJsonArray data,int row);
// 往RecBox中添加图片
void createRecItem();
private:
int row; // 记录当前RecBox实际总行数
int col; // 记录当前RecBox实际每行有几个元素
QJsonArray imageList; // 保存界面上的图片,里面实际为Key,value键值对
RecBox的构造函数中,将row和col默认设置为1和4,count需要具体来计算。
cpp
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;
col = 8;
}
else
{
// 否则:只有一行,为主界面上recListDown
ui->recListDown->hide();
}
// 图片保存起来
imageList = data;
// 往recBox中添加图片
createRecItem();
}
void RecBox::createRecItem()
{
// 创建RecBoxIteam对象,往RecBox中添加
// col
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("path").toString());
// 将RecBoxItem对象添加到RecBox中
ui->recListUpHLayout->addWidget(item);
}
}
运行程序可以看到:
上面RecBox正确,recListUpHLayout中添加了4个RecBoxltem元素并显示出来,recListDownHLayout被隐藏了,而下面的RecBox中内容不对,对于下方RecBox,期望recListUpHLayout中显示添加4个RecBoxItem,recListDownHLayout中显示添加4RecBoxItem,而上述代码往RecBox中添加RecBoxltem时没有添加任何限制。

createRecBoxltem(函数修改如下:
cpp
void RecBox::createRecItem()
{
// 创建RecBoxIteam对象,往RecBox中添加
// col
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("path").toString());
// recMusicBox: col为4,元素添加到ui->recListUpHLayout中
// supplyMusicBox: col为8,ui->recListUpHLayout添加4个,ui->recListDownHLayout添加4个
// 即supplyMusicBox上下两行都要添加
// 如果是recMusicBox: row为1,只能执行else,所有4个RecBoxItem都添加到ui->recListUpHLayout中
// 如果是supplyMusicBox: row为2,col为8,col/2结果为4,i为0 1 2 3时,元素添加到ui->recListDownHLayout中
// i为4 5 6 7时,元素添加到ui->recListUpHLayout中
if(i >= col / 2 && row == 2)
{
// 将RecBoxItem对象添加到RecBox中
ui->recListDownHLayout->addWidget(item);
}
else
{
// 将RecBoxItem对象添加到RecBox中
ui->recListUpHLayout->addWidget(item);
}
}
}

3.2.6 RecBox中btUp和btDown按钮clicked处理
1.添加槽函数
选中recbox.ui文件,分别选中btUp和btDown,右键单击弹出菜单选择转到槽,选中clicked确定,btUp和btDown的槽函数就添加好了。
cpp
void RecBox::on_btUp_clicked()
{
// 点击btUp按钮,显示前4张图片,如果已经是第一张图片,循环从后往前显示
}
void RecBox::on_btDown_clicked()
{
// 点击btUp按钮,显示前8张图片,如果已经是第一张图片,循环从后往前显示
}
2. imageList中图片分组
假设imageList中有24组图片路径和推荐文本信息,如果将信息分组:
如果是recMusicBox,将元素按照col分组,即每4个元素为一组,可分为6组;如果是supplyMuscBox,将元素按照col分组,即每8个元素为一组,可分为3组。
RecBox类中添加currentlndex和count整形成员变量,currentlndex记录当前显示组,count记录总的信息组数。当点击btUp时,currentlndex-,显示前一组,如果currentlndex小于o时,将其设置为count-1;当点击btDown按钮时,currentIndex++显示下一组,当currentlndex为count时,将count设置为0。
这样就实现了轮番显示效果。
cpp
// recbox.h 中新增
private:
int currentIndex; // 标记当前显示第几组图片和推荐信息
int count; // 标记imageList中元素按照col分组总数
// recbox.cpp 中新增
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
// ...
// 图片保存起来
imageList = data;
// 默认显示第0组
currentIndex = 0;
// 计算总共有几组图片,ceil表示向上取整
count = ceil(imageList.size()/col);
// 往recBox中添加图片
createRecItem();
}
void RecBox::on_btUp_clicked()
{
// 点击btUp按钮,显示前4张图片,如果已经是第一张图片,循环从后往前显示
currentIndex--;
if(currentIndex < 0)
{
currentIndex = 0;
}
createRecItem();
}
void RecBox::on_btDown_clicked()
{
// 点击btUp按钮,显示前8张图片,如果已经是第一张图片,循环从后往前显示
currentIndex++;
if(currentIndex >= count)
{
currentIndex = 0;
}
createRecItem();
}
3. 元素重复分析
每次btUp和btDown点击后,应该显示前一组和后一组图片,由于之前recListUpHLayout和recListDownHLayout中已经有元素了,因此需要先将之前的元素删除掉。
cpp
void RecBox::createRecItem()
{
// 移除掉之前旧的元素
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;
}
// 创建RecBoxIteam对象,往RecBox中添加
// ...
}
4. 按照分组计算imageList中元素偏移
|---------|---------|---------|---------|---------|---------|---------|---------|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 001.png | 001.png | 001.png | 001.png | 001.png | 001.png | 001.png | 001.png |
| 第0组 |||| 第1组 ||||
| 第0组起始元素在imageList中元素偏移量为:i=0 |||| 第0组起始元素在imageList中元素偏移量为:i=4 ||||
即i的初始值应该为:i = i + currentIndex*col;
cpp
void RecBox::createRecItem()
{
// 移除掉之前旧的元素
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;
}
// 创建RecBoxIteam对象,往RecBox中添加
// col
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("path").toString());
// recMusicBox: col为4,元素添加到ui->recListUpHLayout中
// supplyMusicBox: col为8,ui->recListUpHLayout添加4个,ui->recListDownHLayout添加4个
// 即supplyMusicBox上下两行都要添加
// 如果是recMusicBox: row为1,只能执行else,所有4个RecBoxItem都添加到ui->recListUpHLayout中
// 如果是supplyMusicBox: row为2,col为8,col/2结果为4,i为0 1 2 3时,元素添加到ui->recListDownHLayout中
// i为4 5 6 7时,元素添加到ui->recListUpHLayout中
if(index >= col / 2 && row == 2)
{
// 将RecBoxItem对象添加到RecBox中
ui->recListDownHLayout->addWidget(item);
}
else
{
// 将RecBoxItem对象添加到RecBox中
ui->recListUpHLayout->addWidget(item);
}
++index;
}
}
5. 程序启动时图片随机显示
仔细观察发现,每次程序启动时,显示的图片都是相同的,这是因为random_shuffle在随机打乱元素时,需要设置随机数种子,否则默认使用的种子是相同的,就导致每次打乱的结果都是相同的,所以每次程序启动时RecBox中显示的内容都是相同的,因此在randomPiction(调用之前需要设置随机数种子。
cpp
// miniMusic类的initUI函数中新增
void MiniMusic::initUI()
{
//...
// 本地下载页面btForm动画默认显示
ui->local->showAnimal(false);
ui->stackedWidget->setCurrentIndex(4);
// 初始化推荐页面
srand(time(nullptr));
ui->recMusicBox->initRecBoxUi(randomPiction(),1);
ui->supplyMusicBox->initRecBoxUi(randomPiction(),2);
}
3.3 自定义CommonPage
3.3.1 CommonPage页面分析
我的音乐下的:我喜欢、本地下载、最近播放三个按钮表面上看对应三个Page页面,分析之后发现,这三个Page页面实际是雷同的,因此只需要定义一个页面CommonPage,将stackedWidget中这三个页面的类型提升为CommonPage即可。

上图为本地音乐的Page页面,对页面拆解后,发现该页面可以分四部分:
①页面说明,比如:本地音乐,该部分实际就是QLabel的提示说明;
②正在播放音乐图片和播放全部按钮;
③音乐列表中每个部分的文本提示,实际就是三个QLabel
④本页面对应的音乐列表,即QListWidget。
3.3.2 CommonPage页面布局
新增加一个设计界面,objectName修改为CommonPage,geometry的宽高修改为80o*500。
① 拖拽一个QLabel、两个Widget和一个List View控件到CommonPage中,objectName从上往下依次修改为pageTittle、musicPlayBox、listLabelBox、pageMusicList,然后选中CommonPage点击垂直布局,将CommonPage的margin和Spacing修改为0。
pageTittle的minimumSize和maximumSize的高度修改为30。
musicPlayBox的minimumSize和maximumSize的高度修改为150。
listLabelBox的minimumSize和maximumSize的高度修改为40。
② 将pageTittle的文本内容修改为"本地音乐"
③ musicPlayBox中
拖拽一个QLabel,objectName修改为musiclmageLabel,minimumSize和maximumSize的宽度
修改为150。
拖拽一个Widget,objectName修改为playAll,minimumSize和maximumSize的宽度修改为120,在其内部拖拽一个PushButton和VerticalSpace(即垂直弹簧),将按钮的objectName修改为playAllBtn,minimumSize和maximumSize的宽和高修改为100*30,文本内容修改为"播放全部",然后选中playAll点击垂直布局。
拖拽一个Horizontal Spacer到CommonPage中,放在playAll之后。
然后选中musicPlayBox,点击水平布局,将margin和spacing设置为0.
④ listLabelBox中
拖拽三个QLabel,内容依次修改为:歌曲名称、歌手名称、专辑名称,objectName从左往右依次
修改为:musicNameLabel、musicSingerLabel、musicAlbumLabel,然后选中listLabelBox,点击水平布局,将margin和spacing设置为0.
⑤ 选中List View,右键单击弹出菜单中选择"变形为",选择QListWidget。

选中miniMusic页面,将stackedWidget中我喜欢、本地下载、最近播放对应的页面提升为CommonPage,页面就处理完成。

cpp
#playAllBtn
{
background-color:#E3E3E3;
border-radius:10px;
}
#playAllBtn:hover
{
background-color:#1ECD97;
}
3.3.3 CommonPage界面设置和显示
CommonPage页面是我喜欢、本地下载、最近播放三个界面的共同类型,因此该类需要提供设置:pageTittle和musiclmageLabel的公共方法,将来在程序启动时完成三个界面信息的设置,因此CommonPage类需要添加一个public的setCommonPageUl函数。
cpp
// 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));
// 让QLabel里的图片自动缩放,填满整个QLabel大小
ui->musicImageLabel->setScaledContents(true);
}
界面设置的函数需要在程序启动时就完成好配置,即需要在miniMusic的initUi()函数中调用完成设置:
cpp
void MiniMusic::initUI()
{
//...
// 设置我喜欢、本地音乐、最近播放页面
ui->likePage->setCommonPageUI("我喜欢",":/images/ilikebg.png");
ui->localPage->setCommonPageUI("本地音乐",":/images/localbg.png");
ui->recentPage->setCommonPageUI("最近播放",":/images/recentbg.png");
}
3.4 自定义ListItemBox
3.4.1 ListItemBox页面分析
CommonPage页面创建好之后,等音乐加载到程序之后,就可以将音乐信息往CommonPage的pageMusicList中显示了。

上图每行都是QListWidget中的一个元素,每个元素中包含多个控件:
① 收藏图标,即QLabel
② 歌曲名称,即QLabel
③ VIP和SQ,VIP即收费会员专享,SQ为无损音乐,也是两个QLabel
④ 歌手名称,即QLabel
⑤ 音乐专辑名称,即QLabel
此处,需要将上述所有QLabel组合在一起,作为一个独立的控件,添加到QListWidget中,因此该控件也需要自定义。
3.4.2 ListItemBox页面布局

添加一个设计师界面,objectName为ListltemBox,geometry的宽度和高度修改为80o*45。
① 拖三个Widget到ListItemBox中,objectName从左往右依次修改为musicNameBox、musicSingerBox、musicAlbumBox,将musicNameBox的minimumSize和maximumSize的宽修改为380,将musicSingerBox的minimumSize和maximumSize的宽修改为200,然后选中ListltemBox,点击水平布局,将ListltemBox的margin和spacing修改为0。
② 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
③ 拖拽一个QLabel到musicSingerBox中,objectName修改为musicSingerLabel,然后选中musicNameBox点击水平布局,将musicSingerBox的margin和spacing修改为0。
④拖拽一个QLabel到albumBox中,objectName修改为albumNameLabel,然后选中albumBox点击水平布局,将musicSingerBox的margin和spacing修改为0。
likeBtn:
css
#likeBtn
{
border:none;
}
VIPLabel:
css
#VIPLabel
{
border: 1px solid #1ECD96;
color:#1ECD96;
border-radius:2px;
}
SQLabel:
css
#SQLabel
{
border:1px solid #FF6600;
color:#FF6600;
border-radius:2px;
}


3.4.3 ListltemBox显示测试
ListItemBox将来要添加到CommonPage页面中的QListWidget中,因此在CommonPage类的初始化方法中添加如下代码:
cpp
// commonpage.h中新增
#include "listitembox.h"
// commpage.cpp中添加
// 设置图片和标题
void CommonPage::setCommonPageUI(const QString &title, const QString &image)
{
// 设置标题
ui->pageTittle->setText(title);
// 设置封面栏
ui->musicImageLabel->setPixmap(QPixmap(image));
// 让QLabel里的图片自动缩放,填满整个QLabel大小
ui->musicImageLabel->setScaledContents(true);
// 测试
// 1. 创建自定义列表项控件
ListItemBox* listItemBox = new ListItemBox(this);
// 2. 创建QListWidget的列表项容器
QListWidgetItem* listWidgetItem = new QListWidgetItem(ui->pageMusicList);
// 3. 设置列表项的尺寸:宽度=列表宽度,高度=45px(固定行高)
listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(),45));
// 4. 把自定义控件绑定到列表项上,显示在列表里
ui->pageMusicList->setItemWidget(listWidgetItem,listItemBox);
}
3.4.4 支持hover效果
ListltemBox添加到CommonPage中的QListWidget之后,自带hover效果,但是背景颜色和界面不太搭配,此处重新实现hover效果,此处重写enterEvent和leaveEvent来实现hover效果。
cpp
// listitembox.h 新增
protected:
void enterEvent(QEvent *event);
void leaveEvent(QEvent *event);
// listitembox.cpp新增
void ListItemBox::enterEvent(QEvent *event)
{
(void)event;
setStyleSheet("background-color:#EFEFEf");
}
void ListItemBox::leaveEvent(QEvent *event)
{
(void)event;
setStyleSheet("");
}
3.5 自定义MusicSlider
由于QT内置的HorizontalSlider(水平滑竿)不是很好看,该控件也采用自定义。
该控件比较简单,实际就是两个QFrame嵌套起来的。
① 添加一个设计师界面,objectName修改为MusicSlider,geometry修改为80o*20。
② 拖拽一个QFrame,objectName修改为inLine,geometry修改为[(o,8),800*4]。
③ 拖拽一个QFrame,objectName修改为outLine,geometry修改为[(o,8),400*4]。
④ 选中MusicSlider,点击水平布局。
⑤ inLine和outLine的样式设置如下:
inLine:
css
#inLine
{
background-color:#EBEEF5;
}
outLine:
css
#outLine
{
background-color:#1ECC94;
}
打开MiniMusic.ui,选中progressBar清除之前样式,将progressBar提升为MusicSlider,运行程序
就能看到效果。


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

① 内部为类似MusicSlider控件+小圆球,圆球实际为一个QPushButton
② 音量大小文本显示,实际为QLabel
③ QPushButton,点击之后在静音和取消静音切换
④ 一个倒三角,Qt未提供三角控件,该控件需要手动绘制,用来提示是播放控制区那个按钮按下的。
3.6.2 VolumeTool界面布局
① 生成一个QT设计师界面,objectName命名为VolumeTool,geometry的宽高修改为100*350。
② 拖拽一个Widget到VolumeTool中,objectName修改为volumeWidgetgeometry修改为:[(10,10),80*300]
拖拽一个QPushButton到volumeWidget,objectName修改为silenceBtnmimimumSize和maximumSize的宽高修改为80*45
拖拽一个QLabel到volumeWidget,objectName修改为volumeRatio,mimimumSize和maximumSize的高修改为30,QLabel的alignment属性修改为水平和垂直居中。
拖拽一个QWidget到volumeWidget,objectName修改为sliderBox。geometry修改为:[(0,0),80*225]
③ 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
css
#volumeWidget
{
background-color:#ffffff;
border-radius:5px;
}
silenceBtn
css
#silenceBtn
{
border:none;
}
#silenceBtn:hover
{
background-color:#F0F0F0;
}
inSlider
css
#inSlider
{
background-color:#ECECEC;
}
outSlider
css
#outSlider
{
background-color:#1ECC94;
}
sliderBtn
css
#sliderBtn
{
background-color:#1ECC94;
border-radius:7px;
}
注意:静音底下的空缺用来绘制三角。
3.6.3 界面设置
该控件属于弹出窗口,即点击了主界面的音量调节按钮后,才需要弹出该界面,点击其他位置该界面自动隐藏。因此在窗口创建时,需要设置窗口为无边框以及为弹出窗口。
cpp
// VolumeTool.cpp 的构造函数中添加如下代码
#include <QGraphicsDropShadowEffect>
VolumeTool::VolumeTool(QWidget *parent) :
QWidget(parent),
ui(new Ui::VolumeTool)
{
ui->setupUi(this);
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
// 在windows上,设置透明效果后,窗口需要加上Qt::FramelessWindowHint格式
// 否则没有控件位置的背景是黑色的
// 由于默认窗口有阴影,因此还需要将窗口的原有阴影去掉,窗口需要加上Qt::NoDropShadowWindowHint
setAttribute(Qt::WA_TranslucentBackground);
// 自定义阴影效果
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0,0);
shadowEffect->setColor("#646464");
shadowEffect->setBlurRadius(10);
setGraphicsEffect(shadowEffect);
// 给按钮设置图标
ui->silenceBtn->setIcon(QIcon(":/images/volume.png"));
// 音量的默认大小是20
ui->volumeRatio->setText("20%");
// 设置outSlider尺寸
QRect rect = ui->outSlider->geometry();
ui->outSlider->setGeometry(rect.x(),180-36+25,rect.width(),36);
ui->sliderBtn->move(ui->sliderBtn->x(),ui->outSlider->y() - ui->sliderBtn->height()/2);
}
3.6.4 界面创建及弹出
音量调节属于主界面上元素,因此在QQMusic类中需要添加VolumeTool的对象,在initUi中new该类的对象。
主界面中音量调节按钮添加clicked槽函数。
cpp
// minimusic.h中新增
#include "volumetool.h"
VolumeTool *volumeTool;
// minimusic.cpp中新增
void MiniMusic::initUI()
{
//...
// 创建音量调节窗口对象并挂到对象树
volumeTool = new VolumeTool(this);
}
// 添加按钮点击槽函数
// 音量按键点击槽函数
void MiniMusic::on_volume_clicked()
{
// 先要调整窗口的显示位置,否则该窗口在主窗口的左上角
// 1.获取该按钮左上角的图标
QPoint point = ui->volume->mapToGlobal(QPoint(0,0));
// 2.计算volume窗口的左上角位置
// 让该窗口显示在鼠标点击的正上方
// 鼠标位置:减去窗口宽度的一半,以及高度恰巧就是窗口的左上角
QPoint volumeLeftTop = point - QPoint(volumeTool->width()/2,volumeTool->height());
// 微调窗口位置
volumeLeftTop.setY(volumeLeftTop.y()+30);
volumeLeftTop.setX(volumeLeftTop.x()+15);
// 3.移动窗口位置
volumeTool->move(volumeLeftTop);
// 4.将窗口显示出来
volumeTool->show();
}
3.6.5 绘制三角
由于Qt中并未给出三角控件,因此三角需要手动绘制,故在VolumeTool类中重写paintEvent事件函
数。
cpp
// volumetool.h中新增
// 画画函数
void paintEvent(QPaintEvent *event);
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,300));
polygon.append(QPoint(70,300));
polygon.append(QPoint(50,320));
// 绘制三角形
painter.drawPolygon(polygon);
}
4. 音乐管理
界面处理好之后,现在就需要将音乐文件加载到程序然后显示在界面上,待后续播放操作。
4.1 音乐加载
QQMusic类中给addLocal添加槽函数。
音乐文件在磁盘中,可以借助QFileDialog类完成音乐文件加载。QFileDialog类中函数介绍:
cpp
// 构造函数
QFileDialog(QWidget *parent = nullptr, // 指定该对象的父对象
const QString &caption = QString(), // 设置标题窗口
const QString &directory = QString(), // 设置默认打开目录
const QString &filter = QString()); // 设置过滤器,可以只打开指定后缀文件
// 默认创建的是打开对话框
#include <QFileDialog>
/*
文件过滤器
筛选所需要格式的文件,格式:每组文件之间用两个分号隔开,同一组内不同后缀之间用空格隔开
比如:打开指定文件夹下所有.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)
示例:
*/
QStringListmimeTypeFilters;
mimeTypeFilters<<"image/jpeg" // will show "JPEG image (*.jpeg *.jpg *.jpe)
<<"image/png"// will show "PNG image(*.png)"
<<"application/octet-stream";//will show"All files (*)"
QFileDialog dialog(this);
dialog.setMimeTypeFilters(mimeTypeFilters);
// 设置打开对话框的类型
QFileDialog::Acceptopen:表示对话框为打开对话框
QFileDialog::AcceptSave:表示对话框为保存对话框
voidsetAcceptMode(QFileDialog::AcceptMode mode);
// 设置选择文件的数量和类型
voidsetFileMode(QFileDialog::FileMode mode);
QFileDialog::AnyFile // 用户可以选择任何文件,甚至指定一个不存在的文件
QFileDialog::ExistingFile // 用户只能选择单个存在的文件名称
QFileDialog::Directory // 用户可以选择一个目录名称
QFileDialog::ExistingFiles //用户可以选择一个或者多个存在的文件名称
//设置文件对话框的当前目录
void setDirectory(const QString &directory);
// 获取当前目录
QDir::currentPath();
打开函数实现如下:
cpp
// minimusic.cpp 中新增
#include <QDir>
#include <QFileDialog>
// 添加歌曲按钮点击槽函数
// 添加歌曲按钮点击槽函数
void MiniMusic::on_addLocal_clicked()
{
// 1.创建一个文件对话框
QFileDialog fileDialog(this);
fileDialog.setWindowTitle("添加本地音乐");
// 2.创建一个打开格式的文件对话框
fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
// 3.设置对话框模式
// 只能选择文件,并且一次性可以选择多个存在的文件
fileDialog.setFileMode(QFileDialog::ExistingFiles);
// 4.设置对话框的MIME过滤器
QStringList mimeList;
mimeList << "appLication/octet-stream";
fileDialog.setMimeTypeFilters(mimeList);
// 5.设置对话框默认的打开路径,设置目录为当前工程所在目录
QDir dir(QDir::currentPath());
dir.cdUp();
QString musicPath = dir.path() + "/MiniMusic/musics/";
fileDialog.setDirectory(musicPath);
// 6. 显示对话框,并接收返回值
// 模态对话框,exec内部是死循环处理
if(fileDialog.exec() == QFileDialog::Accepted)
{
// 切换到本地音乐界面,因为加载完的音乐需要在本地音乐界面显示
ui->stackedWidget->setCurrentIndex(4);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
// 拿到歌曲文件后,将歌曲文件交由musicList进行管理
// ...
}
}
4.2 MusicList类
4.2.1 添加C++类MusicList
将来添加到播放器中的音乐比较多,可借助一个类对所有的音乐进行管理。添加新C++类与添加设计师界面类似:

一个新C++类就添加完成。
4.2.2 歌曲对象存储
每首音乐文件,将来需要获取其内部的歌曲名称、歌手、音乐专辑、歌曲时长等信息,因此在MusicList类中,将所有的歌曲文件以Music对象方式管理起来。
QQMusic中,通过QFileDialog将一组音乐文件的url获取到之后,可以交给MusicList类来管理。
但是QQMusic加载的二进制文件不一定全部都是音乐文件,因此MusicList类中需要对文件的MIME类型再次检测,以筛选出真正的音乐文件。
QMimeDatabase类是Qt中主要用于处理文件的MIME类型,经常用于:
- 文件类型识别
- 文件过滤
- 多媒体文件处理
- 文件导入导出
- 文件管理器
该类中的mimeTypeForFile函数可用于获取给定文件的MIME类型
cpp
// QMimeDatabase类的mimeTypeForfile方法
// 功能: 获取fileName文件的MIME类型
// fileName: 文件的名称
// mode:MatchMode为枚举类型,表明如何匹配文件的MIME类型
// MatechDefault:通过文件名和文件内容来进行查询匹配,文件名优先于文件内容,如果文件扩展名未知
// 或者匹配多个MIME类型,则使用文件内容匹配
// MatchExtension:通过文件
// MatchContent:通过文件内容来查询匹配
#include <QMimeType>
#include <QMimeData>
QMimeType mimeTypeForFile(const QString &fileName,
MatchMode mode = MatchDefault) const
// QMimeType类中的name属性,保存了获取到的MIME类型,
// 可以通过该类的name()方法以字符串方式返回MIME类型
QString name();
// audio/mpeg:适用于mp3格式的音频文件
// audio/flac:表示无损音频压缩格式
// audio/wav:表示wav格式的歌曲文件
// 上述三种音乐格式文件,Qt::QMediaPlayer类都是支持的
对于歌曲文件:
audio/mpeg:适用于mp3格式的音乐文件
audio/flac:无损压缩的音频文件,不会破坏任何原有的音频信息
audio/wav:表示wav格式的歌曲文件
上述歌曲文件格式,Qt的QMediaPlayer类都是支持的。
cpp
// musicList.h中新增
#include <QVector>
QVector<Music> musicList; // Music类是自定义的C++类,描述歌曲相关信息
// 将miniMusic页面中读到的音乐文件,检测是音乐文件后添加到musicList中
void addMusicByUrl(const QList<QUrl> urls);
// musiclist.cpp中新增
void MusicList::addMusicByUrl(const QList<QUrl> urls)
{
for(auto musicUrl : urls)
{
// 由于添加进来的文件不一定是歌曲文件,因此需要再次筛选出音乐文件
QMimeDatabase db;
QMimeType mime = db.mimeTypeForFile(musicUrl.toLocalFile());
if(mime.name() != "audio/mpeg" && mime.name() != "audio/flac")
{
continue;
}
// 如果是音乐文件,加入歌曲列表
musicList.push_back(musicUrl);
}
}
4.3 Music类
4.3.1 Music类介绍
该用来描述一个音乐文件,比如:音乐名称、歌手名称、专辑名称、音乐持续时长,当在界面上点击收藏之后,音乐会被标记为喜欢,播放之后需要标记为历史记录。因此该类中至少需要以下成员:
cpp
// music.h中新增
#include <QUrl>
#include <QString>
class Music
{
public:
Music();
Music(const QUrl &url);
void setIsLike(bool isLike);
void setIsHistory(bool isHistory);
void setMusicName(const QString& musicName);
void setSingerName(const QString& singerName);
void setAlbumName(const QString& albumName);
void setDyration(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:
// 解析媒体元数据
void parseMediametaData();
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)
{
}
void Music::setIsLike(bool isLike)
{
this->isLike = isLike;
}
void Music::setIsHistory(bool isHistory)
{
this->isHistory = isHistory;
}
void Music::setMusicName(const QString &musicName)
{
this->musicName = musicName;
}
void Music::setSingerName(const QString &singerName)
{
this->singerName = singerName;
}
void Music::setAlbumName(const QString &albumName)
{
this->albumName = albumName;
}
void Music::setDyration(const qint64 duration)
{
this->duration = duration;
}
void Music::setMusicUrl(const QUrl &url)
{
this->musicUrl = url;
}
void Music::setMusicId(const QString &musicId)
{
this->musicId = musicId;
}
bool Music::getIsLike()
{
return isLike;
}
bool Music::getIsHistory()
{
return isHistory;
}
QString Music::getMusicName()
{
return musicName;
}
QString Music::getSingerName()
{
return singerName;
}
QString Music::getAlbumName()
{
return albumName;
}
qint64 Music::getDuration()
{
return duration;
}
QUrl Music::getMusicUrl()
{
return musicUrl;
}
QString Music::getMusicId()
{
return musicId;
}
另外,该类还需要添加一个带有歌曲文件路径的构造函数,当给定有效音乐文件后,Music类需要负责将该音乐文件的元数据解析出来。
为了保证Music对象的唯一性,给每个Music对象设置一个UUID。
UUID,即通用唯一识别码(UniversallyUniqueIdentifier),确保在分布式系统中每个元素都有唯一的标识。
UUID由一组32位数的16进制数字组成,形式为8-4-4-4-12的32个字符,比如:"550e8400-e29b-41d4-a716-446655440000"
在Music对象查找和更新时,可以已通过对比UUID,来保证Music对象的唯一性。
Qt中QUuid类可生成UUID。
cpp
Music::Music(const QUrl &url)
:isLike(false)
,isHistory(false)
,musicUrl(url)
{
musicId = QUuid::createUuid().toString();
parseMediametaData();
}
4.3.2 解析音乐文件元数据
对于每首歌曲,将来在界面上需要显示出:歌曲名称、歌手、专辑名称,在播放时还需要拿到歌曲总时长,因此在构造音乐对象时,就需要将上述信息解析出来。
歌曲元数据解析,需要用到QMediaPlayer,该类也是用来进行歌曲播放的类,后续在播放音乐位置详细介绍。
QMediaPlayer类中的setMedia()函数
cpp
// 功能:设置要播放的媒体源,媒体数据从中读取
// media: 要播放的媒体内容,⽐如⼀个视频或⾳频⽂件,该类提供了⼀个QUrl格式的单参构造
void setMedia(const QMediaContent &media, QIODevice *stream = nullptr);
注意:该函数执行后立即返回,不会等待媒体加载完成,也不检查错误,如果在媒体加载时发生错误,会触发mediaStatusChanged和error信号。
由于加载媒体文件需要时间,可以通过QMediaObject类中的isMetaDataAvailable(方法检测媒体数据是否可用
QMediaObject类是QMediaPlayer类的基类。
cpp
// 检测媒体源是否有效,如果是有效的返回true.否则返回false
bool isMetaDataAvailable() const;
媒体元数据加载成功之后,可以通过QMediaObject类的metaData函数获取指定的媒体数据:
cpp
// 返回要获取的媒体数据key的值
QVariant QMediaObject::metaData(const QString &key) const;
文本需要获取媒体的:标题、作者、专辑、持续时长
|------------|-----------------|-------------|
| valye | description | type |
| Title | 媒体的标题 | QString |
| Author | 媒体的作者 | QStringList |
| AlbumTitle | 媒体所属专辑名称 | QString |
| Duration | 媒体的播放时长 | qint64 |
注意:有些媒体中媒体数据可能不全,即有些媒体数据获取不到,比如盗版歌曲。
使用QMediaPlayer媒体播放类时,需要在miniMusic.pro项目工程文件中添加媒体模块multimedia,该模块主要用来播放各种音频视频文件等,该模块中提供了很多类:

cpp
QT += core gui multimedia
添加完成之后,重新将项目构建一下,否则Qtcreate可能识别不过来。
音乐文件的meta数据解析如下:
cpp
// music.h中新增
private:
// 解析媒体元数据
void parseMediametaData();
// music.cpp中新增
#include <QUuid>
#include <QMediaPlayer>
#include <QCoreApplication>
// 解析媒体元数据
void Music::parseMediametaData()
{
// 解析时候需要注意读取歌曲数据,读取歌曲文件需要用到QMediaPlayer类
QMediaPlayer player;
player.setMedia(musicUrl);
// 媒体元数据解析需要时间,只有等待解析完成之后,才能提取音乐信息,此处循环等待
// 循环等待时,主界面消息就无法处理了,因此需要在等待解析期间,让消息循环继续处理
while(!player.isMetaDataAvailable())
{
QCoreApplication::processEvents();
}
// 解析媒体元数据结束,提取元数据信息
if(player.isMetaDataAvailable())
{
musicName = player.metaData("Title").toString();
singerName = player.metaData("Author").toString();
albumName = player.metaData("AlbumTitle").toString();
duration = player.duration();
if(musicName.isEmpty())
{
musicName = "歌曲未知";
}
if(singerName.isEmpty())
{
singerName = "歌手未知";
}
if(albumName.isEmpty())
{
albumName = "专辑名未知";
}
qDebug() << musicName << ":" << singerName << ":" << albumName << ":" << duration;
}
}
/*************************************/
Music::Music(const QUrl &url)
:isLike(false)
,isHistory(false)
,musicUrl(url)
{
musicId = QUuid::createUuid().toString();
// 该函数需要在Music的构造函数中调用,当创建音乐对象时,顺便完成歌曲文件的加载
parseMediametaData();
}
4.3.3Music数据保存
通过QFileDialog将音乐从本地磁盘加载到程序中后,拿到的是所有音乐文件的QUrl,而在程序中需要的是经过元数据解析之后的Music对象,并且Music对象需要管理起来,此时就可以采用MusicList类对解析之后的Music对象进行管理,QQMusic类中只需要保存MusicList的对象,就可以让qqMusic.ui界面中CommonPage对象完成Music信息往界面更新。
cpp
// minimusic.h 新增
#include "musiclist.h"
// 音乐管理
MusicList musicList;
// minimusic.cpp
// 音量按键点击槽函数
void MiniMusic::on_volume_clicked()
{
// 先要调整窗口的显示位置,否则该窗口在主窗口的左上角
// 1.获取该按钮左上角的图标
QPoint point = ui->volume->mapToGlobal(QPoint(0,0));
// 2.计算volume窗口的左上角位置
// 让该窗口显示在鼠标点击的正上方
// 鼠标位置:减去窗口宽度的一半,以及高度恰巧就是窗口的左上角
QPoint volumeLeftTop = point - QPoint(volumeTool->width()/2,volumeTool->height());
// 微调窗口位置
volumeLeftTop.setY(volumeLeftTop.y()+30);
volumeLeftTop.setX(volumeLeftTop.x()+15);
// 3.移动窗口位置
volumeTool->move(volumeLeftTop);
// 4.将窗口显示出来
volumeTool->show();
}
// 添加歌曲按钮点击槽函数
void MiniMusic::on_addLocal_clicked()
{
//...
// 6. 显示对话框,并接收返回值
// 模态对话框,exec内部是死循环处理
if(fileDialog.exec() == QFileDialog::Accepted)
{
// 切换到本地音乐界面,因为加载完的音乐需要在本地音乐界面显示
ui->stackedWidget->setCurrentIndex(4);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
// 拿到歌曲文件后,将歌曲文件交由musicList进行管理
musicList.addMusicByUrl(urls);
// 更新到本地音乐列表
ui->localPage->reFresh(musicList);
}
}
由于添加的是本地音乐,因此音乐信息需要由ui->localPage更新到其内部的QListWidget中。
4.4 音乐分类
QQMusic中,有三个显示歌曲信息的页面:
- likePage:管理和显示点击小心心后收藏的歌曲
- localPage:管理和显示本地加载的歌曲
- recentPage:管理和显示历史播放过的歌曲
这三个页面的类型都是CommonPage,每个页面应该维护自己页面中的歌曲。因此CommonPage类中需要新增:
cpp
// commonpage.h中新增
// 区分不同page页面
// 区分不同的page页面
enum PageType
{
LIKE_PAGE, // 我喜欢页面
LOCAL_PAGE, // 本地下载页面
HISTORY_PAGE // 最近播放页面
};
class CommonPage : public QWidget
{
// 新增成员函数
public:
// 设置音乐列表类型
void setMusicListType(PageType pageType);
private:
// 歌单列表
QVector<QString> musicListOfPage; // 具体某个页面的音乐,将来只需要存储音乐的id即可
PageType pageType; // 标记属于likePage、localPage、recentPage哪个页面
}
// commonpage.cpp中新增
void CommonPage::setMusicListType(PageType pageType)
{
this->pageType = pageType;
}
// minimusic.cpp中新增
void MiniMusic::initUi()
{
//...
// 设置CommonPage的信息
ui->likePage->setMusicListType(PageType::LIKE_PAGE);
ui->likePage->setCommonPageUI("我喜欢",":/images/ilikebg.png");
ui->localPage->setMusicListType(PageType::LOCAL_PAGE);
ui->localPage->setCommonPageUI("本地音乐",":/images/localbg.png");
ui->recentPage->setMusicListType(PageType::HISTORY_PAGE);
ui->recentPage->setCommonPageUI("最近播放",":/images/recentbg.png");
}
QQMusic中,点击addLocal(本地加载)按钮后,会通过其musicList成员变量,将music添加到musicList中管理,在添加过程中,每个歌曲会对应一个Music对象,Music对象在构造时,会完成歌曲文件的加载,顺便完成歌曲名称、作者、专辑名称等元数据的解析。一切准备就绪之后,每个CommonPage页面,通过QQMusic的musicList分离出自己页面的歌曲,保存在musicListOfPage中。
cpp
// connonpage.h 中新增
#include "musiclist.h"
private:
// 音乐页面添加音乐
void addMusicToMusicPage(MusicList &musicList);
// commonpage.cpp 中新增
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
// 将旧内容清空
musicListOfPage.clear();
for(auto& music : musicList)
{
switch (pageType)
{
case LOCAL_PAGE:
musicListOfPage.push_back(music.getMusicId());
break;
case LIKE_PAGE:
if(music.getIsLike())
{
musicListOfPage.push_back(music.getMusicId());
}
break;
case HISTORY_PAGE:
if(music.getIsHistory())
{
musicListOfPage.push_back(music.getMusicId());
}
break;
default:
break;
}
}
}
由于musicList所属类,并不能直接支持范围for,因此需要在MusicList类中新增:
cpp
// musiclist.h中新增
typedef typename QVector<Music>::iterator iterator;
iterator begin();
iterator end();
// musiclist.cpp中新增
MusicList::iterator MusicList::begin()
{
return musicList.begin();
}
MusicList::iterator MusicList::end()
{
return musicList.end();
}
这样就完成了歌曲的分类。
4.5 更新Music信息到ComonPage界面
歌曲分类完成之后,歌曲信息就可以更新到CommonPage页面了。
更新步骤:
- 调用addMusicldPageFromMusicList函数,从musicList中添加当前页面的歌曲
- 遍历musicListOfPage,拿到每首音乐后先检查其是否在,存在则添加。
- 界面上需要更新每首歌曲的:歌曲名称、作者、专辑名称,而commonPage中只保存了歌曲的musicld,因此需要在MusicList中增加通过musicID查找Music对象的方法。
cpp
// commonpage.h 中新增
// 负责将歌曲显示到界面上
void reFresh(MusicList musicList);
// commonpage.cpp 中新增
void CommonPage::reFresh(MusicList musicList)
{
// 从musicList中分离出当前页面的所有音乐
addMusicToMusicPage(musicList);
// 遍历歌单.将歌单中的歌曲显示到界面
for(auto musicId:musicListOfPage)
{
auto it = musicList.findMusicById(musicId);
if(it == musicList.end())
continue;
ListItemBox *listItemBox = new ListItemBox(ui->pageMusicList);
listItemBox->setMusicName(it->getMusicName());
listItemBox->setSinger(it->getSingerName());
listItemBox->setAlbumName(it->getAlbumName());
listItemBox->setLikeIcon(it->getIsLike());
QListWidgetItem *listWidgetItem = new QListWidgetItem(ui->pageMusicList);
listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(),45));
ui->pageMusicList->setItemWidget(listWidgetItem,listItemBox);
}
// 更新完之后刷新一下页面
// 触发窗口重绘paineEvent
// update(); //update()将paintEvent事件放在事件循环队列中,没有立马处理
repaint(); // 立马响应paintEvent事件
}
// musiclist.h 中新增
// 按照Id查找音乐
iterator findMusicById(const QString &musicId);
// musiclist.cpp 中新增
MusicList::iterator MusicList::findMusicById(const QString &musicId)
{
for(iterator it = begin();it != end();++it)
{
if(it->getMusicId() == musicId)
{
return it;
}
}
return end();
}
将歌曲名称、作者、专辑名称、喜欢图片等往ListBoxltem界面中更新时,需要ListBoxltem提供对应的set方法,因此需要在ListltemBox类中新增:
cpp
// listitembox.h中新增
public:
// 歌曲设置名字
void setMusicName(const QString &name);
// 设置歌手
void setSinger(const QString& singer);
// 设置专辑名字
void setAlbumName(const QString &albumName);
// 设置是否喜欢
void setLikeIcon(bool like);
private:
bool isLike;
// listitembox.cpp 中新增
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false) // 默认设置为false,音乐加载上来之后,点击小心心才为true
{
ui->setupUi(this);
}
void ListItemBox::setMusicName(const QString &name)
{
ui->musicNameLabel->setText(name);
}
void ListItemBox::setSinger(const QString &singer)
{
ui->musicSingerLabel->setText(singer);
}
void ListItemBox::setAlbumName(const QString &albumName)
{
ui->albumNameLabel->setText(albumName);
}
void ListItemBox::setLikeIcon(bool like)
{
isLike = like;
if(isLike)
{
ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
}
else
{
ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
}
}
更新音乐信息到界面的函数处理完成之后,需要在QQMusic的addLocal槽函数最后调用。
cpp
// minimusic.cpp 中新增
// 添加歌曲按钮点击槽函数
void MiniMusic::on_addLocal_clicked()
{
//...
// 6. 显示对话框,并接收返回值
// 模态对话框,exec内部是死循环处理
if(fileDialog.exec() == QFileDialog::Accepted)
{
// 切换到本地音乐界面,因为加载完的音乐需要在本地音乐界面显示
ui->stackedWidget->setCurrentIndex(4);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
// 拿到歌曲文件后,将歌曲文件交由musicList进行管理
musicList.addMusicByUrl(urls);
// for(auto &e : urls)
// {
// qDebug() << e << "\n";
// }
// 更新到本地音乐列表
ui->localPage->reFresh(musicList);
}
}
4.6 CommonPage显示不足处理
a. 歌曲作者对齐处理
解析歌曲元数据时,有些歌曲文件中可能不存在歌曲名称、作者、歌曲专辑等,为了界面上显示出歌曲名称,从歌曲文件名中解析出歌曲名称和作者,比如:""2002年的第一场雪-刀郎.mp3"。这样解析出来的歌曲名称后面多一个空格,作者之前多一个空格,导致界面显示的时候歌手名称对不
齐。因此在往界面设置之前,可以将名称前后的空格去除掉。
QString类提供了一个trimmed()方法,专门用来去除字符串前后空白字符的。
cpp
// music.cpp 文件修改
// 解析媒体元数据
void Music::parseMediametaData()
{
//...
QString fileName = musicUrl.fileName();
// 找"-"的位置
int index = fileName.indexOf('-');
if(musicName.isEmpty())
{
if(index != -1)
{
// "2002年的第一场雪 - 刀郎.mp3"
musicName = fileName.mid(0,index).trimmed();
}
else
{
// "2002年的第一场雪.mp3"
musicName = fileName.mid(0,fileName.indexOf('-')).trimmed();
}
}
if(singerName.isEmpty())
{
if(index != -1)
{
singerName = fileName.mid(index+1,fileName.indexOf('.')-index-1).trimmed();
}
else
{
singerName = "未知歌手";
}
}
//...
}
b. 显示延迟问题
在CommonPage的reFresh()函数中,将ListltemBox设置好之后,更新到界面,有时候不会立马显示出来,等鼠标放置ListWidget上或者界面刷新的时候,才会显示出来。
这是因为往界面更新元素的操作,没有引起窗体的重绘,导致不能实时显示出来,因此添加完元素之后,需要触发重绘事件,将元素及时绘制出来。
cpp
// 该⽅法负责将歌曲信息更新到界⾯
void CommonPage::reFresh(MusicList &musicList)
{
// ...
// 该函数最后添加上repaint()函数调⽤
// repaint()会⽴即执⾏paintEvent(),不会等待事件队列的处理
// update()将⼀个paintEvent事件添加到事件队列中,等待稍后执⾏,即不会⽴即执⾏paintEvent。
repaint();
}
c. 移除掉QListWidget的水平滚动条
一般歌曲名称、作者、专辑名称不会将ListltemBox沾满,为了界面好看,可以让CommonPage中的QListWidget控件去除掉水平滚动条。
cpp
CommonPage::CommonPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommonPage)
{
ui->setupUi(this);
// 不要水平滚动条
ui->pageMusicList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
d. QListWidget选中后背景色设置
QListWidget中ListItemBox选中之后,背景颜色和界面不是很搭,用如下QSS代码设置ListltemBox选中后的背景颜色。
css
#pageMusicList::item:selected /*::item表示子控件,即ListItemBox : selected:表示选中*/
{
background-color:#EFEFEF;
}
e. QListWidget的垂直滚动条美化
css
#pageMusicList::item:selected /*::item表示子控件,即ListItemBox : selected:表示选中*/
{
background-color:#EFEFEF;
}
QScrollBar:vertical
{
border:none;
width: 10px;
background-color:#FFFFFF;
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:vertical
{
width:10px;
background-color:#E3E3E3;
border-radius:5px;
min-height: 20px;
}
4.7 音乐收藏
4.7.1 我喜欢图标处理
当CommonPage往界面更新Music信息时,也要根据Music的isLike属性更新对应的图标。因此ListltemBox需要根据当点击我喜欢按钮之后,要切换ListltemBox中的小心心。因此ListltemBox中添加设置bool类型isLike成员变量,以及setlsLike函数,在CommonPage添加Music信息到界面时,要能够设置小心心图片。
cpp
// listitemBox.h 中新增
bool isLike; // 是否喜欢
// 设置是否喜欢
void setLikeIcon(bool like);
// listitemBox.cpp 中新增
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false) // 默认设置为false,音乐加载上来之后,点击小心心才为true
{
ui->setupUi(this);
}
void ListItemBox::setLikeIcon(bool like)
{
isLike = like;
if(isLike)
{
ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
}
else
{
ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
}
}
4.7.2 点击我喜欢按钮处理
当喜欢某首歌曲时,可以点击界面上红色小心心收藏该首歌曲。我喜欢按钮中应该有以下操作:
-
更新小心心图标
-
更新Music的我喜欢属性,但ListltemBox并没有歌曲数据,所以只能发射信号,让其父元素CommonPage来处理
cpp
// listItemBox.h 中新增
public:
// 按钮点击槽函数
void onLikeBtnClicked();
signals:
// 通知更新歌曲数据信号
void setIsLike(bool);
// listItemBox.cpp 中新增
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false) // 默认设置为false,音乐加载上来之后,点击小心心才为true
{
ui->setupUi(this);
// likeBtn按钮连接其点击槽函数
connect(ui->likeBtn,&QPushButton::clicked,this,&ListItemBox::onLikeBtnClicked);
}
void ListItemBox::onLikeBtnClicked()
{
isLike = !isLike;
setIsLike(isLike);
// 发送信号
emit setIsLike(isLike);
}
- CommonPage在往QListWidget中添加元素时,会创建一个个ListltemBox对象,每个对象将来都可能会发射setLikeMusic信号,因此在将ListltemBox添加完之后,CommonPage应该关联先该信号,将需要更新的的Music信息以及是否喜欢,同步给miniMusic。
cpp
// commonpage.h 中新增
signals:
// 歌曲是否喜欢的信号
void updateLikeMusic(bool isLike,QString musicId);
// commonpage.cpp 中新增
// 该方法负责将歌曲信息更新到界面
void CommonPage::reFresh(MusicList musicList)
{
//...
for(auto musicId:musicListOfPage)
{
//...
QListWidgetItem *listWidgetItem = new QListWidgetItem(ui->pageMusicList);
listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(),45));
ui->pageMusicList->setItemWidget(listWidgetItem,listItemBox);
// 接收ListItemBox发射的setIsLike信号
connect(listItemBox,&ListItemBox::setIsLike,this,[=](bool isLike){
emit updateLikeMusic(isLike,it->getMusicId());
});
}
//...
}
- QQMusic收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、recentPage更新其界面的我喜欢歌曲信息。
cpp
// minimusc.h 新增
void onUpdateLikeMusic(bool isLike,QString musicId); // 响应CommonPage发射updateLikeMusic信号
// minimusic.cpp 新增
void MiniMusic::connectSignalAndSlot()
{
// ...
// 关联CommonPage发射的updateLikeMusic信号
connect(ui->likePage,&CommonPage::updateLikeMusic,this,&MiniMusic::onUpdateLikeMusic);
connect(ui->localPage,&CommonPage::updateLikeMusic,this,&MiniMusic::onUpdateLikeMusic);
connect(ui->recentPage,&CommonPage::updateLikeMusic,this,&MiniMusic::onUpdateLikeMusic);
}
void MiniMusic::onUpdateLikeMusic(bool isLike, QString musicId)
{
// 1.找到该首歌曲,并更新对应Music对象信息
auto it = musicList.findMusicById(musicId);
if(it != musicList.end())
{
it->setIsLike(isLike);
}
// 2.通知三个页面更新自己的数据
ui->likePage->reFresh(musicList);
ui->localPage->reFresh(musicList);
ui->recentPage->reFresh(musicList);
}
5.歌曲重复显示问题
当界面上歌曲数据更新之后,CommonPage往页面上更新其musicofPage内容时,musicOfPage和界面中的QListWidget中已经有数据了,需要先将之前的内容清楚掉,否则就会重复。
cpp
// commonpage.cpp 修改
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
// 将旧内容清空
musicListOfPage.clear();
//...
}
void CommonPage::reFresh(MusicList musicList)
{
// 清空旧内容
ui->pageMusicList->clear();
//...
}
5.音乐播放控制
歌曲已经添加到程序并完成解析,解析的信息也更新到界面了,所有前置工作基本完成,接下来重点处理音乐播放,歌曲播放需要用到Qt提供的QMediaPlayer类和QMediaPlaylist类。
5.1 QMediaPlayer类
5.1.1 QMediaPlayer类说明
QMediaPlayer是Qt框架中用于支持各种音频和视频的播放,流媒体的播放,各种播放模式(单曲播放、列表播放、循环播放等),各种播放模式(播放、暂停、停止等),信号槽机制可以让用户在播放状态改变时进行所需控制。
使用时需要包含#include<QMediaPlayer>头文件,并且需要在.pro项目文件中添加媒体库,即:QT+=multimedia,将multimedia模块导入到工程中,就可以使用该模块中提供的媒体播放控制的相关类,比如:QMediaPlayer、QMediaPlayList等。
5.1.2 属性和方法
a.枚举类型
【QMediaPlayer::State枚举类型】
|----------------------------|-----|--------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMediaPlayer::StoppedState | 0 | 播放停止状态 |
| QMediaPlayer::PlayingState | 1 | 播放状态 |
| QMediaPlayer::PausedState | 2 | 播放暂停状态 |
[QMediaPlayer::Flag】
|-----------------------------|-----|--------------------------------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMediaPlayer::LowLatency | 0 | 播放未压缩的音频文件,播放表现为低时 延,主要播放蜂鸣、手机铃声 |
| QMediaPlayer:StreamPlayback | 1 | 播放给予QIODevice构建的媒体文件, QMediaPlayer或自动选择支持的流进行播 放 |
| QMediaPlayer::VideoSurface | 2 | 渲染视频到QAbstractVideoSurface输出 |
【QMediaPlayer::Error】
|-----------------------------------|-----|----------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMediaPlayer::NoError | 0 | 没有错误 |
| QMediaPlayer::ResourceError | 1 | 媒体源无法解析 |
| QMediaPlayer::FormatError | 2 | 媒体源格式不支持,可能会播放,但是没有音频和视频组件 |
| QMediaPlayer::NetworkError | 3 | 网络错误 |
| QMediaPlayer::AccessDeniedError | 4 | 没有媒体源的访问权限 |
| QMediaPlayer::ServiceMissingError | 5 | 没有有效的播放服务,无法继续播放 |
【QMediaPlayer::MediaStatus】
|---------------------------------|-----|-------------------------------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMediaPlayer:UnknownMediaStatus | 0 | 媒体的状态未被定义 |
| QMediaPlayer::NoMedia | 1 | 没有媒体文件,player处于StoppedState |
| QMediaPlayer::LoadingMedia | 2 | 媒体文件加载中,player可以处于任何状 态 |
| QMediaPlayer::LoadedMedia | 3 | 媒体文件已经加载,player处于StoppedState |
| QMediaPlayer::StalledMedia | 4 | 媒体处于延迟或者暂时中断状态,player 处于PlayingState或PauseState |
| QMediaPlayer::BufferingMedia | 5 | 媒体正在缓冲数据,player处于PlayingState或PauseState |
| QMediaPlayer::BufferedMedia | 6 | 媒体数据缓冲完成,player处于PlayingState或PauseState |
| QMediaPlayer::EndOfMedia | 7 | 媒体结束,player处于StoppedState |
| QMediaPlayer::InvalidMedia | 8 | 非法的媒体文件,player处于StoppedState |
b.常用属性
cpp
// 部分常用属性
const qint64 duration; // 保存媒体的总播放时间,单位为毫秒
const QMediaContent currentMedia; // 当前正在播放媒体的媒体内容
const QString error; // 最近一次信息错误
int volume; // 保存音量大小,范围在0~100之间
const bool audioAvailable; // 音频是可用,audioAvailableChanged信号用于监听其状态
QMediaPlaylist* playtlist; // 播放列表
c.常用函数
cpp
qint64 duration() const; // 获取当前媒体的总时间
qint64 position() const; // 获取当前媒体的播放位置
int volume() const; // 获取播放音量大小
bool isMuted() const; // 检测是否静音
State state() const; // 获取当前媒体的播放状态
QMediaContent currentMedia() const; // 获取当前正在播放的媒体内容
QString errorString() const; // 获取最近的一次错误
d.常用槽函数
cpp
void paise(); // 播放媒体
void play(); // 暂停媒体
void stop(); // 停止媒体播放
void setMuted(bool meted); // 设置是否静音,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);
e.常用信号
cpp
void stateChanged(QMediaPlayer::State state); // 播放状态改变时发射该信号
void durationChanged(qint64 duration); // 播放时长改变时发射该信号
void positionChanged(qint64 position); // 播放位置改变时发射该状态
void volumeChanged(int volume); // 音量改变时发射该信号
void metaDateAvailableChanged(bool available); // 源数据改变发出
以上只列出了本次需要用到的属性、方法和槽函数,后续需要使用时请参考Qt帮助手册。
5.2 QMediaPlaylist类
5.2.1 QMediaPlaylist类介绍
QMediaPlaylist类提供了一种灵活而强大的方式管理媒体文件的播放列表。。通过结合QMediaplayer,可以实现顺序播放、循环播放随机播放等多种播放模式,提升用户的媒体播放体验。该类提供了以下功能:
- 添加和删除媒体文件
- 播放模式设置(列表播放、随机播放、单曲循环)
- 控制播放列表(开始,停止,上一曲,下一曲)
- 获取和设置当前媒体文件
- 信号槽支持
若播放多个媒体文件,必须使用该类来管理媒体文件,将该列表设置到player上,就可实现更加灵活的播放支持。
5.2.2 属性和方法
a.枚举类型
[QMediaPlaylist::PlaybackMode]
|-----------------------------------|-----|----------------|
| 枚举状态名称 | 枚举值 | 说明 |
| QMediaPlaylist::CurrentItemOnce | 0 | 单词播放 |
| QMediaPlaylist::CurrentItemInLoop | 1 | 单曲循环 |
| QMediaPlaylist::Sequential | 2 | 从当前选中位置开始,顺序播放 |
| QMediaPlaylist:Loop | 3 | 列表中文件循环播放 |
| QMediaPlaylist::Random | 4 | 列表中文件随机播放 |
b.常见属性
cpp
int currentIndex; // 当前播放的媒体文件在媒体列表中的索引
const QMediaContent currentMedia; // 当前选中的媒体文件
QMediaPlaylist::PlaybackMode playbackMode; // 媒体列表中文件的播放模式
a. 常见方法
cpp
bool addMedia(const QMediaContent &content); // 向媒体列表中添加单个媒体文件
int mediaCount() const; // 获取播放列表中文件的个数
int currentIndex() const; // 获取当前播放的媒体的索引
bool clear(); // 清空媒体列表
QMediaPlaylist::PlaybackMode playbackMode() const; // 获取媒体列表的播放模式
QMediaContent currentMedia() const; // 获取当前播放的媒体文件
QString errorString() const; // 获取最近一次发生过的错误
b. 常用槽函数
cpp
void next(); // 下一曲
void previous(); // 上一曲
void setCurrentIndex(int playlistPosition); // 设置当前播放媒体的索引
void shuffle(); // 媒体顺序打乱,重建媒体索引
c.常用信号
cpp
// 列表播放模式方法改变时发射
void playbackModeChanged(QMediaPlaylist::PlaybackMode mode);
// 当前索引发生改变时发射
void currentIndexChanged(int position);
// 当前媒体文件改变时发生
void currentMediaChanged(const QMediaContent &content);
该类提供的方法非常丰富,此处暂介绍了需要用到的内容,后续开发时需要用到其他内容请参考Qt帮助手册。
5.3 歌曲播放
5.3.1 播放媒体和播放列表初始化
在播放之前,需要先将QMediaPlayer和QMediaPlaylist初始化好。QQMusic类中需要添加QMediaPlayer和QMediaPlaylist的对象指针,在界面初始化时将这两个类的对象创建好。
cpp
#include <QMediaPlayer>
#include <QMediaPlaylist>
// minimusic.h 新增
public:
void initPlayer(); // 初始化媒体对象
private:
// 播放器相关
QMediaPlayer* player;
// 要多首歌曲播放,以及更复杂的播放设置,需要给播放器设置媒体列表
QMediaPlaylist* playList;
// minimusic.cpp 添加
MiniMusic::MiniMusic(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MiniMusic)
{
ui->setupUi(this);
// 界面ui初始化
initUI();
//btForm按钮点击信号处理
connectSignalAndSlot();
// 初始化播放器哦
initPlayer();
}
void MiniMusic::initPlayer()
{
// 创建播放器哦
player = new QMediaPlayer(this);
// 创建播放列表
playList = new QMediaPlaylist(this);
// 设置播放模式:默认为循环播放
playList->setPlaybackMode(QMediaPlaylist::Loop);
// 将播放列表设置给播放器
player->setPlaylist(playList);
// 默认音量大小设置为20
player->setVolume(20);
}
5.3.2 播放列表设置
播放之前,先要将歌曲加入用于播放的媒体列表,由于每个CommonPage页面的歌曲不同,因此CommonPage中新增将其页面歌曲添加到模仿列表的方法。
cpp
// commonpage.h 中新增
#include <QMediaPlaylist>
public:
// 向播放器添加音乐
void addMusicToPlayer(MusicList &musicList,QMediaPlaylist *playList);
// commonpage.cpp 中新增
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
// 将旧内容清空
musicListOfPage.clear();
for(auto& music : musicList)
{
switch (pageType)
{
case LOCAL_PAGE:
musicListOfPage.push_back(music.getMusicId());
break;
case LIKE_PAGE:
if(music.getIsLike())
{
musicListOfPage.push_back(music.getMusicId());
}
break;
case HISTORY_PAGE:
if(music.getIsHistory())
{
musicListOfPage.push_back(music.getMusicId());
}
break;
default:
break;
}
}
}
5.3.3 播放和暂停
当点击播放和暂停按钮时,播放状态应该在播放和暂停之间切换。播放器的状态如下,刚开始为停止状态。
QMediaPlayer的播放状态有:PlayingState()、PausedState()、StoppedState()。
|--------------|---------------|------|
| 播放状态 | 对应槽函数 | 说明 |
| PlayingState | void play(); | 正在播放 |
| PausedState | void pause(); | 暂停 |
| StoppedState | void stop(); | 停止状态 |
cpp
// minimusic.h 中新增
// 播放区域控制
void onPlayCliked(); // 播放按钮
// minimusic.cpp 中新增
void MiniMusic::onPlayCliked()
{
qDebug() << "播放按钮点击";
if(player->state() == QMediaPlayer::PlayingState)
{
// 如果是歌曲正在播放中,按下播放键,此时应该暂停播放
player->pause();
}
else if(player->state() == QMediaPlayer::PausedState)
{
// 如果是暂停状态,按下播放键,继续开始播放
player->play();
}
else if(player->state() == QMediaPlayer::StoppedState)
{
player->play();
}
}
void MiniMusic::connectSignalAndSlot()
{
// ...
// 播放控制区的信号和槽函数关联
connect(ui->play,&QPushButton::clicked,this,&MiniMusic::onPlayCliked);
}
注意:播放时默认是从播放列表索引为0的歌曲开始播放的。
另外播放状态改变的时候,需要修改播放按钮上图标,图片的修改可以在onPlayCliked函数中设置,也可以拦截.
QMediaPlayer中的stateChanged信号,当播放状态改变的时候,QMediaPlayer会触发该信号,在
stateChanged信号中修改播放按钮也可以,此处拦截stateChanged信号。
cpp
// minimusic.h新增
// QMediaPlayer信号处理
// 播放状态发生改变
void onPlayStateChanged();
// minimusic.cpp 新增
// QMediaPlayer信号关联槽函数
void MiniMusic::onPlayStateChanged()
{
qDebug() << "播放状态改变";
if(player->state() == QMediaPlayer::PlayingState)
{
// 播放状态
ui->play->setIcon(QIcon(":/images/play_on.png"));
}
else
{
// 暂停状态
ui->play->setIcon(QIcon(":/images/play3.png"));
}
}
void MiniMusic::initPlayer()
{
// ...
// QMediaPlayer信号和槽函数关联
// 播放状态改变时: 暂停和播放之间切换
connect(player,&QMediaPlayer::stateChanged,this,&MiniMusic::onPlayStateChanged);
}
播放和暂停切换的时候,按钮上的图标有重叠,是因为之前在界面设置的时候,为了能看到效果,给按钮添加了背景图片,背景图片和图标是两种属性,都设置时就ui叠加,因此将按钮上个添加背景图片样式去除掉。
cpp
void MiniMusic::initUI()
{
// ...
// 按钮的背景图片样式去除掉之后,需要设置默认图标
// 播放控制区按钮图标设定
ui->play->setIcon(QIcon(":/images/play_2.png")); // 默认为暂停图标
ui->playMode->setIcon(QIcon(":/images/shuffle_2.png")); // 默认为随机播放
}
5.3.4 上一曲和下一曲
播放列表中,提供了previous(和next()函数,通过设置前一个或者下一个歌曲为当前播放源,player就会播放对应的歌曲。
cpp
// minimusic.h 新增
void onPlayUpCliked(); // 上一曲
void onPlayDownCliked(); // 下一曲
// minimusic.cpp 新增
void MiniMusic::onPlayUpCliked()
{
playList->previous();
}
void MiniMusic::onPlayDownCliked()
{
playList->next();
}
void MiniMusic::connectSignalAndSlot()
{
// ...
// 播放控制区的信号和槽函数关联
connect(ui->play,&QPushButton::clicked,this,&MiniMusic::onPlayUpCliked);
connect(ui->playUp,&QPushButton::clicked,this,&MiniMusic::onPlayCliked);
connect(ui->playDown,&QPushButton::clicked,this,&MiniMusic::onPlayDownCliked);
}
5.3.5 播放模式设置
媒体列表提供了以下播放模式:
|----------------------------------|-----|----------------|
| 枚举状态名称 | 枚举值 | 说明 |
| QMediaPlaylist::CurrentItemOnce | 0 | 单词播放 |
| QMediaPlaylist:CurrentItemInLoop | 1 | 单曲循环 |
| QMediaPlaylist:Sequential | 2 | 从当前选中位置开始,顺序播放 |
| QMediaPlaylist:Loop | 3 | 列表中文件循环播放 |
| QMediaPlaylist::Random | 4 | 列表中文件随机播放 |
QMediaPlaylist提供了获取和设置播放模式的方法:
cpp
QMediaPlaylist::PlaybackMode playbackMode() const; // 获取播放模式
void setPlaybackMode(QMediaPlaylist::PlaybackMode mode); // 设置播放模式
目前暂支持:循环播放、随机播放、单曲循环
cpp
// minimusic.h 中新增
void onPlaybackModeCliked(); // 播放模式设置
// minimusic.cpp 中新增
void MiniMusic::initPlayer()
{
// ...
// 设置播放模式
connect(ui->playMode,&QPushButton::clicked,this,&MiniMusic::onPlaybackModeCliked);
}
void MiniMusic::onPlaybackModeCliked()
{
// 播放模式是针对播放列表的
// 播放模式支持:循环播放、随机播放、单曲循环三种模式
if(playList->playbackMode() == QMediaPlaylist::Loop)
{
// 列表循环
ui->playMode->setToolTip("随机播放");
playList->setPlaybackMode(QMediaPlaylist::Random);
}
else if(playList->playbackMode() == QMediaPlaylist::Random)
{
// 随机播放
ui->playMode->setToolTip("单曲循环");
playList->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
}
else if(playList->playbackMode() == QMediaPlaylist::CurrentItemInLoop)
{
// 随机播放
ui->playMode->setToolTip("列表循环");
playList->setPlaybackMode(QMediaPlaylist::Loop);
}
else
{
qDebug() << "播放格式错误";
}
}
播放模式切换时会触发playbackModechanged信号,在该信号对应槽函数中,完成图片切换。
cpp
// minimusic.h 中新增
// 播放模式切换槽函数
void onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode);
// minimusic.cpp 中新增
void MiniMusic::onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode)
{
if(playbackMode == QMediaPlaylist::Loop)
{
ui->playMode->setIcon(QIcon(":/images/list_play.png"));
}
else if(playbackMode == QMediaPlaylist::Random)
{
ui->playMode->setIcon(QIcon(":/images/shuffle_2.png"));
}
else if(playbackMode == QMediaPlaylist::CurrentItemInLoop)
{
ui->playMode->setIcon(QIcon(":/images/single_play.png"));
}
else
{
qDebug() << "暂不支持该模式";
}
}
void MiniMusic::initPlayer()
{
// ...
// 播放列表的模式发生改变时的信号槽关联
connect(playList,&QMediaPlaylist::playbackModeChanged,this,&MiniMusic::onPlaybackModeChanged);
}
5.3.6 播放所有
播放所有按钮属于CommonPage中的按钮,其对应的槽函数添加在CommonPage类中,但是
CommonPage不具有音乐播放的功能,因此当点击播放所有按钮后之后,播放所有的槽函数应该发射出信号,让MiniMusic类完成播放。
由于likePage、localPage、recentPage三个CommonPage页面都有playAllBtn,因此该信号需要带上PageType参数,需要让MiniMusic在处理该信号时,知道播放哪个页面的歌曲。
cpp
// commonpage.h 中新增
signals:
// 该信号由MiniMusic处理--在构造函数中捕获
void PlayAll(PageType pageType);
// commonpage.cpp 中修改
CommonPage::CommonPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommonPage)
{
ui->setupUi(this);
// 不要水平滚动条
ui->pageMusicList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// playAllBtn按钮的信号槽处理
// 当播放按钮点击时,发射playAll信号,播放当前页面的所有歌曲
// playAll信号交由MiniMusic中处理
connect(ui->playAllBtn,&QPushButton::clicked,this,[=](){
emit PlayAll(pageType);
});
}
在MiniMusic中,给playAll信号关联槽函数,并播放当前Page页面的所有音乐。playAll槽函数中,根据pageType将当前page页面记录下来,默认从该页面的第o首歌曲开始播放。注意不要忘记关联信号槽。
cpp
// minimusic.h 中新增
// 播放所有信号的槽函数
#include "commonpage.h"
// 判断哪个页面的playAll
void onPlayAll(PageType pageType);
// 传入页面和歌曲序列播放歌曲
void playAllOfCommonPage(CommonPage* commonPage,int index);
// minimusic.cpp 中新增
void MiniMusic::onPlayAll(PageType pageType)
{
CommonPage* page = nullptr;
switch (pageType)
{
case PageType::LIKE_PAGE:
page = ui->likePage;
break;
case PageType::LOCAL_PAGE:
page = ui->localPage;
break;
case PageType::HISTORY_PAGE:
page = ui->recentPage;
default:
qDebug() << "扩展";
break;
}
// 从当前页面的零号位置开始播放
playAllOfCommonPage(page,0);
}
void MiniMusic::playAllOfCommonPage(CommonPage *commonPage, int index)
{
// 播放page所在页面的歌曲
// 将播放列表先清空,否则无法播放当前CommonPage页面的歌曲
// 另外:该页面音乐不一定就在播放列表中,因此需要先将该页面音乐添加到播放列表
playList->clear();
// 将当前页面歌曲添加到播放列表
commonPage->addMusicToPlayer(musicList,playList);
// 设置当前播放列表的索引
playList->setCurrentIndex(index);
// 播放
player->play();
}
void MiniMusic::connectSignalAndSlot()
{
// ...
// 关联播放所有的信号和槽函数
connect(ui->likePage,&CommonPage::PlayAll,this,&MiniMusic::onPlayAll);
connect(ui->localPage,&CommonPage::PlayAll,this,&MiniMusic::onPlayAll);
connect(ui->recentPage,&CommonPage::PlayAll,this,&MiniMusic::onPlayAll);
}
5.3.7 双击CommPage页面QListWidget项播放
当QListWidget中的项被双击时,会触发doubleClicked信号,
cpp
// QListWidget中的项被双击时触发
// QModelIndex类中的row()函数会返回被点击的QListWidgetItem在QListWidget中索引
void doubleClicked(const QModelIndex &index);
该信号在QListWidget的基类中定义,有一个index参数,表示被双击的QListWidgetltem在QListWidget中的索引,该索引刚好与QMediaPlaylist中歌曲的所以一致,被双击时直接播放该首歌曲即可。
cpp
// CommonPage.h 中新增
signals:
// 发射在哪个页面第几行歌曲的信号
void playMusicByIndex(CommonPage*,int);
// commonpage.cpp 中新增
CommonPage::CommonPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommonPage)
{
ui->setupUi(this);
// ...
connect(ui->pageMusicList,&QListWidget::doubleClicked,this,[=](const QModelIndex &index){
// 鼠标双击后,发射信号告诉MiniMusic,就能放this页面中被双击的歌曲
emit playMusicByIndex(this,index.row());
});
}
// minimusic.h 中新增
// CommonPage中playMusicByIndex信号对应槽函数
void playMusicByIndex(CommonPage* page,int index);
// minimusic.cpp 中新增
void MiniMusic::playMusicByIndex(CommonPage *page, int index)
{
playAllOfCommonPage(page,index);
}
void MiniMusic::connectSignalAndSlot()
{
// ...
// 处理likePage、localPage、recentPage中ListItemBox双击
connect(ui->likePage,&CommonPage::playMusicByIndex,this,&MiniMusic::playMusicByIndex);
connect(ui->localPage,&CommonPage::playMusicByIndex,this,&MiniMusic::playMusicByIndex);
connect(ui->recentPage,&CommonPage::playMusicByIndex,this,&MiniMusic::playMusicByIndex);
}
5.3.8 最近播放同步
当播放歌曲改变时,即播放的媒体源发生了变化,QMediaPlayer会触发metaDataAvailableChanged信号,QMediaPlaylist也会触发currentIndexChanged信号,该信号会带index参数,表示现在是媒体播放列表中的index歌曲被播放,通过index可以获取到recentPage页面中具体播放的歌曲,将该歌曲对应Music对象的isHistoty属性修改为true,然后更新下rencentPage的歌曲列表,播放过的歌曲就添加到历史播放页面中了。
问题:获取likePage、localPage、recentPage哪个CommonPage页面中的歌曲呢?
答案:QQMusic类中维护CommonPage*变量currentPage,记录当前正在播放的CommonPage页
面,初始时设置为localPage,当播放的页面发生改变时,修改currentPage为当前正在播放页面,其中点击播放所有按钮以及双击QListWidget中项的时候都回引起currentPage的改变。
cpp
// minimusic.h 中新增
// 记录当前正在播放的页面
CommonPage* currentPage;
// minimusic.cpp 中修改
void MiniMusic::initUI()
{
// ...
// 将localPage设置为当前页面
currentPage = ui->localPage;
}
void MiniMusic::playAllOfCommonPage(CommonPage *commonPage, int index)
{
currentPage = commonPage;
// 播放page所在页面的歌曲
// ...
}
准备工作完成之后,同步最近播放歌曲的逻辑实现如下:
cpp
// minimusic.h 中新增
// 支持播放历史记录
void onCurrentIndexChanged(int index);
// minimusic.cpp 中新增
void MiniMusic::initPlayer()
{
// ...
// 播放列表项发生改变时,此时将播放音乐收藏到历史记录中
connect(playList,&QMediaPlaylist::currentIndexChanged,this,&MiniMusic::onCurrentIndexChanged);
}
void MiniMusic::onCurrentIndexChanged(int index)
{
// 音乐的id都在commonPage中的musicListOfPage中存储着
const QString& musicId = currentPage->getMusicIdByIndex(index);
// 有了MusicId就可以在musicList中找到该音乐
auto it = musicList.findMusicById(musicId);
if(it != musicList.end())
{
// 将该音乐设置为历史播放记录
it->setIsHistory(true);
}
ui->recentPage->reFresh(musicList);
}
// commonpage.h 中新增
// 通过index获取音乐的id
const QString getMusicIdByIndex(int index) const;
// commonpage.cpp 中新增
const QString CommonPage::getMusicIdByIndex(int index) const
{
if(index >= musicListOfPage.size())
{
qDebug() << "无此歌曲";
return "";
}
return musicListOfPage[index];
}
5.3.9 音量设置
a.功能分析
当点击静音按钮时,音量应该在静音和非静音之间进行切换,并且按钮上图标需要同步切换。鼠标在滑竿上点击或拖动滑竿时,应该跟进滑竿的高低比率,设置音量大小,同时修改界面音量比率。

b. QMediaPlayer提供支持
QMediaPlayer中音量相关操作如下:
cpp
int volume; // 标记音量大小,值在0~100之间
int volume() const; // 获取音量大小
void setVolume(int); // 槽函数:设置音量大小
bool muted; // 是否静音,true为静音,false为非静音
bool isMuted() const; // 获取静音状态
bool setMuted(bool muted); // 槽函数:设置静音或非静音
c.静音和非静音切换
VolumeTool类中需要添加两个成员变量,并在构造函数中完成默认值的设置。
给静音按钮参加槽函数onSilenceBtnClicked,并在构造函数中connect按钮的clicked信号,当按钮点击时候,调用setMuted(bool nuted)函数,完成静音和非静音的设置。
由于VolumeTool不具备媒体播放控制,因此当静音状态发生改变时,发射设置静音信号,让MinuMusic来处理。
cpp
// volumetool.h 中新增
signals:
void setSilence(bool); // 设置静音信号
public:
void onSilenceBtnClicked(); // 静音按钮槽函数
private:
bool isMuted; // 记录静音或非静音,当点击静音按钮时,在true和false之间切换
int volumeRatio; // 标记音量大小
// volumetool.cpp 中新增
VolumeTool::VolumeTool(QWidget *parent) :
QWidget(parent),
ui(new Ui::VolumeTool),
isMuted(false), // 默认静音
volumeRatio(20) // 默认音量我20%
{
// ...
// 关联静音按钮的信号槽
connect(ui->silenceBtn,&QPushButton::clicked,this,&VolumeTool::onSilenceBtnClicked);
}
void VolumeTool::onSilenceBtnClicked()
{
isMuted = !isMuted;
if(isMuted)
{
ui->silenceBtn->setIcon(QIcon(":/images/silent.png"));
}
else
{
ui->silenceBtn->setIcon(QIcon(":/images/volumn.png"));
}
emit setSilence(isMuted);
}
// minimusic.h 中新增
// 设置是否静音
void setMusicSilence(bool isMuted);
// minimusic.cpp 中新增
void MiniMusic::setMusicSilence(bool isMuted)
{
player->setMuted(isMuted);
}
void MiniMusic::connectSignalAndSlot()
{
// ...
// 设置静音
connect(volumeTool,&VolumeTool::setSilence,this,&MiniMusic::setMusicSilence);
}
d. 鼠标按下、滚动以及释放事件处理
当鼠标在滑竿上按下时,需要设置sliderBtn和outLine的位置,当鼠标在滑竿上移动或者鼠标抬起时,需要设置SliderBtnoutLine结束的位置,即改变VolumeTool中滑竿的显示。具体修改播放媒体音量大小操作应该由于MiniMusic负责处理,因此当鼠标移动或释放时,需要发射信号让MiniMusic知道需要修改播放媒体的音量大小了。
cpp
// volumetool.h 中新增
signals:
void setMusicVolume(int); // 发射修改音量大小槽函数
// 事件过滤器
bool eventFilter(QObject* object,QEvent* event);
// volumetool.cpp 中新增
bool VolumeTool::eventFilter(QObject *object, QEvent *event)
{
// 过滤volumeBox上的事件
if(object == ui->sliderBox)
{
if(event->type() == QEvent::MouseButtonPress)
{
// 如果是鼠标按下事件,修改sliderBtn和outLine的位置,并计算volumeRation
setVolume();
}
else if(event->type() == QEvent::MouseMove)
{
// 如果是鼠标滚动事件,修改sliderBtn和outLine的位置,并计算volumeRation
setVolume();
// 并发射setMusicVolume信号
emit setMusicVolume(volumeRatio);
}
else if(event->type() == QEvent::MouseButtonRelease)
{
// 如果是鼠标释放事件,直接发射setMusicVolume信号
emit setMusicVolume(volumeRatio);
}
return true;
}
return QObject::eventFilter(object,event);
}
VolumeTool::VolumeTool(QWidget *parent) :
QWidget(parent),
ui(new Ui::VolumeTool),
isMuted(false), // 默认静音
volumeRatio(20) // 默认音量我20%
{
// ...
// 安装事件过滤器
ui->sliderBox->installEventFilter(this);
}
e. outLine和SliderBtn以及volumeRation更新
outLine坐标:[(38,25),4*180]。
当outLine最低时,即高度为0时候,outLine左上角坐标为:(38,205)
当outLine最高时,即高度为180时,outLine左上角坐标为:(38,25)
当鼠标在滑竿上滚动时,鼠标坐标转化为volumeBox上相对坐标时,鼠标y坐标必须在[25~205]范围内。
根据鼠标在滑竿上的相对高度更新:SliderBtn、outLine以及volumeRation的值。
cpp
// volumetoo.h 中新增
// 根据鼠标在滑竿上滑动更新滑动界面,并按照比例计算音量大小
void setVolume();
// volumetool.cpp 中新增
void VolumeTool::setVolume()
{
// 1.将鼠标的位置转换为sloderBox上的相对坐标,此处只要获取y坐标
int height = ui->sliderBox->mapFromGlobal(QCursor().pos()).y();
// 2.鼠标在sliderBox中课移动的y范围在[25,205]之间
height = height < 25 ? 25 : height;
height = height > 205 ? 205 : height;
// 3.调整sliderBtn的位置和大小
ui->sliderBtn->move(ui->sliderBtn->x(),height - ui->sliderBtn->height()/2);
// 4.更新outSlider的位置和大小
ui->outSlider->setGeometry(ui->outSlider->x(),height,ui->outSlider->width(),205-height);
// 5.计算音量比率
volumeRatio = (int)((int)ui->outSlider->height()/(float)180*100);
// 6.设置给label显示出来
ui->volumeRatio->setText(QString::number(volumeRatio)+"%");
}
f. MiniMusic类拦截VolumeTool发射的setMusicVolume信号,将音量大小设置为指定值。
cpp
// minimusic.h 中新增
// 设置音量大小
void setPlayerVolume(int volume);
// minimusic.cpp 中新增
void MiniMusic::setPlayerVolume(int volume)
{
player->setVolume(volume);
}
void MiniMusic::connectSignalAndSlot()
{
// ...
// 设置音量大小
connect(volumeTool,&VolumeTool::setMusicVolume,this,&MiniMusic::setPlayerVolume);
}
5.3.10 当前播放时间和总时间更新
a.界面歌曲总时间更新
歌曲总时间在Music对象中可以获取,也可以让player调用自己的duration(方法获取。但是这两种获取歌曲总时间的调用时机不太好确定。我们期望的是当歌曲发生切换时,获取到正在播放歌曲的总时长。
当播放源的持续时长发生改变时,QMediaPlayer会触发durationChanged信号,该信号中提供了将要播放媒体的总时长。
cpp
// duration为将要播放媒体的总时长
void QMediaPlayer::durationChanged(qint64 duration);
因此在MiniMusic类中给该信号关联槽函数,在槽函数中将duration更新到界面总时间即可。
cpp
// minimusic.h 中新增
// 歌曲持续时长改变时(歌曲切换)
void onDurationChanged(qint64 duration);
// minimusic.cpp 中新增
void MiniMusic::onDurationChanged(qint64 duration)
{
ui->totalTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0'))
.arg(duration/1000%60,2,10,QChar('0')));
}
void MiniMusic::initPlayer()
{
// ...
// 媒体持续时长更新,即:音乐切换,时长更新,界面上时间也要更新
connect(player,&QMediaPlayer::durationChanged,this,&MiniMusic::onDurationChanged);
}
b. 界面歌曲当前播放时间更新
媒体在持续播放过程中,QMediaPlayer会发射positionChanged,该信号带有一个qint64类型参数,表示媒体当前持续播放的时间。
cpp
// position:媒体持续播放时间
void positionChanged(qint64 position);
因此,在MiniMusic中捕获该信号,便可获取到正在播放媒体的持续时间。
cpp
// minimusic.h 中新增
// 播放位置改变,即持续播放时间改变
void onPositionChanged(qint64 duration);
// minimusic.cpp 中新增
void MiniMusic::onPositionChanged(qint64 duration)
{
ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0'))
.arg(duration/1000%60,2,10,QChar('0')));
// 界面上的进度条也需要同时修改
}
void MiniMusic::initPlayer()
{
// ...
// 播放位置发生改变,即以及播放时间更新
connect(player,&QMediaPlayer::positionChanged,this,&MiniMusic::onPositionChanged);
}
在持续播放时间改变的同时,界面上的进度条应该也要前进。
5.3.11 进度条处理[seek功能]
1.seek功能介绍
播放器的seek功能指,通过时间或位置快速定位到视频或音频流的特定位置,允许用户在播放过程中随时跳转到特定时间点,从而快速找到感兴趣的内容或重新开始播放。

在界面上的体现是,当在MusicSlider上点击或者拖拽的时候,会跳转到歌曲的指定位置进行播放,并且歌曲的当前持续播放时间要同步修改。
2.进度条界面显示
进度条功能进度界面展示与音量调节位置类似,拦截鼠标按下、鼠标移动、以及鼠标释放消息即可。在内部捕获到鼠标的位置的横坐标x,将x作为outLine的宽度即可。即在鼠标按下、移动、释放的时候,修改outLine的宽度即可。
cpp
// musicslider.h 中新增
void mousePressEvent(QMouseEvent *event); // 重写鼠标按下事件
void mouseMoveEvent(QMouseEvent *event); // 重写鼠标滚动事件
void mouseReleaseEvent(QMouseEvent *event); // 重写鼠标释放事件
void moveSilder(); // 重写鼠标释放事件
private:
int currentPos; // 滑动条当前位置
// musicslider.cpp 中新增
MusicSlider::MusicSlider(QWidget *parent) :
QWidget(parent),
ui(new Ui::MusicSlider)
{
ui->setupUi(this);
// 初始情况下,还没有开始播放,将当前播放进度设置为0
currentPos = 0;
maxWidth = width();
moveSilder();
}
void MusicSlider::mousePressEvent(QMouseEvent *event)
{
// 注意:QMouseEvent中的pos()为鼠标相对于widget的坐标,不是相当于screen
// 因此鼠标位置的x坐标可直接作为outLine的宽度
currentPos = event->pos().x();
moveSilder();
}
void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{
// 如果鼠标不在MusicSlider的矩形内,不进行拖拽
QRect rect = QRect(0,0,width(),height());
QPoint pos = event->pos();
if(!rect.contains(pos))
{
return;
}
// 根据鼠标滑动的位置更新outLine的宽度
if(event->buttons() == Qt::LeftButton)
{
// 验证:鼠标点击的x坐标是否越界,如果越界将其调整到边界
currentPos = event->pos().x();
if(currentPos < 0)
{
currentPos = 0;
}
if(currentPos > maxWidth)
{
currentPos = maxWidth;
}
moveSilder();
}
}
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSilder();
}
void MusicSlider::moveSilder()
{
// 根据当前进度设置外部滑动条的位置
ui->outLine->setMaximumWidth(currentPos);
ui->outLine->setGeometry(0,8,currentPos,4);
}
3. 进度条同步持续播放时间
当鼠标释放之后,计算出进度条当前位置currentPos和总宽度的maxWidth比率,然后发射信号告诉MiniMusic,让player按照该比率更新持续播放时间。
cpp
// musicslider.h新增
signals:
void setMusicSliderPosition(float);
// musicslider.cpp新增
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSilder();
emit setMusicSliderPosition((float)currentPos/(float)maxWidth);
}
// minimusic.h中新增
// 进度条改变
void onMusicSliderChanged(float value);
// minimusic.cpp 中新增
void MiniMusic::onMusicSliderChanged(float value)
{
// 1.计算当前seek位置的时长
qint64 duration = (qint64)(totalTime* value);
// 2.转换为百分制,设置当前时间
ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0'))
.arg(duration/1000%60,2,10,QChar('0')));
// 3.设置当前播放位置
player->setPosition(duration);
}
void MiniMusic::connectSignalAndSlot()
{
// ...
// 进度条拖拽
connect(ui->processBar,&MusicSlider::setMusicSliderPosition,this,&MiniMusic::onMusicSliderChanged);
}
4. 持续时间同步进度条
当播放位置更新时,界面上持续播放时间一直在更新,因此进度条也需要持续向前进,MusicSlider应该提供setStep函数,播放进度持续更新时,也将进度条通过setStep函数更新下。
cpp
// musicslider.h 中新增
void setStep(float bf);
// musicslider.cpp 中新增
void MusicSlider::setStep(float bf)
{
currentPos = maxWidth*bf;
moveSilder();
}
// minimusic.cpp 中修改
void MiniMusic::onPositionChanged(qint64 duration)
{
ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0'))
.arg(duration/1000%60,2,10,QChar('0')));
// 界面上的进度条也需要同时修改
ui->processBar->setStep((float)duration/(float)totalTime);
}
5.3.12 歌曲名称、歌手和封面图片同步
在进行歌曲切换时候,歌曲名称、歌手以及歌曲的封面图,也需要更新到界面。歌曲名称、歌手可以再Music对象中进行获取,歌曲的封面图可以通过player到歌曲的元数据中获取,获取时需要使用"Thumbnaillmage"作为参数,注意有些歌曲可能没有封面图,如果没有设置一张默认的封面图。由于歌曲切换时,player需要将新播放歌曲作为播放源,并解析歌曲文件,如果歌曲文件是有效的才能播放;因此QQMusic类可以给QMediaPlayer发射的metaDataAvailableChanged(bool)信号关联槽函数,当歌曲更换时,完成信息的更新。
cpp
// minimusic.h中新增
void onMetaDataAvailableChanged(bool available);
// minimusic.cpp 中新增
void MiniMusic::onMetaDataAvailableChanged(bool available)
{
// 播放源改变
qDebug()<<"歌曲切换";
// 1. 从player播放歌曲的元数据中获取歌曲信息
QString singer = player->metaData("Author").toStringList().join(",");
QString musicName = player->metaData("Title").toString();
if(musicName.isEmpty())
{
auto it = musicList.findMusicByMusicId(currentPage->getMusicIdByIndex(curPlayMusicIndex));
if(it != musicList.end())
{
musicName = it->getMusicName();
singer = it->getMusicSinger();
}
}
// 2. 设置歌手、歌曲名称、专辑名称
ui->musicName->setText(musicName);
ui->musicSinger->setText(singer);
// 3. 获取封面图片
QVariant coverImage = player->metaData("ThumbnailImage");
if(coverImage.isValid())
{
// 获取封面图片成功
QImage image = coverImage.value<QImage>();
// 设置封面图片
ui->musicCover->setPixmap(QPixmap::fromImage(image));
// 缩放填充到整个Label
ui->musicCover->setScaledContents(true);
currentPage->setImageLabel(QPixmap::fromImage(image));
}
else
{
// 设置默认图片-修改
qDebug()<<"歌曲没有封面图片";
}
}
void CommonForm::setImageLabel(QPixmap pixMap)
{
ui->musicImgLabel->setPixmap(pixMap);
ui->musicImgLabel->setScaledContents(true);
}
5.4 lrc歌词同步
播放歌曲时,当点击"词"按钮后窗口会慢慢弹出,当点击隐藏按钮后,窗口会慢慢隐藏,且没有标题栏。内部显示当前播放歌曲的歌词,以及歌曲名称和作者。当点击下拉按钮时,窗口会隐藏起来。
5.4.1 Irc歌词界面分析
IrcPage中元素种类比较少,具体分析如下:

①和②为QLabel,分别显示作者和歌曲名称;
③~⑨均为QLabel,用来显示歌词,为当前正在播放歌词,③④⑤为当前播放歌词的前三句,⑦⑧⑨为当前播放歌词的后三句。歌词会随着播放时间持续,从下往上移动。
⑩为按钮,点击之后窗口隐藏。
5.4.2 Irc歌词界面布局
在qt create中新创建一个qt 设计师界面,命名为LrcPage,geometry的宽高修改为:1020*680。
①拖一个Widget到LrcPage中,objectName修改为bgStyle,选中LrcPage,然后点击垂直布局,并将LrcPage的margin和spacing修改为0;
②拖两个Widget到bgStyle中,objectName从上往下分别修改为lrcTop和lrcContent,IrcTop的
minimumSize和maximumSize的高修改为50;然后选中bgStyle点击垂直布局,并将bgStyle的
margin和spacing修改为0;
③拖一个按钮到IrcTop中,objectName修改为hideBtn,minimumSize和maximumSize的宽和高修
改为:30*50;拖一个Widget到IrcTop中,objectName修改为titleBox;然后选中lrcTop,点击水平布局,并将lrcTop的margin和spacing修改为0;
④拖两个QLabel到titleBox中,objectName从上往下修改为musicSinger和musicName,然后选中
titleBox,点击垂直布局,并将titleBox的margin和spacing修改为0;
⑤拖六个QLabel到lrcContent中,从上往下将objectName依次修改为:line1、line2、line3、lineCenter、line4、line5、line6,将line1~line6的minimumSize的高度修改为50,font大小修改为15,将lineCenter的minimumSize高度修改为80,font的大小修改为25;拖两个垂直弹簧,一个放在line1上,一个放在line6下,将所有的QLabel撑到中间
⑥选中lrcContent,然后点击垂直布局,将lrcContent的margin和spacing修改为0
bgStyle
css
#bgStyle
{
border-image:url(:/images/bg.png);
}
*
{
color:#FFFFFF;
}
lineCenter
css
#lineCenter
{
color:#1ECE9A;
}
hideBtn
css
#hideBtn
{
border:none;
}

5.4.3 LrcPage显示
在LrcPage的构造函数中,将窗口的标题栏去除掉;并给hideBtn关联clicked信号,当按钮点击时将窗口隐藏。
cpp
// lrcPage.cpp 中添加
LrcPage::LrcPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::LrcPage)
{
ui->setupUi(this);
setWindowFlag(Qt::FramelessWindowHint);
connect(ui->hideBtn,&QPushButton::clicked,this,[=]{
hide();
});
ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));
}
在mINIMusic中,创建LrcPage的指针,并在initUi()方法中创建窗口的对象,创建好之后将窗口隐藏起来;
在MiniMusic中,给lrcWord按钮添加槽函数,在槽函数中将窗口显示出来。
cpp
// minimusic.h中添加
#include "lrcpage.h"
LrcPage *lrcPage;
// 歌词按钮槽函数
void onLrcWordClicked();
// minimusic.cpp 中添加
void MiniMusic::initUI()
{
// ...
// 实例化lrcWord对象
lrcPage = new LrcPage(this);
lrcPage->hide();
}
void MiniMusic::onLrcWordClicked()
{
lrcPage->show();
}
void MiniMusic::connectSignalAndSlot()
{
//...
// 显示歌词窗口
connect(ui->lrcWord,&QPushButton::clicked,this,&MiniMusic::onLrcWordClicked);
}
5.4.4 LrcPage添加动画效果
当点击QQMusic中"歌词"按钮时,IrcPage窗口是以动画效果显示出来的,当点击lrcPage上"下拉"按钮时,窗口先以动画的方式下移,动画结束后窗口隐藏。
1.窗口显示和上移动画
①MiniMusic的initUi函数中,创建lrcPage对象并将窗口隐藏;给lrcPage窗口添加上移动画,动画暂不开启
②MiniMusic中给"歌词"按钮添加槽函数,当按钮点击时,显示窗口,开启动画
cpp
// minimusic.h 中新增
#include <QPropertyAnimation>
// 歌词按钮槽函数
void onLrcWordClicked();
private:
QPropertyAnimation* lrcAnimation;
// minimusic.cpp 中新增
void MiniMusic::initUI()
{
// ...
// 窗口添加阴影效果
QGraphicsDropShadowEffect* shadowEffect2 = new QGraphicsDropShadowEffect(this);
shadowEffect2->setOffset(0,0);
shadowEffect2->setColor("#000000"); // 黑色
// 此处需要将圆角半径不能太大,否则动画效果有问题,可以设置为10
shadowEffect2->setBlurRadius(10);
this->setGraphicsEffect(shadowEffect2);
// 实例化lrcWord对象
lrcPage = new LrcPage(this);
lrcPage->hide();
// lrcPage添加动画效果
lrcAnimation = new QPropertyAnimation(lrcPage,"geometry",this);
lrcAnimation->setDuration(250);
lrcAnimation->setStartValue(QRect(10,10+lrcPage->height(),lrcPage->width(),lrcPage->height()));
lrcAnimation->setEndValue(QRect(10,10,lrcPage->width(),lrcPage->height()));
}
void MiniMusic::onLrcWordClicked()
{
// 显示窗口并开启动画
lrcPage->show();
lrcAnimation->start();
}
void MiniMusic::connectSignalAndSlot()
{
// ...
// 歌词按钮点击信号和槽函数
connect(ui->lrcWord,&QPushButton::clicked,this,&MiniMusic::onLrcWordClicked);
}
2. 窗口隐藏和下移动画
LrcPage类中,在构造窗口时设置下移动画,给"下拉"按钮添加槽函数,当"下拉按钮"点击时,开启动画;当动画结束时,将窗口隐藏。
cpp
// lrcpage.h 中新增
#include <QPropertyAnimation>
private:
QPropertyAnimation* lrcAnimation;
// lrcpage.cpp 中新增
LrcPage::LrcPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::LrcPage)
{
ui->setupUi(this);
//...
// 点击设置下拉按钮时开启动画
connect(ui->hideBtn,&QPushButton::click,this,[=]{
lrcAnimation->start();
});
// 动画结束时,将窗口隐藏
connect(lrcAnimation,&QPropertyAnimation::finished,this,[=]{
hide();
});
}
5.4.5 Irc歌词解析和同步
1.什么是LRC歌词
Irc是英文lyric(歌词)的缩写,被用作歌词文件的扩展名。该文件将歌词和歌词出现的时间编辑到一起,当歌曲播放的时候,按照歌词文件中的时间依次将歌词显示出来。
标准格式:[分钟:秒.毫秒]歌词
其他格式:①[分钟:秒]歌词②[分钟:秒:毫秒]歌词

每首歌的lrc歌词有多行文本,因此lrc歌词中的每行可以采用结构体管理。
cpp
// lrcpage.h 中新增
struct LyricLine
{
qint64 time; // 时间
QString text; // 歌词内容
LyricLine(qint64 qtime,QString qtext)
:time(qtime)
,text(qtext)
{}
};
// lrcPage类中添加成员变量
QVector<LyricLine> lrcLines; // 按照时间的先后次序保存每行歌词
2. 通过歌曲名找LRC文件
一般情况下,播放器在设计之初就会设计好歌曲文件和歌词文件的存放位置,以及对应关系,通常歌曲文件和lrc歌词文件名字相同,后缀不同。在磁盘存放的时候,可以将歌曲文件和lrc文件分两个文件夹存储,也可以存储到一个文件夹下。
本文为了方便处理,存储在一个文件夹下,因此可以通过Music对象快速找到lrc歌词文件。
cpp
// music.h 中新增
QString getLrcFilePath() const;
// music.cpp 中新增
QString Music::getLrcFilePath() const
{
// D:/musics/2002年的第一场学.mp3 歌曲文件路径
// D:/musics/2022年的第一场学.lrc 歌词文件
QString lrcPath = musicUrl.toLocalFile();
lrcPath.replace(".mp3", ".lrc");
lrcPath.replace(".flac", ".lrc");
lrcPath.replace(".mpga", ".lrc");
return lrcPath;
}
也可以将歌曲文件和lrc歌词文件分别存储到两个文件夹。
3. LRC歌词解析
找到Irc歌词文件后,由lrcPage类完成对歌词的解析。解析的大概步骤:
①打开歌词文件
②以行为单位,读取歌词文件中的每一行
③按照Irc歌词文件格式,从每行文本中解析出时间和歌词
00:17.94\]那些失眠的人啊你们还好吗
\[0:58.600.00\]你像一只飞来飞去的蝴蝶
④用\<时间,行歌词\>构建一个LrcLine对象存储到IrcLines中。
```cpp
// lrcpage.h 中新增
bool parseLrcFile(const QString& lrcFilePath);
// lrcpage.cpp 中新增
bool LrcPage::parseLrcFile(const QString &lrcFilePath)
{
// 1. 打开文件
QFile file(lrcFilePath);
if(!file.open(QIODevice::ReadOnly))
{
qDebug()<<"打开lrc文件:"<