教你使用pyqt实现桌面歌词功能

目录

  • 前言
  • 代码实现
  • 后记

前言 酷狗、网抑云和 QQ 音乐都有桌面歌词功能,这篇博客也将使用 pyqt 实现桌面歌词功能,效果如下图所示:


代码实现 桌面歌词部件 LyricWidgetpaintEvent 中绘制歌词。我们可以直接使用 QPainter.drawText 来绘制文本,但是通过这种方式无法对歌词进行描边。所以这里更换为 QPainterPath 来实现,使用 QPainterPath.addText 将歌词添加到绘制路径中,接着使用 Qainter.strokePath 进行描边,Qainter.fillPath 绘制歌词,这里的绘制顺序不能调换。
对于歌词的高亮部分需要特殊处理,假设当前高亮部分的宽度为 w,我们需要对先前绘制歌词的 QPainterPath 进行裁剪,只留下宽度为 w 的部分,此处通过 QPainterPath.intersected 计算与宽度为 w 的矩形路径的交集来实现裁剪。
对于高亮部分的动画,我们既可以使用传统的 QTimer,也可以使用封装地更加彻底的 QPropertyAnimation 来实现(本文使用后者)。这里需要进行动画展示的是高亮部分,也就是说我们只需改变“高亮宽度”这个属性即可。PyQt 为我们提供了 pyqtProperty,类似于 python 自带的 property,使用 pyqtProperty 可以给部件注册一个属性,该属性可以搭配动画来食用。
除了高亮动画外,我们还在 LyricWidget 中注册了滚动动画,用于处理歌词长度大于视口宽度的情况。
# coding:utf-8from PyQt5.QtCore import QPointF, QPropertyAnimation, Qt, pyqtPropertyfrom PyQt5.QtGui import (QColor, QFont, QFontMetrics, QPainter, QPainterPath,QPen)from PyQt5.QtWidgets import QWidgetconfig = {"lyric.font-color": [255, 255, 255],"lyric.highlight-color": [0, 153, 188],"lyric.font-size": 50,"lyric.stroke-size": 5,"lyric.stroke-color": [0, 0, 0],"lyric.font-family": "Microsoft YaHei","lyric.alignment": "Center"}class LyricWidget(QWidget):""" Lyric widget """def __init__(self, parent=None):super().__init__(parent=parent)self.setAttribute(Qt.WA_TranslucentBackground)self.lyric = []self.duration = 0self.__originMaskWidth = 0self.__translationMaskWidth = 0self.__originTextX = 0self.__translationTextX = 0self.originMaskWidthAni = QPropertyAnimation(self, b'originMaskWidth', self)self.translationMaskWidthAni = QPropertyAnimation(self, b'translationMaskWidth', self)self.originTextXAni = QPropertyAnimation(self, b'originTextX', self)self.translationTextXAni = QPropertyAnimation(self, b'translationTextX', self)def paintEvent(self, e):if not self.lyric:returnpainter = QPainter(self)painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)# draw original lyricself.__drawLyric(painter,self.originTextX,config["lyric.font-size"],self.originMaskWidth,self.originFont,self.lyric[0])if not self.hasTranslation():return# draw translation lyricself.__drawLyric(painter,self.translationTextX,25 + config["lyric.font-size"]*5/3,self.translationMaskWidth,self.translationFont,self.lyric[1])def __drawLyric(self, painter: QPainter, x, y, width, font: QFont, text: str):""" draw lyric """painter.setFont(font)# draw background textpath = QPainterPath()path.addText(QPointF(x, y), font, text)painter.strokePath(path, QPen(QColor(*config["lyric.stroke-color"]), config["lyric.stroke-size"]))painter.fillPath(path, QColor(*config['lyric.font-color']))# draw foreground textpainter.fillPath(self.__getMaskedLyricPath(path, width),QColor(*config['lyric.highlight-color']))def __getMaskedLyricPath(self, path: QPainterPath, width: float):""" get the masked lyric path """subPath = QPainterPath()rect = path.boundingRect()rect.setWidth(width)subPath.addRect(rect)return path.intersected(subPath)def setLyric(self, lyric: list, duration: int, update=False):""" set lyricParameters----------lyric: listlist contains original lyric and translation lyricduration: intlyric duration in millisecondsupdate: boolupdate immediately or not"""self.lyric = lyric or [""]self.duration = max(duration, 1)self.__originMaskWidth = 0self.__translationMaskWidth = 0# stop running animationsfor ani in self.findChildren(QPropertyAnimation):if ani.state() == ani.Running:ani.stop()# start scroll animation if text is too longfontMetrics = QFontMetrics(self.originFont)w = fontMetrics.width(lyric[0])if w > self.width():x = self.width() - wself.__setAnimation(self.originTextXAni, 0, x)else:self.__originTextX = self.__getLyricX(w)self.originTextXAni.setEndValue(None)# start foreground color animationself.__setAnimation(self.originMaskWidthAni, 0, w)if self.hasTranslation():fontMetrics = QFontMetrics(self.translationFont)w = fontMetrics.width(lyric[1])if w > self.width():x = self.width() - wself.__setAnimation(self.translationTextXAni, 0, x)else:self.__translationTextX = self.__getLyricX(w)self.translationTextXAni.setEndValue(None)self.__setAnimation(self.translationMaskWidthAni, 0, w)if update:self.update()def __getLyricX(self, w: float):""" get the x coordinate of lyric """alignment = config["lyric.alignment"]if alignment == "Right":return self.width() - welif alignment == "Left":return 0return self.width()/2 - w/2def getOriginMaskWidth(self):return self.__originMaskWidthdef getTranslationMaskWidth(self):return self.__translationMaskWidthdef getOriginTextX(self):return self.__originTextXdef getTranslationTextX(self):return self.__translationTextXdef setOriginMaskWidth(self, pos: int):self.__originMaskWidth = posself.update()def setTranslationMaskWidth(self, pos: int):self.__translationMaskWidth = posself.update()def setOriginTextX(self, pos: int):self.__originTextX = posself.update()def setTranslationTextX(self, pos):self.__translationTextX = posself.update()def __setAnimation(self, ani: QPropertyAnimation, start, end):if ani.state() == ani.Running:ani.stop()ani.setStartValue(start)ani.setEndValue(end)ani.setDuration(self.duration)def setPlay(self, isPlay: bool):""" set the play status of lyric """for ani in self.findChildren(QPropertyAnimation):if isPlay and ani.state() != ani.Running and ani.endValue() is not None:ani.start()elif not isPlay and ani.state() == ani.Running:ani.pause()def hasTranslation(self):return len(self.lyric) == 2def minimumHeight(self) -> int:size = config["lyric.font-size"]h = size/1.5+60 if self.hasTranslation() else 40return int(size+h)@propertydef originFont(self):font = QFont(config["lyric.font-family"])font.setPixelSize(config["lyric.font-size"])return font@propertydef translationFont(self):font = QFont(config["lyric.font-family"])font.setPixelSize(config["lyric.font-size"]//1.5)return fontoriginMaskWidth = pyqtProperty(float, getOriginMaskWidth, setOriginMaskWidth)translationMaskWidth = pyqtProperty(float, getTranslationMaskWidth, setTranslationMaskWidth)originTextX = pyqtProperty(float, getOriginTextX, setOriginTextX)translationTextX = pyqtProperty(float, getTranslationTextX, setTranslationTextX)

