Qt 项目实战 | 音乐播放器
Qt 项目实战 | 音乐播放器
- Qt 项目实战 | 音乐播放器
- 播放器整体架构
- 创建播放器主界面
- 媒体对象状态
- 实现播放列表
- 实现桌面歌词
- 添加系统托盘图标
- 资源下载
官方博客:https://www.yafeilinux.com/
Qt开源社区:https://www.qter.org/
参考书:《Qt 及 Qt Quick 开发实战精解》
Qt 项目实战 | 音乐播放器
开发环境:Qt Creator 3.3.0 + Qt 4.8.6
播放器整体架构
创建播放器主界面
新建 Qt Gui 应用,项目名 myPlayer,基类为 QWidget,类名为 MyWidget。
添加资源文件 myImages,前缀为空,将 images 中的所有图片都添加进去。
myPlayer.pro 添加代码:
QT += phonon
在 mywidget.h 添加头文件和类前置声明:
#include <phonon>class QLabel;
添加私有变量、函数:
Phonon::MediaObject *mediaObject;
QAction *playAction;
QAction *stopAction;
QAction *skipBackwardAction;
QAction *skipForwardAction;
QLabel *topLabel;
QLabel *timeLabel;void initPlayer();
添加私有槽:
private slots:void updateTime(qint64 time);void setPaused();void skipBackward();void skipForward();void openFile();void setPlaylistShown();void setLrcShown();
在 mywidget.cpp 中添加头文件:
#include <QLabel>
#include <QToolBar>
#include <QVBoxLayout>
#include <QTime>
在构造函数中添加代码:
initPlayer();
添加 initPlayer() 函数的定义:
// 初始化播放器
void MyWidget::initPlayer()
{// 设置主界面标题、图标和大小setWindowTitle(tr("音乐播放器"));setWindowIcon(QIcon(":/images/icon.png"));setMinimumSize(320, 160);setMaximumSize(320, 160);// 创建媒体图mediaObject = new Phonon::MediaObject(this);Phonon::AudioOutput* audioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, this);Phonon::createPath(mediaObject, audioOutput);// 关联媒体对象的tick()信号来更新播放时间的显示connect(mediaObject, SIGNAL(tick(qint64)), this, SLOT(updateTime(qint64)));// 创建顶部标签,用于显示一些信息topLabel = new QLabel(tr("<a href = \" https://blog.csdn.net/ProgramNovice \"> https://blog.csdn.net/ProgramNovice </a>"));topLabel->setTextFormat(Qt::RichText);topLabel->setOpenExternalLinks(true);topLabel->setAlignment(Qt::AlignCenter);// 创建控制播放进度的滑块Phonon::SeekSlider* seekSlider = new Phonon::SeekSlider(mediaObject, this);// 创建包含播放列表图标、显示时间标签和桌面歌词图标的工具栏QToolBar* widgetBar = new QToolBar(this);// 显示播放时间的标签timeLabel = new QLabel(tr("00:00 / 00:00"), this);timeLabel->setToolTip(tr("当前时间 / 总时间"));timeLabel->setAlignment(Qt::AlignCenter);timeLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);// 创建图标,用于控制是否显示播放列表QAction* PLAction = new QAction(tr("PL"), this);PLAction->setShortcut(QKeySequence("F4"));PLAction->setToolTip(tr("播放列表(F4)"));connect(PLAction, SIGNAL(triggered()), this, SLOT(setPlaylistShown()));// 创建图标,用于控制是否显示桌面歌词QAction* LRCAction = new QAction(tr("LRC"), this);LRCAction->setShortcut(QKeySequence("F2"));LRCAction->setToolTip(tr("桌面歌词(F2)"));connect(LRCAction, SIGNAL(triggered()), this, SLOT(setLrcShown()));// 添加到工具栏widgetBar->addAction(PLAction);widgetBar->addSeparator();widgetBar->addWidget(timeLabel);widgetBar->addSeparator();widgetBar->addAction(LRCAction);// 创建播放控制动作工具栏QToolBar* toolBar = new QToolBar(this);// 播放动作playAction = new QAction(this);playAction->setIcon(QIcon(":/images/play.png"));playAction->setText(tr("播放(F5)"));playAction->setShortcut(QKeySequence(tr("F5")));connect(playAction, SIGNAL(triggered()), this, SLOT(setPaused()));// 停止动作stopAction = new QAction(this);stopAction->setIcon(QIcon(":/images/stop.png"));stopAction->setText(tr("停止(F6)"));stopAction->setShortcut(QKeySequence(tr("F6")));connect(stopAction, SIGNAL(triggered()), mediaObject, SLOT(stop()));// 跳转到上一首动作skipBackwardAction = new QAction(this);skipBackwardAction->setIcon(QIcon(":/images/skipBackward.png"));skipBackwardAction->setText(tr("上一首(Ctrl+Left)"));skipBackwardAction->setShortcut(QKeySequence(tr("Ctrl+Left")));connect(skipBackwardAction, SIGNAL(triggered()), this, SLOT(skipBackward()));// 跳转到下一首动作skipForwardAction = new QAction(this);skipForwardAction->setIcon(QIcon(":/images/skipForward.png"));skipForwardAction->setText(tr("下一首(Ctrl+Right)"));skipForwardAction->setShortcut(QKeySequence(tr("Ctrl+Right")));connect(skipForwardAction, SIGNAL(triggered()), this, SLOT(skipForward()));// 打开文件动作QAction* openAction = new QAction(this);openAction->setIcon(QIcon(":/images/open.png"));openAction->setText(tr("播放文件(Ctrl+O)"));openAction->setShortcut(QKeySequence(tr("Ctrl+O")));connect(openAction, SIGNAL(triggered()), this, SLOT(openFile()));// 音量控制部件Phonon::VolumeSlider* volumeSlider = new Phonon::VolumeSlider(audioOutput, this);volumeSlider->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);// 添加到工具栏toolBar->addAction(playAction);toolBar->addSeparator();toolBar->addAction(stopAction);toolBar->addSeparator();toolBar->addAction(skipBackwardAction);toolBar->addSeparator();toolBar->addAction(skipForwardAction);toolBar->addSeparator();toolBar->addWidget(volumeSlider);toolBar->addSeparator();toolBar->addAction(openAction);// 创建主界面布局管理器QVBoxLayout* mainLayout = new QVBoxLayout;mainLayout->addWidget(topLabel);mainLayout->addWidget(seekSlider);mainLayout->addWidget(widgetBar);mainLayout->addWidget(toolBar);setLayout(mainLayout);mediaObject->setCurrentSource(Phonon::MediaSource("../myPlayer/music.mp3"));
}
添加 updateTime(qint64 time) 槽的定义:
// 更新 timeLabel 标签显示的播放时间
void MyWidget::updateTime(qint64 time)
{qint64 totalTimeValue = mediaObject->totalTime();QTime totalTime(0, (totalTimeValue / 60000) % 60, (totalTimeValue / 1000) % 60);QTime currentTime(0, (time / 60000) % 60, (time / 1000) % 60);QString str = currentTime.toString("mm:ss") + " / " + totalTime.toString("mm:ss");timeLabel->setText(str);
}
添加 setPaused() 槽的定义:
// 播放或者暂停
void MyWidget::setPaused()
{// 如果先前处于播放状态,那么暂停播放;否则,开始播放if (mediaObject->state() == Phonon::PlayingState)mediaObject->pause();elsemediaObject->play();
}
测试:
媒体对象状态
Phonon 将媒体文件的播放划分为以下状态:
在 mywidget.h 中添加私有槽声明:
void stateChanged(Phonon::State newState, Phonon::State oldState);
在 mywidget.cpp 中添加头文件:
#include <QMessageBox>
#include <QFileInfo>
在 initPlayer() 函数的最后添加代码:
connect(mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)), this,SLOT(stateChanged(Phonon::State, Phonon::State)));
添加 stateChanged() 槽定义:
// 媒体对象状态发生了改变
void MyWidget::stateChanged(Phonon::State newState, Phonon::State oldState)
{switch (newState){case Phonon::ErrorState:if (mediaObject->errorType() == Phonon::FatalError){QMessageBox::warning(this, tr("致命错误"), mediaObject->errorString());}else{QMessageBox::warning(this, tr("错误"), mediaObject->errorString());}break;case Phonon::PlayingState:stopAction->setEnabled(true);playAction->setIcon(QIcon(":/images/pause.png"));playAction->setText(tr("暂停(F5)"));topLabel->setText(QFileInfo(mediaObject->currentSource().fileName()).baseName());break;case Phonon::StoppedState:stopAction->setEnabled(false);playAction->setIcon(QIcon(":/images/play.png"));playAction->setText(tr("播放(F5)"));topLabel->setText(tr("<a href = \" https://blog.csdn.net/ProgramNovice \">""https://blog.csdn.net/ProgramNovice</a>"));timeLabel->setText(tr("00:00 / 00:00"));break;case Phonon::PausedState:stopAction->setEnabled(true);playAction->setIcon(QIcon(":/images/play.png"));playAction->setText(tr("播放(F5)"));topLabel->setText(QFileInfo(mediaObject->currentSource().fileName()).baseName() + tr(" 已暂停!"));break;case Phonon::BufferingState: break;default:;}
}
测试:
实现播放列表
添加 C++ 类,类名为 MyPlaylist,基类为 QTableWidget,继承自 QWidget。
更改 myplaylist.h:
#ifndef MYPLAYLIST_H
#define MYPLAYLIST_H#include <QTableWidget>
#include <QWidget>class MyPlaylist : public QTableWidget
{Q_OBJECT
protected:void contextMenuEvent(QContextMenuEvent* event);void closeEvent(QCloseEvent* event);public:explicit MyPlaylist(QWidget* parent = 0);signals:void playlistClean();private slots:void clearPlaylist();
};#endif // MYPLAYLIST_H
更改 myplaylist.cpp:
#include "myplaylist.h"#include <QContextMenuEvent>
#include <QMenu>// 上下文菜单事件处理函数,当点击鼠标右键时运行一个菜单
void MyPlaylist::contextMenuEvent(QContextMenuEvent* event)
{QMenu menu;menu.addAction(tr("清空列表"), this, SLOT(clearPlaylist()));menu.exec(event->globalPos());
}// 关闭事件处理函数,如果部件处于显示状态,则使其隐藏
void MyPlaylist::closeEvent(QCloseEvent* event)
{if (isVisible()){hide();event->ignore();}
}MyPlaylist::MyPlaylist(QWidget* parent) : QTableWidget(parent)
{setWindowTitle(tr("播放列表"));// 设置窗口标志,表明它是一个独立窗口且有一个只带有关闭按钮的标题栏setWindowFlags(Qt::Window | Qt::WindowTitleHint);// 设置初始大小,并且锁定部件宽度resize(320, 400);setMaximumWidth(320);setMinimumWidth(320);// 设置行列数目setRowCount(0);setColumnCount(3);// 设置表头标签QStringList list;list << tr("标题") << tr("艺术家") << tr("长度");setHorizontalHeaderLabels(list);// 设置只能选择单行setSelectionMode(QAbstractItemView::SingleSelection);setSelectionBehavior(QAbstractItemView::SelectRows);// 设置不显示网格setShowGrid(false);
}// 清空播放列表
void MyPlaylist::clearPlaylist()
{while (rowCount())removeRow(0);// 发射播放列表已清空信号emit playlistClean();
}
可以使用 MediaObject 的 metaData() 函数获取媒体源中的元数据。其中包含的信息如下所示:
在 mywidget.h 添加类声明:
class MyPlaylist;
添加私有变量、函数声明:
MyPlaylist *playlist;
Phonon::MediaObject *metaInformationResolver;
QList<Phonon::MediaSource> sources;
void changeActionState();
添加私有槽声明:
void sourceChanged(const Phonon::MediaSource &source);
void aboutToFinish();
void metaStateChanged(Phonon::State newState, Phonon::State oldState);
void tableClicked(int row);
void clearSources();
在 mywidget.cpp 添加头文件:
#include "myplaylist.h"
#include <QFileDialog>
#include <QDesktopServices>
在 initPlayer() 函数中,删除代码:
mediaObject->setCurrentSource(Phonon::MediaSource("../myPlayer/music.mp3"));
添加代码:
// 创建播放列表
playlist = new MyPlaylist(this);
connect(playlist, SIGNAL(cellClicked(int, int)), this, SLOT(tableClicked(int)));
connect(playlist, SIGNAL(playlistClean()), this, SLOT(clearSources()));// 创建用来解析媒体的信息的元信息解析器
metaInformationResolver = new Phonon::MediaObject(this);
// 需要与AudioOutput连接后才能使用metaInformationResolver来获取歌曲的总时间
Phonon::AudioOutput *metaInformationAudioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, this);
Phonon::createPath(metaInformationResolver, metaInformationAudioOutput);
connect(metaInformationResolver, SIGNAL(stateChanged(Phonon::State, Phonon::State)), this,SLOT(metaStateChanged(Phonon::State, Phonon::State)));connect(mediaObject, SIGNAL(currentSourceChanged(Phonon::MediaSource)), this,SLOT(sourceChanged(Phonon::MediaSource)));
connect(mediaObject, SIGNAL(aboutToFinish()), this, SLOT(aboutToFinish()));// 初始化动作图标的状态
playAction->setEnabled(false);
stopAction->setEnabled(false);
skipBackwardAction->setEnabled(false);
skipForwardAction->setEnabled(false);
topLabel->setFocus();
更改一系列槽的定义:
// 播放上一首,与 skipBackwardAction 动作的触发信号关联
void MyWidget::skipBackward()
{int index = sources.indexOf(mediaObject->currentSource());mediaObject->setCurrentSource(sources.at(index - 1));mediaObject->play();
}// 播放下一首,与 skipForwardAction 动作的触发信号关联
void MyWidget::skipForward()
{int index = sources.indexOf(mediaObject->currentSource());mediaObject->setCurrentSource(sources.at(index + 1));mediaObject->play();
}// 打开文件,与 openAction 动作的触发信号关联
void MyWidget::openFile()
{// 从系统音乐目录打开多个音乐文件QStringList list = QFileDialog::getOpenFileNames(this, tr("打开音乐文件"), QDesktopServices::storageLocation(QDesktopServices::MusicLocation));if (list.isEmpty())return;// 获取当前媒体源列表的大小int index = sources.size();// 将打开的音乐文件添加到媒体源列表后foreach (QString string, list){Phonon::MediaSource source(string);sources.append(source);}// 如果媒体源列表不为空,则将新加入的第一个媒体源作为当前媒体源,// 这时会发射 stateChanged() 信号,从而调用 metaStateChanged() 函数进行媒体源的解析if (!sources.isEmpty())metaInformationResolver->setCurrentSource(sources.at(index));
}// 显示或者隐藏播放列表,与 PLAction 动作的触发信号关联
void MyWidget::setPlaylistShown()
{if (playlist->isHidden()){playlist->move(frameGeometry().bottomLeft());playlist->show();}else{playlist->hide();}
}// 解析媒体文件的元信息
void MyWidget::metaStateChanged(Phonon::State newState, Phonon::State oldState)
{// 错误状态,则从媒体源列表中除去新添加的媒体源if (newState == Phonon::ErrorState){QMessageBox::warning(this, tr("打开文件时出错"), metaInformationResolver->errorString());while (!sources.isEmpty() && !(sources.takeLast() == metaInformationResolver->currentSource())) {};return;}// 如果既不处于停止状态也不处于暂停状态,则直接返回if (newState != Phonon::StoppedState && newState != Phonon::PausedState)return;// 如果媒体源类型错误,则直接返回if (metaInformationResolver->currentSource().type() == Phonon::MediaSource::Invalid)return;// 获取媒体信息QMap<QString, QString> metaData = metaInformationResolver->metaData();// 获取标题,如果为空,则使用文件名QString title = metaData.value("TITLE");if (title == ""){QString str = metaInformationResolver->currentSource().fileName();title = QFileInfo(str).baseName();}QTableWidgetItem* titleItem = new QTableWidgetItem(title);// 设置数据项不可编辑titleItem->setFlags(titleItem->flags() ^ Qt::ItemIsEditable);// 获取艺术家信息QTableWidgetItem* artistItem = new QTableWidgetItem(metaData.value("ARTIST"));artistItem->setFlags(artistItem->flags() ^ Qt::ItemIsEditable);// 获取总时间信息qint64 totalTime = metaInformationResolver->totalTime();QTime time(0, (totalTime / 60000) % 60, (totalTime / 1000) % 60);QTableWidgetItem* timeItem = new QTableWidgetItem(time.toString("mm:ss"));// 插入到播放列表int currentRow = playlist->rowCount();playlist->insertRow(currentRow);playlist->setItem(currentRow, 0, titleItem);playlist->setItem(currentRow, 1, artistItem);playlist->setItem(currentRow, 2, timeItem);// 如果添加的媒体源还没有解析完,那么继续解析下一个媒体源int index = sources.indexOf(metaInformationResolver->currentSource()) + 1;if (sources.size() > index){metaInformationResolver->setCurrentSource(sources.at(index));}else{ // 如果所有媒体源都已经解析完成// 如果播放列表中没有选中的行if (playlist->selectedItems().isEmpty()){// 如果现在没有播放歌曲则设置第一个媒体源为媒体对象的当前媒体源//(因为可能正在播放歌曲时清空了播放列表,然后又添加了新的列表)if (mediaObject->state() != Phonon::PlayingState && mediaObject->state() != Phonon::PausedState){mediaObject->setCurrentSource(sources.at(0));}else{//如果正在播放歌曲,则选中播放列表的第一个曲目,并更改图标状态playlist->selectRow(0);changeActionState();}}else{// 如果播放列表中有选中的行,那么直接更新图标状态changeActionState();}}
}
添加 changeActionState() 函数的定义:
// 根据媒体源列表内容和当前媒体源的位置来改变主界面图标的状态
void MyWidget::changeActionState()
{// 如果媒体源列表为空if (sources.count() == 0){// 如果没有在播放歌曲,则播放和停止按钮都不可用//(因为可能歌曲正在播放时清除了播放列表)if (mediaObject->state() != Phonon::PlayingState && mediaObject->state() != Phonon::PausedState){playAction->setEnabled(false);stopAction->setEnabled(false);}skipBackwardAction->setEnabled(false);skipForwardAction->setEnabled(false);}else{ // 如果媒体源列表不为空playAction->setEnabled(true);stopAction->setEnabled(true);// 如果媒体源列表只有一行if (sources.count() == 1){skipBackwardAction->setEnabled(false);skipForwardAction->setEnabled(false);}else{ // 如果媒体源列表有多行skipBackwardAction->setEnabled(true);skipForwardAction->setEnabled(true);int index = playlist->currentRow();// 如果播放列表当前选中的行为第一行if (index == 0)skipBackwardAction->setEnabled(false);// 如果播放列表当前选中的行为最后一行if (index + 1 == sources.count())skipForwardAction->setEnabled(false);}}
}
添加 sourceChanged() 槽定义:
// 当媒体源改变时,在播放列表中选中相应的行并更新图标的状态
void MyWidget::sourceChanged(const Phonon::MediaSource &source)
{int index = sources.indexOf(source);playlist->selectRow(index);changeActionState();
}
添加 aboutToFinish() 槽定义:
// 当前媒体源播放将要结束时,如果在列表中当前媒体源的后面还有媒体源,那么将它添加到播放队列中,否则停止播放
void MyWidget::aboutToFinish()
{int index = sources.indexOf(mediaObject->currentSource()) + 1;if (sources.size() > index){mediaObject->enqueue(sources.at(index));// 跳转到歌曲最后mediaObject->seek(mediaObject->totalTime());}else{mediaObject->stop();}
}
添加 tableClicked() 槽定义:
// 单击播放列表
void MyWidget::tableClicked(int row)
{// 首先获取媒体对象当前的状态,然后停止播放并清空播放队列bool wasPlaying = mediaObject->state() == Phonon::PlayingState;mediaObject->stop();mediaObject->clearQueue();// 如果单击的播放列表中的行号大于媒体源列表的大小,则直接返回if (row >= sources.size())return;// 设置单击的行对应的媒体源为媒体对象的当前媒体源mediaObject->setCurrentSource(sources.at(row));// 如果以前媒体对象处于播放状态,那么开始播放选中的曲目if (wasPlaying)mediaObject->play();
}
添加 clearSources() 槽定义:
// 清空媒体源列表,它与播放列表的playListClean()信号关联
void MyWidget::clearSources()
{sources.clear();// 更改动作图标状态changeActionState();
}
测试:
实现桌面歌词
LRC 歌词文件的内容:
添加 C++ 类,类名为 MyLrc,基类为 QLabel,继承自 QWidget。
更改 mylrc.h:
#ifndef MYLRC_H
#define MYLRC_H#include <QLabel>
#include <QWidget>class QTimer;class MyLrc : public QLabel
{Q_OBJECT
private:QLinearGradient linearGradient;QLinearGradient maskLinearGradient;QFont font;QTimer* timer;qreal lrcMaskWidth;// 每次歌词遮罩增加的宽度qreal lrcMaskWidthInterval;QPoint offset;protected:void paintEvent(QPaintEvent*);void mousePressEvent(QMouseEvent* event);void mouseMoveEvent(QMouseEvent* event);void contextMenuEvent(QContextMenuEvent* event);public:explicit MyLrc(QWidget* parent = 0);void startLrcMask(qint64 intervalTime);void stopLrcMask();private slots:void timeout();
};#endif // MYLRC_H
更改 mylrc.cpp:
#include "mylrc.h"#include <QContextMenuEvent>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QTimer>void MyLrc::paintEvent(QPaintEvent*)
{QPainter painter(this);painter.setFont(font);// 先绘制底层文字,作为阴影,这样会使显示效果更加清晰,且更有质感painter.setPen(QColor(0, 0, 0, 200));painter.drawText(1, 1, 800, 60, Qt::AlignLeft, text());// 再在上面绘制渐变文字painter.setPen(QPen(linearGradient, 0));painter.drawText(0, 0, 800, 60, Qt::AlignLeft, text());// 设置歌词遮罩painter.setPen(QPen(maskLinearGradient, 0));painter.drawText(0, 0, lrcMaskWidth, 60, Qt::AlignLeft, text());
}// 两个鼠标事件处理函数实现了部件的拖动
void MyLrc::mousePressEvent(QMouseEvent* event)
{if (event->button() == Qt::LeftButton)offset = event->globalPos() - frameGeometry().topLeft();
}void MyLrc::mouseMoveEvent(QMouseEvent* event)
{if (event->buttons() & Qt::LeftButton){setCursor(Qt::PointingHandCursor);move(event->globalPos() - offset);}
}// 实现右键菜单来隐藏部件
void MyLrc::contextMenuEvent(QContextMenuEvent* event)
{QMenu menu;menu.addAction(tr("隐藏"), this, SLOT(hide()));menu.exec(event->globalPos());
}MyLrc::MyLrc(QWidget* parent) : QLabel(parent)
{setWindowFlags(Qt::Window | Qt::FramelessWindowHint);// 设置背景透明setAttribute(Qt::WA_TranslucentBackground);setText(tr("MyPlayer音乐播放器 --- yafeilinux作品"));// 固定部件大小setMaximumSize(800, 60);setMinimumSize(800, 60);// 歌词的线性渐变填充linearGradient.setStart(0, 10);linearGradient.setFinalStop(0, 40);linearGradient.setColorAt(0.1, QColor(14, 179, 255));linearGradient.setColorAt(0.5, QColor(114, 232, 255));linearGradient.setColorAt(0.9, QColor(14, 179, 255));// 遮罩的线性渐变填充maskLinearGradient.setStart(0, 10);maskLinearGradient.setFinalStop(0, 40);maskLinearGradient.setColorAt(0.1, QColor(222, 54, 4));maskLinearGradient.setColorAt(0.5, QColor(255, 72, 16));maskLinearGradient.setColorAt(0.9, QColor(222, 54, 4));// 设置字体font.setFamily("Times New Roman");font.setBold(true);font.setPointSize(30);// 设置定时器timer = new QTimer(this);connect(timer, SIGNAL(timeout()), this, SLOT(timeout()));lrcMaskWidth = 0;lrcMaskWidthInterval = 0;
}// 开启遮罩,需要指定当前歌词开始与结束之间的时间间隔
void MyLrc::startLrcMask(qint64 intervalTime)
{// 这里设置每隔30毫秒更新一次遮罩的宽度,因为如果更新太频繁// 会增加CPU占用率,而如果时间间隔太大,则动画效果就不流畅了qreal count = intervalTime / 30;// 获取遮罩每次需要增加的宽度,这里的800是部件的固定宽度lrcMaskWidthInterval = 800 / count;lrcMaskWidth = 0;timer->start(30);
}// 停止遮罩
void MyLrc::stopLrcMask()
{timer->stop();lrcMaskWidth = 0;update();
}// 定时器溢出时增加遮罩的宽度,并更新显示
void MyLrc::timeout()
{lrcMaskWidth += lrcMaskWidthInterval;update();
}
在 mywidget.h 添加类声明:
class MyLrc;
添加私有变量、函数:
MyLrc *lrc;
QMap<qint64, QString> lrcMap;
void resolveLrc(const QString &sourceFileName);
在 mywidget.cpp 添加头文件:
#include "mylrc.h"
#include <QTextCodec>
在 initPlayer() 函数的最后添加代码:
// 创建歌词部件
lrc = new MyLrc(this);
更改 setLrcShown() 槽定义:
// 显示或者隐藏桌面歌词,与 LRCAction 动作的触发信号关联
void MyWidget::setLrcShown()
{if (lrc->isHidden())lrc->show();elselrc->hide();
}
添加 resolveLrc() 槽定义:
// 解析LRC歌词,在stateChanged()函数的Phonon::PlayingState处和aboutToFinish()函数中调用了该函数
void MyWidget::resolveLrc(const QString& sourceFileName)
{// 先清空以前的内容lrcMap.clear();// 获取LRC歌词的文件名if (sourceFileName.isEmpty())return;QString fileName = sourceFileName;QString lrcFileName = fileName.remove(fileName.right(3)) + "lrc";// 打开歌词文件QFile file(lrcFileName);if (!file.open(QIODevice::ReadOnly)){lrc->setText(QFileInfo(mediaObject->currentSource().fileName()).baseName() + tr(" --- 未找到歌词文件!"));return;}// 设置字符串编码QTextCodec::setCodecForCStrings(QTextCodec::codecForLocale());// 获取全部歌词信息QString allText = QString(file.readAll());// 关闭歌词文件file.close();// 将歌词按行分解为歌词列表QStringList lines = allText.split("\n");// 使用正则表达式将时间标签和歌词内容分离QRegExp rx("\\[\\d{2}:\\d{2}\\.\\d{2}\\]");foreach (QString oneLine, lines){// 先在当前行的歌词的备份中将时间内容清除,这样就获得了歌词文本QString temp = oneLine;temp.replace(rx, "");// 然后依次获取当前行中的所有时间标签,并分别与歌词文本存入QMap中int pos = rx.indexIn(oneLine, 0);while (pos != -1){QString cap = rx.cap(0);// 将时间标签转换为时间数值,以毫秒为单位QRegExp regexp;regexp.setPattern("\\d{2}(?=:)");regexp.indexIn(cap);int minute = regexp.cap(0).toInt();regexp.setPattern("\\d{2}(?=\\.)");regexp.indexIn(cap);int second = regexp.cap(0).toInt();regexp.setPattern("\\d{2}(?=\\])");regexp.indexIn(cap);int millisecond = regexp.cap(0).toInt();qint64 totalTime = minute * 60000 + second * 1000 + millisecond * 10;// 插入到lrcMap中lrcMap.insert(totalTime, temp);pos += rx.matchedLength();pos = rx.indexIn(oneLine, pos);}}// 如果lrcMap为空if (lrcMap.isEmpty()){lrc->setText(QFileInfo(mediaObject->currentSource().fileName()).baseName() + tr(" --- 歌词文件内容错误!"));return;}
}
在 updateTime() 函数的最后添加代码:
// 获取当期时间对应的歌词
if (!lrcMap.isEmpty())
{// 获取当前时间在歌词中的前后两个时间点qint64 previous = 0;qint64 later = 0;foreach (qint64 value, lrcMap.keys()){if (time >= value){previous = value;}else{later = value;break;}}// 达到最后一行,将later设置为歌曲总时间的值if (later == 0)later = totalTimeValue;// 获取当前时间所对应的歌词内容QString currentLrc = lrcMap.value(previous);// 没有内容时if (currentLrc.length() < 2)currentLrc = tr("音乐播放器 --- UesucXiye作品");// 如果是新的一行歌词,那么重新开始显示歌词遮罩if (currentLrc != lrc->text()){lrc->setText(currentLrc);topLabel->setText(currentLrc);qint64 intervalTime = later - previous;lrc->startLrcMask(intervalTime);}
}
else
{ // 如果没有歌词文件,则在顶部标签中显示歌曲标题topLabel->setText(QFileInfo(mediaObject->currentSource().fileName()).baseName());
}
在 stateChanged() 函数的 Phonon::PlayingState 状态中的 break 前添加:
//解析当前媒体源对应的歌词文件
resolveLrc(mediaObject->currentSource().fileName());
在 aboutToFinish() 函数中 mediaObject->seek(mediaObject->totalTime());
后添加代码:
// 停止歌词遮罩并重新解析歌词
lrc->stopLrcMask();
resolveLrc(sources.at(index).fileName());
在 skipBackward() 和 skipForward() 函数的最开始添加代码:
lrc->stopLrcMask();
在 stateChanged() 函数的 Phonon::StoppedState 状态中的 break 前添加:
// 停止歌词遮罩
lrc->stopLrcMask();
lrc->setText(tr("音乐播放器 --- UestcXiye作品"));
在 stateChanged() 函数的 Phonon::PausedState 状态中的 break 前添加:
// 如果该歌曲有歌词文件
if (!lrcMap.isEmpty())
{lrc->stopLrcMask();lrc->setText(topLabel->text());
}
测试:
注意,这里我做了 2 个修改:
- 将 我是明星.lrc 的编码格式改为 UTF-8,把英文逗号都换成了中文逗号。
- 测试中发现桌面歌词部件的长度太短,将 mylrc.cpp 中的构造函数中部件的长度由 800 改成 1000。
添加系统托盘图标
Qt 中提供了 QSystemTrayIcon 类来实现系统托盘图标。
在 mywidget.h 添加头文件、私有槽声明、私有变量:
#include <QSystemTrayIcon>
void trayIconActivated(QSystemTrayIcon::ActivationReason activationReason);
QSystemTrayIcon* trayIcon;
添加关闭事件处理函数的声明:
protected:void closeEvent(QCloseEvent *event);
在 mywidget.cpp 添加头文件:
#include <QMenu>
#include <QCloseEvent>
在 initPlayer() 函数的最后添加代码:
// 创建系统托盘图标
trayIcon = new QSystemTrayIcon(QIcon(":/images/icon.png"), this);
trayIcon->setToolTip(tr("音乐播放器 --- UestcXiye作品"));
// 创建菜单
QMenu *menu = new QMenu;
QList<QAction *> actions;
actions << playAction << stopAction << skipBackwardAction << skipForwardAction;
menu->addActions(actions);
menu->addSeparator();
menu->addAction(PLAction);
menu->addAction(LRCAction);
menu->addSeparator();
menu->addAction(tr("退出"), qApp, SLOT(quit()));
trayIcon->setContextMenu(menu);
// 托盘图标被激活后进行处理
connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this,SLOT(trayIconActivated(QSystemTrayIcon::ActivationReason)));
// 显示托盘图标
trayIcon->show();
添加 trayIconActivated() 槽的定义:
// 系统托盘图标被激活
void MyWidget::trayIconActivated(QSystemTrayIcon::ActivationReason activationReason)
{// 如果单击了系统托盘图标,则显示应用程序界面if (activationReason == QSystemTrayIcon::Trigger){show();}
}
添加关闭事件处理函数的定义:
// 关闭事件处理函数
void MyWidget::closeEvent(QCloseEvent* event)
{if (isVisible()){hide();trayIcon->showMessage(tr("音乐播放器"), tr("点我重新显示主界面"));event->ignore();}
}
测试:
资源下载
GitHub:基于 Qt4 的 Phonon 模块实现的音乐播放器
CSDN:Qt 项目实战 | 音乐播放器
相关文章:

