之前四篇文章论证了利用二维码传输文件的可行性,本章使用tkinter开发【动态二维码文件发送端】,发送端具备文件选择、开始发送文件、停止发送文件以及显示发送状态的功能,程序界面下:
这里下载源码运行↓↓↓
目录
经过简单分析,要开发该程序,需要完成以下五个步骤,1)程序的界面设计;2)选择文件功能开发;3)文件拆分成二维码功能开发;4)发送和停止线程开发;5)发送状态更新功能开发。接一下一个一个步骤进行研究。
一、程序的界面设计
程序界面内容不多,只需用到图片显示组件、文本组件、按钮组件,然后使用tkinter的网格布局.grid(row=,column=,......)把组件放置在对应的位置即可。由于文件路径、图片、状态会随着用户的操作发生变化,这里把文件路径变量_label_filepath、图片对象变量_label_dt、进度和状态变量_label_progress_text、_label_state_text设置为程序的全局变量,方便后续开启线程调用全局global变量,更新上述组件。
主要代码如下:
from tkinter import Tk, Label, Button, StringVar
from PIL import Image, ImageTk
# 显示图片组件
_label_dt = None
# 显示文件路径组件,_label_filepath_text为文件路径
_label_filepath_text = None
# 显示发送状态组件,_label_progress_text为进度,_label_state_text为状态
_label_progress_text = None
_label_state_text = None
def open_window():
# 创建窗口
root = Tk()
# 设置窗口的标题
root.title("动态二维码文件发送端")
# 创建图片组件,None.png为未启动发送时的默认图片
_pil_image = Image.open('None.png')
# 缩放图片
_w, _h = _pil_image.size
_pil_image_resized = resize(_w, _h, 300, 300, _pil_image)
_tk_image = ImageTk.PhotoImage(_pil_image_resized)
# 创建图片组件,放置在root中
global _label_dt
_label_dt = Label(root, image=_tk_image, width=300, height=300)
_label_dt.grid(row=0, columnspan=4, column=0)
_button = Button(root, text="选择待发送文件......", command=open_file)
_button.grid(row=1, column=0,columnspan=2)
# 创建label可变文本,用于动态更新选中的文件路径
global _label_filepath_text
_label_filepath_text = StringVar()
_label_filepath = Label(root, textvariable=_label_filepath_text)
_label_filepath.grid(row=1,column=2, columnspan=2)
# 创新开始、停止按钮
_button_start = Button(root, text="开始发送", command=start_send_file)
_button_stop = Button(root, text="停止发送", command=stop_send_file)
_button_start.grid(row=2, column=0)
_button_stop.grid(row=2, column=1)
# 创建label可变文本,用于动态更新发送文件状态
global _label_progress_text
_label_progress_text = StringVar()
_label_progress_text.set("[0/0]")
_label_progress = Label(root, textvariable=_label_progress_text)
_label_progress.grid(row=2, column=2)
# 创建label可变文本,用于动态更新发送文件状态
global _label_state_text
_label_state_text = StringVar()
_label_state_text.set("待发送")
_label_state = Label(root, textvariable=_label_state_text)
_label_state.grid(row=2, column=3)
# 显示窗口
root.mainloop()
# 缩放图片的函数,w是原来图片的宽,h是原来图片的高,w_box是想缩放的宽,h_box是想缩放的高
# pil_image是原图片对象,函数会返回一个缩放好的图像对象
def resize(w, h, w_box, h_box, pil_image):
f1 = 1.0 * w_box / w
f2 = 1.0 * h_box / h
factor = min([f1, f2])
width = int(w * factor)
height = int(h * factor)
return pil_image.resize((width, height), Image.Resampling.LANCZOS)
二、tkinter选择文件功能开发
第三篇文章中的python程序是通过固定地址读取默认图片文件的。本章为了增加程序的灵活性,实现用户自行选择待发送文件功能。
经过百度搜索和学习,发现tkinter中的filedialog可实现文件选取功能。下面编写open_file函数实现该功能,open_file函数通过filedialog.askopenfilename()获取用户选择的文件路径,然后使用_label_filepath_text.set()方法,把用户选中文件路径更新到界面中。
主要代码如下:
from tkinter import filedialog
# 选择待发送的文件
def open_file():
_file_path = filedialog.askopenfilename(title=u'选择文件', initialdir=(os.path.expanduser('C:/')))
if _file_path is not None:
# 设置并显示选中的文件路径
global _label_filepath_text
_label_filepath_text.set(_file_path)
print(get_file_size(_file_path))
return _file_path
显示效果如下:
三、文件拆分成二维码功能开发
文件拆分成二维码,在之前几篇文章中都有介绍,这里就不多说了。本章主要对拆分代码进行优化:
1)拆解前先对文件进行压缩(使用了zlib库进行压缩),减少拆分发送二维码的数量;
2) 利用_label_state_text.set()把拆分的状态实时显示在界面上。
主要代码如下:
import os
import datetime
import zlib
import base64
# 按固定长度拆分_base64格式的字节流,_file_path为文件路径,_step_length为拆分的固定长度(步长)
def split_file(_file_path, _step_length):
global _label_state_text
_start_time = datetime.datetime.now()
# 使用二进制模式读取以_file_path为路径的文件
_file = open(_file_path, mode="rb")
# 一次性读取所有字节(若文件太大请分段读取避免爆内存)
_label_state_text.set("读取文件")
_contents = _file.read()
_label_state_text.set("压缩文件")
_contents_zip = zlib.compress(_contents, zlib.Z_BEST_COMPRESSION)
# 转换为_base64格式方面传输数据
_label_state_text.set("编码")
_contents_base64 = base64.b64encode(_contents_zip)
# 调用拆分函数
_base64_str_list = []
# 字节流长度
_length = len(_contents_base64)
# //整除(只保留商的整数部分)
_frequency = _length // _step_length
_j = 0
_label_state_text.set("拆分文件信息")
for i in range(0, _length, _step_length):
_temp_str = _contents_base64[i:i + _step_length]
_temp_str_utf8 = '[' + str(_j) + '/' + str(_frequency) + ']' + _temp_str.decode('utf-8')
_base64_str_list.append(_temp_str_utf8)
_j = _j + 1
_end_time = datetime.datetime.now()
print("拆分块数:", _frequency)
print("拆分文件用时(秒):", (_end_time - _start_time).seconds)
return _base64_str_list
四、发送和停止线程开发
文件拆分好后就可以动态生成二维码了,动态显示二维码在第三篇文章中已经探讨过,本章主要对代码进行优化。
1)打印的二维码不用先拼接成链表,可边拆解边显示二维码;
2)可以控制动态二维码的开始和暂停,由于目前没发现python中thread类的停止方法,所以使用循环判断全局变量标志_thread_flag的方法控制线程结束。
主要代码如下:
_thread_flag = None
_thread = None
# 根据拆分后的字节流列表动态显示二维码,_base64_str_list为拆分后的字节列表,_time_interval为显示间隔
def print_qr(_base64_str_list, _time_interval):
global _thread_flag
# 循环生成二维码图片
_qr = qrcode.QRCode()
_pil_image = None
_pil_image_resized = None
_tk_image = None
_list_len = len(_base64_str_list)
global _label_state_text
_label_state_text.set("文件发送中")
while _thread_flag is True:
for i in range(0, _list_len):
# 判断_thread_flag的值,当为False时中断执行
if _thread_flag is True:
# 1)清空原_qr对象数据;2)把列表中的数据加入_qr对象中
_qr.clear()
_qr.add_data(_base64_str_list[i], 0)
# 1)根据列表中的数据制作二维码图片;2)缩放图片;3)显示在界面上
_pil_image = _qr.make_image()
_w, _h = _pil_image.size
_pil_image_resized = resize(_w, _h, 300, 300, _pil_image)
_tk_image = ImageTk.PhotoImage(_pil_image_resized)
# 更新二维码显示组件_label_dt
global _label_dt
_label_dt.configure(image=_tk_image)
_label_dt.image = _tk_image
global _label_progress_text
_label_progress_text.set("[%d/%d]" % (i, _list_len))
# 设置二维码刷新时间间隔
time.sleep(_time_interval)
else:
break
# 用于更新图片的线程
def update_qr_thread(_base64_str_list, _time_interval):
# 设置更新频率为_time_interval秒
# 设置线程参数,target为线程启动函数,name为线程名,args为传递给线程函数的参数
global _thread
_thread = threading.Thread(target=print_qr, name='print_qr', args=(_base64_str_list, _time_interval,))
# 启动线程
_thread.start()
# 点击,发送文件
def start_send_file():
global _label_filepath_text, _thread_flag, _thread, _label_state_text
if _label_filepath_text is not None:
if (_thread is None) or (_thread.is_alive() is False):
_f_size = get_file_size(_label_filepath_text.get())
if 0 < _f_size < 2:
# 拆分选中文件
_temp_list = split_file(_label_filepath_text.get(), 512)
# 调用二维码生成函数
_thread_flag = True
update_qr_thread(_temp_list, 0)
else:
_label_state_text.set("文件应小于2MB")
else:
_label_state_text.set("有发送任务正在进行")
# 点击,停止发送文件
def stop_send_file():
global _thread_flag
_thread_flag = False
五、发送状态更新功能开发
界面状态更新相对简单,图片使用_label_dt.configure(image=_tk_image)更新,文字使用StringVar类调用_label_state_text.set()即可。
六、运行效果和完整代码