第三十三章 音频与视频
《快速掌握PyQt5》专栏已整理成书出版,书名为《PyQt编程快速上手》,详情请见该链接。感谢大家一直以来的支持!祝大家PyQt用得越来越顺!
如果能往应用程序中的按钮上面加个提示音,这样用户每次点击按钮的感觉就会更好,程序也就显得更加讨人喜欢。而要做游戏的话,那对声音的要求就更高了。当然不仅仅是声音,程序中偶尔也需要用到视频播放的功能。PyQt5对多媒体的处理能力已经非常不错了,本章我们会详细介绍跟音频和视频有关的类,并带大家一起做一个简单的音乐播放器来巩固。希望读完本章的小伙伴们能够很好的掌握往程序中添加声音和视频的方法。
33.1 QSound
想要简单快速地播放wav音频文件,使用QSound类并调用play()方法即可:
import sys
from PyQt5.QtMultimedia import QSound
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.sound = QSound('sound.wav', self) # 1
self.play_btn = QPushButton('Play Sound', self)
self.play_btn.clicked.connect(self.sound.play) # 2
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 传入wav文件的路径来实例化一个QSound类;
2. 将按钮的信号和play()槽函数进行连接,这样每次点击按钮就会播放声音了。
看下文档我们看到这个QSound类还提供这些常用的方法:
我们给上面这个程序加个停止播放的功能,也就是使用stop()方法:
import sys
from PyQt5.QtMultimedia import QSound
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.sound = QSound('sound.wav')
self.sound.setLoops(QSound.Infinite) # 1
self.play_btn = QPushButton('Play Sound', self)
self.stop_btn = QPushButton('Stop Sound', self)
self.play_btn.clicked.connect(self.sound.play)
self.stop_btn.clicked.connect(self.sound.stop)
self.h_layout = QHBoxLayout()
self.h_layout.addWidget(self.play_btn)
self.h_layout.addWidget(self.stop_btn)
self.setLayout(self.h_layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 因为音频本来就很短,为了演示功能,就调用setLoops()方法传入QSound.Infinite参数让声音无限循环播放。传入相应的正整数会播放相应的次数。播放后再点击停止按钮就可以停止声音了。
运行截图如下:
33.2 QSoundEffect
该类可以用来播放无压缩的音频文件(典型的是wav文件),不过我们可以通过这个类以低延迟的方式来播放音频文件,而且可以对文件进行更精细化的操作,该类非常适合用来播放交互音效,如弹出框的提示音,游戏音效等:
import sys
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtMultimedia import QSoundEffect
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QSlider, QCheckBox, QHBoxLayout, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.sound_effect = QSoundEffect(self)
self.sound_effect.setSource(QUrl.fromLocalFile('sound.wav')) # 1
self.sound_effect.setVolume(1.0) # 2
self.play_btn = QPushButton('Play Sound', self)
self.play_btn.clicked.connect(self.sound_effect.play)
self.slider = QSlider(Qt.Horizontal, self) # 3
self.slider.setRange(0, 10)
self.slider.setValue(10)
self.slider.valueChanged.connect(self.set_volume_func)
self.checkbox = QCheckBox('Mute', self) # 4
self.checkbox.stateChanged.connect(self.mute_func)
self.h_layout = QHBoxLayout()
self.v_layout = QVBoxLayout()
self.h_layout.addWidget(self.play_btn)
self.h_layout.addWidget(self.checkbox)
self.v_layout.addWidget(self.slider)
self.v_layout.addLayout(self.h_layout)
self.setLayout(self.v_layout)
def set_volume_func(self):
self.sound_effect.setVolume(self.slider.value()/10)
def mute_func(self):
if self.sound_effect.isMuted():
self.sound_effect.setMuted(False)
else:
self.sound_effect.setMuted(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 实例化QSoundEffect类,然后调用setSource()来设置音频源,需要传入QUrl类型参数;
2. setVolume()方法可以设置播放音频时的音量大小,参数为浮点型数值。1.0代表全音量播放,0.0代表静音;
3. 我们加一个QSlider滑动条控件来控制音量大小,在槽函数set_volume_func()中我们将滑动条的当前数值除以10来获取到一个浮点型数值,将该数值作为setVolume()方法的参数;
4. QCheckBox复选框来控制是否需要静音,在mute_func()槽函数中我们先用isMuted()方法判断当前是否已经静音,没有的话则调用setMute()方法来进行相应的设置。
运行截图如下:
33.3 QMovie
QMoive通常被用来播放时间短且没有声音的动态图像,也就是gif和mng类型的文件。该类同时提供了许多方法让我们来操控动态图像,现在通过下面这个例子来了解下:
该程序会通过各个按钮来控制gif图像,比如停止,快进,截图等。
代码如下:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QMovie, QIcon
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QHBoxLayout, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.movie = QMovie(self) # 1
self.movie.setFileName('images/zootopia.gif')
self.movie.jumpToFrame(0)
self.label = QLabel(self) # 2
self.label.setAlignment(Qt.AlignCenter)
self.label.setMovie(self.movie)
self.speed = 100 # 3
self.start_btn = QPushButton(self) # 4
self.pause_btn = QPushButton(self)
self.stop_btn = QPushButton(self)
self.fast_btn = QPushButton(self)
self.back_btn = QPushButton(self)
self.screenshot_btn = QPushButton(self)
self.start_btn.setIcon(QIcon('images/start.png'))
self.pause_btn.setIcon(QIcon('images/pause.png'))
self.stop_btn.setIcon(QIcon('images/stop.png'))
self.fast_btn.setIcon(QIcon('images/fast_forward.png'))
self.back_btn.setIcon(QIcon('images/back_forward.png'))
self.screenshot_btn.setIcon(QIcon('images/screenshot.png'))
self.start_btn.clicked.connect(lambda: self.btn_func(self.start_btn))
self.pause_btn.clicked.connect(lambda: self.btn_func(self.pause_btn))
self.stop_btn.clicked.connect(lambda: self.btn_func(self.stop_btn))
self.fast_btn.clicked.connect(lambda: self.btn_func(self.fast_btn))
self.back_btn.clicked.connect(lambda: self.btn_func(self.back_btn))
self.screenshot_btn.clicked.connect(lambda: self.btn_func(self.screenshot_btn))
self.h_layout = QHBoxLayout()
self.v_layout = QVBoxLayout()
self.h_layout.addWidget(self.back_btn)
self.h_layout.addWidget(self.start_btn)
self.h_layout.addWidget(self.pause_btn)
self.h_layout.addWidget(self.stop_btn)
self.h_layout.addWidget(self.fast_btn)
self.h_layout.addWidget(self.screenshot_btn)
self.v_layout.addWidget(self.label)
self.v_layout.addLayout(self.h_layout)
self.setLayout(self.v_layout)
def btn_func(self, btn): # 5
if btn == self.start_btn:
self.movie.start()
elif btn == self.pause_btn:
self.movie.setPaused(True)
elif btn == self.stop_btn:
self.movie.stop()
self.movie.jumpToFrame(0)
elif btn == self.fast_btn:
self.speed *= 2
self.movie.setSpeed(self.speed)
elif btn == self.back_btn:
self.speed /= 2
self.movie.setSpeed(self.speed)
elif btn == self.screenshot_btn:
self.movie.currentPixmap().save('zootopia.png')
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 实例化一个QMovie对象,再调用setFileName()方法来设置要播放的gif文件。动画由一张张帧组成,而jumpToFrame()这个方法可以选择让gif跳转到某一帧。这里我们传入0,意思是让程序初始化时就让gif文件跳转到第一帧。如果没有调用jumpToFrame(0),那程序刚开始运行起来会是下面这样:
但是如果刚开始传入比0大的数也是上图这样,也就是说在初始化的时候我们最多显示第一帧,如果要使用jumpToFrame()跳转到某一帧的话该帧必须先播放过;
2. QLabel控件用于显示QMovie;
3. self.speed变量用于控制播放速度,在之后快进和快退功能中用到;
4. 实例化几个按钮,分别是:开始、暂停、停止、快进、快退和截图。接着设置按钮的图标,并进行信号和槽的连接;
5. 在槽函数中,我们先判断传入的按钮种类,再进行相应操作:
- start_btn: 调用start()方法来开始播放gif文件
- pause_btn: 调用setPaused(True)来暂停播放,传入False则继续播放
- stop_btn: 调用stop()方法停止播放,此时如果再调用start()方法的话,那gif文件会从第一帧开始播放。这里我们再使用jumpToFrame(0)目的是为了在停止播放后,画面直接跳转到第一帧(在MacOS上这行代码无效)
- fast_btn和back_btn: 先调整self.speed变量大小,在传入setSpeed()方法中来控制播放速度
- screenshot_btn: 使用currentPixmap()方法获取当前帧(此时还是QPixmap类型),接着再调用save()方法将该帧保存到本地
图片下载地址:
start.png: https://www.easyicon.net/download/png/1222647/64/
pause.png: https://www.easyicon.net/download/png/1222644/64/
stop.png: https://www.easyicon.net/download/png/1222651/64/
fast_forward.png: https://www.easyicon.net/download/png/1222639/64/
back_forward.png: https://www.easyicon.net/download/png/1222638/64/
screenshot.png: https://www.easyicon.net/download/png/1168943/64/
当进行某些耗时操作时,我们通常会显示一个加载动画来缓解用户心情,QMovie其实经常被拿来这么用。下面是一个简单例子:
import sys
from PyQt5.QtGui import QMovie
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.label = QLabel('None', self)
self.label.setAlignment(Qt.AlignCenter)
self.movie = QMovie(self)
self.movie.setFileName('loading.gif')
self.btn = QPushButton('Start', self)
self.btn.clicked.connect(self.start_countdown_func)
self.thread = MyThread()
self.thread.ok_signal.connect(self.show_result_func)
self.v_layout = QVBoxLayout()
self.v_layout.addWidget(self.label)
self.v_layout.addWidget(self.btn)
self.setLayout(self.v_layout)
def start_countdown_func(self):
self.label.setMovie(self.movie)
self.movie.start()
self.thread.start()
def show_result_func(self):
self.movie.stop()
self.label.setText("Time's Up!")
class MyThread(QThread):
ok_signal = pyqtSignal()
def __init__(self):
super(MyThread, self).__init__()
self.countdown = 1000000
def run(self):
while self.countdown > 0:
self.countdown -= 1
print(self.countdown)
self.ok_signal.emit()
self.countdown = 1000000
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
当点击按钮时,我们开启多线程来进行耗时操作,在self.countdown变量还大于0时,显示loading.gif。等self.countdown变量小于0时,循环结束,发射信号,self.label显示"Time's Up!"文本。
运行截图如下,刚开始状态:
点击Start按钮后,多线程进行耗时操作,程序界面显示loading.gif:
耗时操作结束后,self.label显示“Time's Up!”文本:
33.4 QMediaPlayer
QMediaPlayer是一个高层次的媒体播放器类,它非常强大,用它我们既可以播放音频,也可以播放视频(在PyQt4中使用Phonon这个模块来播放音视频,现在已被QMediaPlayer代替)。下面先看一个简单的播放音频的例子:
import sys
from PyQt5.Qt import QUrl
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.player = QMediaPlayer(self) # 1
self.media_content = QMediaContent(QUrl.fromLocalFile('/Users/louis/Downloads/music.mp3')) # 2
# self.player.setMedia(QMediaContent(QUrl('http://example.com/music.mp3')))
self.player.setMedia(self.media_content) # 3
self.player.setVolume(80) # 4
self.player.play() # 5
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 首先实例化一个QMediaPlayer;
2. 设置要播放的音频文件,首先要实例化一个QMediaContent类型的对象,在实例化时要传入文件的路径(可以是本地文件,也可以是网络文件)。之后该QMediaContent对象会作为参数传入setMedia()方法中。
3. 调用setMedia()方法来设置播放内容;
4. 调用setVolume()来设置音量。100为默认值(最高音量),传入0相当于静音;
5. 调用play()方法播放音频文件。
音频文件(和之后用到的视频文件)我已经放到以下链接了,读者可以前往下载或者用自己的媒体文件:
链接:https://pan.baidu.com/s/1BKCkPgTsogYgLfKuG1Pv6Q 密码:2y5y
以上只是播放一个音频文件,那如果要播放一连串的文件呢?这时候就需要用到QMediaPlaylist这个类了:
import sys
from PyQt5.Qt import QUrl
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.playlist = QMediaPlaylist(self) # 1
self.player = QMediaPlayer(self)
self.player.setPlaylist(self.playlist)
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile('/Users/louis/Downloads/music1.mp3'))) # 2
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile('/Users/louis/Downloads/music2.mp3')))
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile('/Users/louis/Downloads/music3.mp3')))
self.playlist.setPlaybackMode(QMediaPlaylist.Loop) # 3
self.playlist.setCurrentIndex(2) # 4
self.player.setVolume(80) # 5
self.player.play()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 实例化QMediaPlaylist对象和QMediaPlayer对象,然后调用QMediaPlayer实例的setPlaylist()方法传入QMediaPlaylist实例,以此来设置播放器的播放列表;
2. 调用addMedia()传入要播放的文件,类型为QMediaContent;
3. setPlaybackMode()方法可以设置播放模式,一共有下面几种:
常量 | 值 | 描述 |
QMediaPlaylist.CurrentItemOnce | 0 | 当前内容播放一次 |
QMediaPlaylist.CurrentItemInLoop | 1 | 单曲循环 |
QMediaPlaylist.Sequential | 2 | 顺序播放 |
QMediaPlaylist.Loop | 3 | 列表循环 |
QMediaPlaylist.Random | 4 | 随机播放 |
4. setCurrentIndex()可以设置当前要进行播放文件,传入2代表播放第三个文件;
5. 调用setVolume()方法来设置音量,调用play()方法进行播放。
播放视频文件跟播放音频的代码差不多,不过要播放视频的话我们还需要用到VideoWidget这个控件,该控件用于视频和图像输出,我们只要将它挂到播放器上就可以啦:
import sys
from PyQt5.Qt import QUrl, QVideoWidget
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.playlist = QMediaPlaylist(self)
self.video_widget = QVideoWidget(self) # 1
self.video_widget.resize(self.width(), self.height())
self.player = QMediaPlayer(self)
self.player.setPlaylist(self.playlist)
self.player.setVideoOutput(self.video_widget) # 2
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile('/Users/louis/Downloads/video1.mp4'))) # 3
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile('/Users/louis/Downloads/video2.mp4')))
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile('/Users/louis/Downloads/video3.mp4')))
self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
self.playlist.setCurrentIndex(2)
self.player.setVolume(80)
self.player.play()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 实例化QVideoWidget控件,并将该控件的初始大小调整为窗口大小;
2. 调用播放器的setVideoOutput()方法并传入QVideoWidget实例来设置视频播放设备;
3. 将媒体文件换为视频(mp3->mp4)。
运行截图如下,视频从派拉蒙到福克斯再到迪士尼循环播放:
33.5 制作简单音乐播放器
在34.4节中我们只是介绍了QMediaPlayer的基础用法,接下来我们通过制作一个简单音乐播放器来了解下它的其他方法:
该播放器各部分功能如下:
1. 播放进度条显示和控制播放进度,右边的--/--用于显示剩余时间;
2. 音量滑动条控制音量大小,右边的喇叭按钮控制是否静音;
3. 上一首/播放和暂停/下一首;
4. 该按钮控制播放模式:列表循环,单曲循环和随机播放;
5. 显示和隐藏下方列表
6. 该列表控件用于显示播放内容
现在我们来编写代码,下面是用到的模块:
import sys
from PyQt5.Qt import QUrl
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist
from PyQt5.QtWidgets import QApplication, QWidget, QListWidget, QLabel, QSlider, QPushButton, QHBoxLayout, \
QVBoxLayout
各图标下载地址:
链接:https://pan.baidu.com/s/1aZTiIdthwdI5MRl8jjtP1g 密码:65k3
首先我们来完成界面布局:
import sys
from PyQt5.Qt import QUrl
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist
from PyQt5.QtWidgets import QApplication, QWidget, QListWidget, QLabel, QSlider, QPushButton, QHBoxLayout, \
QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.time_label = QLabel(self) # 1
self.volume_slider = QSlider(self)
self.progress_slider = QSlider(self)
self.sound_btn = QPushButton(self)
self.previous_btn = QPushButton(self)
self.play_pause_btn = QPushButton(self)
self.next_btn = QPushButton(self)
self.mode_btn = QPushButton(self)
self.list_btn = QPushButton(self)
self.list_widget = QListWidget(self)
self.h1_layout = QHBoxLayout()
self.h2_layout = QHBoxLayout()
self.all_v_layout = QVBoxLayout()
self.widget_init()
self.layout_init()
def widget_init(self):
self.time_label.setText('--/--')
self.volume_slider.setRange(0, 100) # 2
self.volume_slider.setValue(100)
self.volume_slider.setOrientation(Qt.Horizontal)
self.progress_slider.setEnabled(False) # 3
self.progress_slider.setOrientation(Qt.Horizontal)
self.sound_btn.setIcon(QIcon('images/sound_on.png')) # 4
self.previous_btn.setIcon(QIcon('images/previous.png'))
self.play_pause_btn.setIcon(QIcon('images/play.png'))
self.next_btn.setIcon(QIcon('images/next.png'))
self.mode_btn.setIcon(QIcon('images/list_loop.png'))
self.list_btn.setIcon(QIcon('images/show.png'))
def layout_init(self):
self.h1_layout.addWidget(self.progress_slider) # 5
self.h1_layout.addWidget(self.time_label)
self.h2_layout.addWidget(self.volume_slider)
self.h2_layout.addWidget(self.sound_btn)
self.h2_layout.addWidget(self.previous_btn)
self.h2_layout.addWidget(self.play_pause_btn)
self.h2_layout.addWidget(self.next_btn)
self.h2_layout.addWidget(self.mode_btn)
self.h2_layout.addWidget(self.list_btn)
self.all_v_layout.addLayout(self.h1_layout)
self.all_v_layout.addLayout(self.h2_layout)
self.all_v_layout.addWidget(self.list_widget)
self.all_v_layout.setSizeConstraint(QVBoxLayout.SetFixedSize) # 6
self.setLayout(self.all_v_layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 实例化需要用到的控件:
- time_label: 显示剩余时间
- volume_slider: 音量滑动条
- progress_slider: 播放进度条
- sound_btn: 喇叭按钮
- previous_btn: 上一首按钮
- play_pause_btn: 播放和暂停按钮
- next_btn: 下一首按钮
- mode_btn: 播放模式按钮
- list_btn: 显示和隐藏列表按钮
- list_widget: 列表控件显示全部播放列表
2. 上面提到QMediaPlayer的音量值范围为0-100,所以我们将其设置为volume_slider的音量值范围;
3. 媒体文件未播放前,播放进度条无法使用;
4. 设置各个按钮的图标;
5. 布局:
6. 显示和隐藏列表会使窗口的大小发生改变,这里我们将布局的sizeConstraint属性设置为SetFixedSize值,这样用户无法修改窗口的大小,而是让布局管理器自己来负责调整。设置之后窗口大小发生改变的话,都能够以最佳的尺寸进行显示。
接着我们来设置QMediaplayer播放器,并往列表控件中添加播放内容。在初始化函数__init__()中实例化QMediaPlayer和QMediaPlaylist(#1和#2处代码):
self.h1_layout = QHBoxLayout()
self.h2_layout = QHBoxLayout()
self.all_v_layout = QVBoxLayout()
self.playlist = QMediaPlaylist(self) # 1
self.player = QMediaPlayer(self) # 2
self.widget_init()
self.layout_init()
然后在widget_init()函数中设置初始状态:
def widget_init(self):
self.time_label.setText('--/--')
self.volume_slider.setRange(0, 100)
self.volume_slider.setValue(100)
self.volume_slider.setOrientation(Qt.Horizontal)
self.progress_slider.setEnabled(False)
self.progress_slider.setOrientation(Qt.Horizontal)
self.sound_btn.setIcon(QIcon('images/sound_on.png'))
self.previous_btn.setIcon(QIcon('images/previous.png'))
self.play_pause_btn.setIcon(QIcon('images/play.png'))
self.next_btn.setIcon(QIcon('images/next.png'))
self.mode_btn.setIcon(QIcon('images/list_loop.png'))
self.list_btn.setIcon(QIcon('images/show.png'))
self.player.setPlaylist(self.playlist)
self.media_list = ['/Users/louis/Downloads/music1.mp3', # 1
'/Users/louis/Downloads/music2.mp4',
'/Users/louis/Downloads/music3.mp3']
for m in self.media_list:
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(m)))
self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
self.list_widget.addItems([m.split('/')[-1] for m in self.media_list]) # 2
1. media_list用于存储各个媒体文件的绝对路径;
2. 添加到列表中的应该是文件名,而不是路径,所以用split()方法获取到路径最后的文件名。
此时程序显示如下:
然后实现各个按钮的功能。在signal_init()函数中完成信号和槽的连接(别忘了将signal_init()放到类的初始化函数__init__()中):
def signal_init(self):
self.sound_btn.clicked.connect(lambda: self.btn_func(self.sound_btn))
self.previous_btn.clicked.connect(lambda: self.btn_func(self.previous_btn))
self.play_pause_btn.clicked.connect(lambda: self.btn_func(self.play_pause_btn))
self.next_btn.clicked.connect(lambda: self.btn_func(self.next_btn))
self.mode_btn.clicked.connect(lambda: self.btn_func(self.mode_btn))
self.list_btn.clicked.connect(lambda: self.btn_func(self.list_btn))
我们将按钮全部连接到btn_func()槽函数上:
def btn_func(self, btn):
if btn == self.sound_btn: # 1
if self.player.isMuted():
self.player.setMuted(False)
self.sound_btn.setIcon(QIcon('images/sound_on'))
else:
self.player.setMuted(True)
self.sound_btn.setIcon(QIcon('images/sound_off'))
elif btn == self.previous_btn: # 2
if self.playlist.currentIndex() == 0:
self.playlist.setCurrentIndex(self.playlist.mediaCount() - 1)
else:
self.playlist.previous()
elif btn == self.play_pause_btn: # 3
if self.player.state() == 1:
self.player.pause()
self.play_pause_btn.setIcon(QIcon('images/play.png'))
else:
self.player.play()
self.play_pause_btn.setIcon(QIcon('images/pause.png'))
elif btn == self.next_btn: # 4
if self.playlist.currentIndex() == self.playlist.mediaCount() - 1:
self.playlist.setCurrentIndex(0)
else:
self.playlist.next()
elif btn == self.mode_btn: # 5
if self.playlist.playbackMode() == 2:
self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
self.mode_btn.setIcon(QIcon('images/item_loop.png'))
elif self.playlist.playbackMode() == 3:
self.playlist.setPlaybackMode(QMediaPlaylist.Random)
self.mode_btn.setIcon(QIcon('images/random.png'))
elif self.playlist.playbackMode() == 4:
self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
self.mode_btn.setIcon(QIcon('images/list_loop.png'))
elif btn == self.list_btn: # 6
if self.list_widget.isHidden():
self.list_widget.show()
self.list_btn.setIcon(QIcon('images/show.png'))
else:
self.list_widget.hide()
self.list_btn.setIcon(QIcon('images/hide.png'))
1. 如果是喇叭按钮按下的话,调用QMediaPlayer类的isMuted()方法判断是否已经静音,再调用setMuted()方法来作出相应的操作,按钮图标同时改变;
2. 如果是上一首按钮的话,则要先调用QMediaPlaylist的currentIndex()方法来知道当前播放文件的索引。如果等于0的话说明正在播放第一个文件,那此时已经没有上一首了,所以我们应该播放最后一首,最后一首的索引也就是mediaCount()-1;如果不等于0那就直接调用QMediaPlayer类的previous()方法来播放上一首歌曲;
3. 先判断播放器的状态,如果正在播放的话,则暂停,否则开始/继续播放。下面是QMediaPlayer.state()可返回的值:
常量 | 值 | 描述 |
QMediaPlayer::StoppedState | 0 | 停止状态 |
QMediaPlayer::PlayingState | 1 | 正在播放状态 |
QMediaPlayer::PausedState | 2 | 暂停状态 |
4. 逻辑同第2点类似;
5. 如果是播放模式按钮的话,就相应的调用setPlaybackMode()方法来改变播放模式,同时还要改变按钮的图标;
6. 如果是列表按钮,就先判断列表控件当前是否可见,再做出相应操作即可;
各个按钮的功能已经完成了,我们接下来完成音量滑动条的功能以及列表双击播放的功能。在signal_init()中加上这两行代码:
self.volume_slider.valueChanged.connect(self.volume_slider_func)
self.list_widget.doubleClicked.connect(self.list_play_func)
volume_slider_func()槽函数如下:
def volume_slider_func(self, value):
self.player.setVolume(value)
if value == 0:
self.sound_btn.setIcon(QIcon('images/sound_off.png'))
else:
self.sound_btn.setIcon(QIcon('images/sound_on.png'))
注:ValueChanged信号每次被发射的话都会带一个value值,也就是滑动条当前的值。之后的sliderMoved, durationChanged和positionChanged信号也是同理,详情见文档。
每次音量滑动条值发生改变,就调用setVolume()方法将播放器的音量设置成相应的值,如果音量为0的话还要记得将喇叭按钮图标改成静音图标:
list_play_func()槽函数如下:
def list_play_func(self):
self.playlist.setCurrentIndex(self.list_widget.currentRow())
self.player.play()
self.play_pause_btn.setIcon(QIcon('images/pause.png'))
首先调用QListWidget类的currentRow()方法获取到当前被双击的索引,然后传入QMediaPlaylist类的setCurrentIndex()方法中,接着调用QMediaPlayer类的play()方法进行播放即可。注意一个细节就是既然已经开始播放的话就要将play_pause_btn的图标改成暂停图标pause.png。
最后要完成的就是播放进度条和剩余时间显示。要完成这两个功能我们肯定要先获取到文件的时长,而QMediaPlayer正好有个duration()方法可以用:
该方法返回当前媒体文件时长,单位是毫秒。不过需要注意的就是在刚开始播放时调用duration()方法只会获取到0,所以文档建议用durationChanged信号来监听获取时长,也就是说我们要在一个槽函数中获取该。知道文件总时长的话,我们就可以设置progress_slider的值范围了。
注:遗憾的一点是,duration()和durationChanged在MacOS上无发使用,值始终为0(Windows和Linux上可以使用)。
播放进度肯定是要改变的,进度条上的按钮肯定要慢慢向右移动,也就是说我们要不断知道当前已经播放了多少时长,并将其设置为progress_slider的当前值。QMediaPlayer类的position()方法可以获取到当前已经播放的时长,而positionChanged信号会在position值发生改变的时候发射。
既然已经知道了要用的方法,那我们就可以开工了。在signal_init()函数中加上下面两行代码:
self.player.durationChanged.connect(self.get_duration_func)
self.player.positionChanged.connect(self.get_position_func)
get_duration_func()槽函数如下:
def get_duration_func(self, d):
self.progress_slider.setRange(0, d)
self.progress_slider.setEnabled(True)
self.get_time_func(d)
设置progress_slider范围,并将其设为可用状态,在get_time_func()函数中我们更新时间:
def get_time_func(self, d):
seconds = int(d / 1000)
minutes = int(seconds / 60)
seconds -= minutes * 60
if minutes == 0 and seconds == 0:
self.time_label.setText('--/--')
self.play_pause_btn.setIcon(QIcon('images/play.png'))
else:
self.time_label.setText('{}:{}'.format(minutes, seconds))
首先获取分钟数和秒数,然后判断是否已经播放完毕(即分秒都为0)。如果播放结束的话,则将时间文本设为--/--,而且还要将播放按钮的图标设为play.png;否则就设置文本设为剩余时间。
get_position()槽函数如下,非常简单,就是设置progress_slider的当前值:
def get_position_func(self, p):
self.progress_slider.setValue(p)
此时运行程序,播放某个文件的话就可以看到进度条走动了,而且时间也会显示出来。现在还差最后一个,就是滑动进度条来改变播放进度。我们在signal_init()中加上如下代码:
self.progress_slider.sliderMoved.connect(self.update_position_func)
注:不能使用valueChanged信号来进行连接,否则会跟positionChanged()信号会起冲突,而sliderMoved这个信号是只有在用户手动改变滑动条值得情况下才会发射。
update_position_func()槽函数如下:
def update_position_func(self, v):
self.player.setPosition(v)
d = self.progress_slider.maximum() - v
self.get_time_func(d)
调用QMediaPlayer的setPosition()方法并传入progress_slider的值来设置当前播放进度,那剩余时间就是进度条最大值减去当前值,接着我们将剩余时间d传入get_time_func来更新即可。
完整代码如下:
import sys
from PyQt5.Qt import QUrl
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist
from PyQt5.QtWidgets import QApplication, QWidget, QListWidget, QLabel, QSlider, QPushButton, QHBoxLayout, \
QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.time_label = QLabel(self)
self.volume_slider = QSlider(self)
self.progress_slider = QSlider(self)
self.sound_btn = QPushButton(self)
self.previous_btn = QPushButton(self)
self.play_pause_btn = QPushButton(self)
self.next_btn = QPushButton(self)
self.mode_btn = QPushButton(self)
self.list_btn = QPushButton(self)
self.list_widget = QListWidget(self)
self.h1_layout = QHBoxLayout()
self.h2_layout = QHBoxLayout()
self.all_v_layout = QVBoxLayout()
self.playlist = QMediaPlaylist(self)
self.player = QMediaPlayer(self)
self.widget_init()
self.layout_init()
self.signal_init()
def widget_init(self):
self.time_label.setText('--/--')
self.volume_slider.setRange(0, 100)
self.volume_slider.setValue(100)
self.volume_slider.setOrientation(Qt.Horizontal)
self.progress_slider.setEnabled(False)
self.progress_slider.setOrientation(Qt.Horizontal)
self.sound_btn.setIcon(QIcon('images/sound_on.png'))
self.previous_btn.setIcon(QIcon('images/previous.png'))
self.play_pause_btn.setIcon(QIcon('images/play.png'))
self.next_btn.setIcon(QIcon('images/next.png'))
self.mode_btn.setIcon(QIcon('images/list_loop.png'))
self.list_btn.setIcon(QIcon('images/show.png'))
self.player.setPlaylist(self.playlist)
self.media_list = ['/Users/louis/Downloads/music1.mp3',
'/Users/louis/Downloads/music2.mp4',
'/Users/louis/Downloads/music3.mp3']
for m in self.media_list:
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(m)))
self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
self.list_widget.addItems([m.split('/')[-1] for m in self.media_list])
def layout_init(self):
self.h1_layout.addWidget(self.progress_slider)
self.h1_layout.addWidget(self.time_label)
self.h2_layout.addWidget(self.volume_slider)
self.h2_layout.addWidget(self.sound_btn)
self.h2_layout.addWidget(self.previous_btn)
self.h2_layout.addWidget(self.play_pause_btn)
self.h2_layout.addWidget(self.next_btn)
self.h2_layout.addWidget(self.mode_btn)
self.h2_layout.addWidget(self.list_btn)
self.all_v_layout.addLayout(self.h1_layout)
self.all_v_layout.addLayout(self.h2_layout)
self.all_v_layout.addWidget(self.list_widget)
self.all_v_layout.setSizeConstraint(QVBoxLayout.SetFixedSize)
self.setLayout(self.all_v_layout)
def signal_init(self):
self.sound_btn.clicked.connect(lambda: self.btn_func(self.sound_btn))
self.previous_btn.clicked.connect(lambda: self.btn_func(self.previous_btn))
self.play_pause_btn.clicked.connect(lambda: self.btn_func(self.play_pause_btn))
self.next_btn.clicked.connect(lambda: self.btn_func(self.next_btn))
self.mode_btn.clicked.connect(lambda: self.btn_func(self.mode_btn))
self.list_btn.clicked.connect(lambda: self.btn_func(self.list_btn))
self.volume_slider.valueChanged.connect(self.volume_slider_func)
self.list_widget.doubleClicked.connect(self.list_play_func)
self.player.durationChanged.connect(self.get_duration_func)
self.player.positionChanged.connect(self.get_position_func)
self.progress_slider.sliderMoved.connect(self.update_position_func)
def btn_func(self, btn):
if btn == self.sound_btn:
if self.player.isMuted():
self.player.setMuted(False)
self.sound_btn.setIcon(QIcon('images/sound_on'))
else:
self.player.setMuted(True)
self.sound_btn.setIcon(QIcon('images/sound_off'))
elif btn == self.previous_btn:
if self.playlist.currentIndex() == 0:
self.playlist.setCurrentIndex(self.playlist.mediaCount() - 1)
else:
self.playlist.previous()
elif btn == self.play_pause_btn:
if self.player.state() == 1:
self.player.pause()
self.play_pause_btn.setIcon(QIcon('images/play.png'))
else:
self.player.play()
self.play_pause_btn.setIcon(QIcon('images/pause.png'))
elif btn == self.next_btn:
if self.playlist.currentIndex() == self.playlist.mediaCount() - 1:
self.playlist.setCurrentIndex(0)
else:
self.playlist.next()
elif btn == self.mode_btn:
if self.playlist.playbackMode() == 2:
self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
self.mode_btn.setIcon(QIcon('images/item_loop.png'))
elif self.playlist.playbackMode() == 3:
self.playlist.setPlaybackMode(QMediaPlaylist.Random)
self.mode_btn.setIcon(QIcon('images/random.png'))
elif self.playlist.playbackMode() == 4:
self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
self.mode_btn.setIcon(QIcon('images/list_loop.png'))
elif btn == self.list_btn:
if self.list_widget.isHidden():
self.list_widget.show()
self.list_btn.setIcon(QIcon('images/show.png'))
else:
self.list_widget.hide()
self.list_btn.setIcon(QIcon('images/hide.png'))
def volume_slider_func(self, value):
self.player.setVolume(value)
if value == 0:
self.sound_btn.setIcon(QIcon('images/sound_off.png'))
else:
self.sound_btn.setIcon(QIcon('images/sound_on.png'))
def list_play_func(self):
self.playlist.setCurrentIndex(self.list_widget.currentRow())
self.player.play()
self.play_pause_btn.setIcon(QIcon('images/pause.png'))
def get_duration_func(self, d):
self.progress_slider.setRange(0, d)
self.progress_slider.setEnabled(True)
self.get_time_func(d)
def get_time_func(self, d):
seconds = int(d / 1000)
minutes = int(seconds / 60)
seconds -= minutes * 60
if minutes == 0 and seconds == 0:
self.time_label.setText('--/--')
self.play_pause_btn.setIcon(QIcon('images/play.png'))
else:
self.time_label.setText('{}:{}'.format(minutes, seconds))
def get_position_func(self, p):
self.progress_slider.setValue(p)
def update_position_func(self, v):
self.player.setPosition(v)
d = self.progress_slider.maximum() - v
self.get_time_func(d)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
33.6 小结
1. QSound和QSoundEffect只能用来播放wav格式的音频文件,后者可以提供更加精细化的操作;
2. QMovie用来播放动态图像;
3. QMediaPlayer可以播放多种类型的媒体文件,用它来播放和控制音频和视频的方法都是相同的,读者可以自己再制作一个简单视频播放器来练练手;
4. duration()方法和durationChanged()信号在MacOS上起不了作用,还有按钮图标改变慢一拍。
欢迎关注我的微信公众号,发现更多有趣内容: