PyQt5教程3 – 发送页面进度条,自定义信号槽和线程

  • xhyl
  • January 22, 2020

在airdrop,当用户想要发送文件出去的时候,需要选择目标机器或者用户,在随后的发送中还会显示发送进度,针对这一交互逻辑,在WiFidrop中,我们也将设计相同的交互逻辑,我们要增加一个发送页面,在这个页面上将能够显示我们可以发送的对象,以及一个进度条。

这一篇文章中,我们将继续在 PyQt5教程2:主页面和拖动 的基础上,加入发送页面,和一些相应的逻辑。

1. 画界面

第一步还是让我们来设计页面吧,同样的使用Qt Creator创建ui文件,这次我们不选择Main Window模板,而是使用Dialog without Buttons模板,即不带按钮的空白对话框模板。文件名使用dialog.ui,放置在根目录中。
然后在窗口中添加一个tableWidget,一个进度条(progress bar),以及一个按钮控件,效果如下图:

2. 写逻辑

在开始之前需要从库中导入必要的类

1
2
3
4
5
6
import sys, os

from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QSpacerItem, QSizePolicy, QFileDialog, QDialog
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QThread
from PyQt5.uic import loadUi

2.1 定义发送对话框

现在我们有了发送页面(对话框),跟主窗口类似,有了ui文件以后,我们还需要将它转换成python的类,以便增加各种逻辑。同样的我们使用loadUi函数加载这个窗口,另外针对取消按钮,我们希望用户点击它以后会关闭本对话框。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class SendDialog(QDialog):
   def __init__(self, url_list):
       super(SendDialog, self).__init__()

       # UI setup - 1 option
       # dynamic load ui for development purpose
       self.ui = loadUi('./dialog.ui', self)
       self.ui.pushButtonClose.setStyleSheet("background-color:#ffffff;");
       self.ui.pushButtonClose.setText("取消");
       self.ui.pushButtonClose.clicked.connect(self.close)

这段代码创建了发送对话框类,将ui文件加载进来,同时将关闭按钮的点击信号绑定到对话框类自带的close槽,实现窗口的关闭。同时初始化函数,设计了一个url_list参数,它是一个list,包含了需要发送的文件的路径。

2.2 发送对话框实体化

在定义好对话框以后,我们希望在点击主界面按钮选择完文件后,打开这个对话框,那么也很简单,我们在主界面按钮上绑定的槽函数内容跟改为:

1
2
3
4
5
6
7
8
9
def pushButtonChoose_clicked(self):
        options = QFileDialog.Options()
        #options |= QFileDialog.DontUseNativeDialog
        file_names, _ = QFileDialog.getOpenFileNames(self,"选择文件", "","Files (*.*)", options=options)
        files = [u for u in file_names]

        if len(files) > 0:
            senddiag = SendDialog(files)
            senddiag.exec()

这里通过下边代码将发送对话框实例化,因为我们需要发送的文件是从主页面获取过来的,所以,将文件列表作为发送对话框初始化的参数传入

1
senddiag = SendDialog(files)

然后执行

1
senddiag.exec()

将窗口显示出来供用户交互,这里调用了.exec()方法,这个方法会锁定焦点到发送对话框,用户不可以跟主窗口交互,如果不想锁定,则使用.show()方法。

为了让软件显得有趣一些,我们希望,用户在选定一个文件发送的时候,主页面拖放区显示一个文件的样子,而用户选定多个文件的时候,主页面拖放区显示一个文件夹的样子。我在网络上找了两个免费的漂亮图标,我们修改主页面按钮绑定的函数,将它们显示出来。

在拖放区(QLabel)上显示图标使用它内置的setPixmap方法,而方法的参数则是我们需要显示的QPixmap实例,创建QPixmap实例的时候选择不同的图标文件就可以将不同的图标显示出来。

下载Treetog-I-Documents图标
下载Treetog-I-Text-File图标

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def pushButtonChoose_clicked(self):
        options = QFileDialog.Options()
        #options |= QFileDialog.DontUseNativeDialog
        file_names, _ = QFileDialog.getOpenFileNames(self,"选择文件", "","Files (*.*)", options=options)
        files = [u for u in file_names]


        if len(files) > 0:
            num_of_files = len(files)
            if num_of_files != 0:
                if num_of_files > 1:
                    # more than one files, show folder icon
                    icon = './Treetog-I-Documents.ico'
                else:
                    # only one file, show file icon
                    icon = './Treetog-I-Text-File.ico'
            pixmap = QPixmap(icon)
            self.ui.dropArea.setPixmap(pixmap)
            senddiag = SendDialog(files)
            senddiag.exec()

运行代码:

2.3 实现通过拖放弹出发送窗口

