QT 音乐播放器【二】 歌词同步+滚动+特效

效果图

在这里插入图片描述


概述

  • 先整体说明一下这个效果的实现,你所看到的歌词都是QGraphicsObject,在QGraphicsView上绘制(paint)出来的。也就是说每一句歌词都是一个图元(item)
  1. 为什么用QGraphicsView框架?

    • 在做歌词滚动效果时,我看网上实现这一效果基本上都是用QLabel,这样或许简单很多,但是效果单一,且不够灵活。使用图形视图这套,图形项可以自由地被移动、缩放、旋转和编辑。当然主要还是为了提升自己,可以更熟悉这套框架。
  2. 如何解析歌词?

    • 这里解析的是lrc文件为一般的歌词文件,格式如下:格式是固定的,那么就可以通过正则表达式来解析。然后存放在一个QMap中,key是时间,value是歌词。
    [02:08.496]乌蒙山连着山外山
    [02:11.138]月光洒下了响水滩
    [02:13.993]有没有人能告诉我
    [02:16.487]可是苍天对你在呼唤
    
  3. 如何同步歌词?

    • QMediaPlayer中有一个信号positionChanged,播放音乐时会时刻刻触发,可以获取当前播放时间。然后和前面我们存放在QMap中的时间进行对比,所以QMap存放的时间格式要按positionChanged发出的时间格式来解析。但我试验过很多次俩者的时间都是无法精确相等的。这里采取的方案是遍历QMap,找到第一个时间大于等于positionChanged发出的时间,然后获取这个时间对应的歌词,这便是当前的歌词。然后通过当前的key在获取前后几句的歌词。
  4. 歌词滚动以及那些特效如何实现的?

    • 同步歌词的时候会获取当前歌词以及前后几句歌词,提前存好对应歌词的特效,如下:这里面存了一个QMap,里面存放了每一句歌词的属性,包括字体大小,位置,透明度等等。
     m_textMapInfolst << QMap<QString, QString>{
    {"index", "1"},
    {"font", "12"},
    {"y", "-100"},
    {"x", "300"},
    {"opacity", "0.2"}};
    

    我这里有七句歌词,那么就存七个QMap在一个QList中,当歌词刷新的时候就去遍历,根据QMap中的属性来设置item歌词,这里的图元要自己实现,重写paint函数。


代码

解析歌词
  • 解析的时候把格式设置GB 2312,不然会是乱码。按行已经QMediaPlayer的时间格式读取数据,并全部存放到listLyricsMap中。
bool Lyrics::readLyricsFile(QString lyricsPath)
{
    listLyricsMap.clear();
    QFile file(lyricsPath);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        listLyricsMap.clear();
        return false;
    }
    QTextStream in(&file);
    in.setCodec("GB 2312");
    QString line;
    while (!in.atEnd())
    {
        line = in.readLine();
        analysisLyricsFile(line);
    }

    return true;
}

bool Lyrics::analysisLyricsFile(QString line)
{
    if (line == NULL || line.isEmpty())
    {
        return false;
    }
    QRegExp timeRegExp("\\[(\\d+):(\\d+\\.\\d+)\\]");

    if (timeRegExp.indexIn(line) != -1)
    {
        qint64 totalTime = timeRegExp.cap(1).toInt() * 60000 + // 分钟
                           timeRegExp.cap(2).toFloat() * 1000; // 秒

        QString lyricText = line.mid(timeRegExp.matchedLength());
        listLyricsMap.insert(totalTime, lyricText);
    }
    return true;
}
歌词同步
  • 绑定信号
    connect(player, SIGNAL(positionChanged(qint64)),
            this, SLOT(updateTextTime(qint64)));
  • 读取对应listLyricsMap中的歌词

void MainWindow::updateTextTime(qint64 position)
{
    auto lrcMap = lyric->getListLyricsMap();
    qint64 previousTime = 0;
    qint64 currentLyricTime = 0;
    QMapIterator<qint64, QString> i(lrcMap);
    while (i.hasNext())
    {
        i.next();
        if (position < i.key())
        {
            QString currentLyric = lrcMap.value(previousTime);
            currentLyricTime = previousTime;
            break;
        }
        previousTime = i.key();
    }

    QStringList displayLyrics; // 存储将要显示的歌词列表。
    // 获取将要显示的歌词
    QMap<qint64, QString>::iterator it = lrcMap.find(currentLyricTime);
    // 显示前三句,如果it不是开头,就向前移动迭代器
    for (int i = 0; i < 3 && it != lrcMap.begin(); i++)
    {
        --it;
        displayLyrics.prepend(it.value());
    }

    // 重置迭代器
    it = lrcMap.find(currentLyricTime);
    QString currntStr = QString();
    // 显示当前句
    if (it != lrcMap.end())
    {
        currntStr = QString("<font color='red'>" + it.value() + "</font>");
        displayLyrics.append(it.value());
    }

    // 显示后三句
    for (int i = 0; i < 3 && it != lrcMap.end(); i++)
    {
        ++it;
        if (it != lrcMap.end())
        {
            displayLyrics.append(it.value());
        }
    }
    //更新显示
    imageViewWindow->textChanged(displayLyrics);
}
歌词特效
  • 同步于上述歌词的改动,清空场景遍历特效m_textMapInfolst,重新进行图元绘制
void ImageViewWindow::textChanged(QStringList &lsit)
{
 m_scene->clear();
 for (int index = 0; index < m_textMapInfolst.size(); index++)
 {
  const auto textInfoMap = m_textMapInfolst[index];
  GraphicsText *item = new GraphicsText();
  item->setStr(lsit[index]);
  item->setStrFont(textInfoMap["font"].toInt());
  item->setItemOffset(QPointF(textInfoMap["x"].toInt() + image_xoffset, textInfoMap["y"].toInt() + image_yoffset));
  item->setZValue(textInfoMap["index"].toInt());
  item->setOpacity(textInfoMap["opacity"].toFloat());
  m_items << item;
  m_scene->addItem(item);
 }
}
  • 自绘图元
