大虾

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

开发了一个小的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循环来监控队列的情况,不过,一定是我不太熟悉这个方法才会这样,不太会管理。

待续。。。

posted on 2022-06-29 15:59  一灯编程  阅读(255)  评论(0)    收藏  举报