在这一步,我们将引入自定义信号的概念,前边我们用到的信号槽绑定,都是基于Qt预定的信号,绑定我们自己的函数。而大部分GUI开发中,我们需要创建自定的事件,并在事件上绑定我们需要的函数。在Qt中我们可以做这样的事件,只需要定义并手动的触发信号就可以了。

接下来,我们希望用户将文件拖放后,同样的弹出发送对话框,所以,我们需要在拖放区的dropEvent中触发一个信号,并将这个信号绑定到发送对话框创建函数上。

首先我们调整下选择按钮的绑定函数的逻辑,将显示图标,实例化发送对话框部分的逻辑拿出来,单独作为一个一个方法(prepare_sending),因为我们希望不管是点击选择按钮还是用户拖放事件,都想使用同样的逻辑,而这个逻辑就是我们的槽,在主窗口类中添加prepare_sending方法和修改pushButtonChoose_clicked方法 。

 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
@pyqtSlot(list)
    def prepare_sending(self, files):
        url_list = list()
        num_of_files = len(files)
        if num_of_files != 0:
            if num_of_files > 1:
                # more than one files, show folder icon
                icon = './Treetog-I-Documents.ico'
            else:
                # only one file, show file icon
                icon = './Treetog-I-Text-File.ico'
            pixmap = QPixmap(icon)
            self.ui.dropArea.setPixmap(pixmap)
            for f in files:
                url_list.append(f)

        senddiag = SendDialog(url_list)
        senddiag.exec()


    def pushButtonChoose_clicked(self):
        options = QFileDialog.Options()
        #options |= QFileDialog.DontUseNativeDialog
        file_names, _ = QFileDialog.getOpenFileNames(self,"选择文件", "","Files (*.*)", options=options)
        files = [u for u in file_names]


        if len(files) > 0:
            self.prepare_sending(files)

prepare_sending作为我们的槽函数,需要接收一个被发送文件列表的参数,我们使用@pyqtSlot(list)装饰器来实现,pyqtSlot装饰的参数即我们的槽函数可以接收的参数,可以是多个参数例如@pyqtSlot(list, int, int)

现在我们的槽函数已经有了,那么怎么让拖放区来触发一个信号,来激活这个函数的调用呢,我们可以在拖放区类中添加一个自定义的信号。

1
2
3
class DropArea(QLabel):

    files_dropped = pyqtSignal(list)

在上边这段代码中我们新建了一个类属性,其为pyqtSignal即信号,该信号可以传递一个list变量,同理,有需要的话,可以定义多个变量参数pyqtSignal(list, int, int)有了信号以后,在dropEvent中,激发(发送)这个信号,使用信号的emit方法,并将需要传递的变量传给它 。

1
2
3
4
5
6
7
8
def dropEvent(self, event):
        print("drop event")
        files = list()
        urls = [u for u in event.mimeData().urls()]
        for url in urls:
            files.append(url.toLocalFile())

        self.files_dropped.emit(files)

到这里我们的拖放控件已经可以激发信号了,那接下来的事情就很简单了,和预制信号一样,仅需要将信号和槽做一个绑定,在主窗口的初始化中添加绑定代码 。

1
self.ui.dropArea.files_dropped.connect(self.prepare_sending)

我们需要的效果就实现了 。

2.4 更新进度条

这一节,我们又将引入一个新的概念,在发送的同时我们希望有一个进度条可以告知我们发送的状态,由于发送文件需要一段时间,如果简单的在窗口逻辑中更新进度条的话,会导致界面卡死无法响应用户,因为在一个线程中同时只能做一件事情,如果主线程在发送文件,那么GUI的响应就没办法得到处理,这对于GUI应用来说是不可以接受的。新建一个线程和主程序并行执行,来做需要在后台完成的任务是这个问题的解决办法,我们可以称之为发送线程。

首先,我们从QThread继承,实现一个新的发送类,这个类将接受来自发送对话框传来的包含要发送文件的url_list,我们定义一个自己的方法pass_para,用来接收这个url_list,而run方法是每一个线程类的主体,也是线程在运行的时候执行的代码,我们在线程运行的代码中,将要发送的文件放到need_to_send中,因为发送会话框传来的有可能是文件,也有可能是文件夹,如果是文件夹,我们需要发送文件夹里边的所有文件,因此使用我们自定义的get_files_in_folder来找到所有要发送的文件。

 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
def get_files_in_folder(folder):
    for root, dirs, files in os.walk(folder):
        for file_name in files:
            _, ext = os.path.splitext(file_name)
            yield os.path.join(root, file_name)

class SocketClientThread(QThread):

    def __init__(self):
        super(SocketClientThread, self).__init__()
        self.running = False
        self.progressbar = None

    def pass_para(self, url_list):
        self.url_list = url_list

    def run(self):
        need_to_send = list()
        # find files
        for url in self.url_list:
            if os.path.isfile(url):
                need_to_send.append(url)
            else:
                for f in get_files_in_folder(url):
                    need_to_send.append(f)
