开发了一个小的GUI项目,能将word转成pdf,并新pdf上添加水印,其中水印文件也是预设好的pdf文件。
技术栈
1、pyqt6 + qt designer
其中pyqt要使用多线程将UI线程和业务线程分开
2、win32com
知名pywin32里面的win32com模块
3、PyPdf2
word转pdf
完成的版本

关键点记录
整体上,用到的都是基础知识(乍一看,都是各种库拼凑起来的。冏!),唯独pyqt6的多线程值得好好记录一下。
1、pyqt的多线程有两种方式
一是使用QThread,二是使用QObject。如果要在多线程中实现自己的业务逻辑,官网强烈推荐使用QObject。本次项目需要在多线程中实现word转pdf再添加水印的逻辑,所以选择了官方推荐的方式。来看看实现代码
from PyQt6.QtCore import QObject, pyqtSignal
class MyDocumentThread(QObject):
# 发送信号
signal_for_send = pyqtSignal(str)
# 接收信号 接收一个文件列表
signal_for_accept = pyqtSignal(list)
def __init__(self, parent=None):
super(MyDocumentThread, self).__init__()
self.lock = QMutex()
def work(self, file_list):
"""
文档处理逻辑
"""
print('子线程接受到的数据:', file_list)
# 上锁
self.lock.lock()
temp_pdf_list = []
for file in file_list[0]:
pdf_file = word2pdf(file)
temp_pdf_list.append(pdf_file)
print('pdf处理后', temp_pdf_list)
self.lock.unlock()
# 将temp_pdf_list增加水印
self.lock.lock()
for index, pdf in enumerate(temp_pdf_list, 1):
done = add_mark(pdf, file_list[-1])
print(index, pdf)
if done == 'success':
# 发送信号给UI主线程
self.signal_for_send.emit(f'{pdf}处理完成...')
if index == len(temp_pdf_list):
self.signal_for_send.emit('done')
self.lock.unlock()
这是个典型的自定义线程,能有效地将UI线程和业务线程分离,实现互不干扰时还能双向通信,说到双向通信,要求主线程能向子线程发信号,子线程也能向主线程发信号,这个又该如何实现呢?在实现前,我们先来了解一下自定义线程的结构。
-
声明信号
使用pyqtSignal声明信号,如声明一个内容为str类型的信号:signal_for_send = pyqtSignal(str),再声明一个内容为list类型的信号:signal_for_accept = pyqtSignal(list)。 -
信号发射
在实例化这个类时,我们便拥有了两个信号,但此时还做不到通信,需要调用信号本身的emit方法才可以实现通信,这个方法能实现信号的发送,即哪个信号调用,哪个信号就是发送方。上面的代码中,我们最后使用了elf.signal_for_send.emit(f'{pdf}处理完成...'),即信号signal_for_send就是发送方,即子线程是信号源。那,谁来接受信号呢?肯定是主线程呀。 -
信号接收
我们先看看UI线程(主线程)的代码
# 省略其他代码
class WaterMarkMainWindow(Ui_MainWindow):
# 重要属性
files_list = []
files_temp_list = []
water_file = None
def __init__(self) -> None:
super(WaterMarkMainWindow, self).__init__()
# 初始化自定义的线程
self.word_thread = MyDocumentThread()
# 初始化内置的线程
self.thread = QtCore.QThread()
# 将自定义的线程转交给内置的线程处理
self.word_thread.moveToThread(self.thread)
# 连接信号槽
# 将子线程返回的数据传给self.start 然后再通过statubar.showMessage展示出来
self.word_thread.signal_for_send.connect(self.show_ret)
# UI主线程传递数据给子线程
self.word_thread.signal_for_accept.connect(self.word_thread.work)
def setupUi(self, MainWindow):
super().setupUi(MainWindow)
# self.statusbar.showMessage('处理中...')
# 信号槽绑定
self.run.clicked.connect(self.start)
self.choose_files.clicked.connect(self.choose_files_btn)
self.choose_water_mark.clicked.connect(self.choose_water_file_btn)
def start(self, signal):
# 开启线程 成为常驻线程
if not self.files_temp_list and not self.water_file:
self.statusbar.showMessage('尚未选择文件和水印', msecs=2000)
elif not self.water_file:
self.statusbar.showMessage('尚未选择水印', msecs=2000)
elif not self.files_temp_list:
self.statusbar.showMessage('尚未选择文件', msecs=2000)
if self.files_temp_list:
# 将文件列表和水印发送到子线程
self.statusbar.showMessage('处理中...')
self.thread.start()
self.word_thread.signal_for_accept.emit((self.files_temp_list, self.water_file))
self.run.setText('处理中')
self.run.setStyleSheet("font: 14pt \"微软雅黑\";color: green")
# 省略其他代码
- 在多线程中实现自己的业务逻辑
在UI初始化时,便初始化了两个线程对象,一个是我自定义的线程MyDocumentThread,一个是内置的QThread,官方推荐:在自定义线程开启后,移交给内置的QThread进行管理,使用自定义线程对象的moToTHread方法可实现。
- 为信号绑定槽
实例化线程对象后,需要为pyqtSignal信号绑定槽,即使用什么函数来接收和处理信号,值得一提的是,槽就是一个函数,这个函数不一定非得是UI元素,比如,也可以是一个发送邮件的函数,也可以是一个和windows环境交互的函数。
```python
# 将子线程返回的数据传给self.start 然后再通过statubar.showMessage展示出来
self.word_thread.signal_for_send.connect(self.show_ret)
# UI主线程传递数据给子线程
self.word_thread.signal_for_accept.connect(self.word_thread.work)
主线程给向子线程通信:
在开始时,我实现了一个自定义线程,其中work是实现业务逻辑的模块,此时我将信号signal_for_accept绑定了自身的work模块,signal_for_accept信号的类型为列表,那么在word中,需要有一个参数来接收这个信号的内容。话说得那么长,其实很简单,打开软件,点击签章的按钮时,UI线程将所有的文件列表传给了自定义线程的work,由work来处理这些文件。对应着类WaterMarkMainWindow,当用户点击签章时,就会调用start方法,从而调用signal_for_accept.emit(),将文件列表发送给了work模块。
子线程向主线程通信:
子线程work在完成文件的处理后,调用signal_for_send.emit('done'),向主线程发了一个内容为'done'字符串的信号,主线程谁来接收这个信号呢?在主线程中,我使用了self.word_thread.signal_for_send.connect(self.show_ret)绑定了show_ret方法来接收和处理,这个方法仅仅是将信号内容通过statusbar展示出来而已,其实说了一堆堆,这个功能无非就是向用户展示处理文件的进度和结果而已。
2、线程的管理
线程的开启、关闭可以根据实际的业务需要进行调整,比如在什么时机开启,手动开启还是自动开启,什么时候需要关闭线程。当然,如果使用线程池来管理,那到不必那么麻烦,这个后面再熟悉吧。在这个小项目中,需要管理的就是一个子线程而已,仅仅一个,是学习pyqt多线程绝好案例,所以就手动管理就好了。放到实际的应用中,子线程是需要处理文件的,点击一次签章, 软件就会工作一次,那么可以大胆地假设,我们可以选择用户在点击签章时,开启子线程,而在完成处理文件时,比如主线程接收到signal_for_send的信号时选择关闭线程。为什么要这样设置呢?如果我们在UI界面初始化便开启了线程,那后面就没办法手动开启线程了,在实际使用中,会造成用户打开软件处理一次文件后,得重启软件才能继续使用,那多蛋疼。
一直挂着线程不关闭可以吗?这个尝试过,发生的现象是,一个信号会被反复发送,反复接收,意味着用户命名只点击了一次签章,但文件反复在被处理。我猜想,这个信号中一直内置了队列,并使用while循环来监控队列的情况,不过,一定是我不太熟悉这个方法才会这样,不太会管理。
待续。。。

浙公网安备 33010602011771号