教你使用pyqt實現桌面歌詞功能
前言
酷狗、網抑雲和 QQ 音樂都有桌面歌詞功能,這篇博客也將使用 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,以上
到此這篇關於教你使用pyqt實現桌面歌詞功能的文章就介紹到這瞭,更多相關pyqt實現桌面歌詞內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Python編程使用PyQt5庫實現動態水波進度條示例
- Python中利用pyqt5制作指針鐘表顯示實時時間(指針時鐘)
- Qt編寫自定義控件實現抽獎轉盤
- python編程使用PyQt創建UE藍圖
- PyQt中實現自定義工具提示ToolTip的方法詳解