前言html
这一节实现一个简易的音乐播放器,其音乐播放的核心功能是采用Qt支持的Phonon框架,该框架在前一篇博文Qt学习之路_13(简易俄罗斯方块) 中已经使用过了,在俄罗斯方块中主要是用来设置背景音乐和消行的声音的。这里用这个框架一样是用来播放,暂停等多媒体的各类控制功能,另外该框架能够自动获取音频文件的一些信息,这样咱们在设计播放列表时能够获取这些信息,好比歌手名,专辑名,时长,文件名等等。程序中桌面歌词的实现是继承了QLabel类,而后使用3层文本显示,最上面一层采用渐进显示的方式来达到歌词播放的动态效果。linux
实验的参考资料为http://www.yafeilinux.com/ 网站上yafei做者提供的代码,本人只是看懂其源码而后本身敲了一遍,代码作了稍微的改变,其设计方法和技巧全是原创做者yafei的功劳。c++
开发环境:WindowsXP+Qt4.8.2+QtCreator2.5.1正则表达式
实验说明网络
本实验没有使用QtDesigner来设计界面,其界面而是直接采用c++代码来写的。下面分如下几个方面来介绍本实验的实现过程当中应该注意的知识点:框架
播放界面设计部分:less
由于主界面的设计是从QWidget类继承而来,可是本程序却没有使用界面设计工具来设计界面,而是直接使用c++代码完成。dom
在界面设计时,首先通常是设置窗口的标题,尺寸,图标等。而后而后本程序时在主界面上面添加了2个工具栏和一个标题栏,这3个栏目构成了播放器的主界面,主界面采用的是垂直布局,即QVBoxLayout. 2个工具栏分别为QAction,里面可使用addAction ()方法直接插入action或者使用addWidget()方法插入widget。对action能够设置其快捷键,提示文本,图标,响应槽函数等。对于widget能够设置其显示内容,提示文本,尺寸属性,对其方式,若是外加网络链接,则也能够设置其是否连接到外部等。ide
在播放媒体文件时,媒体对象MediaObject会在指定的时间间隔发送tick()信号,这个时间间隔可使用setTrickInterval()函数来进行设置。tick()中的参数time指定了媒体对象在媒体流中的当前时间位置,单位是毫秒。程序中关联了这个信号,其主要目的是为了得到当前的播放时间。函数
能够直接调用媒体播放文件的totalTime方法实现统计媒体文件的总播放时长,单位为毫秒,而后能够将其转换保存在QTime对象中,直接使用toString()函数来指定其形式。
媒体对象的各类状态:
当建立了媒体对象后,它就会处于LoadingState状态,只有使用createPath()为其设置了Path,再使用setCurrentSource()为其设置了当前媒体源之后,媒体对象才会进入StoppedState状态。若是在设置了媒体源以后当即调用了play()函数,那么媒体对象就不会进入StoppedState状态了,而是直接进入PlayingState状态。
每当媒体对象的状态发生改变时,就会自动发射stateChanged()信号,这里绑定信号后,就能够用这些状态来进行一些有关的设置。
播放列表:
程序中sources为打开的因此音频文件列表,playlist为音乐播放列表表格对象。程序中并无直接使用meidaObject对象来获取音频文件信息,而是建立了新的MedioObject类对象meta_information_resolver做为元数据的解析器。由于只有在LoadingState完成后才能得到元数据,因此能够先调用解析器的setCurrentSource()函数为其设置一个媒体源,而后关联它的stateChanged()信号,等其进入到StoppedState状态再进行元数据的解析。
桌面歌词:
程序中实现桌面歌词设计是类MyLrc,继承QLabel类。桌面歌词的显示首先须要将部件的背景设置为透明色,而后从新实现其重绘事件处理函数来自定义文本的显示,这里可使用渐变填充来实现多彩的文字。而后再使用定时器,在已经绘制的歌词上面再绘制一个不断变宽的相同的歌词来实现歌词的动态播放效果。所以程序中的歌词共绘制了3遍,第一遍是深黑色,在最底层;第2遍是渐变填充的歌词,为正常显示所用;第3次绘制的是用于遮罩用,实现动态效果。
歌词的解析都在resolve_lrc()函数中实现的,利用正则表达式来获取歌曲文件中的各类信息,通常的歌词文件以.lrc后缀结尾,歌词文件的格式以下所示:
关于歌词的解析部分详见代码部分。
系统图标的设计:
通常的音乐播放器都会有一个系统托盘图标,这样就能够在播放歌曲的时候将主界面最小化到系统托盘图标了。 Qt中是经过QSystemTrayIcon类来实现系统托盘图标的,而且能够很容易在该图标上添加菜单,设置工具栏提示,显示消息和处理各类交互等。
知识点总结
Qt知识点总结:
QAction对象使用setText()方法时,若是在对象的构造函数中已经有了其文字显示,那么action上面显示的就是构造函数中的text文本。这里的setText文本有2个做用,第一个是若是该action对应到了菜单栏中,则菜单栏会自动将其显示出来;第二个时若是构造函数中没有设置文本内容,则该action会显示setText()方法设置的内容,固然了,若是action设置了图标,该文本内容就被覆盖了,退化为文本提示了。
cellClicked(int, int)信号是当表格中的一个cell单元被单击时发出的。它的两个参数分别为表格中cell的行号和列号。
可使用frameGeometry()来得到程序中的主界面,而后该界面的定位函数能够得到与主界面的相对位置,好比说frameGeometry().bottomLeft()就是得到主界面的左下方的位置。
当本身定义了的一个类,该类有对应的头文件和源文件。若是在第二个类的头文件中药使用到第一个类,则能够不用包含第一个类的头文件,直接用class关键字声明就能够了,在第二个类的源文件中则须要包含第一个类的头文件,由于这里须要使用第一个类对象的成员方法。
Qt中正则表达式为类QRegExp,正则表达式是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。好比说程序中的QRegExp rx("\\[\\d{2}:\\d{2}\\.\\d{2}\\]");其实就是表示歌词文件前面的格式,好比[00:05.54]。表达式中的d{2}表示匹配2个数字。
Qt中常见的类的继承总结:
若是须要设计界面,且须要菜单栏,工具栏,状态栏等,通常继承QMainWidget类。
若是须要界面,不须要菜单栏,工具栏,状态栏等,通常继承QDialog类。
若是须要使用自定义视图来画图形,则能够继承QAbstractItem类。
若是须要本身设计场景,好比游戏开发的时候,能够继承QGraphicsView类。
若是须要本身制做一个小图形视图,能够考虑继承QGraphicsObject类,当将这些小视图构成一个视图组时,该组的类能够继承QGraphicsItemGroup类和QObject类。
通常的界面设计也能够继承QWidget类。
通常的文本类能够继承QLabel,好比本实验的桌面歌词类MyLrc。
实验结果
该实验有打开播放文件,播放按钮,暂停按钮,选择上一首歌按钮,选择下一首歌按钮,显示播放列表,单击播放列表实现歌曲播放,动态显示桌面歌词,显示歌曲总时长和已播放时长,调节音乐音量,最小化到系统托盘等功能,其截图效果以下所示:
实验主要部分代码及注释
mywidget.h:
#ifndef MYWIDGET_H #define MYWIDGET_H
#include <QWidget> #include <Phonon> #include <QSystemTrayIcon>
class QLabel; class MyPlaylist; class MyLrc;
namespace Ui { class MyWidget; }
class MyWidget : public QWidget { Q_OBJECT public: explicit MyWidget(QWidget *parent = 0); ~MyWidget(); private: Ui::MyWidget *ui; void InitPlayer(); Phonon::MediaObject *media_object; QAction *play_action; QAction *stop_action; QAction *skip_backward_action; QAction *skip_forward_action; QLabel *top_label; QLabel *time_label;
MyPlaylist *playlist; Phonon::MediaObject *meta_information_resolver; QList<Phonon::MediaSource> sources; void change_action_state();
MyLrc *lrc; QMap<qint64, QString> lrc_map; void resolve_lrc(const QString &source_file_name);
QSystemTrayIcon *tray_icon;
private slots: void UpdateTime(qint64 time); void SetPaused(); void SkipBackward(); void SkipForward(); void OpenFile(); void SetPlayListShown(); void SetLrcShown();
void StateChanged(Phonon::State new_state, Phonon::State old_state); void SourceChanged(const Phonon::MediaSource &source); void AboutToFinish(); void MetaStateChanged(Phonon::State new_state, Phonon::State old_state); void TableClicked(int row); void ClearSources();
void TrayIconActivated(QSystemTrayIcon::ActivationReason activation_reason);
protected: void closeEvent(QCloseEvent *);
};
#endif // MYWIDGET_H myplaylist.h: #ifndef MYPLAYLIST_H #define MYPLAYLIST_H #include <QTableWidget> class MyPlaylist : public QTableWidget { Q_OBJECT public: explicit MyPlaylist(QWidget *parent = 0); signals: void play_list_clean(); public slots: protected: void contextMenuEvent(QContextMenuEvent *); void closeEvent(QCloseEvent *); private slots: void clear_play_list(); }; #endif // MYPLAYLIST_H myplaylist.cpp: #include "myplaylist.h" #include <QContextMenuEvent> #include <QMenu> MyPlaylist::MyPlaylist(QWidget *parent) : QTableWidget(parent) { setWindowTitle(tr("播放列表")); //设置为一个独立的窗口,且只有一个关闭按钮 setWindowFlags(Qt::Window | Qt::WindowTitleHint); resize(400, 400); setMaximumWidth(400); setMinimumWidth(400);//固定窗口大小 setRowCount(0);//初始的行数为0 setColumnCount(3);//初始的列数为1 //设置第一个标签 QStringList list; list << tr("标题") << tr("歌手") << tr("长度"); setHorizontalHeaderLabels(list); setSelectionMode(QAbstractItemView::SingleSelection);//设置只能选择单行 setSelectionBehavior(QAbstractItemView::SelectRows); setShowGrid(false);//设置不显示网格 } void MyPlaylist::clear_play_list() { while(rowCount()) removeRow(0); emit play_list_clean();//删除完后,发送清空成功信号 } void MyPlaylist::contextMenuEvent(QContextMenuEvent *event) { QMenu menu; menu.addAction(tr("清空列表"), this, SLOT(clear_play_list()));//能够直接在这里指定槽函数 menu.exec(event->globalPos());//返回鼠标指针的全局位置 } void MyPlaylist::closeEvent(QCloseEvent *event) { if(isVisible()) { hide(); event->ignore();//清零接收标志 } } mylrc.h: #ifndef MYLRC_H #define MYLRC_H #include <QLabel> class QTimer; class MyLrc : public QLabel { Q_OBJECT public: explicit MyLrc(QWidget *parent = 0); void start_lrc_mask(qint64 intervaltime); void stop_lrc_mask(); protected: void paintEvent(QPaintEvent *); void mousePressEvent(QMouseEvent *ev); void mouseMoveEvent(QMouseEvent *ev); void contextMenuEvent(QContextMenuEvent *ev); signals: public slots: private slots: void timeout(); private: QLinearGradient linear_gradient; QLinearGradient mask_linear_gradient; QFont font; QTimer *timer; qreal lrc_mask_width; qreal lrc_mask_width_interval; QPoint offset; }; #endif // MYLRC_H mylrc.cpp: #include "mylrc.h" #include <QPainter> #include <QTimer> #include <QMouseEvent> #include <QContextMenuEvent> #include <QMenu> MyLrc::MyLrc(QWidget *parent) : QLabel(parent) { //FramelessWindowHint为无边界的窗口 setWindowFlags(Qt::Window | Qt::FramelessWindowHint); setAttribute(Qt::WA_TranslucentBackground); setText(tr("简易音乐播放器")); // 固定显示区域大小 setMaximumSize(800, 60); setMinimumSize(800, 60); //歌词的线性渐变填充 linear_gradient.setStart(0, 10);//填充的起点坐标 linear_gradient.setFinalStop(0, 40);//填充的终点坐标 //第一个参数终点坐标,相对于咱们上面的区域而言,按照比例进行计算 linear_gradient.setColorAt(0.1, QColor(14, 179, 255)); linear_gradient.setColorAt(0.5, QColor(114, 232, 255)); linear_gradient.setColorAt(0.9, QColor(14, 179, 255)); // 遮罩的线性渐变填充 mask_linear_gradient.setStart(0, 10); mask_linear_gradient.setFinalStop(0, 40); mask_linear_gradient.setColorAt(0.1, QColor(222, 54, 4)); mask_linear_gradient.setColorAt(0.5, QColor(255, 72, 16)); mask_linear_gradient.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())); lrc_mask_width = 0; lrc_mask_width_interval = 0; } // 开启遮罩,须要指定当前歌词开始与结束之间的时间间隔 void MyLrc::start_lrc_mask(qint64 intervaltime) { // 这里设置每隔30毫秒更新一次遮罩的宽度,由于若是更新太频繁 // 会增长CPU占用率,而若是时间间隔太大,则动画效果就不流畅了 qreal count = intervaltime / 30; // 获取遮罩每次须要增长的宽度,这里的800是部件的固定宽度 lrc_mask_width_interval = 800 / count; lrc_mask_width = 0; timer->start(30); } void MyLrc::stop_lrc_mask() { timer->stop(); lrc_mask_width = 0; update(); } 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(linear_gradient, 0)); painter.drawText(0, 0, 800, 60, Qt::AlignLeft, text()); // 设置歌词遮罩 painter.setPen(QPen(mask_linear_gradient, 0)); painter.drawText(0, 0, lrc_mask_width, 60, Qt::AlignLeft, text()); } //左击操做 void MyLrc::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) offset = event->globalPos() - frameGeometry().topLeft(); } void MyLrc::mouseMoveEvent(QMouseEvent *event) { //移动鼠标到歌词上时,会显示手型 //event->buttons()返回鼠标点击的类型,分为左击,中击,右击 //这里用与操做表示是左击 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());//globalPos()为当前鼠标的位置坐标 } void MyLrc::timeout() { //每隔一段固定的时间笼罩的长度就增长一点 lrc_mask_width += lrc_mask_width_interval; update();//更新widget,可是并不当即重绘,而是安排一个Paint事件,当返回主循环时由系统来重绘 } main.cpp: #include <QApplication> #include <QTextCodec> #include "mywidget.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); QTextCodec::setCodecForTr(QTextCodec::codecForLocale()); MyWidget w; w.show(); return a.exec(); }