Qt 实战-Music 播放器

目录

一、项目设计思路

[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 工具打包:

  1. 切换到 Release 模式编译项目

  2. 新建文件夹,复制 exe 文件到文件夹中

  3. 按住 Shift + 右键,打开 PowerShell,执行命令:

    windeployqt .\QQMusic.exe

  4. 打包完成后,压缩文件夹即可分发

四、项目扩展与优化

1. 待扩展功能

  • 换肤功能:支持自定义皮肤和主题切换
  • 在线音乐:添加网络模块,支持在线搜索和播放
  • 本地搜索:支持按歌曲名、歌手快速检索
  • 迷你模式:最小化后显示悬浮歌词窗口

2. 性能优化

  • 歌曲元数据解析异步化,避免界面卡顿
  • 大列表优化:使用 QVirtualListWidget 替代 QListWidget
  • 数据库索引:为 musicId 字段添加索引,提升查询速度

五、项目总结

本项目通过 Qt 实现了一款功能完备的音乐播放器,核心亮点在于:

  1. 模块化设计:界面、控件、业务、数据分层,代码复用性高
  2. 自定义控件:封装通用控件,提升界面美观度和交互体验
  3. 完整的业务闭环:从音乐加载、播放控制到数据持久化全覆盖
  4. 工程化实践:包含打包、去重、单例运行等工程化细节

通过这个项目,能熟练掌握 Qt 的核心知识点,包括界面设计、信号槽、动画效果、文件 IO、数据库操作等,是 Qt 进阶学习的优质实践案例。

相关推荐
上不如老下不如小2 小时前
2025年第七届全国高校计算机能力挑战赛 决赛 Python组 编程题汇总
开发语言·python
老华带你飞2 小时前
校园快递|基于Java校园快递管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
smile_Iris3 小时前
Day 32 类的定义和方法
开发语言·python
AndreasEmil3 小时前
JavaSE - 继承
java·开发语言·ide·vscode·intellij-idea·idea
老华带你飞9 小时前
博物馆展览门户|基于Java博物馆展览门户系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·后端
liulilittle9 小时前
FileStream C++
开发语言·c++·cocoa
点PY9 小时前
C++ 中 std::async 和 std::future 的并发性
java·开发语言·c++
不会代码的小猴9 小时前
C++的第九天笔记
开发语言·c++·笔记
CoderYanger10 小时前
Java SE——12.异常(≠错误)《干货笔记》
java·开发语言