制作工具下载器 by tkinter
前言
这是一次想 实现进度条功能 而引发的小程序开发,越做发现涉及的东西越多,本文只做简单成效实现过程的描述,优化项目以后再做补充。
目录
概述
先上效果图
功能介绍
该下载器只能下载已知工具包(即将例如 QQ、python、nginx 等包文件的链接复制粘贴到那个链接 Entry 里),通过点按打开按钮,选择要存放的目录。
视频质量下拉菜单和暂停下载功能暂未实现,有待后期补充,如有大神,请指点一二。
源代码
本程序基于 Python 3.6.6 编写,如用 3.x 版本编辑,问题应该不大,请自行解决。
后期生成 exe 程序,需要用到 PyInstaller,我用的版本是 3.3.1。
实现下载器功能
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/8/27 15:40
# @Author : Nimo
# @File : study.py
# @Software: PyCharm
import os
import urllib
import time
import requests
from tkinter import *
from tkinter.scrolledtext import ScrolledText
from PIL import Image, ImageTk
import threading
from tkinter.filedialog import askdirectory
# 这里引入的两个包是为了后期生成exe程序用,源程序测试时请注释掉这两行以及后面的相关行
import base64
from picture.bak import img as logo
class GetFile(): #下载文件
def __init__(self, url, dir_path):
self.url = url
self.dir_path = dir_path
self.filename = ""
self.re = requests.head(self.url, allow_redirects=True) # 运行head方法时重定向
# url and path 有效性检查
def _is_valid(self):
if self.url is '':
scrolled_text.insert(INSERT, '请输入包件链接...\n')
scrolled_text.see(END)
return None
else:
pattern = '^(https|http|ftp)://.+$'
# pattern = '^(https|http:)//([0-9a-zA-Z]*\.[0-9a-zA-Z]*\.(com|org)/).+$'
url_pattern = re.compile(pattern, re.S)
result = re.search(url_pattern, self.url)
if result is None:
scrolled_text.insert(INSERT, '错误的链接,请重新输入...\n')
scrolled_text.see(END)
return None
else:
if self.dir_path is '':
scrolled_text.insert(INSERT, '请输入包件保存路径...\n')
scrolled_text.see(END)
return None
else:
path_pattern = re.compile('(^[A-Z]:/[0-9a-zA-Z_]+(/[0-9a-zA-Z_]+)*$)|(^[A-K]:/[0-9a-zA-Z_]*$)',
re.S)
result = re.search(path_pattern, self.dir_path)
if result is None:
scrolled_text.insert(INSERT, '错误的文件路径,请重新输入...\n')
scrolled_text.see(END)
return None
else:
return True
# 下载文件主要方法
def getsize(self):
try:
self.file_total = int(self.re.headers['Content-Length']) # 获取下载文件大小
return self.file_total
except:
scrolled_text.insert(INSERT, '无法获取文件大小,请检查url\n')
scrolled_text.see(END)
return None
def getfilename(self): # 获取默认下载文件名
if 'Content-Disposition' in self.re.headers:
n = self.re.headers.get('Content-Disposition').split('name=')[1]
self.filename = urllib.parse.unquote(n, encoding='utf8')
elif os.path.splitext(self.re.url)[1] != '':
self.filename = os.path.basename(self.re.url)
return self.filename
def down_file(self): #下载文件
self.r = requests.get(self.url,stream=True)
with open(self.filename, "wb") as code:
for chunk in self.r.iter_content(chunk_size=1024): #边下载边存硬盘
if chunk:
code.write(chunk)
time.sleep(1)
text = os.getcwd()
scrolled_text.insert(INSERT,str(self.filename) + ' 存放在' + text + ' 目录下' + '\n')
scrolled_text.insert(INSERT, '下载完成!\n')
scrolled_text.see(END)
# 进度条实现方法
def change_schedule(self):
now_size = 0
total_size = self.getsize()
while now_size < total_size:
time.sleep(1)
if os.path.exists(self.filename):
try:
down_rate = (os.path.getsize(self.filename) - now_size)/1024/1024 + 0.001
down_time = (total_size - now_size)/1024/1024/down_rate
now_size = os.path.getsize(self.filename)
# 文件大小进度
canvas.delete("t1")
size_text = '%.2f' % (now_size / 1024 / 1024) + '/' + '%.2f' % (total_size / 1024 / 1024) + 'MB'
canvas.create_text(90, 10, text=size_text, tags="t1")
# 下载速度
speed_text = str('%.2f' % down_rate + "MB/s")
speed.set(speed_text)
# 将下载秒数改为时间格式显示
m, s = divmod(down_time, 60)
h, m = divmod(m, 60)
time_text = "%02d:%02d:%02d" % (h, m, s)
remain_time.set(time_text)
# 进度条更新
canvas.coords(fill_rec, (0, 0, 5 + (now_size / total_size) * 180, 25))
top.update()
if round(now_size / total_size * 100, 2) == 100.00:
time_text = "%02d:%02d:%02d" % (0,0,0)
remain_time.set(time_text)
speed.set("完成")
button_start['text'] = "开始"
except ZeroDivisionError as z:
scrolled_text.insert(INSERT, '出错啦:' + str(z) + '\n')
button_start['text'] = "重新开始"
def run_up(self):
if self._is_valid(): # 判断url的有效性
print("url 和 dir 检查通过")
scrolled_text.insert(INSERT, 'url 和 dir 检查通过\n')
# 改变输入框文本颜色
entry_url['fg'] = 'black'
entry_path['fg'] = 'black'
self.getfilename()
print("开始下载...")
scrolled_text.insert(INSERT, '开始下载...\n')
th1 = threading.Thread(target=self.change_schedule, args=())
th2 = threading.Thread(target=self.down_file, args=())
th = [th1, th2]
for t in th:
t.setDaemon(True)
t.start()
# 由于threading本身不带暂停、停止、重启功能,我试图用线程阻塞的办法来实现,但是还是失败了,问题还在发现、解决中,欢迎网友来评论里交流。
'''+++++++++++++++++++++++++++++++Tk动作+++++++++++++++++++++++++++++++++++'''
def start():
url = entry_url.get()
dir_path = entry_path.get()
os.chdir(dir_path)
scrolled_text.delete('1.0', END)
down_file = GetFile(url, dir_path)
if os.path.exists(down_file.filename):
os.remove(down_file.filename)
down_file.run_up()
# 暂停功能未能实现,这里便注释掉了
# if button_start['text'] == "开始" or button_start['text'] == "继续":
# flag = True
# down_file.run_up(flag)
# button_start['text'] = "暂停"
#
# elif button_start['text'] == "暂停":
# flag = False
# down_file.run_up(flag)
# button_start['text'] = "继续"
def select_path():
path_ = askdirectory()
var_path_text.set(path_)
"""=============================tkinter窗口============================"""
# 顶层窗口
top = Tk() # 创建顶层窗口
top.title('nimo_工具下载器')
screen_width = top.winfo_screenwidth() # 屏幕尺寸
screen_height = top.winfo_screenheight()
window_width, window_height = 600, 450
x, y = (screen_width - window_width) / 2, (screen_height - window_height) / 3
size = '%dx%d+%d+%d' % (window_width, window_height, x, y)
top.geometry(size) # 初始化窗口大小
top.resizable(False, False) # 窗口长宽不可变
# top.maxsize(600, 450)
# top.minsize(300, 240)
# 插入背景图片
tmp = open('bak.png', 'wb+') # 临时文件用来保存png图片
tmp.write(base64.b64decode(logo))
tmp.close()
image = Image.open('bak.png')
bg_img = ImageTk.PhotoImage(image)
label_img = Label(top, image=bg_img, cursor='spider')
os.remove('bak.png')
# 测试时,注释掉上面的图片插入方法
image = Image.open('bak.png')
bg_img = ImageTk.PhotoImage(image)
label_img = Label(top, image=bg_img, cursor='spider')
# 包件链接(Label+Entry)
label_url = Label(top, text='程序下载链接', cursor='xterm')
var_url_text = StringVar()
entry_url = Entry(top, relief=RAISED, fg='gray', bd=2, width=58, textvariable=var_url_text, cursor='xterm')
# 保存路径(Label+Entry)
label_path = Label(top, text='包件保存路径', cursor='xterm')
var_path_text = StringVar()
entry_path = Entry(top, relief=RAISED, fg='gray', bd=2, width=58, textvariable=var_path_text, cursor='xterm')
button_choice = Button(top, relief=RAISED, text='打开', bd=1, width=5, height=1, command=select_path, cursor='hand2')
# 视频清晰度选择(Label+OptionMenu),这里的功能没设计相关方法,其实可单独做一个视频下载器
label_option = Label(top, text='视频质量', cursor='xterm')
options = ['高清HD', '标清SD', '普清LD']
var_option_menu = StringVar()
var_option_menu.set(options[0])
option_menu = OptionMenu(top, var_option_menu, *options)
# 按钮控件
button_start = Button(top, text='开始', command=start, height=1, width=15, relief=RAISED, bd=4, activebackground='pink',
activeforeground='white', cursor='hand2')
# button_pause = Button(top, text='暂停', command='', height=1, width=15, relief=RAISED, bd=4, activebackground='pink',
# activeforeground='white', cursor='hand2')
button_quit = Button(top, text='退出', command=top.quit, height=1, width=10, relief=RAISED, bd=4, activebackground='pink',
activeforeground='white', cursor='hand2')
# 下载进度(标签,进度条,进度条里的已下载大小和总大小,下载速度,剩余时间)
progress_label = Label(top, text='下载进度', cursor='xterm')
canvas = Canvas(top, width=180, height=20, bg="white")
# 进度条填充
out_rec = canvas.create_rectangle(0, 0, 180, 20, outline="white", width=1)
fill_rec = canvas.create_rectangle(0, 0, 0, 0, outline="", width=0, fill="green")
speed = StringVar()
speed_label = Label(top, textvariable=speed, cursor='xterm', width=15, height=1)
remain_time = StringVar()
remain_time_label = Label(top, textvariable=remain_time, cursor='xterm', width=15, height=1)
# 可滚动的多行文本区域
scrolled_text = ScrolledText(top, relief=GROOVE, bd=4, height=14, width=70, cursor='xterm')
# place布局
label_img.place(relx=0.5, rely=0.08, anchor=CENTER)
label_url.place(relx=0.12, rely=0.12, anchor=CENTER)
entry_url.place(relx=0.56, rely=0.12, anchor=CENTER)
label_path.place(relx=0.12, rely=0.20, anchor=CENTER)
entry_path.place(relx=0.56, rely=0.20, anchor=CENTER)
button_choice.place(relx=0.94, rely=0.20, anchor=CENTER)
label_option.place(relx=0.14, rely=0.30, anchor=CENTER)
option_menu.place(relx=0.29, rely=0.30, anchor=CENTER)
button_start.place(relx=0.80, rely=0.30, anchor=CENTER)
# button_pause.place(relx=0.58, rely=0.30, anchor=CENTER)
progress_label.place(relx=0.14, rely=0.40, anchor=CENTER)
canvas.place(relx=0.37, rely=0.4013, anchor=CENTER)
speed_label.place(relx=0.62, rely=0.40, anchor=CENTER, )
remain_time_label.place(relx=0.81, rely=0.40, anchor=CENTER)
scrolled_text.place(relx=0.48, rely=0.69, anchor=CENTER)
button_quit.place(relx=0.92, rely=0.96, anchor=CENTER)
# 输入框默认内容,可按需自行修改
var_url_text.set(r'https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tgz')
var_path_text.set(r'C:/Users')
# 运行这个GUI应用
top.mainloop()
实现 exe 封装
由于下载器设置了背景图片,所以在成功生成 exe 文件后,运行时必须要把背景图和它放到同一目录下,但是这就很 low 了,所以用了下面的方法,将图片分解成可解析的 py 文件,代码如下:
# pic_to_py.py
import base64
def png_to_py(picture_name):
open_png = open("%s.png" % picture_name, 'rb')
b64str = base64.b64encode(open_png.read())
open_png.close()
write_data = 'img = "%s"' % b64str.decode()
f = open('%s.py' % picture_name, 'w+')
f.write(write_data)
f.close()
if __name__ == '__main__':
picture = ['bak']
try:
for p in picture:
png_to_py(p)
except Exception as e:
print(e)
执行 pic_to_py.py 脚本,将在同目录下生成和背景图同名的 py 文件。
将生成的 bak.py 文件里的 img 引入到 download.py 文件中,见上文代码。
执行 exe 文件生成命令
pyinstaller -F -w download.py -i nimo.ico
生成文件效果
后记
本文参考了 polyhedronx 博主的文章