如何在 pyqt 中实现桌面歌词

前言

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

如何在 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-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,以上~~

发表评论

相关文章