前言
酷狗、网抑云和 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
中注册了滚动动画,用于处理歌词长度大于视口宽度的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
|
# 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 不一定):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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实现桌面歌词内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!
原文链接:https://www.cnblogs.com/zhiyiYo/p/16513008.html