当前位置: 首页 > news >正文

Qt QML实现Windows桌面歌词动态播放效果

前言

使用Qt5.15.2,QML实现简单的歌词动态播放效果。
效果图如下:
在这里插入图片描述
注:这里只是为了演示播放效果,并未真正加载音频进行播放。可以在此基础上进行扩展。

正文

关键代码
QML部分

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import LyricsPlayback 1.0Window {id: mainWindowwidth: 800height: 500visible: truetitle: "歌词播放器"// 设置窗口透明color: "transparent"flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground// 自定义播放位置属性property int customPosition: 0// 播放状态属性property bool isPlaying: falseItem{anchors.fill: parent// 允许拖动窗口MouseArea {anchors.fill: parentproperty point clickPos: "0,0"onPressed: {clickPos = Qt.point(mouse.x, mouse.y)}onPositionChanged: {if (pressed) {var delta = Qt.point(mouse.x - clickPos.x, mouse.y - clickPos.y)mainWindow.x += delta.xmainWindow.y += delta.y}}}// 歌词模型LyricsModel {id: lyricsModel}// 不再使用MediaPlayer组件// 主布局Rectangle {anchors.fill: parentcolor: "#80000000" // 半透明黑色背景radius: 10ColumnLayout {anchors.fill: parentanchors.margins: 10spacing: 10// 歌词显示区域ListView {id: lyricsViewLayout.fillWidth: trueLayout.fillHeight: truemodel: lyricsModelclip: truespacing: 5highlightFollowsCurrentItem: truehighlightMoveDuration: 200// 自动滚动到当前歌词onCountChanged: {if (lyricsModel.currentIndex >= 0) {positionViewAtIndex(lyricsModel.currentIndex, ListView.Center)}}Connections {target: lyricsModelfunction onCurrentIndexChanged() {if (lyricsModel.currentIndex >= 0) {lyricsView.positionViewAtIndex(lyricsModel.currentIndex, ListView.Center)}}}delegate: Item {width: lyricsView.widthheight: 40Text {id: lyricTextanchors.centerIn: parentwidth: parent.widthhorizontalAlignment: Text.AlignHCentertext: model.textfont.pixelSize: model.isCurrent ? 24 : 18font.bold: model.isCurrentcolor: model.isCurrent ? Qt.rgba(1, 1, 1, 1) : "#80FFFFFF" // 当前行为白色,其他为半透明白色elide: Text.ElideRight// 当前播放行的渐变效果Rectangle {visible: model.isCurrentanchors.left: parent.leftanchors.top: parent.topanchors.bottom: parent.bottomwidth: parent.width * lyricsModel.progresscolor: "transparent"clip: trueText {anchors.left: parent.leftanchors.top: parent.toptext: model.textfont.pixelSize: 24font.bold: truecolor: "#FF4081" // 高亮颜色width: lyricText.widthhorizontalAlignment: Text.AlignHCenterelide: Text.ElideRight}}}}}// 控制栏RowLayout {Layout.fillWidth: trueheight: 40spacing: 10Button {text: "加载示例歌词"onClicked: {lyricsModel.loadSampleLyrics()isPlaying = falseplayTimer.stop()customPosition = 0lyricsModel.position = 0}}Button {text: isPlaying ? "暂停" : "播放"onClicked: {if (isPlaying) {isPlaying = falseplayTimer.stop()} else {isPlaying = trueplayTimer.start()}}}Button {text: "退出"onClicked: {Qt.quit()}}Slider {Layout.fillWidth: truefrom: 0to: 40000 // 40秒value: customPositiononMoved: {customPosition = valuelyricsModel.position = value}}}}}// 由于没有实际的音频文件,使用定时器模拟播放进度Timer {id: playTimerinterval: 100repeat: truerunning: falseonTriggered: {if (customPosition < 40000) { // 40秒customPosition += 100// 更新歌词模型的位置lyricsModel.position = customPosition} else {stop()customPosition = 0lyricsModel.position = 0}}}Component.onCompleted: {lyricsModel.loadSampleLyrics()}}
}

通过设置歌词格式和时间来判定播放进度

void LyricsModel::setPosition(qint64 position)
{if (m_position != position) {m_position = position;emit positionChanged();// 更新当前歌词索引updateCurrentIndex();// 如果在当前歌词行内,更新进度if (m_currentIndex >= 0 && m_currentIndex < m_lyrics.size()) {emit progressChanged();}}
}QString LyricsModel::lyricsText() const
{return m_lyricsText;
}void LyricsModel::setLyricsText(const QString &text)
{if (m_lyricsText != text) {m_lyricsText = text;emit lyricsTextChanged();// 解析歌词beginResetModel();m_lyrics.clear();if (m_parser.parseLyrics(text)) {QMap<qint64, QString> parsedLyrics = m_parser.getLyrics();QMapIterator<qint64, QString> it(parsedLyrics);while (it.hasNext()) {it.next();LyricLine line;line.time = it.key();line.text = it.value();m_lyrics.append(line);}}endResetModel();// 重置当前索引setCurrentIndex(-1);updateCurrentIndex();}
}qint64 LyricsModel::currentStartTime() const
{if (m_currentIndex >= 0 && m_currentIndex < m_lyrics.size()) {return m_lyrics.at(m_currentIndex).time;}return 0;
}qint64 LyricsModel::currentEndTime() const
{if (m_currentIndex >= 0 && m_currentIndex < m_lyrics.size() - 1) {return m_lyrics.at(m_currentIndex + 1).time;}return 0;
}qreal LyricsModel::progress() const
{if (m_currentIndex >= 0 && m_currentIndex < m_lyrics.size()) {qint64 startTime = m_lyrics.at(m_currentIndex).time;qint64 endTime;if (m_currentIndex < m_lyrics.size() - 1) {// 非最后一行,使用下一行的时间作为结束时间endTime = m_lyrics.at(m_currentIndex + 1).time;} else {// 最后一行,使用开始时间加上固定时长(4秒)作为结束时间endTime = startTime + 4000;}if (endTime > startTime) {if (m_position >= startTime) {if (m_position <= endTime) {// 在时间范围内,正常计算进度return static_cast<qreal>(m_position - startTime) / (endTime - startTime);} else {// 超过结束时间,显示100%进度return 1.0;}}}}return 0.0;
}void LyricsModel::updateCurrentIndex()
{// 查找当前时间对应的歌词行int newIndex = -1;for (int i = 0; i < m_lyrics.size() - 1; ++i) {if (m_position >= m_lyrics.at(i).time && m_position < m_lyrics.at(i + 1).time) {newIndex = i;break;}}// 处理最后一行歌词if (newIndex == -1 && !m_lyrics.isEmpty() && m_position >= m_lyrics.last().time) {newIndex = m_lyrics.size() - 1;}setCurrentIndex(newIndex);
}void LyricsModel::loadSampleLyrics()
{QString sampleLyrics = "[00:00.00]示例歌词 - 测试\n""[00:02.00]作词:测试\n""[00:04.00]作曲:测试\n""[00:06.00]\n""[00:08.00]这是第一行歌词\n""[00:12.00]这是第二行歌词示例\n""[00:16.00]这是第三行歌词演示文本\n""[00:20.00]这是第四行歌词测试内容\n""[00:24.00]这是第五行歌词展示效果\n""[00:28.00]这是最后一行歌词\n""[00:32.00]\n";setLyricsText(sampleLyrics);
}

歌词解析部分:

bool LyricsParser::parseLyrics(const QString &lyricsText)
{// 清空之前的歌词clear();// 按行分割歌词文本QStringList lines = lyricsText.split('\n');// 正则表达式匹配时间标签 [mm:ss.xx]QRegularExpression timeRegex("\\[(\\d+):(\\d+)\\.(\\d+)\\]");for (const QString &line : lines) {// 跳过空行if (line.trimmed().isEmpty()) {continue;}QString remainingLine = line;QRegularExpressionMatch match;int position = 0;// 查找所有时间标签while ((match = timeRegex.match(remainingLine, position)).hasMatch()) {int minutes = match.captured(1).toInt();int seconds = match.captured(2).toInt();int milliseconds = match.captured(3).toInt();// 计算总毫秒数qint64 timeMs = minutes * 60 * 1000 + seconds * 1000 + milliseconds * 10; // 通常LRC中的时间戳精度为百分之一秒position = match.capturedEnd();// 提取歌词文本(去除所有时间标签后的内容)QString lyricText = remainingLine;lyricText.remove(timeRegex);lyricText = lyricText.trimmed();if (!lyricText.isEmpty()) {m_lyrics.insert(timeMs, lyricText);}}}return !m_lyrics.isEmpty();
}

注:这里只是为了演示播放效果,并未真正加载音频进行播放。可以在此基础上进行扩展。

完整Demo下载

相关文章:

  • QtApplets-实现应用程序单例模式,防止重复运行
  • 2025年Q2(流动式)起重机司机考试题
  • 【Windows本地部署n8n工作流自动平台结合内网穿透远程在线访问】
  • Ubuntu利用docker搭建Java相关环境记录(二)
  • Vision Transformer项目分析与介绍
  • 压缩包网页预览(zip-html-preview)
  • Apache Atlas构建安装(Linux)
  • Python 深度学习 第8章 计算机视觉中的深度学习 - 卷积神经网络使用实例
  • YOLO训练多评价指标曲线画图
  • 【2025“华中杯”大学生数学建模挑战赛】选题分析 A题 详细解题思路
  • k8s报错kubelet.go:2461] “Error getting node“ err=“node \“k8s-master\“ not found“
  • 【秣厉科技】LabVIEW工具包——OpenCV 教程(20):拾遗 - imgproc 基础操作(下)
  • Python实例题:Python自动化开发-考勤处理
  • iptables防火墙
  • 深入浅出 Redis:核心数据结构解析与应用场景Redis 数据结构
  • 简述Apache RocketMQ
  • R语言简介与下载安装
  • 面试题之高频面试题
  • 扩展欧几里得算法【Exgcd】的内容与题目应用
  • MySQL-数据查询(CASE练习)-01
  • 国际观察丨美中东政策生变,以色列面临艰难选择
  • 广西桂林、百色、河池等地表态:全力配合中央对蓝天立的审查调查
  • 外交部驻港公署正告美政客:威胁恫吓撼动不了中方维护国家安全的决心
  • 从近200件文物文献里,回望光华大学建校百年
  • 中国恒大披露清盘进展:要求债权人提交债权证明表
  • 哈马斯官员:若实现永久停火,可交出加沙地带控制权