Qt 项目实战 | 音乐播放器
Qt 项目实战 | 音乐播放器 Qt 项目实战 | 音乐播放器播放器整体架构创建播放器主界面媒体对象状态实现播放列表实现桌面歌词添加系统托盘图标 资源下载 官方博客:https://www.yafeilinux.com/ Qt开源社区:https://www.qter.org/ 参考书:《Q…...

JavaScript使用Ajax
Ajax(Asynchronous JavaScript and XML)是使用JavaScript脚本,借助XMLHttpRequest插件,在客户端与服务器端之间实现异步通信的一种方法。2005年2月,Ajax第一次正式出现,从此以后Ajax成为JavaScript发起HTTP异步请求的代名词。2006…...

Python爬虫实战-批量爬取美女图片网下载图片
大家好,我是python222小锋老师。 近日锋哥又卷了一波Python实战课程-批量爬取美女图片网下载图片,主要是巩固下Python爬虫基础 视频版教程: Python爬虫实战-批量爬取美女图片网下载图片 视频教程_哔哩哔哩_bilibiliPython爬虫实战-批量爬取…...

uniapp+uview2.0+vuex实现自定义tabbar组件
效果图 1.在components文件夹中新建MyTabbar组件 2.组件代码 <template><view class"myTabbarBox" :style"{ backgroundColor: backgroundColor }"><u-tabbar :placeholder"true" zIndex"0" :value"MyTabbarS…...
opencv 任意两点切割图像
目录 opencv python直线切割图像,把图像分为两个多边形 升级版,把多边形分割抠图出来,取最小外接矩形:...

