系类往期文章:
PyQt5实战——多脚本集合包,前言与环境配置(一)
PyQt5实战——多脚本集合包,UI以及工程布局(二)
PyQt5实战——多脚本集合包,程序入口QMainWindow(三)
PyQt5实战——操作台打印重定向,主界面以及stacklayout使用(四)
PyQt5实战——UTF-8编码器UI页面设计以及按钮连接(五)
PyQt5实战——UTF-8编码器功能的实现(六)
PyQt5实战——翻译器的UI页面设计以及代码实现(七)
PyQt5实战——翻译的实现,第一次爬取微软翻译经验总结(八)
PyQt5实战——翻译的实现,成功爬取微软翻译(可长期使用)经验总结(九)
PyQt实战——使用python提取JSON数据(十)
前言
这是实现音频编解码器功能模块的第二篇文章,在本文中,我们将实现自定义的特色进度条以及简单介绍编码器的UI布局,本文主要实现特色进度条,在本文中,我们会涉及到的知识点有:特色进度条的实现原理,一个python生成器的用法,PyQt的控件绘制机制。本文仅介绍编解码器UI的布局,因为实现UI时会用到开线程的部分,因此本文先不展示。
本次笔者设计的特色进度条,是涂格子形式的进度条,当进度增加时,进度条内的方格会被随机涂上颜色,供大家参考。
UI框架
如下图所示:
-
编码器的UI布局同样为垂直分布
-
在第一层级,由经典的选择文件构成,这个选择文件的布局样式已经在本系列中多次出现
-
一个下拉选择框,目前只有ADPCM一个算法可供选择
-
两个按钮水平布局,编码按钮启动编码器,解码按钮启动解码器
-
编解码器读取选中的文件,从文件中读取数据。注意,文件最好是txt文件格式,数据是十六进制数1字节为单位,以空格分隔开,可选择一帧数据一行,如下图所示:
-
解码器会在workspaces目录下生成
pcm.txt
数据文件,其中包含了解码后的PCM数据,以文本形式保存,后续将其放入pcm构建器中可生成二进制文件,在PCM播放器中播放即可。 -
编码器会在workspaces目录下生成
adpcm.txt
数据文件,其中包含了编码后的ADPCM数据,以文本形式保存,后续如何处理数据,可自行决定。 -
最下方是进度条,由20*5共100个方格组成,在未启动状态下为灰色,当开始解码或编码时,编码进度每增加1%,那么就会随机涂蓝一个格子,在进度为100%时,即编码完成,则所有格子全部为蓝色。
-
当重新开始编码或解码时,格子会重新初始化,全部被重新涂成灰色。
进度条展示
下面是正在解码过程中的进度条展示,在右边的日志展示区中可以看到,正在进行ADPCM解码,在功能区中,下方进度条在随机选择格子涂蓝,将这些以显示当前进度
下面是解码结束后的进度条样式,可以看到,在右边的日志区中,显示ADPCM decode end
,adpcm解码完成,下方的进度条已被完全涂成蓝色。
代码详解
下面给出进度条的代码:
python">import sys
import random
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor, QPainter, QPainterPath
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton
import queue
class ProgressBar(QWidget):def __init__(self):super().__init__()self.setFixedSize(640, 160) # 设置进度条的大小self.grid_size = 32 # 每个方格的宽度和高度self.total_grids = (self.width() // self.grid_size) * (self.height() // self.grid_size) # 方格总数self.lit_grids = 0 # 当前已点亮的方格数量self.grid_status = [False] * self.total_grids # 每个方格是否被点亮def init_grids(self):self.lit_grids = 0 # 当前已点亮的方格数量self.grid_status = [False] * self.total_gridsself.update() # 更新进度条绘制def update_progress(self,q):"""随机点亮一个未点亮的方格"""while True:percent = q.get()if self.lit_grids < self.total_grids and percent <= 100:# 随机选择一个未点亮的方格unlit_grids = [i for i, lit in enumerate(self.grid_status) if not lit]random_grid = random.choice(unlit_grids)self.grid_status[random_grid] = True # 点亮该方格self.lit_grids += 1 # 增加已点亮的方格数量self.update() # 更新进度条绘制if percent == 100:breakelse:breakdef paintEvent(self, event):"""自定义绘制进度条"""painter = QPainter(self)painter.setRenderHint(QPainter.Antialiasing) # 启用抗锯齿效果,使边角更平滑# 绘制方格for row in range(self.height() // self.grid_size):for col in range(self.width() // self.grid_size):x = col * self.grid_sizey = row * self.grid_size# 计算当前方格的索引index = row * (self.width() // self.grid_size) + colif self.grid_status[index]:color = QColor(144, 203, 251) # 已点亮的部分,蓝色else:color = QColor(224, 224, 224) # 未点亮的部分,灰色painter.fillRect(x, y, self.grid_size, self.grid_size, color) # 绘制方格painter.end()
我们来解释一下上面的代码:
- 在
__init__
方法中,设置了一些基础配置,进度条的大小,方格的大小,方格的总数,已点亮的方格数,每个方格的状态(是否被点亮了),值得注意的是,方格的总是是通过进度条的长除以方格的长,进度条的宽除以方格的宽然后相乘的来的,这里直接翻译为20*5 = 100 init_grids
方法重新初始化方格的基础配置,归零已点亮的方格数量,擦除方格的状态,并重新绘制进度条,即恢复初始状态update_progress
方法用以更新进度,可以看到,该函数接收一个q
参数,该参数为一个队列对象,用以线程之间的沟通(线程方面我们下篇文章讨论),此处做一个while的死循环,从队列中获取来自另一个线程的消息,这个操作是阻塞的,也就是说,当未从队列中获取新的消息,则线程不会继续进行下一条指令。- 如果已经点亮的方格数小于全部方格数,且进度≤100%,则执行以下操作,随机选择一个未点亮的方格,标志为点亮
unlit_grids = [i for i, lit in enumerate(self.grid_status) if not lit]
,解释一下这一段代码:enumerate(self.grid_status)
将会返回一个生成器,生成的是每个元素的索引i
和对应的值lit
,例如:假设self.grid_status = [True, False, True, Flase]
,那么生成器将会返回,(0,True)
,(1,False)
,(2,True)
,(3,False)
for i, lit in enumerate(self.grid_status)
是一个for
循环,用来遍历enumerate(self.grid_status)
返回的每一个(i,lit)
元组,i
是索引,lit
是self.grid_status
中对应的状态值if not lit
这个条件判断会检查lit
是否为False
,如果为False
,就会被加入到列表unlit_grids
中- 总的来说,这是整个列表推导式的语法,它会创建一个包含所有符合条件
if not lit
的元素索引i
的新列表
random.choice
方法在unlit_grids
中随机选择一个方格- 将选择的的方格状态设置为点亮
- 增加已点亮的方格数量
- 更新进度条绘制
- 如果进度条达到100%,则直接退出该函数,不再循环,也不再接收来自其它线程的消息
paintEvent
方法是用以绘制进度条的,创建一个在PyQt中可绘制的对象QPainter。- 两层
for
循环,计算现在的位置,确定这个位置的方格是以点亮还是未点亮,确定颜色,绘制方格。 painter.end
方法结束绘制
总结
进度条的更新实际上是对控件的重新绘制
paintEvent
在什么时候被调用?
paintEvent
是一个 Qt 事件处理函数,它在 需要重新绘制组件时 被自动调用。Qt 会在以下几种情况下触发paintEvent
:
- 窗口或控件首次显示时:
- 当窗口或控件第一次显示时,
paintEvent
会被触发,负责绘制控件的初始状态。- 控件大小发生变化时:
- 如果控件的大小发生了变化(例如,窗口调整大小),
paintEvent
会被调用来重新绘制控件,以适应新的尺寸。- 调用
update()
或repaint()
时:
- 当你调用
update()
或repaint()
方法时,Qt 会标记该控件为“需要重绘”,并会在下一个事件循环中触发paintEvent
。- 其他因素:
- 如果控件的内容发生了变化,或者某些部分被遮挡并随后暴露出来,Qt 会重新触发
paintEvent
来重新绘制这些区域。例如,当窗口被部分遮挡后再显示出来时,Qt 会调用paintEvent
来刷新被遮挡的部分。
在上面的代码中,每次更新进度条的状态时,调用self.update
,重新绘制控件。
如果不适用self.update
,而是直接调用paintEvent
绘制进度条可以吗?
直接调用
paintEvent
来重绘进度条并不是一种推荐的做法。原因在于,Qt 的事件机制是自动管理绘制过程的,而直接调用paintEvent
会绕过事件系统,可能导致一些问题。下面我将详细解释原因和影响。
- Qt 的绘制机制:
- 在 Qt 中,
paintEvent
由事件循环自动管理,通常不需要直接调用。当你调用self.update()
时,Qt 会将控件标记为“需要重绘”,并在合适的时机调用paintEvent
。self.update()
会触发一个绘制事件,但不会立即调用paintEvent
,而是将重绘请求加入事件队列,稍后由 Qt 的事件循环处理。这样做的好处是,Qt 会合理地合并多个绘制请求,避免频繁的重复重绘,提高性能。
- 直接调用
paintEvent
的问题:
- 如果你直接调用
paintEvent
,就会绕过 Qt 的事件循环和绘制机制。paintEvent
通常是由系统在适当时机(如控件需要重绘时)自动触发的。- 直接调用
paintEvent
可能会导致:
- 绘制不一致:因为 Qt 的绘制机制已经负责了缓存和重绘的时机,直接调用
paintEvent
可能会与其他重绘请求冲突,导致绘制不稳定。- 不符合最佳实践:Qt 推荐使用
update()
来请求重绘,因为它利用了 Qt 内部的绘制优化策略。如果你直接调用paintEvent
,可能会破坏这些优化。
通过消息队列传递更新消息
进度条的更新实际上需要与编解码器的编解码进度相关,因为编解码器吃算力,因此不建议将编解码器放在主线程中运行,所以对编解码器开了子线程运行,消息的互传需要通过消息队列来进行。关于线程问题,我们将在下一期内容时更详细地讲解。
祝你变得更强!
因为语音编解码器功能模块的实现较为复杂,而且也增加了一些新的UI设计,因此知识点与代码都无法在一篇文章中全部呈现,但将代码分散在不同的文章里又让一些基础比较薄弱的同学难以快速上手,因此,如若对此模块感兴趣的人比较多,笔者将在这三篇文章的基础上,单独开一篇新的博文,梳理代码的布局以及如何在自己的机器上跑起来,让新手小白也能复制即用。