前言
使用Qt5.15.2,QML实现简单的歌词动态播放效果。
效果图如下:
注:这里只是为了演示播放效果,并未真正加载音频进行播放。可以在此基础上进行扩展。
正文
关键代码
QML部分
bash
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import LyricsPlayback 1.0
Window {
id: mainWindow
width: 800
height: 500
visible: true
title: "歌词播放器"
// 设置窗口透明
color: "transparent"
flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground
// 自定义播放位置属性
property int customPosition: 0
// 播放状态属性
property bool isPlaying: false
Item{
anchors.fill: parent
// 允许拖动窗口
MouseArea {
anchors.fill: parent
property 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.x
mainWindow.y += delta.y
}
}
}
// 歌词模型
LyricsModel {
id: lyricsModel
}
// 不再使用MediaPlayer组件
// 主布局
Rectangle {
anchors.fill: parent
color: "#80000000" // 半透明黑色背景
radius: 10
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// 歌词显示区域
ListView {
id: lyricsView
Layout.fillWidth: true
Layout.fillHeight: true
model: lyricsModel
clip: true
spacing: 5
highlightFollowsCurrentItem: true
highlightMoveDuration: 200
// 自动滚动到当前歌词
onCountChanged: {
if (lyricsModel.currentIndex >= 0) {
positionViewAtIndex(lyricsModel.currentIndex, ListView.Center)
}
}
Connections {
target: lyricsModel
function onCurrentIndexChanged() {
if (lyricsModel.currentIndex >= 0) {
lyricsView.positionViewAtIndex(lyricsModel.currentIndex, ListView.Center)
}
}
}
delegate: Item {
width: lyricsView.width
height: 40
Text {
id: lyricText
anchors.centerIn: parent
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: model.text
font.pixelSize: model.isCurrent ? 24 : 18
font.bold: model.isCurrent
color: model.isCurrent ? Qt.rgba(1, 1, 1, 1) : "#80FFFFFF" // 当前行为白色,其他为半透明白色
elide: Text.ElideRight
// 当前播放行的渐变效果
Rectangle {
visible: model.isCurrent
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * lyricsModel.progress
color: "transparent"
clip: true
Text {
anchors.left: parent.left
anchors.top: parent.top
text: model.text
font.pixelSize: 24
font.bold: true
color: "#FF4081" // 高亮颜色
width: lyricText.width
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
}
}
// 控制栏
RowLayout {
Layout.fillWidth: true
height: 40
spacing: 10
Button {
text: "加载示例歌词"
onClicked: {
lyricsModel.loadSampleLyrics()
isPlaying = false
playTimer.stop()
customPosition = 0
lyricsModel.position = 0
}
}
Button {
text: isPlaying ? "暂停" : "播放"
onClicked: {
if (isPlaying) {
isPlaying = false
playTimer.stop()
} else {
isPlaying = true
playTimer.start()
}
}
}
Button {
text: "退出"
onClicked: {
Qt.quit()
}
}
Slider {
Layout.fillWidth: true
from: 0
to: 40000 // 40秒
value: customPosition
onMoved: {
customPosition = value
lyricsModel.position = value
}
}
}
}
}
// 由于没有实际的音频文件,使用定时器模拟播放进度
Timer {
id: playTimer
interval: 100
repeat: true
running: false
onTriggered: {
if (customPosition < 40000) { // 40秒
customPosition += 100
// 更新歌词模型的位置
lyricsModel.position = customPosition
} else {
stop()
customPosition = 0
lyricsModel.position = 0
}
}
}
Component.onCompleted: {
lyricsModel.loadSampleLyrics()
}
}
}
通过设置歌词格式和时间来判定播放进度
cpp
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);
}
歌词解析部分:
cpp
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();
}
注:这里只是为了演示播放效果,并未真正加载音频进行播放。可以在此基础上进行扩展。