欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

制作工具下载器 by tkinter

程序员文章站 2022-07-13 15:01:15
...

前言

这是一次想 实现进度条功能 而引发的小程序开发,越做发现涉及的东西越多,本文只做简单成效实现过程的描述,优化项目以后再做补充。

目录


概述

  • 先上效果图
    制作工具下载器 by tkinter
    制作工具下载器 by tkinter
    制作工具下载器 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 文件。
制作工具下载器 by tkinter
将生成的 bak.py 文件里的 img 引入到 download.py 文件中,见上文代码。

  • 执行 exe 文件生成命令
    pyinstaller -F -w download.py -i nimo.ico

    生成文件效果
    制作工具下载器 by tkinter
    制作工具下载器 by tkinter

后记

本文参考了 polyhedronx 博主的文章