在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)
然后执行
将窗口显示出来供用户交互,这里调用了.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位置找到。
最终实现的效果为 :
您的浏览器不支持本视频
系列文章传送门: