一、項目介紹?
窗口hand部分:
點擊最小化按鈕,窗口最小化點擊最大化按鈕,窗口最大化點擊關閉按鈕,程序退出
窗口body左側部分:
點擊推薦按鈕,窗口右側顯示:推薦Page(暫時只有頁面)點擊電臺按鈕,窗口右側顯示:電臺Page(未?持)點擊音樂館按鈕,窗口右側顯示:音樂館Page(未?持)點擊我喜歡按鈕,窗口右側顯示:收藏的音樂Page點擊本地下載按鈕,窗口右側顯示:本地音樂Page點擊最近播放按鈕,窗口右側顯示:最近播放Page
?窗口body右側部分
當窗口左側不同按鈕點擊,在窗口右側會展示不同的頁面,本項目暫只支持了本地音樂、喜歡音樂、最近播放音樂的展示。具體功能如下:點擊全部播放按鈕,播放當前頁面列表中所有音樂雙擊列表中某?樂,播放當前選中音樂點擊心支持收藏支持最近播放過音樂記憶點擊推薦按鈕,窗口右側顯示:推薦Page(暫只有頁面)?
播放控制區域
支持seek功能,即拖拽到歌曲指定位置播放支持:隨機、單曲循環、循環播放支持播放上?曲支持播放下?曲支持播放和暫停支音量調節和靜音支持歌曲總時長顯示、當前播放時間顯示支持LRC歌詞同步顯示
二、界面開發
界面大體上分為head和body兩部分
1、head部分分析
從左到右依此為圖標、搜索框、更換皮膚按鈕、最小化按鈕、最大化按鈕、關閉按鈕
2、body部分分析
Body右側部分由?page區、播放進度、播放控制區三個部分組成
①page區域:歌曲信息頁面,點擊 “<” 或 “>”具有輪番播圖效果
②播放進度:當前歌曲播放進度說明,支持seek功能,與播放控制區時間、以及LRC歌詞是同步的
③播放控制區域:顯示歌曲圖片&名稱&歌手播放模式&下一曲&播放暫停&上一曲&音量調節和靜音&添加本地音樂當前播放時間/歌曲總時長&彈出歌詞窗口按鈕
當點擊時的page頁面:
所以我喜歡、本地音樂、最近播放共用一個commonPage頁面,
推薦頁面需要支持點擊按鈕時的輪番展示校效果,所以單獨用一個頁面
?歌詞展示
顯示內容分為:歌曲信息、歌詞部分、左上方收起隱藏按鈕。
歌曲信息由歌曲名稱(QLabel)和歌手名稱(QLabel)構成
歌詞部分展示當前在唱歌詞(QLabel)和在唱部分前三行和后三行歌詞(QLabel)展示,當前播放歌詞突出顯示
點擊收起按鈕后,該頁面會以動畫的方式收起
當歌曲有LRC歌詞時,播放時歌詞會隨播放時間自動調整;歌曲沒有LRC歌詞時,歌詞部分顯示空字符。
歌曲控制區
從左至右依次為
1、歌曲封面? ? ? ? ? ? ? ? ? ? ? 2、歌曲信息? ? ? ? ? ? ? ? ? ? ? ? 3、切換播放模式?
4、上一曲? ? ? ? ? ? ? ? ? ? ? ? ?5、播放/暫停? ? ? ? ? ? ? ? ? ? ? ? 6、下一曲
7、調節聲音? ? ? ? ? ? ? ? ? ? ?8、添加本地音樂? ? ? ? ? ? ? ? ? 9、總時間與當前播放時間
10、顯示歌詞
?QQMusic類
創建一個APPlication,將ui界面的布局完成后對其進行界面美化.
1、widget窗口無標題
將初始化界面的工作放在void initui()里面,在里面添加
// 設置?邊框窗?,即窗?將來?標題欄setWindowFlag(Qt::WindowType::FramelessWindowHint);
2、實現鼠標拖動窗口
重寫QQmusic父類中的mousepressEvent和mousemoveEventvent事件;
鼠標左鍵按下時,記錄下窗口左上角和鼠標的相對位置;
鼠標移動時,會產生新的位置,保持鼠標和窗口左上角相對位置不變,通過move修改窗口的左上角坐標即可。
void QQMusic::mousePressEvent(QMouseEvent* event)
{// 攔截?標左鍵單擊事件if (event->button() == Qt::LeftButton){// event->globalPos():?標按下事件發?時,光標相對于屏幕左上?位置// frameGeometry().topLeft(): ?標按下事件發?時,窗?左上?位置// geometry(): 不包括邊框及頂部標題區的范圍// frameGeometry(): 包括邊框及頂部標題區的范圍// event->globalPos() - frameGeometry().topLeft() 即為:// ?標按下時,窗?左上?和光標之間的距離差// 想要窗??標按下時窗?移動,只需要在mouseMoveEvent中,讓光標和窗?左上?保持相同的位置差// 獲取?標相對于屏幕左上?的全局坐標dragPosition = event->globalPos() - frameGeometry().topLeft();return;}QWidget::mousePressEvent(event);
}
void QQMusic::mouseMoveEvent(QMouseEvent* event)
{if (event->buttons() == Qt::LeftButton){// 根據?標移動更新窗?位置move(event->globalPos() - dragPosition);return;}QWidget::mouseMoveEvent(event);
}
3、給窗口添加陰影
// 設置窗?背景透明
this->setAttribute(Qt::WA_TranslucentBackground);// 給窗?設置陰影效果
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0, 0); // 設置陰影偏移
shadowEffect->setColor("#000000"); // 設置陰影顏?:??
shadowEffect->setBlurRadius(10); // 設置陰影的模糊半徑
this->setGraphicsEffect(shadowEffect);
BtForm類
添加一個新設計師界面,命名為BtForm
?
將bfForm的ui界面設計好后,將QQmusic的ui界面的相關的QWidget全部提升為BtForm。
效果圖:
1、設置按鈕上的圖片和文字信息,以及該按鈕關聯的page頁面
void btFrom::seticon(QString btIcon, QString btText, int mid)
{// 設置?定義按鈕的圖?、?字、以及idui->btIcon->setPixmap(QPixmap(btIcon));ui->btText->setText(btText);this->id = mid;
}
// 設置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);
2、按下btForm鍵后的響應(重寫其父類的mousePressEvent)
當按鈕按下時:①按鈕顏色發生變化 ②給QQMusic類發送click信號
void btFrom::mousePressEvent(QMouseEvent* event)
{// 告訴編譯器不要觸發警告(void)event;// ?標點擊之后,背景變為綠?,?字變為??ui->btStyle->setStyleSheet("#btStyle{ background:rgb(30,206,154);} *{color:#F6F6F6; }");emit click(this->id); // 發送?標點擊信號
}
③QQMusic類處理該信號,內部:實現窗口切換,并清除上次按鈕點擊留下的樣式,因此QQMuisc中需要新增:
// qqmusic.cpp 新增
void QQMusic::connectSignalAndSlot()
{// ...// ?定義的btFrom按鈕點擊信號,當btForm點擊后,設置對應的堆疊窗?connect(ui->rec, &btFrom::click, this, &QQMusic::onBtFormClick);connect(ui->musics, &btFrom::click, this, &QQMusic::onBtFormClick);connect(ui->audio, &btFrom::click, this, &QQMusic::onBtFormClick);connect(ui->like, &btFrom::click, this, &QQMusic::onBtFormClick);connect(ui->local, &btFrom::click, this, &QQMusic::onBtFormClick);connect(ui->recent, &btFrom::click, this, &QQMusic::onBtFormClick);
}void Widget::onBtFormClick(int id)
{// 1.獲取當前??所有btFrom按鈕類型的對象QList<BtForm*> buttonList = this->findChildren<BtForm*>();// 2.遍歷所有對象, 如果不是當前id的按鈕,則把之前設置的背景顏?清除掉foreach(BtForm * btitem, buttonList){if (id != btitem->getId()){btitem->clearBg();}}// 3.設置當前棧空間顯???ui->stackedWidget->setCurrentIndex(id - 1);
}
bfFom類中添加:
// btform.cpp 新增:
void BtForm::clearBg()
{// 清除上?個按鈕點擊的背景效果,恢復之前的樣式ui->btStyle->setStyleSheet("#btStyle:hover{ background:#D8D8D8;} ");
}
int BtForm::getId()
{return id;
}
3、btForm類中的動畫效果
即給bfForm類中的4個QLabel設置動畫效果,在btFrom類中的構造函數中新增,里面的QRect類的參數根據自己設置的ui界面的標準來確定
// 設置line1的動畫效果
line1Animal = new QPropertyAnimation(ui->line1, "geometry", this);
line1Animal->setDuration(1500); //持續時間
line1Animal->setKeyValueAt(0, QRect(0, 15, 2, 0)); //關鍵幀
line1Animal->setKeyValueAt(0.5, QRect(0, 0, 2, 15));
line1Animal->setKeyValueAt(1, QRect(0, 15, 2, 0));
line1Animal->setLoopCount(-1); //循環次數
line1Animal->start();
// 設置line2的動畫效果
line2Animal = new QPropertyAnimation(ui->line2, "geometry", this);
line2Animal->setDuration(1600);
line2Animal->setKeyValueAt(0, QRect(7, 15, 2, 0));
line2Animal->setKeyValueAt(0.5, QRect(7, 0, 2, 15));
line2Animal->setKeyValueAt(1, QRect(7, 15, 2, 0));
line2Animal->setLoopCount(-1);
line2Animal->start();
// 設置line3的動畫效果
line3Animal = new QPropertyAnimation(ui->line3, "geometry", this);
line3Animal->setDuration(1700);
line3Animal->setKeyValueAt(0, QRect(14, 15, 2, 0));
line3Animal->setKeyValueAt(0.5, QRect(14, 0, 2, 15));
line3Animal->setKeyValueAt(1, QRect(14, 15, 2, 0));
line3Animal->setLoopCount(-1);
line3Animal->start();
// 設置line4的動畫效果
line4Animal = new QPropertyAnimation(ui->line4, "geometry", this);
line4Animal->setDuration(1800);
line4Animal->setKeyValueAt(0, QRect(21, 15, 2, 0));
line4Animal->setKeyValueAt(0.5, QRect(21, 0, 2, 15));
line4Animal->setKeyValueAt(1, QRect(21, 15, 2, 0));
line4Animal->setLoopCount(-1);
line4Animal->start();
}
動畫并不是所有頁面都顯示,只有當前選中的頁面顯示,所以默認情況下,動畫隱藏。默認情況下設置addlocal顯示。
// btform.cpp的中新增:
void btFrom::showAnimal()
{// 顯?linebox, 設置顏?為綠?ui->linebox->show();
}
// 本地下載BtForm動畫默認顯?ui->local->showAnimal();ui->stackedWidget->setCurrentIndex(4);
推薦頁面(recPage類)
1、recPage自定義
分析:
①"推薦"文本提示,即QLabel
②"今日為你推薦"文本提示,即QLabel
③具體推薦的歌曲內容,點擊左右兩側翻頁按鈕,具有輪番圖效果,將光標放到圖上,有圖片上移動畫
④"你的歌曲補給站"文本提示,即QLabel具體顯示音樂,和③實際是一樣的,不同的是③中音樂只有一行,⑤中的音樂有兩行因為頁面中元素較多,直接擺到一個頁面太擁擠,從右側的滾動條可以看出,整個頁面中的元素都放置在QScrollArea中。
仔細分析③發現,里面包含了:?
左右各兩個按鈕,點擊之后中間的圖片會左右移動,Qt中未提供類似該種組合控件,因此③實際為自定義控件。
③中按鈕之間的元素,由圖片和底下的文字組成,當光標放在圖片上會有上移的動畫,因此該元素實際也為自定義控件。?
完成布局后的效果:
?2、自定義recBox
完成布局后的效果
將QQMusic主界面中recPage頁面中的recMusicBox和supplyMusicBox提升為RecBox,就能看到如下效果。?
3、自定義recBoxItem
完成布局后的效果
4、RecBoxItem類中添加動畫效果?
#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信號,攔截到該信號,銷毀animationconnect(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信號,攔截到該信號,銷毀animationconnect(animation, &QPropertyAnimation::finished, this, [=]() {delete animation;qDebug() << "圖?上移動畫結束";});return true;}}return QObject::eventFilter(watched, event);
}
注意:攔截事件處理器時一定要先安裝事件處理器
// 注意:不要忘記事件攔截器安裝,否則時間攔截不到,因此需要在構造函數中添加:
// 攔截事件處理器時,?定要安裝事件攔截器
ui->musicImageBox->installEventFilter(this);
該類中還需要添加設置推薦文本和圖片的方法,將來需要在外部來設置每個重新框項目的文本和圖
片:
// 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->recMusicImg->setStyleSheet(imgStyle);
}
5、RecBox添加RecBoxItem
圖片路徑和推薦文本準備
每個RecBoxltem都有對應的圖片和推薦文本,在往RecBox中添加RecBoxltem前需要先將圖片路徑和對應文本準備好。由于圖片和文本具有對應關系,可以以鍵值對方式來進行組織,以下實現的時采用QT內置的QJsonObject對象管理圖片路徑和文本內容。
使用QT內置的QJsonObject對象管理圖片路徑和文本內容,圖片路徑和對應文本的準備工作,應該在QQMusic類中處理好,RecBoxItem只負責設置RecBox,因此該準備工作需要在QQMusic類中進行,在QQMusic中需要添加如下代碼:
// 設置隨機圖?【歌曲的圖?】
QJsonArray QQMusic::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/"+vecImageName[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, QCchar('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;
}
recBox中添加元素
由于recPage頁面中有兩個RecBox控件,上面的RecBox為一行四列,下方的RecBox為2行四列,因此在RecBox類中新增加以下成員變量:
#include <QJsonArray>
public:void initRecBoxUi(QJsonArray data, int row);private:int row; // 記錄當前RecBox實際總?數int col; // 記錄當前RecBox實際每?有?個元素QJsonArray imageList; // 保存界?上的圖?, ??實際為key、value鍵值對
RecBox的構造函數中,將row和col默認設置為1和4,count需要具體來計算:
RecBox::RecBox(QWidget* parent) :QWidget(parent),ui(new Ui::RecBox),row(1),col(4)
{ui->setupUi(this);
}
void RecBox::initRecBoxUi(QJsonArray data, int row)
{// 如果是兩?,說明當前RecBox是主界?上的supplyMusicBoxif (2 == row){this->row = row;this->col = 8;}else{// 否則:只有??,為主界?上recMusicBoxui->recBoxBottom->hide();}// 圖?保存起來imageList = data;// 往RecBox中添加圖?createRecItem();
}
void RecBox::createRecBoxItem()
{// 創建RecBoxItem對象,往RecBox中添加// colfor (int i = 0; i < col; ++i){RecBoxItem* item = new RecBoxItem();// 設置?樂圖?與對應?本QJsonObject obj = imageList[i].toObject();item->setRecText(obj.value("text").toString());item->setRecImage(obj.value("path").toString());// recMusicBox:col為4,元素添加到ui->recListUpHLayout中// supplyMuscBox: col為8, ui->recListUpHLayout添加4個,ui->recListDownHLayout添加4個// 即supplyMuscBox上下兩?都要添加// 如果是recMusicBox:row為1,只能執?else,所有4個RecBoxItem都添加到ui->recListUpHLayout中// 如果是supplyMuscBox: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){ui->recListDownHLayout->addWidget(item);}else{ui->recListUpHLayout->addWidget(item);}}
}
6、RecBox中btUp和btDown按鈕clicked處理
添加槽函數
選中recbox.ui文件,分別選中btUp和btDown,右鍵單擊彈出菜單選擇轉到槽,選中clicked確定,
btUp和btDown的槽函數就添加好了。
void RecBox::on_btUp_clicked()
{// 點擊btUp按鈕,顯?前4張圖?,如果已經是第?張圖?,循環從后往前顯?
}
void RecBox::on_btDown_clicked()
{// 點擊btUp按鈕,顯?前8張圖?,如果已經是第?張圖?,循環從后往前顯?
}
假設imageList中有24組圖片路徑和推薦文本信息,如果將信息分組:
如果是recMusicBox,將元素按照col分組,即每4個元素為一組,可分為6組
如果是supplyMuscBox,將元素按照col分組,即每8個元素為一組,可分為3組。
RecBox類中添加currentIndex和count整形成員變量,currentIndex記錄當前顯示組,count記錄總的信息組數。當點擊btUp時,currentIndex-,顯示前一組,如果currentIndex小于O時,將其設置為count-1;
點擊btDown按鈕時,currentIndex++顯示下一組,當currentIndex為count時,將count
設置為0。
// recbox.cpp 中新增
void RecBox::initRecBoxUi(QJsonArray data, int row)
{// ...imageList = data;// 默認顯?第0組currentIndex = 0;// 計算總共有?組圖?,ceil表?向上取整count = ceil(imageList.size() / col);// 在RecBox控件添加RecBoxItemcreateRecBoxItem();
}
void RecBox::on_btUp_clicked()
{// 點擊btUp按鈕,顯?前?組圖?,如果已經是第?組圖?,顯?最后?組currentIndex--;if (currentIndex < 0){currentIndex = 0;}createRecBoxItem();
}
void RecBox::on_btDown_clicked()
{// 點擊btDown按鈕,顯?下?組圖?,如果已經是最后?組圖?,顯?第0組currentIndex++;if (currentIndex >= count){currentIndex = 0;}createRecBoxItem();
}
?元素重復分析
每次btUp和btDown點擊后,應該顯示前一組和后一組圖片,由于之前recListUpHLayout和
recListDownHLayout中已經有元素了,因此需要先將之前的元素刪除掉。
在RecBox::createRecBoxItem()成員函數中新加
void RecBox::createRecBoxItem()
{// 溢出掉之前舊元素QList<RecBoxItem*> recUpList = ui->recListUp->findChildren<RecBoxItem*>();for (auto e : recUpList){ui->recListUpHLayout->removeWidget(e);delete e;}QList<RecBoxItem*> recDownList = ui->recListDown->findChildren<RecBoxItem*>();for (auto e : recDownList){ui->recListDownHLayout->removeWidget(e);delete e;}
}
程序啟動時圖片隨機顯示
每次程序啟動時,顯示的圖片都是相同的,這是因為random_shuffle在隨機打亂元素時,需要設置隨機數種子,否則默認使用的種子是相同的,就導致每次打亂的結果都是相同的,所以每次程序啟動時RecBox中顯示的內容都是相同的,因此在randomPiction()調用之前需要設置隨機數種子。
QQMusic類的initUi函數中新增:
srand(time(NULL)); ui->recMusicBox->initRecBoxUi(randomPiction(), 1); ui->supplyMuscBox->initRecBoxUi(randomPiction(), 2);
commonPage頁面
1、commonPage頁面分析
我的音樂下的:我喜歡、本地下載、最近播放三個按鈕表面上看對應三個Page頁面,分析之后發現,這三個Page頁面實際是雷同的,因此只需要定義一個頁面CommonPage,將stackedWidget中這三個頁面的類型提升為CommonPage即可。
①頁面說明,比如:本地音樂,該部分實際就是QLabel的提示說明;
②正在播放音樂圖片和播放全部按鈕;
③音樂列表中每個部分的文本提示,實際就是三個QLabel
④本頁面對應的音樂列表,即QListWidget。
2、commonPage界面設計和顯示?
把connomPage的界面布局和QSS樣式設置好后分析。
CommonPage頁面是我喜歡、本地下載、最近播放三個界面的共同類型,因此該類需要提供設置:pageTittle和 musicImageLabel的公共方法,將來在程序啟動時完成三個界面信息的設置,因此CommonPage類需要添加一個public的setCommonPageUI函數。
// commonpage.cpp 中新增
void CommonPage::setCommonPageUI(const QString& title, const QString& image)
{// 設置標題ui->pageTittle->setText(title);// 設置封?欄ui->musicImageLabel->setPixmap(QPixmap(image));ui->musicImageLabel->setScaledContents(true);
}
界面設置的函數需要在程序啟動時就完成好配置,即需要在QQMusic的initUi(函數中調用完成設置:
//在Widget::initUi()中新增
// 設置我喜歡、本地?樂、最近播放??
ui->likePage->setCommonPageUI("我喜歡", ":/images/ilikebg.png");
ui->localPage->setCommonPageUI("本地?樂", ":/images/localbg.png");
ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");
自定義ListItemBox
1、ListItemBox界面分析
CommonPage頁面創建好之后,等音樂加載到程序之后,就可以將音樂信息往CommonPage的
pageMusicList中顯示了。
上圖每行都是QListWidget中的一個元素,每個元素中包含多個控件:
①收藏圖標,即QLabel
②歌曲名稱,即QLabel
③VIP和SQ,VIP即收費會員專享,SQ為無損音樂,也是兩個QLabel
④歌手名稱,即QLabel
⑤音樂專輯名稱,即QLabel
此處,需要將上述所有QLabel組合在一起,作為一個獨立的控件,添加到QListWidget中,因此該控件也需要自定義。?
2、ListItemBox顯示測試
設置完ListItemBox的界面布局和QSS樣式表后,ListItemBox將來要添加到CommonPage頁面中的QListWidget中,因此在CommonPage類的初始化方法中添加如下代碼:
void CommonPage::setCommonPageUI(const QString& title, const QString& image)
{// 設置標題ui->pageTittle->setText(title);// 設置封?欄ui->musicImageLabel->setPixmap(QPixmap(image));ui->musicImageLabel->setScaledContents(true);// 測試ListItemBox* listItemBox = new ListItemBox(this);QListWidgetItem* listWidgetItem = new QListWidgetItem(ui->pageMusicList);listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(), 45));ui->pageMusicList->setItemWidget(listWidgetItem, listItemBox);
}
3、支持hover效果
ListltemBox添加到CommonPage中的QListWidget之后,自帶hover效果,但是背景顏色和界面不太搭配,此處重新實現hover效果,此處重寫enterEvent和leaveEvent來實現hover效果。
// listitembox.cpp 新增
void ListItemBox::enterEvent(QEvent* event)
{(void)event;setStyleSheet("background-color:#EFEFEF");
}
void ListItemBox::leaveEvent(QEvent* event)
{(void)event;setStyleSheet("");
}
自定義MusicSlider
由于QT內置的HorizontalSlider(水平滑竿)不是很好看,該控件也采用自定義。該控件比較簡單,實際就是兩個QFrame嵌套起來的,達到如下效果:
?自定義VolumeTool
1、VolumeTool控件分析
①內部為類似MusicSlider控件+小圓球,圓球實際為一個QPushButton
音量大小文本顯示,實際為QLabel
③QPushButton,點擊之后在靜音和取消靜音切換
④一個倒三角,Qt未提供三角控件,該控件需要手動繪制,用來提示是播放控制區那個按鈕按下
2、界面設計
該控件屬于彈出窗口,即點擊了主界面的音量調節按鈕后,才需要彈出該界面,點擊其他位置該界面自動隱藏。因此在窗口創建時,需要設置窗口為無邊框以及為彈出窗口。
// 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::NoDropShadowWindowHintsetAttribute(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/volumn.png"));// ?量的默認??是20ui->outLine->setGeometry(ui->outLine->x(), 180 - 36 - 25, ui->outLine -> width(), 20);//根據自定義的控件大小來ui->silderBtn->move(ui->silderBtn->x(), ui->outLine->y() - ui -> silderBtn->height() / 2);ui->volumeRatio->setText("20%");
}
?3、界面創建及彈出
音量調節屬于主界面上元素,因此在QQMusic類中需要添加VolumeTool的對象,在initUi中new該類的對象。主界面中音量調節按鈕添加clicked槽函數。
// qqmusic.cpp中新增
void QQMusic::initUi()
{// ...// 創建?量調節窗?對象并掛到對象樹volumeTool = new VolumeTool(this);
}
void QQMusic::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();
}
4、繪制三角
由于Qt中并未給出三角控件,因此三角需要手動繪制,故在VolumeTool類中重寫paintEvent事件函
數。
// volumetool.cpp中新增
#include <QPainter>
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(Qt::white);// 創建?個三?形QPolygon polygon;polygon.append(QPoint(30, 300));//坐標根據vooltool控件來確定polygon.append(QPoint(70, 300));polygon.append(QPoint(50, 320));// 繪制三?形painter.drawPolygon(polygon);
}
音樂管理
1、音樂加載
QQMusic類中給addLocal添加槽函數。音樂文件在磁盤中,可以借助QFileDialog類完成音樂文件加載。
// qqmusic.cpp 中新增
#include <QDir>
#include <QFileDialog>
void QQMusic::on_addLocal_clicked()
{// 1. 創建?個?件對話框QFileDialog fileDialog(this);fileDialog.setWindowTitle("添加本地?樂");// 2. 創建?個打開格式的?件對話框fileDialog.setAcceptMode(QFileDialog::AcceptOpen);// 3. 設置對話框模式// 只能選擇?件,并且?次性可以選擇多個存在的?件fileDialog.setFileMode(QFileDialog::ExistingFiles);// 4. 設置對話框的MIME過濾器QStringList mimeList;mimeList << "application/octet-stream";fileDialog.setMimeTypeFilters(mimeList);// 5. 設置對話框默認的打開路徑,設置?錄為當前?程所在?錄QDir dir(QDir::currentPath());dir.cdUp();QString musicPath = dir.path() + "/QQMusic/musics/";fileDialog.setDirectory(musicPath);// 6. 顯?對話框,并接收返回值// 模態對話框, exec內部是死循環處理if (fileDialog.exec() == QFileDialog::Accepted){// 切換到本地?樂界?,因為加載完的?樂需要在本地?樂界?顯?ui->stackedWidget->setCurrentIndex(4);// 獲取對話框的返回值QList<QUrl> urls = fileDialog.selectedUrls();// 拿到歌曲?件后,將歌曲?件交由musicList進?管理// ...}
}
MusicList類
將來添加到播放器中的音樂比較多,可借助一個類對所有的音樂進行管理。
1、歌曲對象存儲
每首音樂文件,將來需要獲取其內部的歌曲名稱、歌手、音樂專輯、歌曲時長等信息,因此在
MusicList類中,將所有的歌曲文件以Music對象方式管理起來。在QQMusic中,通過QFileDialog將一組音樂文件的url獲取到之后,可以交給MusicList類來管理。但是QQMusic加載的二進制文件不一定全部都是音樂文件,因此MusicList類中需要對文件的MIME類型再次檢測,以篩選出真正的音樂文件。
QMimeDatabase類是Qt中主要用于處理文件的MIME類型,經常用于:文件類型識別、文件過濾
、多媒體文件處理、文件導入導出、文件管理器,該類中的mimeTypeForFile函數可用于獲取給定文件的MIME類型。
對于歌曲文件:
audio/mpeg:適用于mp3格式的音樂文件
audio/flac:無損壓縮的音頻文件,不會破壞任何原有的音頻信息
audio/wav:表示wav格式的歌曲文件
上述歌曲文件格式,Qt的QMediaPlayer類都是支持的。
// musiclist.h 中新增
#include <QVector>
QVector<Music> musicList; // Music類是?定義的C++類,描述歌曲相關信息
// 將QQMusic??中讀取到的?樂?件,檢測是?樂?件后添加到musicList中
void addMusicByUrl(const QList<QUrl>& urls);
// musiclist.cpp中新增
void MusicList::addMusicByUrl(const QList<QUrl>& urls)
{for (auto musicUrl : urls){// 由于添加進來的?件不?定是歌曲?件,因此需要再次篩選出?樂?件QMimeDatabase db;QMimeType mime = db.mimeTypeForFile(musicUrl.toLocalFile());if (mime.name() != "audio/mpeg" && mime.name() != "audio/flac"){continue;}// 如果是?樂?件,加?歌曲列表musicList.push_back(musicUrl);}
}
?Music類
1、介紹
該用來描述一個音樂文件,比如:音樂名稱、歌手名稱、專輯名稱、音樂持續時長,當在界面上點擊收藏之后,音樂會被標記為喜歡,播放之后需要標記為歷史記錄。因此該類中至少需要以下成員:
// music.h中新增
#include <QUrl>
#include <QString>
class Music
{
public:Music();Music(const QUrl& url);void setIsLike(bool isLike);void setIsHistory(bool isHistory);void setMusicName(const QString& musicName);void setSingerName(const QString& singerName);void setAlbumName(const QString& albumName);void setDuration(const qint64 duration);void setMusicUrl(const QUrl& url);void setMusicId(const QString& musicId);bool getIsLike();bool getIsHistory();QString getMusicName();QString getSingerName();QString getAlbumName();qint64 getDuration();QUrl getMusicUrl();QString getMusicId();
private:bool isLike; // 標記?樂是否為我喜歡bool isHistory; // 標記?樂是否播放過// ?樂的基本信息有:歌曲名稱、歌?名稱、專輯名稱、總時?QString musicName;QString singerName;QString albumName;qint64 duration; // ?樂的持續時?,即播放總的時?// 為了標記歌曲的唯?性,給歌曲設置id// 磁盤上的歌曲?件經常刪除或者修改位置,導致播放時找不到?件,或者重復添加// 此處?musicId來維護播放列表中?樂的唯?性QString musicId;QUrl musicUrl; // ?樂在磁盤中的位置
};
Music.cpp新增?
// 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::setDuration(const qint64 duration)
{this->duration = duration;
}
void Music::setMusicUrl(const QUrl& url)
{this->musicUrl = url;
}
void Music::setMusicId(const QString& musicId)
{this->musicId = musicId;
}
bool Music::getIsLike()
{return isLike;
}
bool Music::getIsHistory()
{return isHistory;
}
QString Music::getMusicName()
{return musicName;
}
QString Music::getSingerName()
{return singerName;
}
QString Music::getAlbumName()
{return albumName;
}
qint64 Music::getDuration()
{return duration;
}
QUrl Music::getMusicUrl()
{return musicUrl;
}
QString Music::getMusicId()
{return musicId;
}
?另外,該類還需要添加一個帶有歌曲文件路徑的構造函數,當給定有效音樂文件后,Music類需要負責將該音樂文件的元數據解析出來。為了保證Music對象的唯一性,給每個Music對象設置一個UUID。UUID,即通用唯一識別碼(Universally UniqueIdentifier),確保在分布式系統中每個元素都有唯一的標識。UUID由一組32位數的16進制數字組成,形式為8-4-4-4-12的32個字符,比如:"550e8400-e29b-41d4-a716-446655440000"在Music對象查找和更新時,可以已通過對比UUID,來保證Music對象的唯一性。Qt中QUuid類可生成UUID。
Music::Music(const QUrl& url): isLike(false), isHistory(false), musicUrl(url) {musicId = QUuid::createUuid().toString(); }
?2、解析音樂文件源數據
對于每首歌曲,將來在界面上需要顯示出:歌曲名稱、歌手、專輯名稱,在播放時還需要拿到歌曲總時長,因此在構造音樂對象時,就需要將上述信息解析出來。歌曲元數據解析,需要用到QMediaPlayer,該類也是用來進行歌曲播放的類。
//QMediaPlayer類中的setMedia()函數
// 功能:設置要播放的媒體源,媒體數據從中讀取
// media: 要播放的媒體內容,?如?個視頻或?頻?件,該類提供了?個QUrl格式的單參構造
void setMedia(const QMediaContent& media, QIODevice* stream = nullptr)
//注意:該函數執?后?即返回,不會等待媒體加載完成,也不檢查錯誤,如果在媒體加載時發?錯
//誤,會觸發mediaStatusChanged和error信號// 檢測媒體源是否有效,如果是有效的返回true,否則返回false
bool isMetaDataAvailable() const;//媒體元數據加載成功之后,可以通過QMediaObject類的metaData函數獲取指定的媒體數據:
// 返回要獲取的媒體數據key的值
QVariant QMediaObject::metaData(const QString& key) const;
該項目中需要獲取媒體的標題、作者、專輯、持續時長。
音樂文件的mate數據解析代碼如下:
// music.h 中新增
private:void parseMediaMetaData();
// music.cpp 中新增
#include <QMediaPlayer>
#include <QCoreApplication>
#include <QUuid>
void Music::parseMediaMetaData()
{// 解析時候需要讀取歌曲數據,讀取歌曲?件需要?到QMediaPlayer類QMediaPlayer player;player.setMedia(musicUrl);// 媒體元數據解析需要時間,只有等待解析完成之后,才能提取?樂信息,此處循環等待// 循環等待時:主界?消息循環就?法處理了,因此需要在等待解析期間,讓消息循環繼續處理while (!player.isMetaDataAvailable()){QCoreApplication::processEvents();}// 解析媒體元數據結束,提取元數據信息if (player.isMetaDataAvailable()){musicName = player.metaData("Title").toString();singerName = player.metaData("Author").toString();albumName = player.metaData("AlbumTitle").toString();duration = player.duration();if (musicName.isEmpty()){musicName = "歌曲未知";}if (singerName.isEmpty()){singerName = "歌?未知";}if (albumName.isEmpty()){albumName = "專輯名未知";}qDebug() << musicName << " " << singerName << " " << albumName << " " << duration;}
}
// 該函數需要在Music的構造函數中調?,當創建?樂對象時,順便完成歌曲?件的加載
Music::Music(const QUrl& url): isLike(false), isHistory(false), musicUrl(url)
{musicId = QUuid::createUuid().toString();parseMediaMetaData();
}
?3、Music數據保存
通過QFileDialog將音樂從本地磁盤加載到程序中后,拿到的是所有音樂文件的QUrl,而在程序中需要的是經過元數據解析之后的Music對象,并且Music對象需要管理起來,此時就可以采用MusicList類對解析之后的Music對象進行管理,QQMusic類中只需要保存MusicList的對象,就可以讓qqMusic.ui界面中CommonPage對象完成Music信息往界面更新。
// qqmusic.h 新增
#include "musiclist.h"
MusicList musicList;
// qqmusic.cpp
void QQMusic::on_addLocal_clicked()
{// ....// 6. 顯?對話框,并接收返回值// 模態對話框, exec內部是死循環處理if (fileDialog.exec() == QFileDialog::Accepted){// 切換到本地?樂界?,因為加載完的?樂需要在本地?樂界?顯?ui->stackedWidget->setCurrentIndex(4);// 獲取對話框的返回值QList<QUrl> urls = fileDialog.selectedUrls();// 拿到歌曲?件后,將歌曲?件交由musicList進?管理musicList.addMusicByUrl(urls);// 更新到本地?樂列表ui->localPage->reFresh(musicList);}
}
4、音樂分類
QQMusic中,有三個顯示歌曲信息的頁面:
likePage:管理和顯示點擊小v心心后收藏的歌曲
localPage:管理和顯示本地加載的歌曲
recentPage:管理和顯示歷史播放過的歌曲
這三個頁面的類型都是CommonPage,每個頁面應該維護自己頁面中的歌曲。因此CommonPage類中需要新增:
// commonpage.h中新增
// 區分不同page??
enum PageType
{LIKE_PAGE, // 我喜歡??LOCAL_PAGE, // 本地下載??HISTORY_PAGE // 最近播放??
};
class CommonForm : public QWidget
{// 新增成員函數
public:void setMusicListType(PageType pageType);// 新增成員變量
private:// 歌單列表QVector<QString> musicListOfPage; // 具體某個??的?樂,將來只需要存儲?樂的id即可PageType pageType; // 標記屬于likePage、localPage、recentPage哪個??
};
// commonpage.cpp中新增:
void CommonPage::setMusicListType(PageType pageType)
{this->pageType = pageType;
}
// qqmusic.cpp中新增:
void initUi()
{// ...// 設置CommonPage的信息ui->likePage->setMusicListType(PageType::LIKE_PAGE);ui->likePage->setCommonPageUI("我喜歡", ":/images/ilikebg.png");ui->localPage->setMusicListType(PageType::LOCAL_PAGE);ui->localPage->setCommonPageUI("本地?樂", ":/images/localbg.png");ui->recentPage->setMusicListType(PageType::HISTORY_PAGE);ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");
}
QQMusic中,點擊addLocal(本地加載)按鈕后,會通過其musicList成員變量,將music添加到
musicList中管理,在添加過程中,每個歌曲會對應一個Music對象,Music對象在構造時,會完成歌曲文件的加載,順便完成歌曲名稱、作者、專輯名稱等元數據的解析。一切準備就緒之后,每個
CommonPage頁面,通過QQMusic的musicList分離出自己頁面的歌曲,保存在musicListOfPage
中。
// commonpage.h中新增:
#include "musiclist.h"
private:void addMusicToMusicPage(MusicList& musicList);// commonpage.cpp 中新增:void CommonPage::addMusicToMusicPage(MusicList& musicList){// 將舊內容清空musicListOfPage.clear();for (auto& music : musicList){switch (musicListType){case LOCAL_LIST:musicListOfPage.push_back(music.getMusicId());break;case LIKE_LIST:{if (music.getIsLike()){musicListOfPage.push_back(music.getMusicId());}break;}case HOSTORY_LIST:{if (music.getIsHistory()){musicListOfPage.push_back(music.getMusicId());break;}}default:break;}}}
由于musicList所屬類,并不能直接支持范圍for,因此需要在MusicList類中新增:
// musiclist.h中新增:
typedef typename QVector<Music>::iterator iterator;
iterator begin();
iterator end();
// musiclist.cpp中新增:
iterator MusicList::begin()
{return musicList.begin();
}
iterator MusicList::end()
{return musicList.end();
}
5、更新Muic對象到CommonPage頁面
步驟:
1. 調用addMusicldPageFromMusicList函數,從musicList中添加當前頁面的歌曲
2.遍歷musicListOfPage,拿到每首音樂后先檢查其是否在,存在則添加。
3.界面上需要更新每首歌曲的:歌曲名稱、作者、專輯名稱,而commonPage中只保存了歌曲的musicld,因此需要在MusicList中增加通過musicID查找Music對象的方法。
// commonpage.h中新增
void reFresh(MusicList& musicList);
// commonpage.cpp 中新增:
void CommonPage::reFresh(MusicList& musicList)
{// 從musicList中分離出當前??的所有?樂addMusicIdPageFromMusicList(musicList);// 遍歷歌單,將歌單中的歌曲顯?到界?for (auto musicId : musicListOfPage){auto it = musicList.findMusicById(musicId);if (it == musicList.end())continue;ListItemBox* listItemBox = new ListItemBox(ui->pageMusicList);listItemBox->setMusicName(it->getMusicName());listItemBox->setSinger(it->getSingerName());listItemBox->setAlbumName(it->getAlbumName());listItemBox->setLikeIcon(it->getIsLike());QListWidgetItem* listWidgetItem = new QListWidgetItem(ui -> pageMusicList);listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(), 45));ui->pageMusicList->setItemWidget(listWidgetItem, listItemBox);}// 更新完成后刷新下界?repaint();
}
// musiclist.h中新增
iterator findMusicById(const QString& musicId);
// musiclist.cpp中新增
iterator MusicList::findMusicById(const QString& musicId)
{for (iterator it = begin(); it != end(); ++it){if (it->getMusicId() == musicId){return it;}}return end();
}
?將歌曲名稱、作者、專輯名稱、喜歡圖片等往ListBoxItem界面中更新時,需要ListBoxItem提供對應的set方法,因此需要在ListltemBox類中新增:
// 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->musicAlbumLabel->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槽函數最后調用。
// qqmusic.cpp中新增:
void QQMusic::onAddLocalClick()
{// ...// 6. 顯?對話框,并接收返回值// 模態對話框, exec內部是死循環處理if (fileDialog.exec() == QFileDialog::Accepted){// 切換到本地?樂界?,因為加載完的?樂需要在本地?樂界?顯?ui->stackedWidget->setCurrentIndex(4);// 獲取對話框的返回值QList<QUrl> urls = fileDialog.selectedUrls();// 注意:后序需要將?樂信息添加到數據庫,否則每次打開是都需要添加?樂太?煩了musicList.addMusicByUrl(urls);// 更新到本地?樂列表ui->localPage->reFresh(musicList);}
}
音樂收藏(點擊小心心)
1、收藏圖標處理
當CommonPage往界面更新Music信息時,也要根據Music的isLike屬性更新對應的圖標。因此
ListItemBox需要根據當點擊我喜歡按鈕之后,要切換ListItemBox中的小心心。因此ListItemBox中添加設置bool類型isLike成員變量,以及setIsLike函數,在CommonPage添加Music信息到界面時,要能夠設置小心心圖片。
// listItemBox.h 中新增
bool isLike;
void setLikeMusic(bool isLike);
// listItemBox.cp 中新增
ListItemBox::ListItemBox(QWidget* parent) :QWidget(parent),ui(new Ui::ListItemBox),isLike(false)
{ui->setupUi(this);
}
void ListItemBox::setLikeMusic(bool isLike)
{this->isLike = isLike;if (isLike){ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));}else{ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));}
}
2、點擊收藏按鈕處理
當喜歡某首歌曲時,可以點擊界面上紅色小心心收藏該首歌曲。我喜歡按鈕中應該有以下操作:
1.更新小心心圖標
2.更新Music的我喜歡屬性,但ListItemBox并沒有歌曲數據,所以只能發射信號,讓其父元素
CommonPage來處理
// listItemBox.h 中新增
public:void onLikeBtnClicked(); // 按鈕點擊槽函數signals:void setIsLike(bool); // 通知更新歌曲數據信號// ListItemBox.cpp 中新增ListItemBox::ListItemBox(QWidget* parent) :QWidget(parent),ui(new Ui::ListItemBox),isLike(false){ui->setupUi(this);// likeBtn按鈕連接其點擊槽函數connect(ui->likeBtn, &QPushButton::clicked, this,&ListItemBox::onLikeBtnClicked);}void ListItemBox::onLikeBtnClicked(){isLike = !isLike;setIsLike(isLike);emit setIsLike(isLike);}
3.CommonPage在往QListWidget中添加元素時,會創建一個個ListItemBox對象,每個對象將來都
可能會發射setLikeMusic信號,因此在將ListItemBox添加完之后,CommonPage應該關聯先該信
號,將需要更新的的Music信息以及是否喜歡,同步給QQMusiC。
// commonpage.h中新增
signals:void updateLikeMusic(bool isLike, QString musicId);// commonpage.cpp中新增// 該?法負責將歌曲信息更新到界?void CommonPage::reFresh(MusicList& musicList){// ...for (auto musicId : musicOfPage){// ...QListWidgetItem* item = new QListWidgetItem(ui->pageMusicList);item->setSizeHint(QSize(listItemBox->width(), listItemBox->height()));ui->pageMusicList->setItemWidget(item, listItemBox);// 接收ListItemBox發射的setLikeMusic信號connect(listItemBox, &ListItemBox::setIsLike, this, [=](bool isLike) {emit updateLikeMusic(isLike, it->getMusicId());});}ui->pageMusicList->repaint();}
QQMusic收到CommonPage發射的updateLikePage信號后,通知其上的likePage、localPage、
recentPage更新其界面的我喜歡歌曲信息。
// qqmusic.h 新增
void onUpdateLikeMusic(bool isLike, QString musicId); // 響應CommonPage發射updateLikeMusic信號
// qqmusic.cpp新增
void QQMusic::connectSignalAndSlots()
{// ...// 關聯CommonPage發射的updateLikeMusic信號connect(ui->likePage, &CommonPage::updateLikeMusic, this,&QQMusic::onUpdateLikeMusic);connect(ui->localPage, &CommonPage::updateLikeMusic, this,&QQMusic::onUpdateLikeMusic);connect(ui->recentPage, &CommonPage::updateLikeMusic, this,&QQMusic::onUpdateLikeMusic);
}
void QQMusic::onUpdateLikeMusic(bool isLike, QString musicId)
{// 1. 找到該?歌曲,并更新對應Music對象信息auto it = musicList.findMusicByMusicId(musicId);if (it != musicList.end()){it->setIsLike(isLike);}// 2. 通知三個??更新??的數據ui->likePage->reFresh(musicList);ui->localPage->reFresh(musicList);ui->recentPage->reFresh(musicList);
}
3、歌曲重復顯示問題
// commonpage.cpp修改
void CommonPage::addMusicToMusicPage(MusicList& musicList)
{musicOfPage.clear();// ...
}
void CommonPage::reFresh(MusicList& musicList)
{ui->pageMusicList->clear();//...
}
音樂播放控制
1、播放媒體和播放列表初始化
#include <QMediaPlayer>
#include <QMediaPlaylist>
// qqmusic.h 新增
public:void initPlayer(); // 初始化媒體對象
private://播放器相關QMediaPlayer* player;// 要多?歌曲播放,以及更復雜的播放設置,需要給播放器設置媒體列表QMediaPlaylist* playList;// qqmusic.cpp 中添加QQMusic::QQMusic(QWidget* parent): QWidget(parent), ui(new Ui::QQMusic){ui->setupUi(this);// 窗?控件的初始化?作initUI();// 初始化播放器initPlayer();// 關聯所有信號和槽connectSignalAndSlot();}void QQMusic::initPlayer(){// 創建播放器player = new QMediaPlayer(this);// 創建播放列表playList = new QMediaPlaylist(this);// 設置播放模式:默認為循環播放playList->setPlaybackMode(QMediaPlaylist::Loop);// 將播放列表設置給播放器player->setPlaylist(playList);// 默認?量??設置為20player->setVolume(20);}
2、播放列表設置
播放之前,先要將歌曲加入用于播放的媒體列表,由于每個CommonPage頁面的歌曲不同,因此
CommonPage中新增將其頁面歌曲添加到模仿列表的方法。
// commonpage.h 中新增
#include <QMediaPlaylist>
void addMusicToPlayer(MusicList& musicList, QMediaPlaylist* playList);
// commonpage.cpp 中新增
void CommonPage::addMusicToPlayer(MusicList& musicList, QMediaPlaylist* playList)
{// 根據?樂列表中?樂所屬的??,將?樂添加到playList中for (auto music : musicList){switch (pageType){case LOCAL_PAGE:{playList->addMedia(music.getMusicUrl());break;}case LIKE_PAGE:{if (music.getIsLike()){playList->addMedia(music.getMusicUrl());}break;}case HISTORY_PAGE:{if (music.getIsHistory()){playList->addMedia(music.getMusicUrl());}break;}default:break;}}
}
3、播放和暫停
當點擊播放和暫停按鈕時,播放狀態應該在播放和暫停之間切換。播放器的狀態如下,剛開始為停止狀態QMediaPlayer的播放狀態有:PlayingState()、PausedState()、StoppedState()。
// qqmusic.h 中新增
// 播放控制區域
void onPlayCliked(); // 播放按鈕
// qqmusic.cpp 中新增
void QQMusic::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 QQMusic::connectSignalAndSlots()
{// ...// 播放控制區的信號和槽函數關聯connect(ui->play, &QPushButton::clicked, this, &QQMusic::onPlayMusic);
}
注意:播放時默認是從播放列表索引為0的歌曲開始播放的。
另外播放狀態改變的時候,需要修改播放按鈕上圖標,圖片的修改可以在onPlayCliked函數中設置,也可以攔截QMediaPlayer中的stateChanged信號,當播放狀態改變的時候,QMediaPlayer會觸發該信號,在stateChanged信號中修改播放按鈕也可以,此處攔截stateChanged信號。
// qqmusic.h 新增
// QMediaPlayer信號處理
// 播放狀態發?改變
void onPlayStateChanged();// qqmusic.cpp 新增
// QMediaPlayer信號關聯槽函數
void QQMusic::onPlayStateChanged()
{qDebug() << "播放狀態改變";if (player->state() == QMediaPlayer::PlayingState){// 播放狀態ui->play->setIcon(QIcon(":/images/play_on.png"));}else{// 暫停狀態ui->play->setIcon(QIcon(":/images/play3.png"));}
}
void QQMusic::initPlayer()
{// ...// QMediaPlayer信號和槽函數關聯// 播放狀態改變時:暫停和播放之間切換connect(player, &QMediaPlayer::stateChanged, this, &QQMusic::onPlayStateChanged);
}
播放和暫停切換的時候,按鈕上的圖標有重疊,是因為之前在界面設置的時候,為了能看到效果,給按鈕添加了背景圖片,背景圖片和圖標是兩種屬性,都設置時就ui疊加,因此將按鈕上個添加背景圖片樣式去除掉。
void QQMusic::initUi()
{// 按鈕的背景圖?樣式去除掉之后,需要設置默認圖標// 播放控制區按鈕圖標設定ui->play->setIcon(QIcon(":/images/play_2.png")); // 默認為暫停圖標ui->playMode->setIcon(QIcon(":/images/shuffle_2.png")); // 默認為隨機播放volumeTool = new VolumeTool(this);
}
4、上一曲和下一曲
播放列表中,提供了previous()和next()函數,通過設置前一個或者下一個歌曲為當前播放源,player就會播放對應的歌曲。
// qqmusic.h 新增
void onPlayUpCliked(); // 上?曲
void onPlayDownCliked(); // 下?曲
// qqmusic.cpp 新增
void QQMusic::onPlayUpCliked()
{playList->previous();
}
void QQMusic::onPlayDownCliked()
{playList->next();
}
void QQMusic::connectSignalAndSlots()
{// ...// 播放控制區的信號和槽函數關聯connect(ui->play, &QPushButton::clicked, this, &QQMusic::onPlayMusic);connect(ui->playUp, &QPushButton::clicked, this, &QQMusic::onPlayUpClicked);connect(ui->playDown, &QPushButton::clicked, this, &QQMusic::onPlayDownClicked);
}
4、切換播放模式
// qqmusic.h 中新增
void onPlaybackModeCliked(); // 播放模式設置
// qqmusic.cpp 中新增
void QQMusic::initPlayer()
{// ...// 設置播放模式connect(ui->playMode, &QPushButton::clicked, this,&QQMusic::onPlaybackModeCliked);
}
void QQMusic::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信號,在該信號對應槽函數中,完成圖片切換。
// qqmusic.h 中新增
// 播放模式切換槽函數
void onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode);
// qqmusic.cpp 中新增
void QQMusic::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 QQMusic::initPlayer()
{// ...// 播放列表的模式放?改變時的信號槽關聯connect(playList, &QMediaPlaylist::playbackModeChanged, this,&QQMusic::onPlaybackModeChanged);
}
5、播放所有
播放所有按鈕屬于CommonPage中的按鈕,其對應的槽函數添加在CommonPage類中,但是
CommonPage不具有音樂播放的功能,因此當點擊播放所有按鈕后之后,播放所有的槽函數應該發射出信號,讓QQMusic類完成播放。由于likePage、localPage、recentPage三個CommonPage頁面都有playAllBtn,因此該信號需要帶上PageType參數,需要讓QQMusic在處理該信號時,知道播放哪個頁面的歌曲。
// commonpage.h 中新增加
signals:// 該信號由QQMusic處理--在構函數中捕獲void playAll(PageType pageType);// commonpage.cpp 中修改CommonPage::CommonPage(QWidget* parent) :QWidget(parent),ui(new Ui::CommonPage){ui->setupUi(this);// playAllBtn按鈕的信號槽處理// 當播放按鈕點擊時,發射playAll信號,播放當前??的所有歌曲// playAll信號交由QQMusic中處理connect(ui->playAllBtn, &QPushButton::clicked, this, [=]() {emit playAll(pageType);});// ...}
在QQMusic中,給playAll信號關聯槽函數,并播放當前Page頁面的所有音樂。playAll槽函數中,根據pageType將當前page頁面記錄下來,默認從該頁面的第0首歌曲開始播放。注意不要忘記關聯信號槽。
// qqmusic.h 中新增
// 播放所有信號的槽函數
#include "commonpage.h"
void onPlayAll(PageType pageType);
void playAllOfCommonPage(CommonPage* commonPage, int index);
// qqmusic.cpp 中新增
void QQMusic::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::HOSTORY_PAGE:page = ui->recentPage;break;default:qDebug() << "擴展";}// 從當前??的零號位置開始播放playAllOfCommonPage(page, 0);
}
void QQMusic::playAllOfCommonPage(CommonPage* commonPage, int index)
{// 播放page所在??的?樂// 將播放列表先清空,否則?法播放當前CommonPage??的歌曲// 另外:該???樂不?定就在播放列表中,因此需要先將該???樂添加到播放列表playList->clear();// 將當前??歌曲添加到播放列表page->addMusicToPlayer(musicList, playList);// 設置當前播放列表的索引playList->setCurrentIndex(index);// 播放player->play();
}
void QQMusic::connectSignalAndSlots()
{// ...// 關聯播放所有的信號和槽函數connect(ui->likePage, &CommonPage::playAll, this, &QQMusic::onPlayAll);connect(ui->localPage, &CommonPage::playAll, this, &QQMusic::onPlayAll);connect(ui->recentPage, &CommonPage::playAll, this, &QQMusic::onPlayAll);
}
6、鼠標雙擊播放
當QListWidget中的項被雙擊時,會觸發doubleClicked信號,該信號在QListWidget的基類中定義,有一個index參數,表示被雙擊的QListWidgetItem在QListWidget中的索引I,該索引剛好與QMediaPlaylist中歌曲的所以一致,被雙擊時直接播放該首歌曲即可。
// CommonPage.h 中新增
signals:void playMusicByIndex(CommonPage*, int);// commonpage.cpp 中新增CommonPage::CommonPage(QWidget* parent) :QWidget(parent),ui(new Ui::CommonPage){// ...connect(ui->pageMusicList, &QListWidget::doubleClicked, this, [=](constQModelIndex& index) {// ?標雙擊后,發射信號告訴QQMusic,博能放this??中共被雙擊的歌曲emit playMusicByIndex(this, index.row());});}// qqmusic.h 中新增// CommonPage中playMusicByIndex信號對應槽函數void playMusicByIndex(CommonPage* page, int index);// qqmusic.cpp 中新增void QQMusic::playMusicByIndex(CommonPage* page, int index){playAllMusicOfCommonPage(page, index);}void QQMusic::connectSignalAndSlots(){// ...// 處理likePage、localPage、recentPage中ListItemBox雙擊connect(ui->likePage, &CommonPage::playMusicByIndex, this,&QQMusic::playMusicByIndex);connect(ui->localPage, &CommonPage::playMusicByIndex, this,&QQMusic::playMusicByIndex);connect(ui->recentPage, &CommonPage::playMusicByIndex, this,&QQMusic::playMusicByIndex);}
7、同步最近播放的歌曲
當播放歌曲改變時,即播放的媒體源發生了變化,QMediaPlayer會觸metaDataAvailableChanged信號,QMediaPlaylist也會觸發currentIndexChanged信號,該信號會帶index參數,表示現在是媒體播放列表中的index歌曲被播放,通過index可以獲取到recentPage頁面中具體播放的歌曲,將該歌曲對應Music對象的isHistoty屬性修改為true,然后更新下rencentPage的歌曲列表,播放過的歌曲就添加到歷史播放頁面中了。
問題:獲取likePage、localPage、recentPage哪個CommonPage頁面中的歌曲呢?
答案:QQMusic類中維護CommonPage*變量currentPage,記錄當前正在播放的CommonPage頁
面,初始時設置為localPage,當播放的頁面發生改變時,修改currentPage為當前正在播放頁面,其中點擊播放所有按鈕以及雙擊QListWidget中項的時候都回引起currentPage的改變。
// qqmusic.h 中新增
CommonPage* curPage;
// qqmusic.cpp 中修改
void QQMusic::initUi()
{// ...// 將localPage設置為當前??ui->stackedWidget->setCurrentIndex(4);currentPage = ui->localPage;// ...
}
void QQMusic::playAllOfCommonPage(CommonPage* commonPage, int index)
{currentPage = commonPage;// 播放page所在??的?樂// 將播放列表先清空,否則?法播放當前CommonPage??的歌曲// 另外:該???樂不?定就在播放列表中,因此需要先將該???樂添加到播放列表playList->clear();// ...
}
準備工作完成之后,同步最近播放歌曲的邏輯實現如下:
// qqmusic.h 中新增
// ?持播放歷史記錄
void onCurrentIndexChanged(int index);// qqmusic.cpp 中新增
void QQMusic::initPlayer(int index)
{// ...// 播放列表項發?改變,此時將播放?樂收藏到歷史記錄中connect(playList, &QMediaPlaylist::currentIndexChanged, this,&QQMusic::onCurrentIndexChanged);
}
void QQMusic::onCurrentIndexChanged(int index)
{// ?樂的id都在commonPage中的musicListOfPage中存儲著const QString& musicId = currentPage->getMusicIdByIndex(index);// 有了MusicId就可以再musicList中找到該?樂auto it = musicList.findMusicByMusicId(musicId);if (it != musicList.end()){// 將該?樂設置為歷史播放記錄it->setIsHistory(true);}ui->recentPage->reFresh(musicList);
}
// commmonpage.h 中新增
const QString& getMusicIdByIndex(int index) const;
// commonpage.cpp 中新增
QString CommonPage::getMusicIdByIndex(int index)
{if (index >= musicOfPage.size()){qDebug() << "?此歌曲";return "";}return musicOfPage[index];
}
8、音量設置
a、功能分析
當點擊靜音按鈕時,音量應該在靜音和非靜音之間進行切換,并且按鈕上圖標需要同步切換。
鼠標在滑竿上點擊或拖動滑竿時,應該跟進滑竿的高低比率,設置音量大小,同時修改界面音量比
率。
b. QMediaPlayer提供支持
QMediaPlayer中音量相關操作如下: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(boolnuted)函數,完成靜音和非靜音的設置。
由于VolumeTool不具備媒體播放控制,因此當靜音狀態發生改變時,發射設置靜音信號,讓
QQMusic來處理。?
// volumetool.h 中新增
signals:void setSilence(bool); // 設置靜?信號void onSilenceBtnClicked(); // 靜?按鈕槽函數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);}// qqMusic.h 中新增void setMusicSilence(bool isMuted);// qqmusic.cpp 中新增void QQMusic::setMusicSilence(bool isMuted){player->setMuted(isMuted);}void QQMusic::connectSignalAndSlots(){// ...// 設置靜?connect(volumeTool, &VolumeTool::setSilence, this,&QQMusic::setMusicSilence);}
d.鼠標按下、滾動以及釋放事件處理
當鼠標在滑竿上按下時,需要設置sliderBtn和outLine的位置,當鼠標在滑竿上移動或者鼠標抬起時,需要設置SliderBtnoutLine結束的位置,即改變VolumeTool中滑竿的顯示。具體修改播放媒體音量大小操作應該由于QQMusic負責處理,因此當鼠標移動或釋放時,需要發射信號讓QQMusic知道需要修改播放媒體的音量大小了。// volumetool.h 中新增 // 發射修改?量??槽函數 void setMusicVolume(int); // 事件過濾器 bool eventFilter(QObject* object, QEvent* event); // volumetool.cpp 中新增bool VolumeTool::eventFilter(QObject * object, QEvent * event) {// 過濾volumeBox上的事件if (object == ui->volumeBox){if (event->type() == QEvent::MouseButtonPress){// 如果是?標按下事件,修改sliderBtn和outLine的位置,并計算volumeRationsetVolume();}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) {// ...// 安裝事件過濾器ui->volumeBox->installEventFilter(this); }
e.outLine和SliderBtn以及volumeRation更新?
// volumetool.h 中新增
// 根據?標在滑竿上滑動更新滑動界?,并按照?例計算?量??
void setVolume();
// volumetool.cpp 中新增
void VolumeTool::setVolume()
{// 1. 將?標的位置轉換為sloderBox上的相對坐標,此處只要獲取y坐標int height = ui->volumeBox->mapFromGlobal(QCursor().pos()).y();// 2. ?標在volumeBox中可移動的y范圍在[25, 205之間]height = height < 25 ? 25 : height;height = height > 205 ? 205 : height;// 3. 調整sloderBt的位置ui->silderBtn->move(ui->silderBtn->x(), height - ui->silderBtn -> height() / 2);// 4. 更新outline的位置和??ui->outLine->setGeometry(ui->outLine->x(), height, ui->outLine->width(),205 - height);// 5. 計算?量?率volumeRatio = (int)((int)ui->outLine->height() / (float)180 * 100);// 6. 設置給label顯?出來ui->volumeRatio->setText(QString::number(volumeRatio) + "%");
}
f.QQMusic類攔截VolumeTool發射的setMusicVolume信號,將音量大小設置為指定值。
// qqmusic.h 中新增
void setPlayerVolume(int vomume); // 設置?量??
// qqmusic.cpp 中新增
void QQMusic::setPlayerVolume(int volume)
{player->setVolume(volume);
}
void QQMusic::connectSignalAndSlots()
{// ...// 設置?量??connect(volumeTool, &VolumeTool::setMusicVolume, this,&QQMusic::setPlayerVolume);
}
9、當前播放時間和總時間更新
a、界面歌曲總時間更新
歌曲總時間在Music對象中可以獲取,也可以讓player調用自己的duration()方法獲取。但是這兩種
獲取歌曲總時間的調用時機不太好確定。我們期望的是當歌曲發生切換時,獲取到正在播放歌曲的
總時長。當播放源的持續時長發生改變時,QMediaPlayer會觸發durationChanged信號,該信號中提供了將要播放媒體的總時長。因此在QQMusic類中給該信號關聯槽函數,在槽函數中將duration更新到界面總時間即可。
// qqmusic.h 中新增
// 歌曲持續時?改變時[歌曲切換]
void onDurationChanged(qint64 duration);
// qqmusic.cpp 中新增
void QQMusic::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 QQMusic::initPlayer()
{// ....// 媒體持續時?更新,即:?樂切換,時?更新,界?上時間也要更新connect(player, &QMediaPlayer::durationChanged, this,&QQMusic::onDurationChanged);
}
b、界面歌曲當前播放時間更新
媒體在持續播放過程中,QMediaPlayer會發射positionChanged,該信號帶有一個qint64類型參
數,表示媒體當前持續播放的時間。因此,在QQMusic中捕獲該信號,便可獲取到正在播放媒體的持續時間。
// qqmusic.h 中新增
// 播放位置改變,即持續播放時間改變
void onPositionChanged(qint64 duration);
// qqmusic.cpp 中新增
void QQMusic::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 QQMusic::initPlayer()
{// ....// 播放位置發?改變,即已經播放時間更新connect(player, &QMediaPlayer::positionChanged, this,&QQMusic::onPositionChanged);
}
10、進度條處理
a、seek功能介紹
播放器的seek功能指,通過時間或位置快速定位到視頻或音頻流的特定位置,允許用戶在播放過程中隨時跳轉到特定時間點,從而快速找到感興趣的內容或重新開始播放。
b、進度條界面顯示?
進度條功能進度界面展示與音量調節位置類似,攔截鼠標按下、鼠標移動、以及鼠標釋放消息即可。在內部捕獲到鼠標的位置的橫坐標x,將x作為outLine的寬度即可。即在鼠標按下、移動、釋放的時候,修改outLine的寬度即可。
// musicslider.h 中新增
void mousePressEvent(QMouseEvent * event); // 重寫?標按下事件
void mouseMoveEvent(QMouseEvent * event); // 重寫?標滾動事件
void mouseReleaseEvent(QMouseEvent * event); // 重寫?標釋放事件
void moveSilder(); // 修改outLine的寬度為currentPosprivate:int currentPos; // 滑動條當前位置// musicslider.cpp 中新增MusicSlider::MusicSlider(QWidget * parent) :QWidget(parent),ui(new Ui::MusicSlider){ui->setupUi(this);// 初始情況下,還沒有開始播放,將當前播放進度設置為0currentPos = 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);}
c、進度條同步持續播放時間
當鼠標釋放之后,計算出進度條當前位置currentPos和總寬度的maxWidth比率,然后發射信號告訴QQMusic,讓player按照該比率更新持續播放時間。
// musicslider.h 新增
signals:void setMusicSliderPosition(float);// musicslider.cpp 中新增void MusicSlider::mouseReleaseEvent(QMouseEvent* event){currentPos = event->pos().x();moveSilder();emit setMusicSliderPosition((float)currentPos / (float)maxWidth);}// qqmusic.h 中新增void onMusicSliderChanged(float value); // 進度條改變// qqmusic.cpp 中新增void QQMusic::onMusicSliderChanged(float value){// 1. 計算當前seek位置的時?qint64 duration = (qint64)(totalDuration * 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 QQMusic::connectSignalAndSlots(){// ...// 進度條拖拽connect(ui->progressBar, &MusicSlider::setMusicSliderPosition, this,&QQMusic::onMusicSliderChanged);}
d、持續時間同步進度條
當播放位置更新時,界面上持續播放時間一直在更新,因此進度條也需要持續向前進。MusicSlider應該提供setStep函數,播放進度持續更新時,也將進度條通過setStep函數更新下。
// musicslider.h 中新增
void setStep(float bf);
// musicslider.cpp 中新增
void MusicSlider::setStep(float bf)
{currentPos = maxWidth * bf;moveSilder();
}
// qqmusic.cpp 中修改
void QQMusic::onPositionChanged(qint64 duration)
{// 1. 更新已經播放時間ui->currentTime->setText(QString("%1:%2").arg(duration / 1000 / 60, 2, 10,QChar('0')).arg(duration / 1000 % 60, 2, 10,QChar('0')));// 2. 進度條處理ui->progressBar->setStep((float)duration / (float)totalDuration);
}
?e、歌曲名、歌手和封面時間同步
在進行歌曲切換時候,歌曲名稱、歌手以及歌曲的封面圖,也需要更新到界面。歌曲名稱、歌手可以再Music對象中進行獲取,歌曲的封面圖可以通過player到歌曲的元數據中獲取,獲取時需要使
用"Thumbnaillmage"作為參數,注意有些歌曲可能沒有封面圖,如果沒有設置一張默認的封面圖。
由于歌曲切換時,player需要將新播放歌曲作為播放源,并解析歌曲文件,如果歌曲文件是有效的才能播放;因此QQMusic類可以給QMediaPlayer發射的metaDataAvailableChanged(bool))信
號關聯槽函數,當歌曲更換時,完成信息的更新。
// qqmusic.h中新增
void onMetaDataAvailableChangedChanged(bool available)
// qqmusic.cpp 中新增
void QQMusic::onMetaDataAvailableChangedChanged(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));// 縮放填充到整個Labelui->musicCover->setScaledContents(true);currentPage->setImageLabel(QPixmap::fromImage(image));}else{// 設置默認圖?-修改qDebug() << "歌曲沒有封?圖?";}
}
void CommonForm::setImageLabel(QPixmap pixMap)
{ui->musicImgLabel->setPixmap(pixMap);ui->musicImgLabel->setScaledContents(true);
}
Lrl歌詞同步
1、界面分析
①和②為QLabel,分別顯示作者和歌曲名稱;
③~⑨均為QLabel,用來顯示歌詞,⑥為當前正在播放歌詞,③④⑤為當前播放歌詞的前三句,⑦⑧⑨為當前播放歌詞的后三句。歌詞會隨著播放時間持續,從下往上移動。
①為按鈕,點擊之后窗口隱藏。?
2、Lrc歌詞顯示?
在LrcPage的構造函數中,將窗口的標題欄去除掉;并給hideBtn關聯clicked信號,當按鈕點擊時將窗口隱藏。
// lrcPage.cpp 中添加
LyricsPage::LyricsPage(QWidget* parent) :QWidget(parent),ui(new Ui::LyricsPage)
{ui->setupUi(this);setWindowFlag(Qt::FramelessWindowHint);connect(ui->hideBtn, &QPushButton::clicked, this, [=] {hide();});ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));
}
在QQMusic中,創建LrcPage的指針,并在initUi)方法中創建窗口的對象,創建好之后將窗口隱藏起來;在QQMusic中,給IrcWord按鈕添加槽函數,在槽函數中將窗口顯示出來。
// qqmusic.h 中添加
#include "lrcpage.h"
LrcPage* lrcPage;
void onLrcWordClicked();
// qqmusic.cpp 中添加
void QQMusic::initUI()
{// ...// 創建lrc歌詞窗?lrcPage = new LrcPage(this);lrcPage->hide();
}
void QQMusic::onLrcWordClicked()
{lrcPage->show();
}
void QQMusic::connectSignalAndSlot()
{// ...// 顯?歌詞窗?connect(ui->lrcWord, &QPushButton::clicked, this,&QQMusic::onLrcWordClicked);
}
3、LcrPage添加動畫效果
a、上移動畫效果
①QQMusic的initUi函數中,創建IrcPage對象并將窗口隱藏;給IrcPage窗口添加上移動畫,動畫暫
不開啟
②QQMusic中給"歌詞"按鈕添加槽函數,當按鈕點擊時,顯示窗口,開啟動畫
// qqmusic.h 中新增
#include <QPropertyAnimation>
// 歌詞按鈕槽函數
void onLrcWordClicked();
private:QPropertyAnimation* lrcAnimation;// qqmusic.cpp 中新增void QQMusic::initUi(){// ...// 窗?添加陰影效果QGraphicsDropShadowEffect* shadowEffect = newQGraphicsDropShadowEffect(this);shadowEffect->setOffset(0, 0);shadowEffect->setColor("#000000"); // ??// 此處需要將圓?半徑不能太?,否則動畫效果有問題,可以設置為10shadowEffect->setBlurRadius(20);this->setGraphicsEffect(shadowEffect);// ...// 實例化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 QQMusic::onLrcWordClicked(){lrcPage->show();lrcAnimation->start();}void QQMusic::connectSignalAndSlots(){// ...// 歌詞按鈕點擊信號和槽函數connect(ui->lrcWord, &QPushButton::clicked, this,&QQMusic::onLrcWordClicked);// ...
b、隱藏窗口和下移動畫
LrcPage類中,在構造窗口時設置下移動畫,給"下拉"按鈕添加槽函數,當"下拉按鈕"點擊時,開啟動畫;當動畫結束時,將窗口隱藏。
// lrcpage.h 中新增
#include <QPropertyAnimation>
private:QPropertyAnimation* lrcAnimation;// lrcpage.cpp 中新增LrcPage::LrcPage(QWidget* parent) :QWidget(parent),ui(new Ui::LrcPage){ui->setupUi(this);// ... lrcAnimation = new QPropertyAnimation(this, "geometry", this);lrcAnimation->setDuration(250);lrcAnimation->setStartValue(QRect(10, 10, width(), height()));lrcAnimation->setEndValue(QRect(10, 10 + height(), width(), height()));// 點擊設置下拉按鈕時開啟動畫connect(ui->hideBtn, &QPushButton::clicked, this, [=] {lrcAnimation->start();});// 動畫結束時,將窗?隱藏connect(lrcAnimation, &QPropertyAnimation::finished, this, [=] {hide();});}
4、Lrc歌詞解析和同步
每首歌的Irc歌詞有多行文本,因此Irc歌詞中的每行可以采用結構體管理。
// lrcpage.h 中新增
struct LyricLine
{qint64 time; // 時間QString text; // 歌詞內容LyricLine(qint64 qtime, QString qtext): time(qtime), text(qtext){}
};
// LrcPage類中添加成員變量
QVector<LrcLine> lrcLines; // 按照時間的先后次序保存每?歌詞
a、通過歌名找lrc文件
一般情況下,播放器在設計之初就會設計好歌曲文件和歌詞文件的存放位置,以及對應關系,通常歌曲文件和Irc歌詞文件名字相同,后綴不同。在磁盤存放的時候,可以將歌曲文件和Irc文件分兩個文件夾存儲,也可以存儲到一個文件夾下。本文為了方便處理,存儲在一個文件夾下,因此可以通過Music對象快速找到Irc歌詞文件。
// music.h 中新增
QString getLrcFilePath() const;
// music.cpp 中新增
QString Music::getLrcFilePath() const
{// ?頻?件和LRC?件在?個?件夾下// 直接將?頻?件的后綴替換為.lrcQString path = musicUrl.toLocalFile();path.replace(".mp3", ".lrc");path.replace(".flac", ".lrc");path.replace(".mpga", ".lrc");return path;
}
b、歌詞解析
找到Irc歌詞文件后,由IrcPage類完成對歌詞的解析。解析的大概步驟:
①打開歌詞文件
②以行為單位,讀取歌詞文件中的每一行
③按照Irc歌詞文件格式,從每行文本中解析出時間和歌詞
[00:17.94]那些失眠的人啊你們還好嗎
[0:58.600.00]你像一只飛來飛去的蝴蝶
④用<時間,行歌詞>構建一個LrcLine對象存儲到IrcLines中。
// lrcpage.h 中新增
bool parseLrc(const QString& lrcPath);
// lrcpage.cpp 中新增
bool LrcPage::parseLrc(const QString& lrcPath)
{lrcLines.clear();// 打開歌詞?件QFile lrcFile(lrcPath);if (!lrcFile.open(QFile::ReadOnly)){qDebug() << "打開?件:" << lrcPath;return false;}while (!lrcFile.atEnd()){QString lrcWord = lrcFile.readLine(1024);// [00:17.94]那些失眠的?啊 你們還好嗎// [0:58.600.00]你像?只?來?去的蝴蝶int left = lrcWord.indexOf('[');int right = lrcWord.indexOf(']');// 解析時間qint64 lineTime = 0;int start = 0;int end = 0;QString time = lrcWord.mid(left, right - left + 1);// 解析分鐘start = 1;end = time.indexOf(':');lineTime += lrcWord.mid(start, end - start).toInt() * 60 * 1000;// 解析秒start = end + 1;end = time.indexOf('.', start);lineTime += lrcWord.mid(start, end - start).toInt() * 1000;// 解析毫秒start = end + 1;end = time.indexOf('.', start);lineTime += lrcWord.mid(start, end - start).toInt();// 解析歌詞QString word = lrcWord.mid(right + 1).trimmed();lrcLines.push_back(LrcLine(lineTime, word.trimmed()));}// 測試驗證for (auto word : lrcLines){qDebug() << word.time << " " << word.text;}return true;
}
c、根據歌曲播放位置獲取歌詞并顯示
當歌曲播放進度改變時候,QMediaPlayer的positionChanged信號會觸發,該信號同步播放時間的時候已經在QQMusic類中處理過了,在其槽函數中就能拿到當前歌曲的播放時間,通過播放時間,就能在LrcPage中找到對應行的歌詞。
// lrcpage.h 中新增
// lrcpage.cpp 中新增
int LrcPage::getLineLrcWordIndex(qint64 pos)
{// 如果歌詞是空的,返回-1if (lrcLines.isEmpty()){return -1;}if (lrcLines[0].time > pos){return 0;}// 通過時間?較,獲取下標for (int i = 1; i < lrcLines.size(); ++i){if (pos > lrcLines[i - 1].time && pos <= lrcLines[i].time){return i - 1;}}// 如果沒有找到,返回最后??return lrcLines.size() - 1;
}QString LrcPage::getLineLrcWord(qint64 index)
{if (index < 0 || index >= lrcLines.size()){return "";}return lrcLines[index].text;
}
void LrcPage::showLrcWord(int time)
{// 先要獲取歌詞--根據歌詞的時間進?獲取int index = getLineLrcWordIndex(time);if (-1 == index){ui->line1->setText("");ui->line2->setText("");ui->line3->setText("");ui->lineCenter->setText("當前歌曲?歌詞");ui->line4->setText("");ui->line5->setText("");ui->line6->setText("");}else{ui->line1->setText(getLineLrcWord(index - 3));ui->line2->setText(getLineLrcWord(index - 2));ui->line3->setText(getLineLrcWord(index - 1));ui->lineCenter->setText(getLineLrcWord(index));ui->line4->setText(getLineLrcWord(index + 1));ui->line5->setText(getLineLrcWord(index + 2));ui->line6->setText(getLineLrcWord(index + 3));}
}
d、Irc歌詞同步播放進度
當歌曲發生切換時,需要完成Irc歌詞文件的解析;
當歌曲播放進度發生改變時,根據歌曲的當前播放時間,通過IrcPage找到對應行歌詞并顯示出來。
// qqmusic.cpp 添加
void QQMusic::onMetaDataAvailableChanged(bool available)
{// 歌曲名稱、歌曲作者直接到Musci對象中獲取// 此時需要知道媒體源在播放列表中的索引QString musicId = currentPage->getMusicIdByIndex(currentIndex);auto it = musicList.findMusicByMusicId(musicId);// ...// 加載lrc歌詞并解析if (it != musicList.end()){lrcPage->parseLrc(it->getLrcFilePath());}
}
void QQMusic::onPositionChanged(qint64 position)
{// 1. 更新當前播放時間ui->currentTime->setText(QString("%1:%2").arg(position / 1000 / 60, 2, 10,QChar('0')).arg(position / 1000 % 60, 2, 10,QChar('0')));// 2. 更新進度條的位置ui->progressBar->setStep(position / (float)totalTime);// 3. 同步lrc歌詞if (playList->currentIndex() >= 0){lrcPage->showLrcWord(position);}
}
歌曲數據支持持久化
支持播放相關功能之后,每次在驗證功能時都需要從磁盤中加載歌曲文件,非常麻煩。而且之前收藏的喜歡歌曲以及播放記錄在程序關閉之后就沒有了,這一般是無法接受的。因此需要將每次在播放器上進行的操作保留下來,比如:所加載的歌曲、以及歌曲信息;收藏歌曲信息;歷史播放等信息保存起來,當下次程序啟動時,將保存的信息加載到播放器即可,這樣就能將在播放器上的操作記錄保留下來了。要永久性保存,最簡單的方式就是直接保存到文件,但是保存文件不安全,而且需要自己操作文件比較麻煩,本文采用數據庫完成信息的持久保存。
1、SQLite數據庫?
SQLite主要特征:
。管理簡單,甚至可以認為無需管理。
。操作方便,SQLite生成的數據庫文件可以在各個平臺無縫移植。
。可以非常方便的以多種形式嵌入到其他應用程序中,如靜態庫、動態庫等。
。易于維護。
2、QQMuic中數據庫支持?
a、數據庫初始化
// qqmusic.h 中新增
#include <QSqlDatabase>
QSqlDatabase sqlite;
// qqmusic.cpp 中新增
void QQMusic::initSQLite()
{// 1. 創建數據庫連接sqlite = QSqlDatabase::addDatabase("QSQLITE");// 2. 設置數據庫名稱sqlite.setDatabaseName("QQMusic.db");// 3. 打開數據庫if (!sqlite.open()){QMessageBox::critical(this, "打開QQMusicDB失敗",sqlite.lastError().text());return;}qDebug() << "SQLite連接成功,并創建 [QQMusic.db] 數據庫!!!";// 4. 創建數據庫表QString sql = ("CREATE TABLE IF NOT EXISTS musicInfo(\id INTEGER PRIMARY KEY AUTOINCREMENT,\musicId varchar(200) UNIQUE,\musicName varchar(50),\musicSinger varchar(50),\albumName varchar(50),\duration BIGINT,\musicUrl varchar(256),\isLike INTEGER,\isHistory INTEGER)");QSqlQuery query;if (!query.exec(sql)){QMessageBox::critical(this, "創建數據庫表失敗",query.lastError().text());return;}qDebug() << "創建 [musicInfo] 表成功!!!";
}
QQMusic::QQMusic(QWidget* parent): QWidget(parent), ui(new Ui::QQMusic), currentIndex(-1)
{ui->setupUi(this);initUi();// 初始化數據庫initSQLite();initPlayer();connectSignalAndSlots();
}
b、歌曲信息寫入數據庫
當程序退出的時候,通過musicList獲取到所有music對象,然后將music對象寫入數據庫。
// musiclist.h 中新增
// 所有歌曲信息更新到數據庫
void writeToDB();
// musiclist.cpp 中新增
void MusicList::writeToDB()
{for (auto music : musicList){// 讓music對象將??寫?數據庫music.insertMusicToDB();}
}
// music.h 中新增
// 將當前Music對象更新到數據庫
void insertMusicToDB();
// music.cpp 中新增
#include <QSqlQuery>
#include <QSqlError>
void Music::insertMusicToDB()
{// 1. 檢測music是否在數據庫中存在QSqlQuery query;// 當SELECT 1...查詢到結果后,我們需要知道是否存在// SELECT EXISTS(?查詢) : ?查詢中如果有記錄,SELECT EXISTS返回TRUE// 如果?查詢中沒有滿?條件的記錄, SELECT EXISTS返回FALSEquery.prepare("SELECT EXISTS (SELECT 1 FROM MusicInfo WHERE musicId = ?)");query.addBindValue(musicId);if (!query.exec()){qDebug() << "查詢失敗: " << query.lastError().text();return;}if (query.next()){bool isExists = query.value(0).toBool();if (isExists){// musicId的歌曲已經存在// 2. 存在:不需要再插?musci對象,此時只需要將isLike和isHistory屬性進?更新query.prepare("UPDATE MusicInfo SET isLike = ?, isHistory = ? WHERE musicId = ? ");query.addBindValue(isLike ? 1 : 0);query.addBindValue(isHistory ? 1 : 0);query.addBindValue(musicId);if (!query.exec()){qDebug() << "更新失敗: " << query.lastError().text();}qDebug() << "更新music信息: " << musicName << " " << musicId;}else{// 3. 不存在:直接將music的屬性信息插?數據庫query.prepare("INSERT INTO MusicInfo(musicId, musicName, musicSinger, albumName, musicUrl, \duration, isLike, isHistory)\VALUES(? , ? , ? , ? , ? , ? , ? , ? )");query.addBindValue(musicId);query.addBindValue(musicName);query.addBindValue(musicSinger);query.addBindValue(musicAlbumn);query.addBindValue(musicUrl.toLocalFile());query.addBindValue(duration);query.addBindValue(isLike ? 1 : 0);query.addBindValue(isHistory ? 1 : 0);if (!query.exec()){qDebug() << "插?失敗: " << query.lastError().text();return;}qDebug() << "插?music信息: " << musicName << " " << musicId;}}
}
// qqmusic.cpp 中新增
void QQMusic::on_quit_clicked()
{// 更新數據庫musicList.writeToDB();// 關閉數據庫連接sqlite.close();// 關閉窗?close();
}
?c、程序啟動時讀取數據庫恢復歌曲數據
在程序啟動時,從數據庫中讀取到歌曲的信息,將歌曲信息設置到musicList中,然后讓likePage、
localPage、recentPage將musicList中個歌曲更新到各自頁面中。從數據庫讀取歌曲數據的操作,應該讓MusicList類完成,因為該類管理所有的Music對象。
// musiclist.h 中新增
void readFromDB();
// musiclist.cpp 中新增
#include <QSqlQuery>
#include <QSqlError>
void MusicList::readFromDB()
{QString sql("SELECT musicId, musicName, musicSinger, albumName,\duration, musicUrl, isLike, isHistory \FROM musicInfo");QSqlQuery query;if (!query.exec(sql)){qDebug() << "數據庫查詢失敗";return;}while (query.next()){Music music;music.setMusicId(query.value(0).toString());music.setMusicName(query.value(1).toString());music.setMusicSinger(query.value(2).toString());music.setMusicAlbum(query.value(3).toString());music.setMusicDuration(query.value(4).toLongLong());music.setMusicUrl(query.value(5).toString());music.setIsLike(query.value(6).toBool());music.setIsHistory(query.value(7).toBool());musicList.push_back(music);}
}
// qqmusic.h 中新增
void initMusicList();
// qqmusic.cpp 中新增
void QQMusic::initMusicList()
{// 1. 從數據庫讀取歌曲信息musicList.readFromDB();// 2. 更新CommonPage??// 設置CommonPage的信息ui->likePage->setMusicListType(PageType::LIKE_PAGE);ui->likePage->reFresh(musicList);ui->localPage->setMusicListType(PageType::LOCAL_PAGE);ui->localPage->reFresh(musicList);ui->recentPage->setMusicListType(PageType::HISTORY_PAGE);ui->recentPage->reFresh(musicList);
}
QQMusic::QQMusic(QWidget* parent): QWidget(parent), ui(new Ui::QQMusic), currentIndex(-1)
{// ...// 初始化數據庫initSQLite();// 加載數據庫歌曲?件initMusicList();// ...
}
QQMusic中的initUi中將其去掉