前言
酷狗、网抑云和 QQ 音乐都有桌面歌词功能,这篇博客也将使用 pyqt 实现桌面歌词功能,效果如下图所示:
代码实现
桌面歌词部件 LyricWidget
在 paintEvent
中绘制歌词。我们可以直接使用 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-8 from PyQt5.QtCore import QPointF, QPropertyAnimation, Qt, pyqtProperty from PyQt5.QtGui import (QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPen) from PyQt5.QtWidgets import QWidget config = { "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 = 0 self.__originMaskWidth = 0 self.__translationMaskWidth = 0 self.__originTextX = 0 self.__translationTextX = 0 self.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: return painter = QPainter(self) painter.setRenderHints( QPainter.Antialiasing | QPainter.TextAntialiasing) # draw original lyric self.__drawLyric( painter, self.originTextX, config["lyric.font-size"], self.originMaskWidth, self.originFont, self.lyric[0] ) if not self.hasTranslation(): return # draw translation lyric self.__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 text path = 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 text painter.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 lyric Parameters ---------- lyric: list list contains original lyric and translation lyric duration: int lyric duration in milliseconds update: bool update immediately or not """ self.lyric = lyric or [""] self.duration = max(duration, 1) self.__originMaskWidth = 0 self.__translationMaskWidth = 0 # stop running animations for ani in self.findChildren(QPropertyAnimation): if ani.state() == ani.Running: ani.stop() # start scroll animation if text is too long fontMetrics = QFontMetrics(self.originFont) w = fontMetrics.width(lyric[0]) if w > self.width(): x = self.width() - w self.__setAnimation(self.originTextXAni, 0, x) else: self.__originTextX = self.__getLyricX(w) self.originTextXAni.setEndValue(None) # start foreground color animation self.__setAnimation(self.originMaskWidthAni, 0, w) if self.hasTranslation(): fontMetrics = QFontMetrics(self.translationFont) w = fontMetrics.width(lyric[1]) if w > self.width(): x = self.width() - w self.__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() - w elif alignment == "Left": return 0 return self.width()/2 - w/2 def getOriginMaskWidth(self): return self.__originMaskWidth def getTranslationMaskWidth(self): return self.__translationMaskWidth def getOriginTextX(self): return self.__originTextX def getTranslationTextX(self): return self.__translationTextX def setOriginMaskWidth(self, pos: int): self.__originMaskWidth = pos self.update() def setTranslationMaskWidth(self, pos: int): self.__translationMaskWidth = pos self.update() def setOriginTextX(self, pos: int): self.__originTextX = pos self.update() def setTranslationTextX(self, pos): self.__translationTextX = pos self.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) == 2 def minimumHeight(self) -> int: size = config["lyric.font-size"] h = size/1.5+60 if self.hasTranslation() else 40 return int(size+h) @property def originFont(self): font = QFont(config["lyric.font-family"]) font.setPixelSize(config["lyric.font-size"]) return font @property def translationFont(self): font = QFont(config["lyric.font-family"]) font.setPixelSize(config["lyric.font-size"]//1.5) return font originMaskWidth = 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实现桌面歌词内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!