• 如何在 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 中注册了滚动动画,用于处理歌词长度大于视口宽度的情况。

     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)
    
    ">复制# 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,以上~~

  • 相关阅读:
    ICO操作Bean管理的(bean的作用域和生命周期)
    创建MySQL只读权限用户
    通过面积证明:两个函数相乘 / 相除的导数为什么长成这样?
    前端Vue的循环forEach等各种循环下,取每一项下面的有个对象里面的值方法
    (王道考研计算机网络)第五章传输层-第三节1-5:TCP拥塞控制
    wxPython 4.2.0 发布
    docker安装以及常用命令
    Win10 笔记本本地摄像头提供 Rtsp 视频流服务
    ARMday2(环境创建+工程配置+创建文件+单步调试)
    券商交易接口开放,为什么很少有人用量化软件QE做AIMD做AIMD,该用什么软件
  • 原文地址:https://www.cnblogs.com/zhiyiYo/p/16513008.html