网络编程
位置:首页>> 网络编程>> Python编程>> 教你使用pyqt实现桌面歌词功能

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

作者:之一Yo  发布时间:2021-04-22 16:11:04 

标签:pyqt,桌面歌词

前言

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

教你使用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,以上

来源:https://www.cnblogs.com/zhiyiYo/p/16513008.html

0
投稿

猜你喜欢

手机版 网络编程 asp之家 www.aspxhome.com