【QT的音乐播放器(简单版)】

前言


自己闲来无事自己搞一个音乐播放器,原型是仿照qq音乐,对于一些功能实现,我在网上搜了一些,加上自己理解瞎搞一通,目前项目还有很多不足,只搞了个大概,项目还是有很多要改进的,仅供学习参考,一起进步!

一、主体效果

在这里插入图片描述


二、主要技术点:

1. mp3的ID3V2格式文件解析:作者、歌手、时长、专辑图片等

对于目前来说,只做个单机版mp3音乐播放器,那我们有的文件就只有:xxx.mp3与xxx.lrc,一个是音频文件,一个是歌词文件,如下图:
在这里插入图片描述如何解码xxx.mp3文件得到作者、歌手、歌名、专辑图片、专辑名等就非常有必要。
大家可以参考:QT 读取mp3ID3V2 获取mp3专辑图片、专辑名称、标题、作者(一)链接: link.

1.1 需要工具:

  • Binary Viewer:可以查看以二进制查看文件,有助于自己格式解码 链接: link.
  • 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按键样式表

  1. 水平的QSlider看我这片文章:【QSlider样式表的详细设置及含义】链接: link.
  2. 垂直的我就直接给出了
  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.


总结

现在整个项目还有很多要的优化的地方,有很多不人性化的地方,也没什么新的创新点,不能联网访问数据等问题,后续有时间,我会继续完善。制作不易,关注加点赞一波,啊啊啊啊,谢谢!

  • 13
    点赞
  • 95
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Ubuntu是一种基于Linux的操作系统,而Qt是一种跨平台的应用程序开发框架。因此,Ubuntu和Qt能够很好地结合在一起,用于开发各种应用程序,其中包括音乐播放器。 Ubuntu Qt 音乐播放器是一种能够在Ubuntu操作系统上运行的音乐播放软件。它基于Qt框架开发,因此具有跨平台的优势,可以在其他操作系统上运行,而不仅限于Ubuntu。这种播放器具有很多功能和特点。 首先,Ubuntu Qt 音乐播放器支持多种音频格式,包括MP3、WAV、FLAC等,用户可以根据自己的需求选择不同的格式进行播放。它还支持创建和管理播放列表,用户可以根据自己的喜好和需求来组织和播放音乐。 其次,这种音乐播放器还具有良好的用户界面设计,界面简洁直观,用户可以轻松地浏览和操作。它提供了播放、暂停、上一曲、下一曲等基本播放控制按钮,还可以通过拖动滑块来调整音乐的播放进度。 此外,Ubuntu Qt 音乐播放器还提供了一些额外的功能,例如歌词显示、音量调整、音频均衡器等。用户可以根据自己的需求进行设置和调整,以达到最佳音乐体验。 总而言之,Ubuntu Qt 音乐播放器是一种可在Ubuntu操作系统上运行的音乐播放软件,具有多种音频格式支持、良好的用户界面设计和一些额外的功能。通过这个播放器,用户可以方便地管理和播放自己喜欢的音乐。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值