教你使用pyqt實現(xiàn)桌面歌詞功能
前言
酷狗、網抑云和 QQ 音樂都有桌面歌詞功能,這篇博客也將使用 pyqt 實現(xiàn)桌面歌詞功能,效果如下圖所示:

代碼實現(xiàn)
桌面歌詞部件 LyricWidget 在 paintEvent 中繪制歌詞。我們可以直接使用 QPainter.drawText 來繪制文本,但是通過這種方式無法對歌詞進行描邊。所以這里更換為 QPainterPath 來實現(xiàn),使用 QPainterPath.addText 將歌詞添加到繪制路徑中,接著使用 Qainter.strokePath 進行描邊,Qainter.fillPath 繪制歌詞,這里的繪制順序不能調換。
對于歌詞的高亮部分需要特殊處理,假設當前高亮部分的寬度為 w,我們需要對先前繪制歌詞的 QPainterPath 進行裁剪,只留下寬度為 w 的部分,此處通過 QPainterPath.intersected 計算與寬度為 w 的矩形路徑的交集來實現(xiàn)裁剪。
對于高亮部分的動畫,我們既可以使用傳統(tǒng)的 QTimer,也可以使用封裝地更加徹底的 QPropertyAnimation 來實現(xiàn)(本文使用后者)。這里需要進行動畫展示的是高亮部分,也就是說我們只需改變“高亮寬度”這個屬性即可。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)
# 創(chuàng)建桌面歌詞
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_()后記
至此關于桌面歌詞的實現(xiàn)方案已經介紹完畢,完整的播放器界面代碼可參見:https://github.com/zhiyiYo/Groove,以上
到此這篇關于教你使用pyqt實現(xiàn)桌面歌詞功能的文章就介紹到這了,更多相關pyqt實現(xiàn)桌面歌詞內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
pytorch torchvision.ImageFolder的用法介紹
今天小編就為大家分享一篇pytorch torchvision.ImageFolder的用法介紹,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-02-02
基于Python+Matplotlib繪制漸變色扇形圖與等高線圖
這篇文章主要為大家介紹了如何利用Python中的Matplotlib繪制漸變色扇形圖與等高線圖,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下方法2022-04-04
解決Pycharm中恢復被exclude的項目問題(pycharm source root)
今天小編就為大家分享一篇解決Pycharm中恢復被exclude的項目問題(pycharm source root),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-02-02