void GraphicsText::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
 painter->setFont(m_font);
 if (m_font.pointSize() > 20)
 {
  painter->setPen(QPen(Qt::red));
 }
 else
 {
  painter->setPen(QPen(Qt::blue));
 }
 painter->drawText(offset, str);
}

总结

  • 实现这个功能遇到的问题挺多的,比如绘制文本的时候只有一根线显示,是要view设置setViewportUpdateMode(QGraphicsView::FullViewportUpdate),类似的问题挺多,还不好找。
  • 歌词特效这块还可以再扩展,字体,入场效果等都可以设置。
  • 当然这个功能还有很多可以优化的地方,BUG或许也不少,实现标题的功能的逻辑就是如上,可以作为参考。
  • 29
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
Qt中实现音乐播放器歌词可以通过以下步骤实现: 1. 创建一个QLabel用于显示当前播放的歌词,设置其字体、颜色、对齐方式等属性。 2. 创建一个QListWidget用于显示所有的歌词,将其隐藏。 3. 读取歌词文件,将歌词按照时间轴进行排序,存储到一个QMap中,其中key为歌词出现的时间,value为歌词内容。 4. 在音乐播放器中添加一个QTimer,每隔一段时间获取当前播放时间,从QMap中查找对应的歌词,将其显示在QLabel中。 5. 根据当前播放时间,将QListWidget中对应的歌词项高亮显示,同时将其前面和后面的歌词发送给其他对应的QLabel,实现动态效果。 以下是一个简单的示例代码: ```python from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QFont, QColor from PyQt5.QtWidgets import QWidget, QLabel, QListWidget, QListWidgetItem, QHBoxLayout, QVBoxLayout class MusicPlayer(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): # 创建QLabel用于显示当前播放的歌词 self.lyric_label = QLabel(self) self.lyric_label.setAlignment(Qt.AlignCenter) self.lyric_label.setFont(QFont('Arial', 20)) self.lyric_label.setStyleSheet('color: white') # 创建QListWidget用于显示所有的歌词 self.lyric_list = QListWidget(self) self.lyric_list.hide() # 将QLabel和QListWidget添加到水平布局中 hbox = QHBoxLayout() hbox.addWidget(self.lyric_label) hbox.addWidget(self.lyric_list) # 创建垂直布局并将水平布局添加到其中 vbox = QVBoxLayout() vbox.addStretch(1) vbox.addLayout(hbox) vbox.addStretch(1) self.setLayout(vbox) # 读取歌词文件并将歌词按照时间轴进行排序 self.lyric_map = {} with open('lyric.txt', 'r', encoding='utf-8') as f: for line in f.readlines(): if line.strip() == '': continue time_str, lyric = line.strip().split(']') time = self.time2ms(time_str[1:]) self.lyric_map[time] = lyric # 创建QTimer用于更新歌词 self.timer = QTimer(self) self.timer.timeout.connect(self.updateLyric) def time2ms(self, time_str): # 将时间字符串转换为毫秒数 minute, second = time_str.split(':') return int(minute) * 60 * 1000 + int(second) * 1000 def updateLyric(self): # 获取当前播放时间 current_time = self.player.getCurrentTime() # 在QMap中查找对应的歌词 lyric = '' for time in self.lyric_map.keys(): if current_time >= time: lyric = self.lyric_map[time] else: break # 将歌词显示在QLabel中 self.lyric_label.setText(lyric) # 将QListWidget中对应的歌词项高亮显示 for i in range(self.lyric_list.count()): item = self.lyric_list.item(i) if item.data(Qt.UserRole) <= current_time: item.setForeground(QColor(255, 255, 255)) item.setBackground(QColor(0, 0, 0)) else: item.setForeground(QColor(128, 128, 128)) item.setBackground(QColor(255, 255, 255)) # 将当前歌词的前面和后面的歌词发送给其他对应的QLabel for i in range(self.lyric_list.count()): item = self.lyric_list.item(i) if item.data(Qt.UserRole) == current_time: self.lyric_label.setText(item.text()) if i > 0: self.lyric_list.item(i - 1).setForeground(QColor(255, 255, 255)) self.lyric_list.item(i - 1).setBackground(QColor(0, 0, 0)) if i < self.lyric_list.count() - 1: self.lyric_list.item(i + 1).setForeground(QColor(255, 255, 255)) self.lyric_list.item(i + 1).setBackground(QColor(0, 0, 0)) def loadLyric(self, lyric_file): # 读取歌词文件并将歌词按照时间轴进行排序 self.lyric_map = {} with open(lyric_file, 'r', encoding='utf-8') as f: for line in f.readlines(): if line.strip() == '': continue time_str, lyric = line.strip().split(']') time = self.time2ms(time_str[1:]) self.lyric_map[time] = lyric # 将歌词添加到QListWidget中 self.lyric_list.clear() for time, lyric in self.lyric_map.items(): item = QListWidgetItem(lyric) item.setData(Qt.UserRole, time) item.setForeground(QColor(128, 128, 128)) item.setBackground(QColor(255, 255, 255)) self.lyric_list.addItem(item) def play(self): # 开始播放音乐并启动QTimer self.player.play() self.timer.start(100) def pause(self): # 暂停播放音乐并停止QTimer self.player.pause() self.timer.stop() # 相关问题: --相关问题--:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值