前言

酷狗、网抑云和 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-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 中实现桌面歌词的更多相关文章

  1. 如何在pyqt中自定义无边框窗口

    前言 之前写过很多关于无边框窗口并给窗口添加特效的博客,按照时间线罗列如下: 如何在pyqt中实现窗口磨砂效果 如何在pyqt中实现win10亚克力效果 如何在pyqt中通过调用SetWindowCo ...

  2. 如何在pyqt中给无边框窗口添加DWM环绕阴影

    前言 在之前的博客<如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果>中,我们实现了窗口的亚克力效果,同时也用SetWindowC ...

  3. 如何在pyqt中实现窗口磨砂效果

    磨砂效果的实现思路 这两周一直在思考怎么在pyqt上实现窗口磨砂效果,网上搜了一圈,全都是 C++ 的实现方法.正好今天查python的官方文档的时候看到了 ctypes 里面的 HWND,想想倒不如 ...

  4. 如何在pyqt中实现win10亚克力效果

    亚克力效果的实现思路 上一篇博客<如何在pyqt中实现窗口磨砂效果> 中实现了win7中的Aero效果,但是和win10的亚克力效果相比,Aero还是差了点内味.所以今天早上又在网上搜了一 ...

  5. 如何在pyqt中通过调用 SetWindowCompositionAttribute 实现Win10亚克力效果

    亚克力效果 在<如何在pyqt中实现窗口磨砂效果>和<如何在pyqt中实现win10亚克力效果>中,我们调用C++ dll来实现窗口效果,这种方法要求电脑上必须装有MSVC.V ...

  6. 如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)

    无边框窗体的实现思路 在pyqt中只要 self.setWindowFlags(Qt.FramelessWindowHint) 就可以实现边框的去除,但是没了标题栏也意味着窗口大小无法改变.窗口无法拖 ...

  7. 如何在pyqt中实现带动画的动态QMenu

    弹出菜单的视觉效果 QLineEdit 原生的菜单弹出效果十分生硬,而且样式很丑.所以照着Groove中单行输入框弹出菜单的样式和动画效果写了一个可以实现动态变化Item的弹出菜单,根据剪贴板的内容是 ...

  8. 如何在 pyqt 中捕获并处理 Alt+F4 快捷键

    前言 如果在 Windows 系统的任意一个窗口中按下 Alt+F4,默认行为是关闭窗口(或者最小化到托盘).对于使用了亚克力效果的窗口,使用 Alt+F4 最小化到托盘,再次弹出窗口的时候可能出现亚 ...

  9. 如何在pyqt中使用 QGraphicsView 实现图片查看器

    前言 在 PyQt 中可以使用很多方式实现照片查看器,最朴素的做法就是重写 QWidget 的 paintEvent().mouseMoveEvent 等事件,但是如果要在图像上多添加一些形状,那么在 ...

随机推荐

  1. 简单易懂的 Go 泛型使用和实现原理介绍

    原文:A gentle introduction to generics in Go by Dominik Braun 万俊峰Kevin:我看了觉得文章非常简单易懂,就征求了作者同意,翻译出来给大家分 ...

  2. 从OC角度思考OKR的底层逻辑

    原创不易,求分享.求一键三连 扩展阅读:什么是OKR OC:Organization Cultrue即组织文化,标题用OC纯粹为了装逼... ​自从接受公司文化建设工作后,思维发生了很大的变化,文化, ...

  3. Spring Ioc源码分析系列--前言

    Spring Ioc源码分析系列--前言 为什么要写这个系列文章 首先这是我个人很久之前的一个计划,拖了很久没有实施,现在算是填坑了.其次,作为一个Java开发者,Spring是绕不开的课题.在Spr ...

  4. 图解BM(Boyer-Moore)字符串匹配算法+代码实现

    简介 本篇文章主要分为两个大的部分,第一部分通过图解的方式讲解BM算法,第二部分则代码实现一个简易的BM算法. 基本概念 bm是一个字符串匹配算法,有实验统计,该算法是著名kmp算法性能的3-4倍,其 ...

  5. Spring Boot 2.7.0发布,2.5停止维护,节奏太快了吧

    这几天是Spring版本日,很多Spring工件都发布了新版本, Spring Framework 6.0.0 发布了第 4 个里程碑版本,此版本包含所有针对 5.3.20 的修复补丁,以及特定于 6 ...

  6. 探究MySQL中SQL查询的成本

    成本 什么是成本,即SQL进行查询的花费的时间成本,包含IO成本和CPU成本. IO成本:即将数据页从硬盘中读取到内存中的读取时间成本.通常1页就是1.0的成本. CPU成本:即是读取和检测是否满足条 ...

  7. Vue项目中的接口进阶使用

    创建services文件夹 1.文件夹apis.index.request的三个文件. 2.apis文件放接口 export const apis = { checkDeviceNo: '/api/c ...

  8. ExtJS 布局-Border 布局(Border layout)

    更新记录: 2022年6月11日 发布. 2022年6月1日 开始. 1.说明 边框布局允许根据区域(如中心.北部.南部.西部和东部)指定子部件的位置.还可以调整子组件的大小和折叠. 2.设置布局方法 ...

  9. 离线环境使用NuGet

    更新记录 本文迁移自Panda666原博客,原发布时间:2021年5月13日. 在Visual Studio中直接使用GUI的方式操作NuGet包非常的方便.如果喜欢命令行,也可以使用包管理控制台或者 ...

  10. 【Redis】ziplist压缩列表

    压缩列表 压缩列表是列表和哈希表的底层实现之一: 如果一个列表只有少量数据,并且数据类型是整数或者比较短的字符串,redis底层就会使用压缩列表实现. 如果一个哈希表只有少量键值对,并且每个键值对的键 ...