项目概述
该项目是一个基于 QT框架和C++ 开发的桌面端音乐播放软件。项目的核心目标是综合运用QT知识,模拟实现主流音乐软件的基础功能,包括界面布局、自定义控件、音乐管理、播放控制及数据持久化。
项目主要分为四大部分,分别是界面设计、音乐管理、音乐播放控制和持久化存储。
界面设计
界面采用 Widget嵌套 + 布局管理器 的方式构建,主窗口被清晰地划分为三大区域:


顶部 (Head)
左侧为Logo,右侧集成了搜索框、皮肤切换按钮和窗口控制按钮(最小化、最大化、关闭)。窗口设置为无边框,并自定义了窗口拖拽和阴影效果。
左侧 (BodyLeft)
音乐分类导航栏。包含"在线音乐"(推荐、音乐馆、电台)和"我的音乐"(我喜欢、本地下载、最近播放)两组。每个导航项都是自定义控件 BtForm,集成了图标、文字。
右侧 (BodyRight)
核心内容显示区,由三部分组成:

页面区 (StackedWidget)
使用 QStackedWidget 管理6个页面,通过点击左侧导航切换。主要包含两种不同的自定义控件:推荐页和通用列表页。
推荐页 (recPage):内含轮播图效果的自定义控件 RecBox 和带动画效果的 RecBoxItem。
通用列表页 (CommonPage):用于"我喜欢"、"本地下载"、"最近播放"三个雷同页面,内部统一包含标题、封面、播放全部按钮和音乐列表(QListWidget)。实现了音乐列表的通用展示,ListItemBox 每一项都包含收藏、歌曲名、VIP/SQ标识、歌手和专辑名。
播放进度区 (ProgressBar)
自定义的 MusicSlider 控件,支持点击/拖拽定位到相应的位置。
播放控制区 (ControlBox)
提供完整的控制按钮(歌曲信息展示、播放模式、上一曲/下一曲、播放/暂停、音量调节、歌词按钮)。
歌词页面 (LrcPage)
一个独立的弹出窗口,用于同步显示当前播放音乐的LRC歌词,并伴有平滑的弹出和隐藏动画。
音量调节窗口 (VolumeTool)
一个自定义的弹出窗口,包含垂直滑块和静音按钮。
音乐管理
1. 音乐加载与解析
通过 QFileDialog 加载本地音频文件 (MP3, FLAC等)。使用 QMimeDatabase 精确识别MIME类型,过滤非音频文件。
在 MusicMessage 类的构造函数中,直接利用 QMediaPlayer 解析并提取音乐的元数据(歌名、歌手、专辑、时长)。
核心代码:
cpp
// 音乐加载
void Music::on_addLocal_clicked()
{
QStringList files = QFileDialog::getOpenFileNames(this, "选择音乐文件", ".",
"音频文件 (*.mp3 *.flac *.wav *.wma)");
QList<QUrl> urls;
for(const QString &file : files)
{
urls.append(QUrl::fromLocalFile(file));
}
musicList.addMusicsByUrl(urls);
// 刷新页面
ui->likePage->reFrash(musicList);
ui->localPage->reFrash(musicList);
ui->recentPage->reFrash(musicList);
}
// MusicMessage 构造函数(元数据解析)
MusicMessage::MusicMessage(QUrl url)
: musicUrl(url), isLike(false), isHistory(false)
{
musicId = QUuid::createUuid().toString();
parseMediaMetaMusic();
}
// 解析元数据
void MusicMessage::parseMediaMetaMusic()
{
QMediaPlayer player;
player.setMedia(musicUrl);
// 等待元数据就绪
QEventLoop loop;
connect(&player, &QMediaPlayer::metaDataAvailableChanged, &loop, &QEventLoop::quit);
loop.exec();
if(player.isMetaDataAvailable())
{
musicName = player.metaData("Title").toString();
musicSinger = player.metaData("Author").toString();
musicAlbum = player.metaData("AlbumTitle").toString();
duration = player.duration();
}
// 如果元数据为空,从文件名提取
if(musicName.isEmpty())
{
QString fileName = musicUrl.fileName();
musicName = fileName.left(fileName.lastIndexOf("."));
}
}
调用逻辑:
- 音乐加载触发:用户点击"本地下载"页面的"添加本地音乐"按钮
- 执行流程:
- 调用
QFileDialog::getOpenFileNames打开文件选择对话框 - 将选择的文件路径转换为
QUrl列表 - 调用
musicList.addMusicsByUrl(urls)添加音乐 - 刷新所有 CommonPage 页面
- 调用
- 元数据解析:在
MusicMessage构造函数中,使用临时QMediaPlayer解析音乐文件的元数据
音乐管理与状态同步
核心数据模型 MusicMessage 类:封装了音乐文件的所有信息(URL、元数据(歌名、歌手、专辑、时长)、唯一ID musicId、是否喜欢 isLike、是否历史 isHistory)。
统一管理 MusicList 类 :用 QVector<MusicMessage> 管理所有加载的音乐,并提供CRUD接口。通过 musicId (QUuid生成) 保证音乐对象的唯一性。
页面分类:CommonPage 维护自己的 musicListOfPage (存储 musicId),通过"状态"从 MusicList 中筛选出不同页面的歌曲,并调用 reFresh() 方法更新界面。
收藏同步:ListItemBox 发射信号 -> CommonPage 转发 -> Music 主窗口更新 MusicMessage 对象状态 -> 触发所有 CommonPage 刷新,确保状态全局一致。
核心代码:
cpp
// MusicList 类
void MusicList::addMusicsByUrl(const QList<QUrl> &urls)
{
for(const QUrl &url : urls)
{
QString path = url.toString();
if(!musicPaths.contains(path))
{
MusicMessage music(url);
musicList.append(music);
musicPaths.insert(path);
music.InsertMusicToDB();
}
}
}
// 收藏状态同步
void Music::onUpdateLikeMusic(bool isLike, QString musicId)
{
auto it = musicList.findMusicById(musicId);
if(it != musicList.end())
{
it->setIsLike(isLike);
it->InsertMusicToDB(); // 更新数据库
// 刷新所有页面
ui->likePage->reFrash(musicList);
ui->localPage->reFrash(musicList);
ui->recentPage->reFrash(musicList);
}
}
// CommonPage 刷新
void CommonPage::reFrash(MusicList &musicList)
{
ui->listWidget->clear();
musicOfPage.clear();
for(auto it = musicList.begin(); it != musicList.end(); ++it)
{
bool shouldAdd = false;
switch(pageType)
{
case PageType::LIKE:
shouldAdd = it->getIsLike();
break;
case PageType::LOCAL:
shouldAdd = true;
break;
case PageType::RECENT:
shouldAdd = it->getIsHistory();
break;
default:
break;
}
if(shouldAdd)
{
ListItemBox *itemBox = new ListItemBox(*it, this);
QListWidgetItem *item = new QListWidgetItem(ui->listWidget);
item->setSizeHint(QSize(0, 60));
ui->listWidget->addItem(item);
ui->listWidget->setItemWidget(item, itemBox);
musicOfPage.append(it->getMusicId());
connect(itemBox, &ListItemBox::likeChanged, this, [=](bool isLike, QString musicId) {
emit updateLikeMusic(isLike, musicId);
});
}
}
}
调用逻辑:
- 音乐添加:调用
addMusicsByUrl方法添加音乐,自动去重并写入数据库 - 收藏状态更新:
- ListItemBox 中的收藏按钮被点击,发射
likeChanged信号 - CommonPage 接收信号并转发
updateLikeMusic信号 - Music 主窗口接收信号,更新 MusicMessage 的 isLike 状态
- 调用
InsertMusicToDB()更新数据库 - 刷新所有 CommonPage 页面
- ListItemBox 中的收藏按钮被点击,发射
- 页面刷新:每个 CommonPage 根据自己的类型(LIKE/LOCAL/RECENT)筛选音乐并显示
音乐播放控制
媒体播放
使用 QMediaPlayer 进行播放、暂停、Seek、音量调节等。
播放列表
使用 QMediaPlaylist 管理播放队列,支持顺序、随机、单曲循环模式。
播放触发
支持"播放全部"按钮和双击列表项播放,两者都会先将当前 CommonPage 的音乐填充到 QMediaPlaylist 再启动播放。
核心代码:
cpp
// 播放全部
void Music::onPlayAll(PageType pageType)
{
CommonPage *targetPage = nullptr;
switch(pageType)
{
case PageType::LIKE:
targetPage = ui->likePage;
break;
case PageType::LOCAL:
targetPage = ui->localPage;
break;
case PageType::RECENT:
targetPage = ui->recentPage;
break;
default:
return;
}
playAllMusicOfCommonPage(targetPage, 0);
}
// 播放所有音乐
void Music::playAllMusicOfCommonPage(CommonPage *commonPage, int index)
{
currentPage = commonPage;
currentIndex = index;
playList->clear();
commonPage->addMusicToPlayList(musicList, playList);
playList->setCurrentIndex(index);
player->play();
}
// 双击播放
void Music::playMusicByIndex(CommonPage *page, int index)
{
playAllMusicOfCommonPage(page, index);
}
// CommonPage 添加音乐到播放列表
void CommonPage::addMusicToPlayList(MusicList &musicList, QMediaPlaylist *playList)
{
for(const QString &musicId : musicOfPage)
{
auto it = musicList.findMusicById(musicId);
if(it != musicList.end())
{
playList->addMedia(it->getMusicUrl());
}
}
}
调用逻辑:
- 播放全部触发:用户点击 CommonPage 中的"播放全部"按钮
- 发射
playAll信号,传递页面类型 - Music 主窗口接收信号,确定目标页面
- 调用
playAllMusicOfCommonPage方法
- 发射
- 双击播放触发:用户双击 CommonPage 中的列表项
- 发射
playMusicByIndex信号,传递页面和索引 - Music 主窗口接收信号,调用
playAllMusicOfCommonPage方法
- 发射
- 播放列表构建:
- 清空当前播放列表
- 调用
addMusicToPlayList将页面中的音乐添加到播放列表 - 设置当前播放索引
- 开始播放
播放进度同步
双向同步。QMediaPlayer::positionChanged 信号驱动进度条、当前时间Label和LRC歌词更新;MusicSlider 的Seek操作则通过信号修改 QMediaPlayer 的播放位置。
核心代码:
cpp
// 进度更新
void Music::onPositionChanged(qint64 position)
{
ui->musicSlider->setValue(position);
int currentMinutes = position / 60000;
int currentSeconds = (position % 60000) / 1000;
QString currentTime = QString("%1:%2").arg(currentMinutes, 2, 10, QChar('0')).arg(currentSeconds, 2, 10, QChar('0'));
ui->currentTime->setText(currentTime);
// 同步歌词
lrcPage->updateCurrentLyric(position);
}
// 进度条拖动
void Music::onMusicSliderChanged(float value)
{
qint64 position = totalTime * value;
player->setPosition(position);
}
// 信号连接(在 connectSingalAndSlots 中)
connect(player, &QMediaPlayer::positionChanged, this, &Music::onPositionChanged);
connect(ui->musicSlider, &MusicSlider::valueChanged, this, &Music::onMusicSliderChanged);
调用逻辑:
- 播放进度更新:
QMediaPlayer::positionChanged信号触发onPositionChanged方法- 更新音乐滑块的位置
- 更新当前时间显示
- 调用
lrcPage->updateCurrentLyric更新歌词
- 手动拖动进度条:
- 用户拖动 MusicSlider,触发
valueChanged信号 onMusicSliderChanged方法计算新的播放位置- 调用
player->setPosition设置播放位置
- 用户拖动 MusicSlider,触发
歌曲切换联动
监听 QMediaPlayer::metaDataAvailableChanged 信号,在歌曲切换时同步更新主界面(封面、歌名、歌手)、CommonPage 封面以及解析新的LRC文件。
核心代码:
cpp
// 元数据可用时更新
void Music::onMetaDataAvailableChanged(bool available)
{
if(available)
{
// 更新歌曲信息
QString title = player->metaData("Title").toString();
QString author = player->metaData("Author").toString();
ui->musicName->setText(title);
ui->musicSinger->setText(author);
// 更新歌词页面信息
lrcPage->setMusicInfo(title, author);
// 更新封面
QVariant coverImage = player->metaData("ThumbnailImage");
if(coverImage.isValid())
{
QImage image = coverImage.value<QImage>();
ui->musicCover->setPixmap(QPixmap::fromImage(image));
if(currentPage)
{
currentPage->setMusicImage(QPixmap::fromImage(image));
}
}
else
{
QString defaultCover = ":/images/default_cover.png";
ui->musicCover->setPixmap(defaultCover);
if(currentPage)
{
currentPage->setMusicImage(defaultCover);
}
}
// 解析歌词
if(m_isPlayingSearchResult && !m_currentSearchMusicId.isEmpty())
{
auto it = musicList.findMusicById(m_currentSearchMusicId);
if(it != musicList.end())
{
QString lrcPath = it->getLrcFilePath();
lrcPage->parseLrcFile(lrcPath);
}
}
else if(currentPage)
{
int currentIndex = playList->currentIndex();
if(currentIndex >= 0 && currentIndex < currentPage->getMusicCount())
{
QString musicId = currentPage->getMusicIdByIndex(currentIndex);
auto it = musicList.findMusicById(musicId);
if(it != musicList.end())
{
QString lrcPath = it->getLrcFilePath();
lrcPage->parseLrcFile(lrcPath);
it->setIsHistory(true);
it->InsertMusicToDB();
}
}
}
}
}
调用逻辑:
- 元数据更新触发:当 QMediaPlayer 加载新歌曲并解析完元数据后,触发
metaDataAvailableChanged信号 - 执行流程:
- 更新主界面的歌曲名称和歌手
- 更新歌词页面的歌曲信息
- 更新封面图(从元数据中获取或使用默认封面)
- 解析并加载歌词文件
- 如果是从 CommonPage 播放的,标记为历史记录并更新数据库
LRC歌词同步
定义了 LyricLine 结构体,根据LRC文件标准格式逐行解析出时间和文本。
在当前播放位置变更时,通过时间查找当前应显示的歌词行索引,并同步更新前后三行歌词到 LrcPage 的Label上。
核心代码:
cpp
// LrcPage 解析歌词
void LrcPage::parseLrcFile(const QString &lrcPath)
{
QFile file(lrcPath);
if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
return;
lyricLines.clear();
QTextStream in(&file);
while(!in.atEnd())
{
QString line = in.readLine();
// 解析时间标签 [mm:ss.xx]
QRegExp reg("\\[(\\d+):(\\d+)(\\.\\d+)?\\]");
int pos = 0;
while((pos = reg.indexIn(line, pos)) != -1)
{
int minute = reg.cap(1).toInt();
int second = reg.cap(2).toInt();
qint64 time = minute * 60 * 1000 + second * 1000;
QString text = line.right(line.length() - reg.pos() - reg.matchedLength());
lyricLines.append({time, text});
pos += reg.matchedLength();
}
}
std::sort(lyricLines.begin(), lyricLines.end(), [](const LyricLine &a, const LyricLine &b) {
return a.time < b.time;
});
}
// 歌词同步
void LrcPage::updateCurrentLyric(qint64 position)
{
int currentIndex = -1;
for(int i = 0; i < lyricLines.size(); ++i)
{
if(position < lyricLines[i].time)
{
currentIndex = i - 1;
break;
}
}
if(currentIndex < 0) currentIndex = 0;
if(currentIndex >= lyricLines.size()) currentIndex = lyricLines.size() - 1;
// 更新显示
ui->currentLyric->setText(lyricLines[currentIndex].text);
if(currentIndex > 0) ui->prevLyric->setText(lyricLines[currentIndex - 1].text);
if(currentIndex < lyricLines.size() - 1) ui->nextLyric->setText(lyricLines[currentIndex + 1].text);
}
// 显示时同步歌曲信息
void LrcPage::setMusicInfo(const QString &musicName, const QString &musicSinger)
{
ui->musicName->setText(musicName);
ui->musicSinger->setText(musicSinger);
}
调用逻辑:
- 歌词解析:当播放新歌曲时,调用
parseLrcFile解析对应的LRC文件- 打开LRC文件并逐行读取
- 使用正则表达式解析时间标签 [mm:ss.xx]
- 提取时间和歌词文本,存储到
lyricLines容器 - 按时间排序
- 歌词同步:播放器位置变化时,调用
updateCurrentLyric更新显示- 根据当前播放位置查找对应的歌词行
- 更新当前、前一行和后一行歌词的显示
- 信息同步:显示歌词窗口时,调用
setMusicInfo同步歌曲信息
持久化存储 (SQLite)
目的
保存用户加载的音乐、收藏状态和播放历史,避免每次启动都要重新加载。
实现
在程序启动时,从 QQMusic.db 的 musicInfo 表中读取数据并恢复 MusicList。在程序退出(点击托盘退出)时,遍历 MusicList 将所有 MusicMessage 对象的信息(使用 INSERT OR UPDATE 逻辑)写入数据库。
核心代码:
cpp
// 数据库初始化
void Music::initSqlite()
{
sqlite = QSqlDatabase::addDatabase("QSQLITE");
sqlite.setDatabaseName("QQMusic.db");
if(!sqlite.open())
{
qDebug() << "数据库打开失败:" << sqlite.lastError().text();
return;
}
// 创建表
QSqlQuery query;
QString createTable = R"(
CREATE TABLE IF NOT EXISTS musicInfo(
id INTEGER PRIMARY KEY AUTOINCREMENT,
musicId varchar(50) UNIQUE,
musicName varchar(50),
musicSinger varchar(50),
albumName varchar(50),
duration BIGINT,
musicUrl varchar(256),
isLike INTEGER,
isHistory INTEGER
)
)";
if(!query.exec(createTable))
{
qDebug() << "创建表失败:" << query.lastError().text();
}
// 从数据库加载
musicList.readFromMusicDB();
// 刷新页面
ui->likePage->reFrash(musicList);
ui->localPage->reFrash(musicList);
ui->recentPage->reFrash(musicList);
}
// MusicList 从数据库读取
void MusicList::readFromMusicDB()
{
QSqlDatabase db = QSqlDatabase::database();
QSqlQuery query(db);
QString sql = "SELECT * FROM musicInfo";
if(query.exec(sql))
{
while(query.next())
{
QString musicId = query.value("musicId").toString();
QString musicName = query.value("musicName").toString();
QString musicSinger = query.value("musicSinger").toString();
QString albumName = query.value("albumName").toString();
qint64 duration = query.value("duration").toLongLong();
QString musicUrl = query.value("musicUrl").toString();
bool isLike = query.value("isLike").toBool();
bool isHistory = query.value("isHistory").toBool();
MusicMessage music(QUrl(musicUrl));
music.setMusicId(musicId);
music.setMusicName(musicName);
music.setSingerName(musicSinger);
music.setAlbumName(albumName);
music.setDuration(duration);
music.setIsLike(isLike);
music.setIsHistory(isHistory);
musicList.append(music);
musicPaths.insert(musicUrl);
}
}
}
// MusicMessage 写入数据库
void MusicMessage::InsertMusicToDB()
{
QSqlDatabase db = QSqlDatabase::database();
QSqlQuery query(db);
QString sql = R"(
INSERT OR REPLACE INTO musicInfo(
musicId, musicName, musicSinger, albumName, duration, musicUrl, isLike, isHistory
) VALUES(
:musicId, :musicName, :musicSinger, :albumName, :duration, :musicUrl, :isLike, :isHistory
)
)";
query.prepare(sql);
query.bindValue(":musicId", musicId);
query.bindValue(":musicName", musicName);
query.bindValue(":musicSinger", musicSinger);
query.bindValue(":albumName", musicAlbum);
query.bindValue(":duration", duration);
query.bindValue(":musicUrl", musicUrl.toString());
query.bindValue(":isLike", isLike);
query.bindValue(":isHistory", isHistory);
if(!query.exec())
{
qDebug() << "保存失败:" << query.lastError().text();
}
}
// 退出时保存
void Music::onMusicQuit()
{
// 所有音乐已经在修改时实时保存,这里可以做一些清理工作
qApp->quit();
}
调用逻辑:
- 数据库初始化:程序启动时调用
initSqlite创建表并加载数据- 打开或创建 SQLite 数据库
- 创建 musicInfo 表(如果不存在)
- 调用
musicList.readFromMusicDB()从数据库加载音乐 - 刷新所有 CommonPage 页面
- 数据写入:当音乐信息变更时,调用
InsertMusicToDB()实时保存- 使用
INSERT OR REPLACE语句处理重复记录 - 绑定所有音乐信息参数
- 执行 SQL 语句
- 使用
- 程序退出:点击托盘菜单的"退出"按钮,调用
onMusicQuit()退出程序
歌曲重复加载
使用 QSet musicPaths 存储文件绝对路径,在添加和数据库恢复时进行查重,实现O(1)高效去重。
核心代码:
cpp
// 添加音乐时去重
void MusicList::addMusicsByUrl(const QList<QUrl> &urls)
{
for(const QUrl &url : urls)
{
QString path = url.toString();
if(!musicPaths.contains(path))
{
// 添加音乐
musicPaths.insert(path);
}
}
}
// 从数据库恢复时去重
void MusicList::readFromMusicDB()
{
// ...
musicPaths.insert(musicUrl);
// ...
}
调用逻辑:
- 添加音乐时:检查
musicPaths集合中是否已存在该路径 - 数据库恢复时:将从数据库读取的音乐路径添加到
musicPaths集合
新增搜索功能
功能描述
- 支持按歌曲名、歌手名、专辑名进行模糊搜索
- 搜索结果以独立窗口显示
- 支持双击或点击播放按钮播放搜索结果
- 支持将搜索结果添加到播放列表
实现方案
SearchResultPage 类:
- 独立的搜索结果窗口
- 支持搜索结果的显示和操作
- 自适应搜索策略(内存搜索/SQL搜索)
核心代码:
cpp
// 搜索触发
void Music::onSearchTriggered(const QString &keyword)
{
if(keyword.isEmpty())
return;
searchResultPage->search(keyword, musicList, sqlite);
ui->searchEdit->clear();
}
// SearchResultPage 搜索
void SearchResultPage::search(const QString &keyword, MusicList &musicList, QSqlDatabase &sqlite)
{
if(keyword.isEmpty())
return;
m_searchResultIds.clear();
ui->resultListWidget->clear();
// 自适应搜索策略
if(musicList.size() >= SEARCH_THRESHOLD)
{
searchBySql(keyword, sqlite);
}
else
{
// 内存搜索
for(auto it = musicList.begin(); it != musicList.end(); ++it)
{
QString musicName = it->getMusicName();
QString singerName = it->getSingerName();
QString albumName = it->getAlbumName();
bool match = fuzzyMatch(musicName, keyword) ||
fuzzyMatch(singerName, keyword) ||
fuzzyMatch(albumName, keyword);
if(match)
{
m_searchResultIds.append(it->getMusicId());
// 添加到列表
QListWidgetItem *item = new QListWidgetItem(ui->resultListWidget);
item->setSizeHint(QSize(0, 60));
ui->resultListWidget->addItem(item);
// 创建列表项控件
SearchResultItem *resultItem = new SearchResultItem(*it, this);
ui->resultListWidget->setItemWidget(item, resultItem);
// 连接信号
connect(resultItem, &SearchResultItem::playMusic, this, [=]() {
emit playMusicBySearchResult(it->getMusicId());
});
connect(resultItem, &SearchResultItem::addToPlaylist, this, [=]() {
emit addMusicToPlaylist(it->getMusicId());
});
}
}
}
// 显示结果数量
ui->resultCountLabel->setText(QString("共找到 %1 首歌曲").arg(m_searchResultIds.size()));
if(m_searchResultIds.isEmpty())
{
QMessageBox::information(this, "提示", "没有找到匹配的歌曲");
}
else
{
show();
raise();
activateWindow();
}
}
// SQL 搜索
void SearchResultPage::searchBySql(const QString &keyword, QSqlDatabase &sqlite)
{
QSqlQuery query(sqlite);
QString sql = R"(
SELECT musicId FROM musicInfo
WHERE musicName LIKE :keyword OR
musicSinger LIKE :keyword OR
albumName LIKE :keyword
)";
query.prepare(sql);
query.bindValue(":keyword", "%" + keyword + "%");
if(query.exec())
{
while(query.next())
{
QString musicId = query.value("musicId").toString();
m_searchResultIds.append(musicId);
// 查找音乐对象并添加到列表
auto it = musicList.findMusicById(musicId);
if(it != musicList.end())
{
QListWidgetItem *item = new QListWidgetItem(ui->resultListWidget);
item->setSizeHint(QSize(0, 60));
ui->resultListWidget->addItem(item);
SearchResultItem *resultItem = new SearchResultItem(*it, this);
ui->resultListWidget->setItemWidget(item, resultItem);
connect(resultItem, &SearchResultItem::playMusic, this, [=]() {
emit playMusicBySearchResult(it->getMusicId());
});
connect(resultItem, &SearchResultItem::addToPlaylist, this, [=]() {
emit addMusicToPlaylist(it->getMusicId());
});
}
}
}
}
// 模糊匹配
bool SearchResultPage::fuzzyMatch(const QString &source, const QString &target)
{
if(source.isEmpty() || target.isEmpty())
return false;
QString sourceLower = source.toLower();
QString targetLower = target.toLower();
return sourceLower.contains(targetLower);
}
// 播放搜索结果
void Music::onPlaySearchResult(const QString &musicId)
{
auto it = musicList.findMusicById(musicId);
if(it == musicList.end())
{
QMessageBox::warning(this, "错误", "音乐不存在");
return;
}
// 标记正在播放搜索结果
m_isPlayingSearchResult = true;
m_currentSearchMusicId = musicId;
// 使用播放列表机制
playList->clear();
playList->addMedia(it->getMusicUrl());
playList->setCurrentIndex(0);
player->play();
}
// 添加搜索结果到播放列表
void Music::onAddSearchResultToPlaylist(const QString &musicId)
{
auto it = musicList.findMusicById(musicId);
if(it == musicList.end())
{
QMessageBox::warning(this, "错误", "音乐不存在");
return;
}
playList->addMedia(it->getMusicUrl());
QMessageBox::information(this, "提示", "已添加到播放列表");
}
调用逻辑:
- 搜索触发:用户在搜索框输入关键词并按回车
- 调用
onSearchTriggered方法 - 清空搜索输入框
- 调用
- 搜索执行:
SearchResultPage::search方法根据音乐数量选择搜索方式- 内存搜索:遍历
musicList,使用fuzzyMatch进行模糊匹配 - SQL搜索:执行 LIKE 查询,从数据库中获取匹配的音乐
- 构建搜索结果列表,显示在
resultListWidget中
- 播放操作:
- 点击搜索结果项的播放按钮,发射
playMusicBySearchResult信号 Music::onPlaySearchResult方法处理信号- 标记
m_isPlayingSearchResult = true和m_currentSearchMusicId - 清空播放列表,添加搜索结果,开始播放
- 点击搜索结果项的播放按钮,发射
- 添加到播放列表:
- 点击搜索结果项的"添加到播放列表"按钮,发射
addMusicToPlaylist信号 Music::onAddSearchResultToPlaylist方法处理信号- 将音乐添加到当前播放列表
- 点击搜索结果项的"添加到播放列表"按钮,发射
系统托盘功能
实现方案
- 最小化到托盘:点击关闭按钮时隐藏到系统托盘
- 单击恢复:单击托盘图标显示主窗口
- 右键菜单:提供"显示"和"退出"选项
核心代码:
cpp
// 系统托盘初始化(在 initUi 中)
QSystemTrayIcon *trayIcon = new QSystemTrayIcon(this);
trayIcon->setIcon(QIcon(":/images/tubiao.png"));
trayIcon->show();
QMenu *trayMenu = new QMenu(this);
trayMenu->addAction("显示", this, &QWidget::showNormal);
trayMenu->addAction("退出", this, &Music::onMusicQuit);
trayIcon->setContextMenu(trayMenu);
connect(trayIcon, &QSystemTrayIcon::activated, this, [=](QSystemTrayIcon::ActivationReason reason) {
if(reason == QSystemTrayIcon::Trigger) {
showNormal();
activateWindow();
}
});
// 关闭按钮处理
void Music::on_quit_clicked()
{
this->hide();
}
// 退出处理
void Music::onMusicQuit()
{
qApp->quit();
}
调用逻辑:
- 系统托盘初始化:程序启动时在
initUi中创建系统托盘图标和菜单 - 最小化到托盘:用户点击关闭按钮时,调用
on_quit_clicked隐藏主窗口 - 单击恢复:用户单击托盘图标时,触发
activated信号,显示并激活主窗口 - 右键菜单:用户右键点击托盘图标时,显示菜单,可选择"显示"或"退出"
项目总结
-
界面设计:
- 使用 Widget 嵌套和布局管理器构建复杂界面
- 自定义控件(BtForm、MusicSlider、RecBox等)提升用户体验
- 无边框窗口、窗口拖拽、阴影效果等特性
-
音乐管理:
- MusicMessage 封装音乐信息,MusicList 统一管理
- 元数据解析使用 QMediaPlayer
- 状态同步通过信号槽机制实现
- 去重处理使用 QSet 实现 O(1) 时间复杂度
-
播放控制:
- QMediaPlayer 和 QMediaPlaylist 实现完整的播放功能
- 播放进度双向同步
- 歌曲切换时的信息同步
- 多种播放模式支持
-
歌词同步:
- LRC文件解析
- 基于时间的歌词匹配
- 平滑的歌词显示效果
-
持久化存储:
- SQLite 数据库存储
- 实时数据更新
- 启动时数据恢复
-
搜索功能:
- 自适应搜索策略(内存/SQL)
- 模糊匹配算法
- 搜索结果的展示和操作
-
系统托盘:
- 最小化到托盘
- 单击恢复功能
- 右键菜单操作
该项目综合运用了Qt的核心功能,包括信号槽机制、事件处理、数据库操作、媒体播放等,是一个完整的桌面应用开发实践。