
一、项目介绍这篇文章介绍我用 Qt 做的一个桌面项目 ——MusiPlayer一个仿 QQ 音乐界面的本地音乐播放器。界面、播放、数据存储都是基于 Qt Widgets 从零写的不依赖任何现成的音乐接口播的全是本地音乐文件。项目不大但功能比较完整6 个自定义控件、3 种播放模式、SQLite 持久化、LRC 歌词同步另外还处理了一些桌面应用绕不开的细节 —— 无边框窗口拖拽、系统托盘、单实例运行。下面先看整体效果推荐页面本地下载喜欢的音乐最近播放播放歌曲歌词界面二、功能音乐管理本地音乐导入文件对话框支持多选添加自动过滤掉非音乐文件只收 mp3/flac重复添加同一首歌会被跳过不会在列表里出现两份歌曲信息自动解析导入时从媒体文件元数据里自动读出歌名、歌手、专辑、时长文件标签不全的就显示 未知歌手 / 未知专辑不会摆一堆空白三个列表页本地下载、我喜欢的、最近播放三个页面共用同一套页面模板只是填充的数据不同播放控制基础操作播放 / 暂停、上一曲 / 下一曲三种播放模式列表循环、随机播放、单曲循环点一下按钮循环切换图标跟着变进度条 seek进度条可以拖拽松手后从指定位置继续播播放时间、总时长实时显示格式 mm:ss音量控制滑杆调音量一键静音静音会先记住当前音量取消静音后自动恢复不会一取消就变 0歌曲信息同步播放时底部同步显示当前歌曲的封面图、歌名、歌手封面直接从音乐文件的元数据里取收藏与历史红心收藏列表里每首歌都有红心点了就收藏 / 取消收藏三个页面同步刷新底部播放栏的红心状态也跟着变最近播放只要播放过的歌自动记进 最近播放 页面数据持久化收藏状态和历史记录都写进 SQLite程序重启后依然在不用每次重新收一遍歌词LRC 解析与同步播放时自动找到同目录下的 .lrc 歌词文件逐行解析当前唱到哪句高亮哪句前后 3 行也会显示出来独立歌词窗口可以弹出来单独看歌词再点一下收起带 500ms 的滑动动画编码自适应歌词文件不管是 UTF-8 还是 GBK 编码都能正确显示中文不会乱码推荐页与桌面细节推荐页轮播主界面左侧点 推荐右侧是两组推荐卡片今日为你推荐、你的歌曲补给站可以上下翻页循环鼠标悬停时卡片会上浮无边框窗口窗口没有系统边框鼠标按住任意位置可以拖拽移动系统托盘点关闭按钮不会真退出而是隐藏到系统托盘托盘右键菜单可以显示 / 退出单实例运行同一时间只允许开一个 MusiPlayer重复启动会弹提示而不是开两个悬停反馈左侧导航按钮悬停高亮、点击后右侧有跳动的竖条动画音量按钮悬停会弹出音量滑杆鼠标移开后自动收起三、技术架构整个程序分成三层加一个中枢Widget 主窗口是中枢持有播放器、播放列表、歌单容器、数据库连接这些核心对象下面是 UI 自定义控件层、播放引擎层、数据持久化层。层与层之间不直接互相调用全部通过信号槽传消息。① UI 自定义控件层—— 这是我花时间最多的地方。界面上的控件大多不是现成的 QPushButton、QLabel 直接拼而是自己封装btForm左侧 6 个导航按钮悬停高亮 选中动画RecBox推荐页轮播卡片组可翻页循环CommonPage三个列表页共用的页面模板一个类实例化三次ListItemBox列表项序号 / 歌名 / 歌手 / 专辑 / 红心MusicSlider进度条VolumeTool悬浮音量滑杆LrcPage歌词窗口封装最大的好处是复用CommonPage 一个类撑起 我喜欢、本地下载、最近播放 三个页面ListItemBox 通过setItemWidget嵌进 QListWidget改一处样式三处生效。② 播放引擎层—— 用 Qt Multimedia 的 QMediaPlayer QMediaPlaylist。QMediaPlayer 负责解码播放、音量、位置、时长和封面元数据QMediaPlaylist 负责歌单和播放模式。三种播放模式列表循环 / 随机 / 单曲循环其实是它内置的能力我只要在按钮点击时切换枚举、同步图标就行不用自己实现轮播逻辑。③ 数据持久化层—— 歌曲数据没有只存在内存里而是落进 SQLite。Music 是歌曲模型用 UUID 当唯一 IDMusicList 是QVectorMusic的容器负责添加、查找、导入去重SQLite 里一张 musicInfo 表存所有状态启动时readFromDB恢复界面退出时writeToDB保存。信号槽怎么串起来的—— 三条最典型的链路收藏列表项点红心 → ListItemBox 发setIsLike→ CommonPage 转发updateLikeMusic→ Widget 改 MusicList 里的状态 → 三个页面统一刷新 底部红心同步。界面控件完全不碰数据全部汇到 Widget 一处处理。播放双击列表项或点 全部播放 → CommonPage 发信号 → Widget 按页面类型把对应歌曲装进 QMediaPlaylist → QMediaPlayer 开始播放。进度拖进度条 → MusicSlider 发位置信号 → Widget 换算成毫秒调setPosition()完成 seek反过来播放位置变化时Widget 再更新进度条和歌词。层间不直接互相调用全部通过信号槽解耦 —— Widget 是唯一的数据操作入口这个设计让我写界面控件时不用管数据从哪来改数据时不用管界面怎么刷新两边只认信号。这也是我对 Qt 信号槽机制理解最深的一处。四、核心实现1. 自定义控件一个类撑起三个页面三个列表页我喜欢、本地下载、最近播放布局完全一样只是数据不同。我一开始也想过各写一个页面类但很快就发现没必要 —— 它们的行为模式是一样的按类型筛选歌曲 → 往列表里塞条目 → 响应播放 / 收藏。所以我只写了一个CommonPage用枚举PageType区分实例化三次。筛选逻辑也很简单一个addMusicToPage就搞定本地页收所有歌我喜欢页只收isLike true的最近播放页只收isHistory true的。往列表里塞条目用的是setItemWidget把自定义的ListItemBox嵌进QListWidgetItemQListWidgetItem *item new QListWidgetItem(ui-pageMusicList); item-setSizeHint(QSize(ui-pageMusicList-width(), 45)); ui-pageMusicList-setItemWidget(item, listItemBox);这样改一处样式、修一个 bug三个页面同时生效。后来加功能比如收藏同步刷新基本都是一次改动覆盖全局。2. 数据持久化SQLite 存状态导入自动去重歌曲的收藏状态、历史记录要是只在内存里程序一关就没了。所以我把这些状态写进了 SQLite一张musicInfo表字段就是歌曲的全部信息加上isLike、isHistory两个状态位。写入时我做了个处理先查musicId在不在表里在的话只更新状态字段不在才整条插入// 已存在 → UPDATE isLike/isHistory不存在 → INSERT 完整记录 query.prepare(SELECT EXISTS (SELECT 1 FROM musicInfo WHERE musicId ?));导入去重也是当时出现bug才加的文件对话框支持多选用户手滑把同一首歌加两遍列表里就出现两条。我的做法是按本地文件路径去重添加前先遍历一遍路径相同就跳过。查找歌曲时也统一按路径匹配和添加逻辑保持一致不会出现 列表里明明有却查不到 的错位。另外有个细节歌曲的唯一 ID 我用的是 UUID而不是列表索引。一开始我是按索引记当前播放的歌曲后来发现列表增删一次索引全变播放状态就串了。改成 UUID 之后歌曲的身份和它在列表里的位置彻底解耦稳定性好很多。3. LRC 歌词编码自动识别 时间戳解析歌词这块最大的坑是编码。网上下的 .lrc 文件有的是 UTF-8有的是 GBK直接按一种编码读中文全是乱码。我做了个自动识别先按 UTF-8 解码如果解码出非法字符就判定是 GBK重新解码QTextCodec::ConverterState state; QString content utf8Codec-toUnicode(data, size, state); if (state.invalidChars 0) // UTF-8 解出非法字符 content QTextCodec::codecForName(GBK)-toUnicode(data);解析用正则的思路手写每行格式是[mm:ss.xx]歌词先把[和]之间的时间戳取出来分钟 × 60 秒、秒 × 1000、毫秒直接加拼成总的毫秒数存进LrcLine{time, text}结构体数组空行直接过滤掉。同步就靠播放器的positionChanged信号播放位置一变就在歌词数组里找 当前时间落在哪一行之间锁定当前行显示它和前后各 3 行当前行高亮。歌词窗口是独立的无边框窗口点按钮弹出 / 收起配了 500ms 的QPropertyAnimation滑动动画效果是窗口从上往下滑出来。4. 播放引擎三种模式、seek 和封面播放核心就两个类QMediaPlayer负责解码和播放QMediaPlaylist负责歌单。播放模式不用自己写轮播逻辑 —— 列表循环、随机、单曲循环都是QMediaPlaylist内置的我只需要在按钮点击时按顺序切换枚举值再同步一下图标和 tooltip。播放 / 暂停有个容易写崩的边界播放器的状态有播放、暂停、停止三种按钮点一下要分别处理。我的写法是三分支if (player-state() QMediaPlayer::PlayingState) player-pause(); else if (player-state() QMediaPlayer::PausedState) player-play(); else if (player-state() QMediaPlayer::StoppedState) { if (playList-currentIndex() -1) // 停止且没有当前曲目 playList-setCurrentIndex(0); player-play(); }seek 是双向同步的拖进度条 → 自定义的MusicSlider发位置信号 → 换算成毫秒调setPosition()反过来播放位置变化时再更新进度条和时间显示。封面是从音乐文件元数据里直接取的player-metaData(ThumbnailImage)拿到 QImage显示在底部播放栏和当前页面取不到就退回一张默认图不会显示破图。5. 桌面细节无边框、托盘、单实例、音量记忆桌面应用有些细节不做用起来就很别扭这块我单独处理了几件事无边框拖拽窗口设了FramelessWindowHint去掉系统边框再用QGraphicsDropShadowEffect加阴影不然会 飘 在屏幕上很怪。拖拽靠重写mousePressEvent/mouseMoveEvent记录按下时的偏移量移动时跟着走。这里有个坑不加标志位的话点任何按钮都可能触发窗口移动所以加了个isDrag标志只有长按住左键才进入拖拽。音量滑杆的悬停收起音量滑块是悬浮窗口鼠标移到音量按钮上才弹出。难点是移出后什么时候收起 —— 如果鼠标正从按钮滑向滑块中间会短暂离开两个区域直接收起就会闪。我的处理是加 160ms 延时再判断QTimer::singleShot(160, this, []() { QPoint p QCursor::pos(); bool inBtn ui-playVolume-rect().contains(ui-playVolume-mapFromGlobal(p)); bool inTool volumeTool-rect().contains(volumeTool-mapFromGlobal(p)); if (!inBtn !inTool) volumeTool-hide(); });静音记忆静音前把当前音量存进m_lastVolume取消静音时恢复不会出现一取消就变 0 的情况。托盘和单实例点关闭按钮不是退出而是隐藏到系统托盘托盘菜单里有 显示 / 退出。退出时才真正writeToDB落库再关闭。单实例用QSharedMemory启动时尝试 attach能 attach 说明已经有实例在跑直接弹提示退出。五、问题记录1. 歌词全是乱码现象导入的歌有 .lrc 歌词文件播放时歌词窗口全是乱码排查查了一圈才发现.lrc 文件的编码不是统一的 —— 有的 UTF-8有的 GBK。我一开始固定按 GBK 读UTF-8 的文件乱改成固定 UTF-8GBK 的又乱。怎么改都有一半是错的。修复不能靠猜得自动识别。先按 UTF-8 解码如果解码过程出现非法字符就判定为 GBK 重新解码。总共就 3 行代码但排查花了一晚上最后发现 QT 的QTextCodec::ConverterState能直接告诉我解码有没有出错。2. 同一首歌加进列表两遍现象往本地列表添加歌曲列表里出现两条一模一样的歌排查文件对话框支持多选选中的文件列表里可能本身就有重复更常见的是手滑把同一首歌加了两轮。当时的MusicList::addMusicByUrl拿到 URL 就push_back完全没考虑去重。修复添加前先按本地文件路径遍历检查已经存在的直接跳过。顺手把查找逻辑也统一成按路径匹配保证 添加时的判断 和 查找时的判断 用的是同一套规则不会出现列表里有、却查不到的错位。3. 点导航按钮窗口跟着鼠标跑现象点击左侧导航按钮切换页面偶尔窗口会跟着鼠标移动一段距离非常奇怪排查主窗口为了支持拖拽重写了mousePressEvent但条件没写严 —— 任何位置的左键按下都会置isDrag true。而左侧导航按钮内部是自定义 QWidget鼠标点下去事件会冒泡到主窗口被当成 准备拖窗口。修复两头一起改。一是拖拽标志只在真正需要时置位二是给导航按钮装了事件过滤器点击时event-accept()并返回true消费掉事件不让它继续往上冒泡。这也是我对 Qt 事件传播机制理解最深的一次。六、收获项目做完收获最大的其实不是哪个 API 用得熟而是几个设计习惯界面和数据解耦控件只管发信号数据只在一个地方改出问题好定位加功能好下手复用CommonPage 一个类撑起三个页面少写了两倍的重复代码还顺带保证了三个页面行为一致面向对象建模把一首歌抽象成Music、把歌单抽象成MusicList后面加收藏、加历史记录都只是加两个字段的事稳定 ID 的意识用 UUID 而不是列表索引当歌曲身份索引会变ID 不会七、扩展方向做完之后我很清楚它离 产品 还差不少如果继续做优先级大概是搜索功能搜索框的位置已经留好、皮肤切换、接在线音乐接口现在纯本地、歌词逐字滚动。这几个我都有大致思路后面会逐个补上。这就是 MusiPlayer 项目的全部内容 —— 一个纯本地的 Qt 音乐播放器界面、播放、数据存储全部自己实现。如果你也在写 Qt 项目希望这篇文章能有一点参考价值。