zoukankan      html  css  js  c++  java
  • 制作工具下载器 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 博主的文章

  • 相关阅读:
    Java IO流
    博客园禁止pc端以及手机端选中复制粘贴
    eclipse debug模式出现 source not found
    Winform之跨线程访问控件(在进度条上显示字体)
    WPF中DataGrid的ComboBox的简单绑定方式(绝对简单)
    WPF制作QQ列表(仿qq列表特效)
    WPF柱状图(支持数据库动态更新)之组件的数据动态化
    WPF柱状图(支持数据库动态更新)
    WPF仿微软事件和属性窗体,效果更炫!
    DataGrid缓冲加载数据
  • 原文地址:https://www.cnblogs.com/nimo97/p/9704762.html
Copyright © 2011-2022 走看看