基于Qt的音乐播放器项目

项目概述

该项目是一个基于 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("."));
    }
}

调用逻辑

  • 音乐加载触发:用户点击"本地下载"页面的"添加本地音乐"按钮
  • 执行流程:
    1. 调用 QFileDialog::getOpenFileNames 打开文件选择对话框
    2. 将选择的文件路径转换为 QUrl 列表
    3. 调用 musicList.addMusicsByUrl(urls) 添加音乐
    4. 刷新所有 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 方法添加音乐,自动去重并写入数据库
  • 收藏状态更新:
    1. ListItemBox 中的收藏按钮被点击,发射 likeChanged 信号
    2. CommonPage 接收信号并转发 updateLikeMusic 信号
    3. Music 主窗口接收信号,更新 MusicMessage 的 isLike 状态
    4. 调用 InsertMusicToDB() 更新数据库
    5. 刷新所有 CommonPage 页面
  • 页面刷新:每个 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 中的"播放全部"按钮
    1. 发射 playAll 信号,传递页面类型
    2. Music 主窗口接收信号,确定目标页面
    3. 调用 playAllMusicOfCommonPage 方法
  • 双击播放触发:用户双击 CommonPage 中的列表项
    1. 发射 playMusicByIndex 信号,传递页面和索引
    2. Music 主窗口接收信号,调用 playAllMusicOfCommonPage 方法
  • 播放列表构建:
    1. 清空当前播放列表
    2. 调用 addMusicToPlayList 将页面中的音乐添加到播放列表
    3. 设置当前播放索引
    4. 开始播放

播放进度同步

双向同步。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);

调用逻辑

  • 播放进度更新:
    1. QMediaPlayer::positionChanged 信号触发 onPositionChanged 方法
    2. 更新音乐滑块的位置
    3. 更新当前时间显示
    4. 调用 lrcPage->updateCurrentLyric 更新歌词
  • 手动拖动进度条:
    1. 用户拖动 MusicSlider,触发 valueChanged 信号
    2. onMusicSliderChanged 方法计算新的播放位置
    3. 调用 player->setPosition 设置播放位置

歌曲切换联动

监听 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 信号
  • 执行流程:
    1. 更新主界面的歌曲名称和歌手
    2. 更新歌词页面的歌曲信息
    3. 更新封面图(从元数据中获取或使用默认封面)
    4. 解析并加载歌词文件
    5. 如果是从 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文件
    1. 打开LRC文件并逐行读取
    2. 使用正则表达式解析时间标签 [mm:ss.xx]
    3. 提取时间和歌词文本,存储到 lyricLines 容器
    4. 按时间排序
  • 歌词同步:播放器位置变化时,调用 updateCurrentLyric 更新显示
    1. 根据当前播放位置查找对应的歌词行
    2. 更新当前、前一行和后一行歌词的显示
  • 信息同步:显示歌词窗口时,调用 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 创建表并加载数据
    1. 打开或创建 SQLite 数据库
    2. 创建 musicInfo 表(如果不存在)
    3. 调用 musicList.readFromMusicDB() 从数据库加载音乐
    4. 刷新所有 CommonPage 页面
  • 数据写入:当音乐信息变更时,调用 InsertMusicToDB() 实时保存
    1. 使用 INSERT OR REPLACE 语句处理重复记录
    2. 绑定所有音乐信息参数
    3. 执行 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, "提示", "已添加到播放列表");
}

调用逻辑

  • 搜索触发:用户在搜索框输入关键词并按回车
    1. 调用 onSearchTriggered 方法
    2. 清空搜索输入框
  • 搜索执行:
    1. SearchResultPage::search 方法根据音乐数量选择搜索方式
    2. 内存搜索:遍历 musicList,使用 fuzzyMatch 进行模糊匹配
    3. SQL搜索:执行 LIKE 查询,从数据库中获取匹配的音乐
    4. 构建搜索结果列表,显示在 resultListWidget
  • 播放操作:
    1. 点击搜索结果项的播放按钮,发射 playMusicBySearchResult 信号
    2. Music::onPlaySearchResult 方法处理信号
    3. 标记 m_isPlayingSearchResult = truem_currentSearchMusicId
    4. 清空播放列表,添加搜索结果,开始播放
  • 添加到播放列表:
    1. 点击搜索结果项的"添加到播放列表"按钮,发射 addMusicToPlaylist 信号
    2. Music::onAddSearchResultToPlaylist 方法处理信号
    3. 将音乐添加到当前播放列表

系统托盘功能

实现方案

  • 最小化到托盘:点击关闭按钮时隐藏到系统托盘
  • 单击恢复:单击托盘图标显示主窗口
  • 右键菜单:提供"显示"和"退出"选项

核心代码

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 信号,显示并激活主窗口
  • 右键菜单:用户右键点击托盘图标时,显示菜单,可选择"显示"或"退出"

项目总结

  1. 界面设计

    • 使用 Widget 嵌套和布局管理器构建复杂界面
    • 自定义控件(BtForm、MusicSlider、RecBox等)提升用户体验
    • 无边框窗口、窗口拖拽、阴影效果等特性
  2. 音乐管理

    • MusicMessage 封装音乐信息,MusicList 统一管理
    • 元数据解析使用 QMediaPlayer
    • 状态同步通过信号槽机制实现
    • 去重处理使用 QSet 实现 O(1) 时间复杂度
  3. 播放控制

    • QMediaPlayer 和 QMediaPlaylist 实现完整的播放功能
    • 播放进度双向同步
    • 歌曲切换时的信息同步
    • 多种播放模式支持
  4. 歌词同步

    • LRC文件解析
    • 基于时间的歌词匹配
    • 平滑的歌词显示效果
  5. 持久化存储

    • SQLite 数据库存储
    • 实时数据更新
    • 启动时数据恢复
  6. 搜索功能

    • 自适应搜索策略(内存/SQL)
    • 模糊匹配算法
    • 搜索结果的展示和操作
  7. 系统托盘

    • 最小化到托盘
    • 单击恢复功能
    • 右键菜单操作

该项目综合运用了Qt的核心功能,包括信号槽机制、事件处理、数据库操作、媒体播放等,是一个完整的桌面应用开发实践。

相关推荐
小短腿的代码世界2 小时前
Qt国际化完全指南:从源码机制到工程实践
qt
2401_882273722 小时前
golang如何处理zip压缩包_golang zip压缩包处理思路
jvm·数据库·python
tankeven2 小时前
贪心算法(Greedy Algorithm)详解:从理论到C++实践
c++·算法
Hesionberger2 小时前
LeetCode72.编辑距离(多维动态规划)
java·开发语言·c++·python·算法
猫的玖月2 小时前
SQL语法简介
数据库·sql·oracle
郝学胜-神的一滴2 小时前
从底层看透Linux高性能服务器:epoll自定义封装与超时清理实战
linux·服务器·c++·网络协议·tcp/ip·unix
tjc199010052 小时前
Golang怎么实现分布式定时任务_Golang如何保证集群中定时任务不重复执行【进阶】
jvm·数据库·python
2301_773553622 小时前
构建 Go CLI 应用的最佳实践:纯 Go 交互式命令行库选型与使用指南
jvm·数据库·python
qq_372906932 小时前
c#如何添加按钮点击事件_c#添加按钮点击事件的几种常见用法
jvm·数据库·python