目錄
前言
酷狗、網(wǎng)抑云和 QQ 音樂(lè)都有桌面歌詞功能,這篇博客也將使用 pyqt 實(shí)現(xiàn)桌面歌詞功能,效果如下圖所示:
代碼實(shí)現(xiàn)
桌面歌詞部件 LyricWidget
在 paintEvent
中繪制歌詞。我們可以直接使用 QPainter.drawText
來(lái)繪制文本,但是通過(guò)這種方式無(wú)法對(duì)歌詞進(jìn)行描邊。所以這里更換為 QPainterPath
來(lái)實(shí)現(xiàn),使用 QPainterPath.addText
將歌詞添加到繪制路徑中,接著使用 Qainter.strokePath
進(jìn)行描邊,Qainter.fillPath
繪制歌詞,這里得繪制順序不能調(diào)換。
對(duì)于歌詞得高亮部分需要特殊處理,假設(shè)當(dāng)前高亮部分得寬度為 w
,我們需要對(duì)先前繪制歌詞得 QPainterPath
進(jìn)行裁剪,只留下寬度為 w
得部分,此處通過(guò) QPainterPath.intersected
計(jì)算與寬度為 w
得矩形路徑得交集來(lái)實(shí)現(xiàn)裁剪。
對(duì)于高亮部分得動(dòng)畫(huà),我們既可以使用傳統(tǒng)得 QTimer
,也可以使用封裝地更加徹底得 QPropertyAnimation
來(lái)實(shí)現(xiàn)(本文使用后者)。這里需要進(jìn)行動(dòng)畫(huà)展示得是高亮部分,也就是說(shuō)我們只需改變“高亮寬度”這個(gè)屬性即可。PyQt 為我們提供了 pyqtProperty
,類似于 python 自帶得 property
,使用 pyqtProperty
可以給部件注冊(cè)一個(gè)屬性,該屬性可以搭配動(dòng)畫(huà)來(lái)食用。
除了高亮動(dòng)畫(huà)外,我們還在 LyricWidget
中注冊(cè)了滾動(dòng)動(dòng)畫(huà),用于處理歌詞長(zhǎng)度大于視口寬度得情況。
# coding:utf-8from PyQt5.QtCore import QPointF, QPropertyAnimation, Qt, pyqtPropertyfrom PyQt5.QtGui import (QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPen)from PyQt5.QtWidgets import QWidgetconfig = { "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)
上述代碼對(duì)外提供了兩個(gè)接口 setLyric(lyric, duration, update)
和 setPlay(isPlay)
,用于更新歌詞和控制歌詞動(dòng)畫(huà)得開(kāi)始與暫停。下面是一個(gè)最小使用示例,里面使用 Qt.SubWindow
標(biāo)志使得桌面歌詞可以在主界面最小化后仍然顯示在桌面上,同時(shí)不會(huì)多出一個(gè)應(yīng)用圖標(biāo)(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() # 設(shè)置歌詞 self.lyricWidget.setLyric(["Test desktop lyric style", "測(cè)試桌面歌詞樣式"], 3000) self.lyricWidget.setPlay(True)if __name__ == '__main__': app = QApplication(sys.argv) w = Demo() w.show() app.exec_()
后記
至此關(guān)于桌面歌詞得實(shí)現(xiàn)方案已經(jīng)介紹完畢,完整得播放器界面代碼可參見(jiàn):https://github.com/zhiyiYo/Groove,以上
到此這篇關(guān)于教你使用pyqt實(shí)現(xiàn)桌面歌詞功能得內(nèi)容就介紹到這了,更多相關(guān)pyqt實(shí)現(xiàn)桌面歌詞內(nèi)容請(qǐng)搜索之家以前得內(nèi)容或繼續(xù)瀏覽下面得相關(guān)內(nèi)容希望大家以后多多支持之家!