制作工具下载器 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, '请输入包件链接...
')
            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, '错误的链接,请重新输入...
')
                scrolled_text.see(END)
                return None
            else:
                if self.dir_path is '':
                    scrolled_text.insert(INSERT, '请输入包件保存路径...
')
                    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, '错误的文件路径,请重新输入...
')
                        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
')
            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 + ' 目录下' + '
')
        scrolled_text.insert(INSERT, '下载完成!
')
        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) + '
')
                    button_start['text'] = "重新开始"

    def run_up(self):
        if self._is_valid():  # 判断url的有效性
            print("url 和 dir 检查通过")
            scrolled_text.insert(INSERT, 'url 和 dir 检查通过
')
            # 改变输入框文本颜色
            entry_url['fg'] = 'black'
            entry_path['fg'] = 'black'
            self.getfilename()
            print("开始下载...")
            scrolled_text.insert(INSERT, '开始下载...
')

            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

    生成文件效果
    生成1
    生成2

后记

本文参考了 polyhedronx 博主的文章

原文地址:https://www.cnblogs.com/nimo97/p/9704762.html