模拟发送:

我们使用time.sleep(0.5)来模拟发送文件需要的时间,当每个文件发送完毕以后,我们会发送带有当前进度的信号出去。更新SocketClientThread如下,添加progress_updated信号,并在,线程运行中,计算发送进度,然后通过信号触发出去。

 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
class SocketClientThread(QThread):

    progress_updated = pyqtSignal(int)

    def __init__(self):
        super(SocketClientThread, self).__init__()
        self.running = False
        self.progressbar = None

    def pass_para(self, url_list):
        self.url_list = url_list

    def run(self):
        need_to_send = list()
        # find files
        for url in self.url_list:
            if os.path.isfile(url):
                need_to_send.append(url)
            else:
                for f in get_files_in_folder(url):
                    need_to_send.append(f)
        import time
        for i, f in enumerate(need_to_send):
            progress = (100.0*i)/len(need_to_send)
            # sending file with socket
            #
            self.progress_updated.emit(progress)
            time.sleep(0.5)
        self.progress_updated.emit(100)
进度条更新

进度条的更新主要是使用setValue来更新当前进度(0到100),在每个文件发送完毕以后,计算已发送的百分比,然后使用setValue方法来更新进度条控件的当前位置。因为进度条在发送对话框上,因此在SendDialog上增加一个槽函数,该函数接收当前进度,然后更新进度条的值,如果进度条到达100%以后,关闭按钮上的文字变成“确认”,颜色变为绿色 。

1
2
3
4
5
6
@pyqtSlot(int)
    def update_progress(self, progress):
        self.ui.progressBar.setValue(progress)
        if progress == 100:
            self.ui.pushButtonClose.setText("确认");
            self.ui.pushButtonClose.setStyleSheet("background-color:#03fc8c;");

最后,因为文件的发送是伴随着发送对话框发生的,所以,我们在只要发送对话框出现的同时创建发送线程就可以,同时我们将线程发送的信号,和对话框上新建的更新进度条的槽连接起来 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class SendDialog(QDialog):
    def __init__(self, url_list):
        super(SendDialog, self).__init__()


        # UI setup - 1 option
        # dynamic load ui for development purpose
        self.ui = loadUi('./dialog.ui', self)
        self.ui.pushButtonClose.setStyleSheet("background-color:#ffffff;");
        self.ui.pushButtonClose.setText("取消");
        self.ui.pushButtonClose.clicked.connect(self.close)


        self.socket_client_thread = SocketClientThread()
        self.socket_client_thread.pass_para(url_list)
        self.socket_client_thread.progress_updated.connect(self.update_progress)
        self.socket_client_thread.start()

本章节最终实现的源代码可以在git仓库https://github.com/pythonlibrary/wifidrop 中的 tutorial-3 tag的commit位置找到。

最终实现的效果为 :




系列文章传送门:

类似文章

PyQt5教程2:主页面和拖动

PyQt5教程2:主页面和拖动

  • xhyl
  • January 15, 2020

这一篇文章中,我们要实现的是主界面和主界面上需要支持的一些功能,我们将使用Qt Creator来创建我们的基本主界面。 1. UI设计 打开Qt Creator,在文件菜单中选择“新建文件或项目”,因为我们只是用Qt Creator来设计UI,所以我们只用它来创建和编辑.

阅读更多
PyQt5教程1:项目介绍和环境搭建

PyQt5教程1:项目介绍和环境搭建

  • xhyl
  • January 13, 2020

编程从来都是一门实践性很强的技术,最好的学习方式就是动起手来 写在前面 关于如何使用PyQt(PyQt4或者PyQt5)在网上有很多简单或者详细的教程,但是我发现大部分的教程是从一个最简单的点展开,试图从一个Hello World讲起,手动创建控件,比如如何用代码创建一个按钮,如果在按钮上绑定一个事件,如何利用该事件输出文字到文字框等等。还有一些教程,非常详尽的讲解每一个控件具有哪些功能,并展示如何使用。前者更像是一本教材,准备由浅入深,但是很多人可能在Hello World之后就因为枯燥的示例被劝退,后者更像一本参考资料,很少希望从头到尾的学习完所有控件。

阅读更多
Linux内核开发之编译和运行

Linux内核开发之编译和运行

在本篇文章中,读者可以跟着我们的一步一步的教程最终学会如何搭建Linux内核开发环境,并最终能成功运行自己编译的内核,本文主要是针对arm架构来编译Linux内核,因此读者不仅可以学习到如果编译内核,还将学会如何使用QEMU搭建arm仿真环境。 本文将介绍两种Linux内核编译方法 第一种为不借助任何编译系统的原始编译方法,相对较为复杂 第二种则借助BuildRoot编译系统实现了一键编译,相对比较简单 编译Linux内核需要在Linux系统中运行,推荐使用Ubuntu,并安装 build-essentials 包,本

阅读更多