目录
[1. 核心目标](#1. 核心目标)
[2. 整体架构设计](#2. 整体架构设计)
[3. 技术选型](#3. 技术选型)
[1. 项目初始化与环境配置](#1. 项目初始化与环境配置)
[1.1 创建工程](#1.1 创建工程)
[1.2 主窗口初始化](#1.2 主窗口初始化)
[2. 自定义控件开发](#2. 自定义控件开发)
[2.1 导航按钮控件(BtForm)](#2.1 导航按钮控件(BtForm))
[2.2 进度条控件(MusicSlider)](#2.2 进度条控件(MusicSlider))
[3. 音乐管理模块](#3. 音乐管理模块)
[3.1 音乐数据模型(Music 类)](#3.1 音乐数据模型(Music 类))
[3.2 音乐列表管理(MusicList 类)](#3.2 音乐列表管理(MusicList 类))
[4. 播放控制模块](#4. 播放控制模块)
[4.1 播放器初始化](#4.1 播放器初始化)
[4.2 核心播放控制](#4.2 核心播放控制)
[5. 歌词同步模块](#5. 歌词同步模块)
[5.1 LRC 歌词解析](#5.1 LRC 歌词解析)
[5.2 歌词与播放进度同步](#5.2 歌词与播放进度同步)
[6. 数据持久化模块](#6. 数据持久化模块)
[6.1 数据库初始化](#6.1 数据库初始化)
[6.2 歌曲信息读写](#6.2 歌曲信息读写)
[1. 核心功能测试](#1. 核心功能测试)
[2. 项目打包](#2. 项目打包)
[1. 待扩展功能](#1. 待扩展功能)
[2. 性能优化](#2. 性能优化)
作为 Qt 进阶实战项目,QQMusic 播放器涵盖了界面设计、自定义控件、媒体播放、数据持久化等核心技能,是串联 Qt 知识点的绝佳案例。本文将从设计思路、核心实现、完整代码三个维度,带大家从零复刻这款功能完备的音乐播放器。
一、项目设计思路
1. 核心目标
基于 Qt 实现一款界面友好、功能完整的音乐播放器,支持本地音乐加载、播放控制、歌词同步、数据持久化等核心功能,同时兼顾代码复用性和可扩展性。
2. 整体架构设计
采用模块化分层设计,分为四大核心模块:
- 界面层:负责 UI 布局、美化和交互,基于 Qt Designer 可视化设计,配合 QSS 样式优化
- 控件层:自定义通用控件(如导航按钮、进度条、音量调节),提升复用性
- 业务层:处理音乐加载、分类、播放控制等核心逻辑
- 数据层:通过 SQLite 实现歌曲信息、收藏状态、播放记录的持久化存储
3. 技术选型
- 开发框架:Qt 5.14+(基于 QWidget)
- 界面设计:Qt Designer + QSS 样式表
- 媒体播放:QMediaPlayer + QMediaPlaylist
- 数据存储:SQLite(Qt 内置 QSqlDatabase)
- 核心技术:自定义控件、信号槽机制、QPropertyAnimation 动画、文件 IO、正则解析
二、核心模块实现
1. 项目初始化与环境配置
1.1 创建工程
cpp
// QQMusic.pro 配置文件
QT += core gui multimedia sql
TARGET = QQMusic
TEMPLATE = app
SOURCES += main.cpp \
qqmusic.cpp \
btform.cpp \
musiclist.cpp \
music.cpp \
commonpage.cpp \
listitembox.cpp \
musicslider.cpp \
volumetool.cpp \
lrcpage.cpp
HEADERS += qqmusic.h \
btform.h \
musiclist.h \
music.h \
commonpage.h \
listitembox.h \
musicslider.h \
volumetool.h \
lrcpage.h
FORMS += qqmusic.ui \
btform.ui \
commonpage.ui \
listitembox.ui \
musicslider.ui \
volumetool.ui \
lrcpage.ui
RESOURCES += \
imagesresource.qrc
1.2 主窗口初始化
核心实现无边框窗口、拖拽功能和阴影效果:
cpp
// qqmusic.h
#include <QWidget>
#include <QPoint>
#include <QGraphicsDropShadowEffect>
namespace Ui {
class QQMusic;
}
class QQMusic : public QWidget
{
Q_OBJECT
public:
explicit QQMusic(QWidget *parent = nullptr);
~QQMusic();
private:
void initUI(); // 初始化界面
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
Ui::QQMusic *ui;
QPoint dragPosition; // 拖拽位置记录
};
// qqmusic.cpp
void QQMusic::initUI()
{
// 设置无边框窗口
setWindowFlag(Qt::FramelessWindowHint);
// 设置背景透明(用于阴影效果)
setAttribute(Qt::WA_TranslucentBackground);
// 添加阴影效果
QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0, 0);
shadowEffect->setColor("#000000");
shadowEffect->setBlurRadius(10);
this->setGraphicsEffect(shadowEffect);
}
void QQMusic::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
// 记录鼠标与窗口左上角的相对位置
dragPosition = event->globalPos() - frameGeometry().topLeft();
}
QWidget::mousePressEvent(event);
}
void QQMusic::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() == Qt::LeftButton)
{
// 保持相对位置不变,移动窗口
move(event->globalPos() - dragPosition);
}
QWidget::mouseMoveEvent(event);
}
2. 自定义控件开发
2.1 导航按钮控件(BtForm)
支持图标 + 文本 + 动画效果,点击时右侧显示跳动竖条:
cpp
// btform.h
#include <QWidget>
#include <QPropertyAnimation>
namespace Ui {
class BtForm;
}
class BtForm : public QWidget
{
Q_OBJECT
public:
explicit BtForm(QWidget *parent = nullptr);
void setIcon(QString btIcon, QString content, int mid); // 设置图标、文本、ID
void showAnimal(bool isShow); // 显示/隐藏动画
int getId() { return id; }
signals:
void click(int id); // 点击信号
protected:
void mousePressEvent(QMouseEvent *event) override;
private:
Ui::BtForm *ui;
int id; // 关联的页面ID
QPropertyAnimation *animationLine1;
QPropertyAnimation *animationLine2;
QPropertyAnimation *animationLine3;
QPropertyAnimation *animationLine4;
};
// btform.cpp
BtForm::BtForm(QWidget *parent) : QWidget(parent), ui(new Ui::BtForm)
{
ui->setupUi(this);
// 初始化动画(跳动竖条)
animationLine1 = new QPropertyAnimation(ui->line1, "geometry", this);
animationLine1->setDuration(1500);
animationLine1->setKeyValueAt(0, QRect(0, 15, 2, 0));
animationLine1->setKeyValueAt(0.5, QRect(0, 0, 2, 15));
animationLine1->setKeyValueAt(1, QRect(0, 15, 2, 0));
animationLine1->setLoopCount(-1); // 无限循环
// 初始化其他3条线的动画(略,时长分别为1600/1700/1800ms)
}
void BtForm::mousePressEvent(QMouseEvent *event)
{
// 点击后背景变绿色,文字变白
ui->btStyle->setStyleSheet("#btStyle{background:rgb(30,206,154);}*{color:#F6F6F6;}");
emit click(this->id);
}
void BtForm::showAnimal(bool isShow)
{
isShow ? ui->lineBox->show() : ui->lineBox->hide();
}
2.2 进度条控件(MusicSlider)
支持拖拽定位(seek 功能),自定义样式:
cpp
// musicslider.h
#include <QWidget>
namespace Ui {
class MusicSlider;
}
class MusicSlider : public QWidget
{
Q_OBJECT
public:
explicit MusicSlider(QWidget *parent = nullptr);
void setStep(float bf); // 设置进度
signals:
void setMusicSliderPosition(float value); // 进度变化信号
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
void moveSilder(); // 更新滑块位置
Ui::MusicSlider *ui;
int currentPos; // 当前进度位置
int maxWidth; // 最大宽度
};
// musicslider.cpp
void MusicSlider::mousePressEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSilder();
}
void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() == Qt::LeftButton)
{
currentPos = event->pos().x();
currentPos = qBound(0, currentPos, maxWidth); // 限制范围
moveSilder();
}
}
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSilder();
// 发送进度变化信号
emit setMusicSliderPosition((float)currentPos / maxWidth);
}
void MusicSlider::moveSilder()
{
ui->outLine->setGeometry(0, 8, currentPos, 4);
}
3. 音乐管理模块
3.1 音乐数据模型(Music 类)
存储歌曲信息及状态:
cpp
// music.h
#include <QUrl>
#include <QString>
#include <QUuid>
class Music
{
public:
Music() : isLike(false), isHistory(false) {}
Music(const QUrl& url);
// setter/getter方法
void setIsLike(bool isLike) { this->isLike = isLike; }
void setIsHistory(bool isHistory) { this->isHistory = isHistory; }
QString getMusicName() { return musicName; }
QString getSingerName() { return singerName; }
QUrl getMusicUrl() { return musicUrl; }
QString getLrcFilePath() const; // 获取歌词文件路径
private:
void parseMediaMetaData(); // 解析歌曲元数据
bool isLike; // 收藏状态
bool isHistory; // 播放记录状态
QString musicName; // 歌曲名
QString singerName; // 歌手名
QString albumName; // 专辑名
qint64 duration; // 时长(毫秒)
QString musicId; // 唯一ID(UUID)
QUrl musicUrl; // 文件路径
};
// music.cpp
Music::Music(const QUrl& url) : isLike(false), isHistory(false), musicUrl(url)
{
musicId = QUuid::createUuid().toString(); // 生成唯一ID
parseMediaMetaData(); // 解析元数据
}
void Music::parseMediaMetaData()
{
QMediaPlayer player;
player.setMedia(musicUrl);
// 等待元数据解析完成
while (!player.isMetaDataAvailable())
QCoreApplication::processEvents();
// 提取元数据
musicName = player.metaData("Title").toString().isEmpty() ? "歌曲未知" : player.metaData("Title").toString();
singerName = player.metaData("Author").toString().isEmpty() ? "歌手未知" : player.metaData("Author").toString();
albumName = player.metaData("AlbumTitle").toString().isEmpty() ? "专辑未知" : player.metaData("AlbumTitle").toString();
duration = player.duration();
}
QString Music::getLrcFilePath() const
{
QString path = musicUrl.toLocalFile();
path.replace(".mp3", ".lrc");
path.replace(".flac", ".lrc");
return path;
}
3.2 音乐列表管理(MusicList 类)
管理所有歌曲,支持加载、分类、持久化:
cpp
// musiclist.h
#include <QVector>
#include <QList>
#include <QUrl>
#include "music.h"
class MusicList
{
public:
void addMusicByUrl(const QList<QUrl>& urls); // 加载本地音乐
void writeToDB(); // 写入数据库
void readFromDB(); // 从数据库读取
// 迭代器支持(用于范围for循环)
typedef QVector<Music>::iterator iterator;
iterator begin() { return musicList.begin(); }
iterator end() { return musicList.end(); }
private:
QVector<Music> musicList; // 歌曲列表
QSet<QString> musicPaths; // 用于去重的文件路径集合
};
4. 播放控制模块
4.1 播放器初始化
cpp
// qqmusic.h 新增
#include <QMediaPlayer>
#include <QMediaPlaylist>
class QQMusic : public QWidget
{
// ... 其他代码
private:
void initPlayer(); // 初始化播放器
QMediaPlayer* player;
QMediaPlaylist* playList;
};
// qqmusic.cpp
void QQMusic::initPlayer()
{
player = new QMediaPlayer(this);
playList = new QMediaPlaylist(this);
playList->setPlaybackMode(QMediaPlaylist::Loop); // 默认循环播放
player->setPlaylist(playList);
player->setVolume(20); // 默认音量20%
// 关联播放状态变化信号
connect(player, &QMediaPlayer::stateChanged, this, &QQMusic::onPlayStateChanged);
// 关联进度变化信号
connect(player, &QMediaPlayer::positionChanged, this, &QQMusic::onPositionChanged);
}
4.2 核心播放控制
cpp
// 播放/暂停
void QQMusic::onPlayClicked()
{
switch (player->state())
{
case QMediaPlayer::PlayingState:
player->pause();
break;
case QMediaPlayer::PausedState:
case QMediaPlayer::StoppedState:
player->play();
break;
}
}
// 上一曲/下一曲
void QQMusic::onPlayUpClicked() { playList->previous(); }
void QQMusic::onPlayDownClicked() { playList->next(); }
// 播放模式切换(循环/随机/单曲循环)
void QQMusic::onPlaybackModeClicked()
{
switch (playList->playbackMode())
{
case QMediaPlaylist::Loop:
playList->setPlaybackMode(QMediaPlaylist::Random);
ui->playMode->setIcon(QIcon(":/images/shuffle_2.png"));
break;
case QMediaPlaylist::Random:
playList->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
ui->playMode->setIcon(QIcon(":/images/single_play.png"));
break;
case QMediaPlaylist::CurrentItemInLoop:
playList->setPlaybackMode(QMediaPlaylist::Loop);
ui->playMode->setIcon(QIcon(":/images/list_play.png"));
break;
}
}
// 进度条拖拽定位
void QQMusic::onMusicSliderChanged(float value)
{
qint64 duration = (qint64)(player->duration() * value);
player->setPosition(duration);
ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60, 2, 10, QChar('0')).arg(duration/1000%60, 2, 10, QChar('0')));
}
5. 歌词同步模块
5.1 LRC 歌词解析
cpp
// lrcpage.h
#include <QWidget>
#include <QVector>
struct LyricLine
{
qint64 time; // 歌词时间(毫秒)
QString text; // 歌词内容
LyricLine(qint64 t, QString s) : time(t), text(s) {}
};
namespace Ui {
class LrcPage;
}
class LrcPage : public QWidget
{
Q_OBJECT
public:
explicit LrcPage(QWidget *parent = nullptr);
bool parseLrc(const QString& lrcPath); // 解析LRC文件
void showLrcWord(qint64 pos); // 显示当前歌词
private:
int getLineLrcWordIndex(qint64 pos); // 根据时间获取歌词索引
QString getLineLrcWord(int index); // 获取指定索引的歌词
Ui::LrcPage *ui;
QVector<LyricLine> lrcLines; // 歌词列表
};
// lrcpage.cpp
bool LrcPage::parseLrc(const QString& lrcPath)
{
lrcLines.clear();
QFile lrcFile(lrcPath);
if (!lrcFile.open(QFile::ReadOnly)) return false;
while (!lrcFile.atEnd())
{
QString line = lrcFile.readLine().trimmed();
int left = line.indexOf('[');
int right = line.indexOf(']');
if (left == -1 || right == -1) continue;
// 解析时间 [mm:ss.zzz]
QString timeStr = line.mid(left+1, right-left-1);
int colon = timeStr.indexOf(':');
int dot = timeStr.indexOf('.');
int minute = timeStr.mid(0, colon).toInt();
int second = timeStr.mid(colon+1, dot-colon-1).toInt();
int msec = timeStr.mid(dot+1).toInt();
qint64 totalTime = minute*60*1000 + second*1000 + msec;
// 解析歌词内容
QString text = line.mid(right+1).trimmed();
lrcLines.append(LyricLine(totalTime, text));
}
return true;
}
void LrcPage::showLrcWord(qint64 pos)
{
int index = getLineLrcWordIndex(pos);
if (index == -1)
{
ui->lineCenter->setText("当前歌曲无歌词");
return;
}
// 显示当前歌词及前后三行
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));
}
5.2 歌词与播放进度同步
cpp
// qqmusic.cpp
void QQMusic::onPositionChanged(qint64 position)
{
// 更新当前播放时间
ui->currentTime->setText(QString("%1:%2").arg(position/1000/60, 2, 10, QChar('0')).arg(position/1000%60, 2, 10, QChar('0')));
// 更新进度条
if (player->duration() > 0)
ui->progressBar->setStep((float)position / player->duration());
// 同步歌词
lrcPage->showLrcWord(position);
}
6. 数据持久化模块
6.1 数据库初始化
cpp
// qqmusic.cpp
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QMessageBox>
void QQMusic::initSQLite()
{
// 连接SQLite数据库
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("QQMusic.db");
if (!db.open())
{
QMessageBox::critical(this, "错误", "数据库连接失败:" + db.lastError().text());
return;
}
// 创建歌曲信息表
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());
}
6.2 歌曲信息读写
cpp
// music.cpp 新增
#include <QSqlQuery>
void Music::insertMusicToDB()
{
QSqlQuery query;
// 检查歌曲是否已存在
query.prepare("SELECT EXISTS(SELECT 1 FROM musicInfo WHERE musicId = ?)");
query.addBindValue(musicId);
query.exec();
query.next();
bool isExists = query.value(0).toBool();
if (isExists)
{
// 更新收藏和播放状态
query.prepare("UPDATE musicInfo SET isLike = ?, isHistory = ? WHERE musicId = ?");
query.addBindValue(isLike ? 1 : 0);
query.addBindValue(isHistory ? 1 : 0);
}
else
{
// 插入新歌曲
query.prepare("INSERT INTO musicInfo(musicId, musicName, musicSinger, albumName, musicUrl, duration, isLike, isHistory) "
"VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
query.addBindValue(musicId);
query.addBindValue(musicName);
query.addBindValue(singerName);
query.addBindValue(albumName);
query.addBindValue(musicUrl.toLocalFile());
query.addBindValue(duration);
query.addBindValue(isLike ? 1 : 0);
query.addBindValue(isHistory ? 1 : 0);
}
query.exec();
}
三、完整功能测试与打包
1. 核心功能测试
- 本地音乐加载:支持 MP3、FLAC 格式,自动去重
- 播放控制:播放 / 暂停、上一曲 / 下一曲、模式切换、音量调节
- 歌词同步:LRC 歌词自动解析,实时同步播放进度
- 数据持久化:收藏状态、播放记录重启后保留
- 界面交互:窗口拖拽、阴影效果、控件 hover 反馈
2. 项目打包
使用 Qt 自带的 windeployqt 工具打包:
-
切换到 Release 模式编译项目
-
新建文件夹,复制 exe 文件到文件夹中
-
按住 Shift + 右键,打开 PowerShell,执行命令:
windeployqt .\QQMusic.exe
-
打包完成后,压缩文件夹即可分发
四、项目扩展与优化
1. 待扩展功能
- 换肤功能:支持自定义皮肤和主题切换
- 在线音乐:添加网络模块,支持在线搜索和播放
- 本地搜索:支持按歌曲名、歌手快速检索
- 迷你模式:最小化后显示悬浮歌词窗口
2. 性能优化
- 歌曲元数据解析异步化,避免界面卡顿
- 大列表优化:使用 QVirtualListWidget 替代 QListWidget
- 数据库索引:为 musicId 字段添加索引,提升查询速度
五、项目总结
本项目通过 Qt 实现了一款功能完备的音乐播放器,核心亮点在于:
- 模块化设计:界面、控件、业务、数据分层,代码复用性高
- 自定义控件:封装通用控件,提升界面美观度和交互体验
- 完整的业务闭环:从音乐加载、播放控制到数据持久化全覆盖
- 工程化实践:包含打包、去重、单例运行等工程化细节
通过这个项目,能熟练掌握 Qt 的核心知识点,包括界面设计、信号槽、动画效果、文件 IO、数据库操作等,是 Qt 进阶学习的优质实践案例。