上述代码对外提供了两个接口 setLyric(lyric, duration, update)setPlay(isPlay),用于更新歌词和控制歌词动画的开始与暂停。下面是一个最小使用示例,里面使用 Qt.SubWindow 标志使得桌面歌词可以在主界面最小化后仍然显示在桌面上,同时不会多出一个应用图标(Windows 是这样,Linux 不一定):
class Demo(QWidget):def __init__(self):super().__init__(parent=None)# 创建桌面歌词self.desktopLyric = QWidget()self.lyricWidget = LyricWidget(self.desktopLyric)self.desktopLyric.setAttribute(Qt.WA_TranslucentBackground)self.desktopLyric.setWindowFlags(Qt.FramelessWindowHint | Qt.SubWindow | Qt.WindowStaysOnTopHint)self.desktopLyric.resize(800, 300)self.lyricWidget.resize(800, 300)# 必须有这一行才能显示桌面歌词界面self.desktopLyric.show()# 设置歌词self.lyricWidget.setLyric(["Test desktop lyric style", "测试桌面歌词样式"], 3000)self.lyricWidget.setPlay(True)if __name__ == '__main__':app = QApplication(sys.argv)w = Demo()w.show()app.exec_()


后记 至此关于桌面歌词的实现方案已经介绍完毕,完整的播放器界面代码可参见:https://github.com/zhiyiYo/Groove,以上
【教你使用pyqt实现桌面歌词功能】到此这篇关于教你使用pyqt实现桌面歌词功能的文章就介绍到这了,更多相关pyqt实现桌面歌词内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    推荐阅读