rust变量绑定、拷贝、转移、引用
目录 一,clone、copy 1,基本类型 2,类型的clone特征 3,显式声明结构体的clone特征 4,类型的copy特征 5,显式声明结构体的clone特征 5,变量和字面量的特征 6,特征总结 二&am…...
Java多种方式向图片添加自定义水印、图片转换及webp图片压缩
给个创建水印的示例: /*** 获取水印** param watermarkText 水印文字* return 水印bufferimage*/public static BufferedImage getWatermark(String watermarkText) {BufferedImage measureBufferdImage new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB…...
基于Pytorch框架的LSTM算法(二)——多维度单步预测
1.项目说明 **选用Close和Low两个特征,使用窗口time_steps窗口的2个特征,然后预测Close这一个特征数据未来一天的数据 当batch_firstTrue,则LSTM的inputs(batch_size,time_steps,input_size) batch_size len(data)-time_steps time_steps 滑动窗口&…...
cnn感受野计算方法
No. Layers Kernel Size Stride 1 Conv1 33 1 2 Pool1 22 2 3 Conv2 33 1 4 Pool2 22 2 5 Conv3 33 1 6 Conv4 33 1 7 Pool3 2*2 2 感受野初始值 l 0 1 l_0 1l 0 1,每层的感受野计算过程如下: l 0 1 l_0 1l 0 1 l 1 1 ( 3 − 1 ) 3 l_1 1…...

百分点科技受邀参加“第五届治理现代化论坛”
11月4日,由北京大学政府管理学院主办的“面向新时代的人才培养——第五届治理现代化论坛”举行,北京大学校党委常委、副校长、教务长王博,政府管理学院院长燕继荣参加开幕式并致辞,百分点科技董事长兼CEO苏萌受邀出席论坛…...

基于Springboot的智慧食堂设计与实现(有报告)。Javaee项目,springboot项目。
演示视频: 基于Springboot的智慧食堂设计与实现(有报告)。Javaee项目,springboot项目。 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 项…...

「Verilog学习笔记」多功能数据处理器
专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点,刷题网站用的是牛客网 分析 注意题目要求输入信号为有符号数,另外输出信号可能是输入信号的和,所以需要拓展一位,防止溢出。 timescale 1ns/1ns module data_…...
OpenHarmony 4.0 Release 编译异常处理
一、环境配置 编译环境:Ubuntu 20.04 OpenHarmony 软件版本:4.0 Release 设备平台:rk3568 二、下拉代码 参考官网步骤: OpenHarmony 4.0 Release 源码获取 repo init -u https://gitee.com/openharmony/manifest -b OpenHarmo…...

软件测试|MySQL LIKE:深入了解模糊查询
简介 在数据库查询中,模糊查询是一种强大的技术,可以用来搜索与指定模式匹配的数据。MySQL数据库提供了一个灵活而强大的LIKE操作符,使得模糊查询变得简单和高效。本文将详细介绍MySQL中的LIKE操作符以及它的用法,并通过示例演示…...
linux防火墙设置
#查看firewall的状态 firewall-cmd --state (systemctl status firewalld.service) #安装 yum install firewalld #启动, systemctl start firewalld (systemctl start firewalld.service) #设置开机启动 systemctl enable firewalld #关闭 systemctl stop firewalld #取消…...
http 403
一、什么是HTTP ERROR 403 403 Forbidden 是HTTP协议中的一个状态码(Status Code)。可以简单的理解为没有权限访问此站,服务器受到请求但拒绝提供服务。 二、HTTP 403 状态码解释大全 403.1 -执行访问禁止。 403.2 -读访问禁止。 403.3 -写访问禁止。 403.4要…...

RAW图像处理软件Capture One 23 Enterprise mac中文版功能特点
Capture One 23 Enterprise mac是一款专业的图像处理软件,旨在为企业用户提供高效、快速和灵活的工作流程。 Capture One 23 Enterprise mac软件的特点和功能 强大的图像编辑工具:Capture One 23 Enterprise提供了一系列强大的图像编辑工具,…...

Linux 进程终止和等待
目录 一:进程常见的退出方法 1. main 函数返回值 2.调用 exit 3.调用 _exit 二:异常问题 三:进程等待 1.概念 2.进程等待的必要性 3.进程等待的方法 <1>:wait --- 系统调用 <2>:waitpid 进程…...

python用tkinter随机数猜数字大小
python用tkinter随机数猜数字大小 没事做,看到好多人用scratch做的猜大小的示例,也用python的tkinter搞一个猜大小的代码玩玩。 猜数字代码 from tkinter import * from random import randint# 定义确定按钮的点击事件 def hit(x,y):global s_Labprint(…...
程序员们保住自己饭碗
在现代社会中,程序员扮演着至关重要的角色。他们不仅仅是编写代码的人,更是保障数字世界安全稳定的守护者。随着科技的迅猛发展,程序员保住自己饭碗的护城河变得愈发重要。本文将探讨程序员如何通过不断学习、技术创新和软实力的发展…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...

Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...

P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...