基于QT的音乐播放器
- 前言
- 一、主体效果
- 二、主要技术点:
- 1. mp3的ID3V2格式文件解析:作者、歌手、时长、专辑图片等
- 1.1 需要工具:
- 1.2 ID3V2文件格式
- 1.3 mp3ID3V2解析代码
- 2. 音乐列表拖拽、临界自动滚动、信息栏。
- 2.1要实现效果:
- 2.2 QListView的样式表
- 2.3 重写QListView四个事件函数,发出需要信号,及自动滚动效果
- 2.4 关联信号实现拖拽释放,移动iterm效果
- 3. 歌词实时显示,滚动查看、跳转归位、双击播放
- 3.1 要实现效果
- 3.2 歌词文本解析(GBK解码)
- 3.3 实时显示,歌词滚动
- 3.4 鼠标滑轮显示,不操作跳转归位/操作双右键就播放当前
- 4. 其他(播放顺序、各种样式表、背景图模糊)
- 4.1 背景图模糊
- 4.2 QPushButton按键样式表
- 4.3 QSlider按键样式表
- 三、项目代码分享
- 总结
前言
自己闲来无事自己搞一个音乐播放器,原型是仿照qq音乐,对于一些功能实现,我在网上搜了一些,加上自己理解瞎搞一通,目前项目还有很多不足,只搞了个大概,项目还是有很多要改进的,仅供学习参考,一起进步!
一、主体效果
二、主要技术点:
1. mp3的ID3V2格式文件解析:作者、歌手、时长、专辑图片等
对于目前来说,只做个单机版mp3音乐播放器,那我们有的文件就只有:xxx.mp3与xxx.lrc,一个是音频文件,一个是歌词文件,如下图:
如何解码xxx.mp3文件得到作者、歌手、歌名、专辑图片、专辑名等就非常有必要。
1.1 需要工具:
- Binary Viewer:可以查看以二进制查看文件,
QQ音乐播放器:可以下载ID3V2格式,既xxx.mp3与xxx.lrc
Binary Viewer网上一搜就有了都是免费的,QQ音乐需要手动设置下,才能下载ID3V2与歌词。 这点需要注意,不然下载下来都是ID3V1格式,就没有专辑图像等一些信息
1.2 ID3V2文件格式
- 结构还是挺简单的,找到标签帧需要内容,读取内容就可以,但不是所有标签帧都能找到,qq音乐下载一般都有:有"TIT2"、“TPE1”、“TALB”、“TXXX”、“TRCK”、“TPOS”、“TCON”、"APIC"内容。这样我们就可以获得作者,歌曲名,专辑名,专辑图片。
- 歌曲时间长度的话可以采用:其QT的QMediaPlayer类获取。 方式一:QMediaPlayer->duration();方式二:通过void onDurationChanged(qint64 duration)槽函数获取。
1.3 mp3ID3V2解析代码
/*"music_ananly.h"的头文件*/
#ifndef MUSIC_ANANLY_H
#define MUSIC_ANANLY_H
#include <QString>
#include <QMediaPlayer>
#include <QObject>
typedef struct music_info_st
{
QString mic_name; //曲名
QString mic_songer; //歌手
QString mic_time; //时长
QString mic_album; //专辑
QString mic_path; //mp3所在路径
QString pic_flag="0"; //是否有图片
QString pic_path; //歌曲图片
QString pic_type; //图片类型 jpg,png
QString showlist; //列表显示
}MUSIC_info;
//mp3IDV2格式:-》标签头+标签帧1头帧+标签帧1内容1+标签帧2头帧+标签帧2内容+....+ 最后音乐正式内容
typedef struct TAB_info_st //标签头:开始前10位
{
char format[3]; //格式
char version; //版本
char unuse[2];
char header_size[4]; //标签帧+标签头的size
}TAB_info;
typedef struct head_info_st //标签帧头帧:每帧前8位
{
char FrameID[4]; /*用四个字符标识一个帧,说明其内容,稍后有常用的标识对照表*/
char Size[4]; /*帧内容的大小,不包括帧头,不得小于1*/
char Flags[2]; /*存放标志,只定义了6位,稍后详细解说*/
}HEAD_info;
class music_ananly : public QObject
{
Q_OBJECT
public:
explicit music_ananly(QObject *parent = nullptr);
music_ananly();
MUSIC_info m_music_info; //记录当前mp3IDV2格式文件信息
QMediaPlayer *temp_MP; //播放器用来得到时间长短
bool analyse_music(QString path); //music analyse api
signals:
void music_ananly_complete_signal(MUSIC_info); //处理完成信号
private slots:
void onDurationChanged(qint64);
};
#endif // MUSIC_ANANLY_H
/*"music_ananly.cpp"的文件*/
#include "music_ananly.h"
#include <QFile>
#include <QTextStream>
#include <QDebug>
#include <QTextCodec>
#include <QMediaPlayer>
#include <QObject>
music_ananly::music_ananly(QObject *parent) : QObject(parent)
{
//创建一个播放器
temp_MP = new QMediaPlayer;
temp_MP->setVolume(0);
connect(temp_MP,SIGNAL(durationChanged(qint64)),this,SLOT(onDurationChanged(qint64)));
}
//MP3IDV2格式解析函数
bool music_ananly::analyse_music(QString path) //music analyse api
{
//初始化
m_music_info.mic_songer="";
m_music_info.mic_name="";
m_music_info.mic_time="";
m_music_info.pic_flag="0";
m_music_info.pic_type="";
m_music_info.pic_path="";
//目前只支持mp3IDV2解析
QFile file(path);
bool isok=file.open(QIODevice::ReadOnly);
TAB_info tab_info;
qint64 head_size=0; //头部大小
qint64 file_seek=0; //文件指针
quint64 len;
if(isok==false)
{
qDebug()<<"open error";
file.close();
return false;
}
//文件打开成功
m_music_info.mic_path=path; //记录mp3文件的路径
file.read((char*)&tab_info,sizeof(tab_info));
file_seek=file_seek+10;
//判断是否为mp3的IDV2格式
//qDebug()<<QString(tab_info.format);
if(QString(tab_info.format)!="ID3\u0003"||(int)tab_info.version !=3)
{
qDebug()<<"mp3 is not ID3V2 error";
return false;
}
head_size=(tab_info.header_size[0]&0xff)<<21 |
(tab_info.header_size[1]&0xff)<<14 |
(tab_info.header_size[2]&0xff)<<7 |
(tab_info.header_size[3]&0xff); //每8位只用前7位,第8位无效恒为0;
HEAD_info head_info;
quint32 size;
while(file_seek<head_size)
{
//读取头部信息
len=file.read((char*)&head_info,sizeof(head_info));
file_seek=file_seek+len;
size=(head_info.Size[0]&0xff) <<24|(head_info.Size[1]&0xff)<<16|(head_info.Size[2]&0xff)<<8|(head_info.Size[3]&0xff);
//有"TIT2""TPE1""TALB""TXXX" "TRCK""TPOS""TCON""APIC"
//qDebug()<<QString(head_info.FrameID);
if(QString(head_info.FrameID)=="TIT2") //曲名
{
QTextStream stream(&file);
stream.seek(file.pos()+1);
QString all= stream.readLine((int)(size/2-1)); //unicode编码中文是两个字节为一个中文,外加结束为零。
QTextCodec *codec = QTextCodec::codecForName("GBK");
QString name = codec->toUnicode(all.toLocal8Bit().data());
//qDebug()<<name;
m_music_info.mic_name=name;
file_seek=file_seek+size;
file.seek(file_seek);
continue;
}
if(QString(head_info.FrameID)=="TPE1") //歌手
{
QTextStream stream(&file);
stream.seek(file.pos()+1);
QString all= stream.readLine((int)(size/2-1)); //unicode编码中文是两个字节为一个中文,外加结束为零。
QTextCodec *codec = QTextCodec::codecForName("GBK");
QString author = codec->toUnicode(all.toLocal8Bit().data());
//qDebug()<<author;
m_music_info.mic_songer=author;
file_seek=file_seek+size;
file.seek(file_seek);
continue;
}
if(QString(head_info.FrameID)=="TALB") //专辑
{
QTextStream stream(&file);
stream.seek(file.pos()+1);
QString all= stream.readLine((int)(size/2-1)); //unicode编码中文是两个字节为一个中文,外加结束为零。
QTextCodec *codec = QTextCodec::codecForName("GBK");
QString album = codec->toUnicode(all.toLocal8Bit().data());
//qDebug()<<album;
m_music_info.mic_album=album;
file_seek=file_seek+size;
file.seek(file_seek);
continue;
}
if(QString(head_info.FrameID)=="APIC") //图片
{
m_music_info.pic_flag="1";
file_seek=file_seek+14; //去掉14位为照片描述
file.seek(file_seek);
char *piture =(char *)malloc(size);
file.read((char *)piture,size-14);
file_seek=file_seek+size-14;
//判断照片的存储格式jpg/png
if(((uchar)piture[0]== 255) && ((uchar)piture[1]== 216)) //0xff 0xd8 ->jpg
{
m_music_info.pic_type="jpg";
}else if(((uchar)piture[0]== 137) && ((uchar)piture[1]== 80)) //0x89 0x50 ->png
{
m_music_info.pic_type="png";
}
QString path =QString("../db/Pictures_db/%1.%2").arg(m_music_info.mic_name).arg(m_music_info.pic_type);
m_music_info.pic_path=path;
QFile testpic(path);
testpic.open(QIODevice::WriteOnly);
testpic.write(piture,size-14);
testpic.close();
free(piture);
continue;
}
//其他信息不需要
file_seek=file_seek+size;
file.seek(file_seek);
}
file.close();
//读取歌曲时间:
temp_MP->setMedia(QUrl(path)); //指定源为qrc文件,获取时长
return true;
}
void music_ananly::onDurationChanged(qint64 duration)
{
if(duration!=0)
{
//qDebug()<<"信号";
int secs = duration/1000; //全部秒数
int mins = secs/60;//分
secs = secs % 60;//秒
QString durationTime = QString::asprintf("%2d:%2d",mins,secs);
m_music_info.mic_time=QString(durationTime);
// qDebug()<<m_music_info.mic_name;
// qDebug()<<m_music_info.mic_name.length();
// qDebug()<<m_music_info.mic_songer.length();
//设置展示的信息
//1.统一歌名长度:17字符,过短+空格,过长截取+...
QString tempname=m_music_info.mic_name;
int nameadd=17-m_music_info.mic_name.length();
if(nameadd>=0)
{
for(int n=0;n<nameadd;n++)
{
tempname+=" ";
}
}else
{
tempname=tempname.mid(0,15)+"...";
}
//2.统一歌手长度:6字符,过短+空格,过长截取+...
QString tempsonger=m_music_info.mic_songer;
int songeradd=6-m_music_info.mic_songer.length();
if(songeradd>=0)
{
for(int n=0;n<songeradd;n++)
{
tempsonger+=" ";
}
}else
{
tempsonger=tempsonger.mid(0,5)+"...";
}
m_music_info.showlist=QString("%1\t%2\t%3").arg(tempname).arg(tempsonger).arg(m_music_info.mic_time);
emit music_ananly_complete_signal(m_music_info);
}
}
如果你有做sqlite3库保存,你就可得到:
后期可以跟列表联合进行数据查找,实现重启数据排列不丢失。
2. 音乐列表拖拽、临界自动滚动、信息栏。
2.1要实现效果:
- 要能拖拽项目实现任意排序;
- 要能拖动时候到达上下限,如果未达首尾端能自动滚动
- 要能左键单击实现播放,右键双击打开信息提示(跟随位置打开)
这时,就得用到qt的QListView控件来实现,需要重写: 来获得鼠标单击、双击位置,获得拖拽开始点、释放点。后面告诉再给代码: - dragEnterEvent(QDragEnterEvent *event) //drap的入口函数
- dragMoveEvent(QDragMoveEvent *event) //drap拖拽移动函数,可以获得移动位置
- dropEvent(QDropEvent *event) //drop拖拽释放的函数,获得插入位置
- mouseReleaseEvent(QMouseEvent *e) //获取鼠标单击,双击触发信号
当然如果不知道拖拽事件,可以先简单了解下:鼠标事件是当鼠标移动、按下、释放时qt内部自动触发的处理的槽函数,你没重写就默认执行每个控件写好的虚函数,如果你写了就执行你的鼠标事件函数,所以有时候重写事件函数应该要注意是否会影响原来的信号,例如:button重写mousepress鼠标事件后,button的click,press事件都会不响应。当然一个除外,那就是重写mouseRelease事件,他是释放后发出信号,不会提前影响你的click,press信号发出。而拖拽事件就是按住拉动控件qt内部自动触发的处理槽函数。我们用来实现拖拽QListView中的iterm小项目发出位置及iterm的序号。
2.2 QListView的样式表
第一步当然是外观,代码不行,审美一定要跟上去,不让仿出来都没人看得上;
/*QListView的样式表*/
QListView {
/*alternate-background-color: yellow;*/ /*自己试验:QT官方例子如果开启这个,下面不启动了,就只有yellow跟白色相间,所以不开启*/
show-decoration-selected: 1; /* make the selection span the entire width of the view */
}
/*iterm的高度*/
QListView::item{
height: 35px; /*插入iterm高度*/
}
QListView::item:alternate {
background: #EEEEEE; //交替的样式
}
QListView::item:selected {
border: 1px solid #6a6ea9; //选中外环加粗1px与加粗样式
}
/*当前index的颜色*/
QListView::item:selected:!active {
background: #838383
}
/*鼠标选中的颜色*/
QListView::item:selected:active {
background: #c3c3c3
}
/*鼠标悬停的颜色*/
QListView::item:hover {
background: #c3c3c3
}
2.3 重写QListView四个事件函数,发出需要信号,及自动滚动效果
//mylistveiw的头文件
#ifndef MYLISTVEIW_H
#define MYLISTVEIW_H
#include <QListView>
#include <QDragEnterEvent>
#include <QDragLeaveEvent>
#include <QDragMoveEvent>
#include <QTimer>
class MyListVeiw : public QListView
{
Q_OBJECT
public:
explicit MyListVeiw(QWidget *parent = nullptr);
protected:
//这里四个重写事件:
void dragEnterEvent(QDragEnterEvent *event);
void dragMoveEvent(QDragMoveEvent *event);
void dropEvent(QDropEvent *event);
void mouseReleaseEvent(QMouseEvent *e);
signals:
void dropEvent_signal(QPoint); //发送拖拽结束信号
void open_music_info_signal(QPoint); //右键单击:发送打开music_info窗口信号
void music_play_signal(int row); //左键双击击:开启播放音乐
void music_LeftRelese(); //左键单击:
public slots:
private:
QTimer *left_dobpress_timer; //左键双击的定时器
QTimer *autoup_wheel_timer; //实现自动滚轮定时器
QTimer *autodown_wheel_timer;
int left_dobpress_flag=0; //左键双击的标志 0:无 1:单击
};
#endif // MYLISTVEIW_H
#include "mylistveiw.h"
#include <QDebug>
#include <QDragEnterEvent>
#include <QDragLeaveEvent>
#include <QDragMoveEvent>
#include <QApplication>
#include <QScrollBar>
#include <QTimer>
MyListVeiw::MyListVeiw(QWidget *parent) : QListView(parent)
{
//listView初始化设置
this->setFrameShape(QListView::NoFrame);//无边框
this->setEditTriggers(QAbstractItemView::NoEditTriggers); //不可编辑
this->setMovement(QListView::Free);//设置可以移动iterm
this->setAlternatingRowColors(true);/*设置行交替颜色开启*/
this->setAcceptDrops(true);//设置MyListVeiw接收drag和drop
this->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);//这点非常重要,默认是以iterm作为移动的滑条,不开启以像素你没办法玩自动滚动。(踩得第一个坑)
this->verticalScrollBar()->setMaximum(100);
this->verticalScrollBar()->setMinimum(0);
this->verticalScrollBar()->setValue(0);
left_dobpress_timer=new QTimer(this);
connect(left_dobpress_timer,&QTimer::timeout,
[=]()
{
left_dobpress_flag=0; //规定时间内自动复位0;
});
autoup_wheel_timer=new QTimer(this);
connect(autoup_wheel_timer,&QTimer::timeout,
[=]()
{
qDebug()<<"开始up";
autoup_wheel_timer->stop();
int value=this->verticalScrollBar()->value();
int maximum=this->verticalScrollBar()->maximum();
if(value<maximum)
{
this->verticalScrollBar()->setValue(value+5);
autoup_wheel_timer->start(50);
}else
{
autoup_wheel_timer->stop();
}
});
autodown_wheel_timer=new QTimer(this);
connect(autodown_wheel_timer,&QTimer::timeout,
[=]()
{
qDebug()<<"开始down";
autodown_wheel_timer->stop();
int value=this->verticalScrollBar()->value();
int maximum=this->verticalScrollBar()->maximum();
if(value<maximum)
{
this->verticalScrollBar()->setValue(value-5);
autodown_wheel_timer->start(50);
}else
{
autodown_wheel_timer->stop();
}
});
}
void MyListVeiw::dragEnterEvent(QDragEnterEvent *event)
{
//qDebug()<<event;
//qDebug()<<"开始位置:"<<event->posF().x()<<event->posF().y();
//传递事件
event->acceptProposedAction();//传递开启dragMoveEvent等事件,不开启默认不执行dragMoveEvent,我们要执行所以开启
}
void MyListVeiw::dragMoveEvent(QDragMoveEvent *event)
{
//实现自动滚轮
//其他区域停止auto_wheel_timer;
autoup_wheel_timer->stop();
autodown_wheel_timer->stop();
int value=this->verticalScrollBar()->value();
int maximum=this->verticalScrollBar()->maximum();
int minimum=this->verticalScrollBar()->minimum();
if(event->posF().y()>250)
{
if(value<maximum)
{
this->verticalScrollBar()->setValue(value+5);
autoup_wheel_timer->start(100);
}
return;
}
if(event->posF().y()<60)
{
if(value>minimum)
{
this->verticalScrollBar()->setValue(value-5);
autodown_wheel_timer->start(100);
}
return;
}
}
void MyListVeiw::dropEvent(QDropEvent *event)
{
//其他区域停止auto_wheel_timer;
autoup_wheel_timer->stop();
autodown_wheel_timer->stop();
//qDebug()<<"结束位置:"<<event->posF().x()<<event->posF().y();
QPoint dropPoint=QPoint(event->posF().x(),event->posF().y());
emit dropEvent_signal(dropPoint);
}
void MyListVeiw::mouseReleaseEvent(QMouseEvent *e)
{
QPoint point=QPoint(e->x(),e->y());
//1.重写单击右键效果
if(e->button()==Qt::RightButton)
{
emit open_music_info_signal(point);
}
//2.重写双击左击效果
if(e->button()==Qt::LeftButton)
{
if(left_dobpress_flag==0) //单击
{
left_dobpress_flag=1;
left_dobpress_timer->start(300); //开启300ms自动复位,在规定内再次单击,视为双击。
/*下面自己实现自己代码,我是发出信号*/
emit music_LeftRelese();
}else //双击
{
//qDebug()<<"双击";
left_dobpress_flag=0;
/*下面自己实现自己代码,我是发出信号*/
emit music_play_signal(this->indexAt(point).row());
}
}
}
//要测试只要把你的QListView控件提升为上面我们自己写的MyListVeiw,往MyListVeiw插入几个QStandardItem就能看到效果。
//QStandardItem *item= new QStandardItem(“xxxxxx”);
//MyListVeiw(名字)->appendRow(item);
我们就完成了对QListView的重写,当然仅仅重写QListView这是个开始,我们需要在主项目关联自己发出信号,进行操作:完成拉动效果(不然拉动释放也是在原位不动,要让他插入我们拖拽释放点)
2.4 关联信号实现拖拽释放,移动iterm效果
下面是我关联的槽函数:
list_ItemModel = new QStandardItemModel(this);
ui->listView->setModel(list_ItemModel);
connect(ui->listView,SIGNAL(dropEvent_signal(QPoint)),this,SLOT(deal_dropEvent_slot(QPoint)));
用到主要函数:
- QModelIndex QListView::indexAt(const QPoint &p) const; //indexAt拿出该点位置的iterm的index;
- QList<QStandardItem *> QStandardItemModel::takeRow(int row); //takeRow会拿出该行,并删除行!
- void QStandardItemModel::insertRow(int row, const QList<QStandardItem *> &items); //insertRow插入该行
- void QStandardItemModel::appendRow(const QList<QStandardItem *> &items); //appendRow尾追加
void list::deal_dropEvent_slot(QPoint P)
{
lst_hide_time->start(16000);
//判断要插入的序号
int up= ui->listView->indexAt(QPoint(P.x(),P.y()-18)).row();
int min=ui->listView->indexAt(P).row();
int dowm=ui->listView->indexAt(QPoint(P.x(),P.y()+18)).row();
//qDebug()<<"up:"<<up<<"min:"<<min<<"dowm:"<<dowm;
//判断为源行==目标行
if(index_form==min)
{
//qDebug()<<"源行==目标行:";
return;
}
//拿出源行
QList<QStandardItem *> item = list_ItemModel->takeRow(index_form); //takeRow会拿出源行,并删除!
int distance;
if((up==-1&&min==-1&&dowm==-1)||(up>=0&&min==-1&&dowm==-1))
{
//qDebug()<<"最后面插入:";
list_ItemModel->appendRow(item);
distance=list_ItemModel->rowCount()-1;
ui->listView->setCurrentIndex(list_ItemModel->item(distance)->index());
return;
}
if(index_form<min) //源行比目标行高
{
//qDebug()<<"index_form比目标行高";
if(up==min) //偏min的下面
{
distance=min+1-1;
list_ItemModel->insertRow(distance,item);
ui->listView->setCurrentIndex(list_ItemModel->item(distance)->index());
return;
}
if(dowm==min) //偏min的上面
{
distance=min+0-1;
list_ItemModel->insertRow(distance,item);
ui->listView->setCurrentIndex(list_ItemModel->item(distance)->index());
return;
}
if(up!=min && dowm!=min) //在min中央
{
distance=min+0-1;
list_ItemModel->insertRow(distance,item);
ui->listView->setCurrentIndex(list_ItemModel->item(distance)->index());
return;
}
}else //源行比目标行低
{
//qDebug()<<"index_form比目标行低";
if(up==min) //偏min的下面
{
if(dowm==-1)
{
list_ItemModel->appendRow(item);
distance=list_ItemModel->rowCount()-1;
ui->listView->setCurrentIndex(list_ItemModel->item(distance)->index());
return;
}
distance=min+1-0;
list_ItemModel->insertRow(distance,item);
ui->listView->setCurrentIndex(list_ItemModel->item(distance)->index());
return;
}
if(dowm==min) //偏min的上面
{
distance=min+0-0;
list_ItemModel->insertRow(distance,item);
ui->listView->setCurrentIndex(list_ItemModel->item(distance)->index());
return;
}
if(up!=min && dowm!=min) //在min中央
{
distance=min+0-0;
list_ItemModel->insertRow(distance,item);
ui->listView->setCurrentIndex(list_ItemModel->item(distance)->index());
return;
}
}
}
打开信息提示框,按照鼠标当前位置打开功能就比较简单,有QListView重载后能发出位置信号,处理很简单,我就不贴出来。
3. 歌词实时显示,滚动查看、跳转归位、双击播放
3.1 要实现效果
- 歌词滚动
- 滑轮滚动查看
- 双击跳转播放/不操作则自动归位
3.2 歌词文本解析(GBK解码)
第一步是歌词解析,从QQ音乐下载下来的歌词就得用上了,大家可以自己用文本打开看里面的格式,基本都是:【时间】+歌词,这就好办了,直接读取文本,先设置一个类:记录如下:
typedef struct one_lyric_st
{
int pos; //歌词位置
qint64 time; //歌词时间ms
QString lyricStr; //歌词内容
QString timeStr; //时间字符串,后期可以显示滑轮时对应的时间提示
}one_lyric;
下面就依次读取就OK了,我是这样读取的,其中有个问题是读取中文字符串编码格式为Unicode,所以要用到这个格式转换:
- QTextCodec *codec =QTextCodec::codecForName(“GBK”);
- QString lineStr= codec->toUnicode(lineByte);
来将其Unicode编码转换成GBK格式显示,不然就是一堆????。
void Lyric_ananly::lyricFile_ananly(QString path)
{
int pos=0;
QFile file(path);
file.open(QIODevice::ReadOnly |QIODevice::Text );
//初始化
this->curpos=0;
this->lyrics_size=0;
while(file.atEnd()==false)
{
QByteArray lineByte =file.readLine();
lineByte.resize(lineByte.size()-1); //去掉'\n';
QTextCodec *codec = QTextCodec::codecForName("GBK");
QString lineStr = codec->toUnicode(lineByte);
//qDebug()<<lineStr;
if(pos>=5)
{
lyrics_size++;
QStringList resultList=lineStr.split(']');
QString timeStr= resultList.at(0); //时间
QString lyricStr= resultList.at(1); //歌词
QStringList templist=timeStr.split(':');
QString min=templist.at(0); //分钟
min=min.split('[').at(1);
QString tempStr=templist.at(1);
templist=tempStr.split('.');
QString sec=templist.at(0); //秒
//qDebug()<<pos<<min<<sec;
this->lyrics[pos-5].pos=pos-5;
this->lyrics[pos-5].time=min.toInt()*60*1000+sec.toInt()*1000;
this->lyrics[pos-5].lyricStr=lyricStr;
this->lyrics[pos-5].timeStr=min+":"+sec;
//qDebug()<<lyrics[pos-5].pos<<lyrics[pos-5].time<<lyrics[pos-5].lyricStr;
}
pos++;
}
file.close();
}
3.3 实时显示,歌词滚动
歌词滚动显示,采用QGroupBox+QLable显示,在QGroupBox内添加歌曲每行歌词QLable,进行move就可以实现歌词滚动效果。这就要求我们要实时获得歌曲的进度时间与lyr文件中每个歌词时间进行对比。主要用到下面几个函数:
- void QWidget::setParent(QWidget *parent); //将qlable的指定QGroupBox为父类,就在move出范围不会显示。
- move(int x, int y); //用于qlabel的移动
- connect(mus_MediaPlayer,SIGNAL(positionChanged(qint64)),this,SLOT(onPositionChanged(qint64))); //每当歌曲位置改变,都会以默认100ms进入这个函数(setNotifyInterval()这个可以修改默认进入时间频率)。
//创建QMediaPlayer播放器
mus_MediaPlayer = new QMediaPlayer;
connect(mus_MediaPlayer,SIGNAL(positionChanged(qint64)),this,SLOT(onPositionChanged(qint64)));
connect(mus_MediaPlayer,SIGNAL(durationChanged(qint64)),this,SLOT(onDurationChanged(qint64)));
//mus_MediaPlayer->setMedia(QUrl("../musics/陈奕迅 - 遥远的她 (Live).mp3")); //指定源为qrc文件
mus_MediaPlayer->setVolume(mus_volume); //默认音量为20;
mus_MediaPlayer->setNotifyInterval(1000); //设置频率,单位是ms,默认也是1000,你想跟精确跟随歌词,可以修改这个进入时间。
//创造默认200个歌词
lyric_lable = new MyLabel[200];
for(int n=0;n<200;n++)
{
lyric_lable[n].setParent(ui->Lyric_groupBox);
lyric_lable[n].move(0,n*30);
}
void Widget::onPositionChanged(qint64 position)
{
//qDebug()<<position;
if(ui->horizontalSlider->isSliderDown())
return;//如果手动调整进度条,则不处理
ui->horizontalSlider->setSliderPosition(position);
//有歌词
if(lyric_flag==1)
{
//移动歌词,自己写的lyric_setplayTime函数,可以用于移动歌词。(可以根据自己写一个,下面会给出代码)
lyric_ananly.lyric_setplayTime(position,&lyric_lable,ui->Lyric_groupBox); //需要提前可以改这里;
}
int secs = position/1000;
int mins = secs/60;
secs = secs % 60;
QString positionTime = QString::asprintf("%2d:%2d",mins,secs);
ui->label_lefttime->setText(positionTime);
//qDebug()<<mins<<secs<<positionTime;
}
其中用到我自己写的操作歌词的代码、分析等等,我把他归到一个歌词操作类lyric_ananly
/***lyric_ananly.h****/
#ifndef LYRIC_ANANLY_H
#define LYRIC_ANANLY_H
#include <QObject>
#include "mylabel.h"
#include <QGroupBox>
#include <QObject>
typedef struct one_lyric_st
{
int pos; //歌词位置
qint64 time; //歌词时间ms
QString lyricStr; //歌词内容
QString timeStr; //时间字符串,后期可以显示滑轮时对应的时间提示
}one_lyric;
class Lyric_ananly : public QObject
{
Q_OBJECT
public:
explicit Lyric_ananly(QObject *parent = nullptr);
void lyricFile_ananly(QString path); //分析歌词文件(入口),设置歌词时间->对应歌词
void lyric_Lable_setTexts(MyLabel **); //给定歌词组指针,设置全文的歌词
void lyric_restate(MyLabel ** ); //歌词复位
int find_needshowpos(qint64 time); //给定时间找出播放位置
void lyric_setplayTime(qint64 time,MyLabel **lyric_p,QGroupBox * grounpBox); //给定时间自动调整歌词到目前位置
void lyric_center_color(QGroupBox *); //中央颜色设置
void lyric_move(int distance,MyLabel **lyric_p,QGroupBox * grounpBox); //歌词移动distance像素,-为up;+为down
void lyric_up_half(MyLabel **lyric_p,QGroupBox * grounpBox); //歌词向前移动半个节拍15像素,lyric_one_flag如果为整数1则lyric添加进歌词
void lyric_down_half(MyLabel **lyric_p,QGroupBox * grounpBox); //歌词向后移动半个节拍15像素,lyric_one_flag如果为整数1则lyric添加进歌词
one_lyric lyric_curentinfo(); //获取当前歌词的信息
void lyric_wheel_retreat(qint64 time,MyLabel **lyric_p,QGroupBox * grounpBox,int time_delay); //歌词每time_delay毫秒,滚动到time时刻;
bool isTop(); //歌词到头部
bool isDown(); //歌词到尾部
one_lyric lyrics[500]; //默认500个歌词够用
int lyrics_size=0; //歌曲的行数
int loopnumber=0; //用于记录lyric_wheel_retreat需要开启的半拍次数
int delay_ms=100; //用于记录lyric_wheel_retreat开启延时时间ms;
int curpos=0; //当前播放位置*2 ->可以用于半拍显示
signals:
lyric_wheel_retreat_isok_signal(); //lyric_wheel_retreat完成信号
public slots:
};
#endif // LYRIC_ANANLY_H
/***lyric_ananly.cpp****/
#include "lyric_ananly.h"
#include <QFile>
#include <QDebug>
#include <QTextStream>
#include <QTextCodec>
#include <QTimer>
#include <QObject>
Lyric_ananly::Lyric_ananly(QObject *parent) : QObject(parent)
{
}
void Lyric_ananly::lyricFile_ananly(QString path)
{
int pos=0;
QFile file(path);
file.open(QIODevice::ReadOnly |QIODevice::Text );
//初始化
this->curpos=0;
this->lyrics_size=0;
while(file.atEnd()==false)
{
QByteArray lineByte =file.readLine();
lineByte.resize(lineByte.size()-1); //去掉'\n';
QTextCodec *codec = QTextCodec::codecForName("GBK");
QString lineStr = codec->toUnicode(lineByte);
//qDebug()<<lineStr;
if(pos>=5)
{
lyrics_size++;
QStringList resultList=lineStr.split(']');
QString timeStr= resultList.at(0); //时间
QString lyricStr= resultList.at(1); //歌词
QStringList templist=timeStr.split(':');
QString min=templist.at(0); //分钟
min=min.split('[').at(1);
QString tempStr=templist.at(1);
templist=tempStr.split('.');
QString sec=templist.at(0); //秒
//qDebug()<<pos<<min<<sec;
this->lyrics[pos-5].pos=pos-5;
this->lyrics[pos-5].time=min.toInt()*60*1000+sec.toInt()*1000;
this->lyrics[pos-5].lyricStr=lyricStr;
this->lyrics[pos-5].timeStr=min+":"+sec;
//qDebug()<<lyrics[pos-5].pos<<lyrics[pos-5].time<<lyrics[pos-5].lyricStr;
}
pos++;
}
file.close();
}
int Lyric_ananly::find_needshowpos(qint64 time)
{
//找到播放位置
int need_showpos=0; //需到播放位置*2 ->可以用于半拍显示
for(int n=0;n<lyrics_size;n++)
{
if(time==lyrics[n].time)
{
//qDebug()<<"找到位置:"<<n;
need_showpos=lyrics[n].pos*2;
return need_showpos;
}else if(time<lyrics[n].time)
{
need_showpos=(lyrics[n].pos-1)*2;
return need_showpos;
}
}
//找不到
need_showpos=(lyrics_size-1)*2;
return need_showpos;
}
void Lyric_ananly::lyric_setplayTime(qint64 time,MyLabel **lyric_p,QGroupBox * grounpBox)
{
//获得需要播放位置
int need_showpos=0; //需到播放位置*2 ->可以用于半拍显示
need_showpos=find_needshowpos(time);
if(need_showpos==curpos) //退出不操作
{
return;
}
if(need_showpos>curpos)
{
//up 向上
int distance=0-(need_showpos-curpos)/2*30;
lyric_move(distance,lyric_p,grounpBox);
}else
{
//down 向上
int distance=(curpos-need_showpos)/2*30;
lyric_move(distance,lyric_p,grounpBox);
}
}
void Lyric_ananly::lyric_Lable_setTexts(MyLabel ** lyric_p)
{
//歌词复位
lyric_restate(lyric_p);
MyLabel* lyric_label = *lyric_p;
//从第三条依次填充歌词
for(int n=0;n<lyrics_size;n++)
{
lyric_label[n+3].setText(lyrics[n].lyricStr);
}
}
void Lyric_ananly::lyric_restate(MyLabel ** lyric_p)
{
MyLabel* lyric_label = *lyric_p;
//清空所有lyric——label回复开始状态 //注意200为上限值,widget改动是要注意,我这里就不用定义常量直接用
for(int n=0;n<200;n++)
{
lyric_label[n].setText("");
lyric_label[n].move(0,n*30);
}
curpos=0;
}
void Lyric_ananly::lyric_center_color(QGroupBox * grounpBox)
{
//歌词显示
QLabel *lyric0 = (QLabel*)grounpBox->childAt(0, 0);
lyric0->setStyleSheet("QLabel{color:white}");
QLabel *lyric1 = (QLabel*)grounpBox->childAt(0, 30);
lyric1->setStyleSheet("QLabel{color:white}");
//.设置中央上一条歌曲显示white色
QLabel *centerup_lyric = (QLabel*)grounpBox->childAt(0, 60);
centerup_lyric->setStyleSheet("QLabel{color:white}");
//.设置中央歌曲显示blue色
QLabel *center_lyric = (QLabel*)grounpBox->childAt(0, 90);
center_lyric->setStyleSheet("QLabel{color:rgb(71,244,131)}");
//.设置中央下一条歌曲显示white色
QLabel *centerdown_lyric = (QLabel*)grounpBox->childAt(0, 120);
centerdown_lyric->setStyleSheet("QLabel{color:white}");
QLabel *lyric5 = (QLabel*)grounpBox->childAt(0, 150);
lyric5->setStyleSheet("QLabel{color:white}");
QLabel *lyric6 = (QLabel*)grounpBox->childAt(0, 150);
lyric6->setStyleSheet("QLabel{color:white}");
}
void Lyric_ananly::lyric_move(int distance,MyLabel **lyric_p,QGroupBox * grounpBox)
{
MyLabel* lyric_label = *lyric_p;
for(int n=0;n<200;n++)
{
lyric_label[n].move(0,lyric_label[n].y()+distance);
}
lyric_center_color(grounpBox);
curpos=curpos-distance/15;
}
void Lyric_ananly::lyric_up_half(MyLabel **lyric_p,QGroupBox * grounpBox)
{
if(isDown()==true)
return;
MyLabel* lyric_label = *lyric_p;
for(int n=0;n<200;n++)
{
lyric_label[n].move(0,lyric_label[n].y()-15);
}
lyric_center_color(grounpBox);
curpos++;
}
void Lyric_ananly::lyric_down_half(MyLabel **lyric_p, QGroupBox *grounpBox)
{
if(isTop()==true)
return;
MyLabel* lyric_label = *lyric_p;
for(int n=0;n<200;n++)
{
lyric_label[n].move(0,lyric_label[n].y()+15);
}
lyric_center_color(grounpBox);
curpos--;
}
one_lyric Lyric_ananly::lyric_curentinfo()
{
int center=curpos/2;
return lyrics[center];
}
void Lyric_ananly::lyric_wheel_retreat(qint64 time, MyLabel **lyric_p, QGroupBox *grounpBox, int time_delay)
{
int need_showpos=0; //需到播放位置*2 ->可以用于半拍显示
need_showpos=find_needshowpos(time);
delay_ms=time_delay;
//相等不动作
if(curpos==need_showpos)
return;
//up 1 down -1
if(curpos<need_showpos)
{
loopnumber=need_showpos-curpos;
QTimer *temptimer_1=new QTimer;
connect(temptimer_1,&QTimer::timeout,
[=]()
{
if(loopnumber==0)
{
temptimer_1->stop();
//2.开启自动播放
emit lyric_wheel_retreat_isok_signal();
delete temptimer_1;
}else
{
lyric_up_half(lyric_p,grounpBox);
temptimer_1->start(delay_ms); //1ms
loopnumber--;
}
});
temptimer_1->start(10);
}else
{
loopnumber=curpos-need_showpos;
QTimer *temptimer_2=new QTimer;
connect(temptimer_2,&QTimer::timeout,
[=]()
{
if(loopnumber==0)
{
temptimer_2->stop();
//2.开启自动播放
emit lyric_wheel_retreat_isok_signal();
delete temptimer_2;
}else
{
lyric_down_half(lyric_p,grounpBox);
temptimer_2->start(delay_ms); //1ms
loopnumber--;
}
});
temptimer_2->start(10);
}
}
bool Lyric_ananly::isTop()
{
if(curpos<=0)
{
return true;
}else
{
return false;
}
}
bool Lyric_ananly::isDown()
{
if(curpos>=(lyrics_size-1)*2)
{
return true;
}else
{
return false;
}
}
3.4 鼠标滑轮显示,不操作跳转归位/操作双右键就播放当前
基于上面做好的操作歌词需要操作,剩下就是如何利用鼠标滑轮触发信号、双击右键能自动播放当前歌词。
我们需要一个可以获取这些信号控件。
解决办法:在歌词显示区域上面覆盖一个qlabel,重写这两个事件就能获得触发信号:
- void MyMouseLable::wheelEvent(QWheelEvent *event) // 滚轮事件
- void MyMouseLable::mousePressEvent(QMouseEvent *ev) //鼠标按下事件
代码;
/*************mymouselable.h******************/
#ifndef MYMOUSELABLE_H
#define MYMOUSELABLE_H
#include <QLabel>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QTimer>
class MyMouseLable : public QLabel
{
Q_OBJECT
public:
explicit MyMouseLable(QWidget *parent = nullptr);
protected:
void wheelEvent(QWheelEvent *event);
void mousePressEvent(QMouseEvent *ev);
signals:
void wheel_signal(int flag); //-1 向上 1向下
void wheel_play_lyirc_signal(); //滑轮播放信号
public slots:
private:
QTimer *timer; //左键双击计时器
int left_dobpress_flag=0; //左键双击的标志 0:无 1:单击
};
#endif // MYMOUSELABLE_H
/*************mymouselable.cpp******************/
#include "mymouselable.h"
#include <QLabel>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QTimer>
#include <QDebug>
MyMouseLable::MyMouseLable(QWidget *parent) : QLabel(parent)
{
this->setMouseTracking(true);
timer =new QTimer(this);
connect(timer,&QTimer::timeout,
[=]()
{
left_dobpress_flag=0; //规定时间内自动复位0;
});
}
void MyMouseLable::wheelEvent(QWheelEvent *event) // 滚轮事件
{
if(event->delta() > 0){ // 当滚轮远离使用者时
//qDebug()<<"向上滚动";
wheel_signal(-1);
}else{ // 当滚轮向使用者方向旋转时
//qDebug()<<"向下滚动";
wheel_signal(1);
}
}
void MyMouseLable::mousePressEvent(QMouseEvent *ev)
{
//2.重写双击左击效果
if(ev->button()==Qt::LeftButton)
{
if(left_dobpress_flag==0) //单击
{
left_dobpress_flag=1;
timer->start(300); //开启300ms自动复位,在规定内再次单击,视为双击。
}else //双击
{
//qDebug()<<"滑轮播放:";
emit wheel_play_lyirc_signal();
left_dobpress_flag=0;
}
}
}
获取wheel_play_lyirc_signal信号,进行滚动:
//mouselable的初始化
connect(ui->label_mouse,SIGNAL(wheel_signal(int)),this,SLOT(deal_wheel_slot(int)));
connect(ui->label_mouse,SIGNAL(wheel_play_lyirc_signal()),this,SLOT(deal_wheel_play_lyirc_slot()));
//滑轮未3s内操作双击或者滚动,就自动歌词复位定时器
wheel_time =new QTimer(this);
wheel_time->setInterval(3000);
connect(wheel_time,SIGNAL(timeout()),this,SLOT(deal_wheel_timeout_slot()));
//up 1 down -1
void Widget::deal_wheel_slot(int flag)
{
//qDebug()<<flag;
//无歌词不响应!!!
if(lyric_flag==0)
{
return;
}
//开启定时器3s不操作,自动复位
ui->label_waitleft->show();
ui->label_waitright->show();
ui->label_wheeltime->show();
ui->button_wheel->show();
wheel_time->start(3000);
//1.开启滑轮滚动事件
if(lyric_flag==1)
{
lyric_flag=2; //改变歌词显示的方式
}
//2.移动
if(flag==1) //up
{
lyric_ananly.lyric_up_half(&lyric_lable,ui->Lyric_groupBox);
}else //down
{
lyric_ananly.lyric_down_half(&lyric_lable,ui->Lyric_groupBox);
}
//3.获取当前歌词信息
one_lyric reslut =lyric_ananly.lyric_curentinfo();
ui->label_wheeltime->setText(reslut.timeStr);
}
void Widget::deal_wheel_play_lyirc_slot()
{
if(lyric_flag==2) //开启滑轮滚动下有效
{
//关闭自动跳转时钟
wheel_time->stop();
//播放到当前指定位置
one_lyric reslut =lyric_ananly.lyric_curentinfo();
int playtime=reslut.time;
//开启自动播放
mus_MediaPlayer->setPosition(playtime);
lyric_flag=1;
//隐藏自动复位
ui->label_waitleft->hide();
ui->label_waitright->hide();
ui->label_wheeltime->hide();
ui->button_wheel->hide();
}
}
void Widget::deal_wheel_timeout_slot()
{
//自动复位
ui->label_waitleft->hide();
ui->label_waitright->hide();
ui->label_wheeltime->hide();
ui->button_wheel->hide();
wheel_time->stop();
//歌词回跳到当前位置
lyric_ananly.lyric_wheel_retreat(mus_MediaPlayer->position(),&lyric_lable,ui->Lyric_groupBox,15);
}
4. 其他(播放顺序、各种样式表、背景图模糊)
4.1 背景图模糊
主要用到QGraphicsBlurEffect ,可以通过setBlurRadius来设置模糊程度。
//界面背景图显示
//1.图片模糊
QGraphicsBlurEffect *blureffect = new QGraphicsBlurEffect;
blureffect->setBlurHints(QGraphicsBlurEffect::QualityHint);
blureffect->setBlurRadius(50); //数值越大,越模糊
//2.图片显示
ui->label_background->setGraphicsEffect(blureffect);//设置模糊特效
ui->label_background->setStyleSheet("QLabel{"
"border-image:url(:/background/MyqqPitcure/background.jpg)"
"}");
//专辑显示
ui->label_muspic->setStyleSheet("QLabel{"
"border-image:url(:/background/MyqqPitcure/background.jpg)"
"}");
4.2 QPushButton按键样式表
//1. 图片的
ui->button_love->setStyleSheet("QPushButton{" /*图片*/
"border-image:url(:/background/MyqqPitcure/love_w.png)"
"}"
"QPushButton:hover{" /*鼠标悬停图片*/
"border-image:url(:/background/MyqqPitcure/love_r.png)"
"}"
);
//2.透明背景的
ui->button_listadd->setFlat(true);
4.3 QSlider按键样式表
- 水平的QSlider看我这片文章
- 垂直的我就直接给出了
QSlider::groove:vertical {
background: red;
position: absolute;
left: 8px; right: 8px;
}
QSlider::add-page:vertical {
background: rgb(71,244,131);
}
QSlider::sub-page:vertical {
background: rgb(193,204,208);
}
/*3.平时滑动的滑块设计参数*/
QSlider::handle:vertical {
height: 10px;
border-radius: 5px;
background: rgb(71,244,131);
margin: 0 -4px; /* expand outside the groove */
}
/*4.手动拉动时显示的滑块设计参数*/
QSlider::handle:vertical:hover {
height: 10px;
border-radius: 5px;
background: rgb(71,244,131);
margin: 0 -4px; /* expand outside the groove */
}
三、项目代码分享
控件图片资源+Qsqlite库+整个音乐播放器项目:下载下来就能直接用。
资源: 【QT的音乐播放器(简单版)】 链接: link.
总结
现在整个项目还有很多要的优化的地方,有很多不人性化的地方,也没什么新的创新点,不能联网访问数据等问题,后续有时间,我会继续完善。制作不易,关注加点赞一波,啊啊啊啊,谢谢!