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

前言

使用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();
}

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

完整Demo下载

相关推荐
鄃鳕1 小时前
QSS【QT】
开发语言·qt
全栈开发圈2 小时前
新书速览|OpenCV计算机视觉开发实践:基于Qt C++
qt·opencv·计算机视觉
学习同学4 小时前
设计模式 建造者模式
qt·设计模式·建造者模式
LoveXming7 小时前
Qt工具栏中添加按钮QAction
开发语言·qt
whoarethenext8 小时前
使用c++调用deepseek的api(附带源码)
服务器·开发语言·c++·qt·deepseek
永不停转10 小时前
QT 实现 C++ 数据类与 json 的转换
c++·qt
忒可君15 小时前
QT 5.15 程序打包
开发语言·c++·qt
永不停转16 小时前
借助 QT 的反射机制实现数据类的序列化
c++·qt
musk121216 小时前
qt 配置 mysql 驱动问题:Cannot load library qsqlmysql;QMYSQL driver not loaded
qt·mysql