使用 Kivy 应用程序下载文件而不锁定事件循环

Mic*_*rie 2 python kivy

这是一个带有按钮和进度条的 Kivy 应用程序。按下按钮后,会从 Web 下载 ZIP 文件并解压缩。进度条会前进以标记下载进度。

问题是,下载锁定了 Kivy 事件循环,在下载过程中冻结了应用程序。如何在后台下载和解压文件?

from __future__ import division
import os
import requests
import zipfile
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout

ZIP_URL = 'https://www.python.org/ftp/python/3.5.1/python-3.5.1-embed-win32.zip'
ZIP_FILENAME = 'Python351.zip'

kv_string = """
<RootWidget>
    BoxLayout:
        orientation: "vertical"
        Button:
            id: download_button
            text: "Download content"
            on_press: self.parent.parent.download_content()
        ProgressBar:
            id: download_progress_bar
            max: 1
            value: 0.1
"""

Builder.load_string(kv_string)


class RootWidget(BoxLayout):
    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)

    def download_content(self):
        self.ids["download_button"].disabled = True

        total_size_KiB = 6023182 / 1024 # DEBUG: hardcoded
        print("Downloading file: %s" % ZIP_URL)
        with open(ZIP_FILENAME, 'wb') as handle:
            response = requests.get(ZIP_URL, stream=True)

            if not response.ok:
                print("Something went wrong.")
                return

            current_size_KiB = 0
            for block in response.iter_content(1024):
                percent_downloaded = current_size_KiB / total_size_KiB
                if not current_size_KiB % 100:
                    print("%.2f%% downloaded so far" % (percent_downloaded * 100))
                self.ids['download_progress_bar'].value = percent_downloaded

                handle.write(block)

                current_size_KiB += 1

        self.unzip_content()

    def unzip_content(self):
        print("Unzipping file")
        fh = open(ZIP_FILENAME, 'rb')
        z = zipfile.ZipFile(fh)
        ZIP_EXTRACT_FOLDER = ZIP_FILENAME + '_extracted'
        if not os.path.exists(ZIP_EXTRACT_FOLDER):
            os.makedirs(ZIP_EXTRACT_FOLDER)
        z.extractall(ZIP_EXTRACT_FOLDER)
        fh.close()
        os.remove(ZIP_FILENAME)

        print("Done")


class MyApp(App):

    def build(self):
        return RootWidget()


if __name__ == '__main__':
    MyApp().run()
Run Code Online (Sandbox Code Playgroud)

Mic*_*rie 5

首先,正如@Nykakin 所建议的,要使下载异步,请使用kivy.network.urlrequest.UrlRequest

def download_content(self):
    self.ids["download_button"].disabled = True
    req = UrlRequest(ZIP_URL, on_progress=self.update_progress,
                     chunk_size=1024, on_success=self.unzip_content,
                     file_path=ZIP_FILENAME)

def update_progress(self, request, current_size, total_size):
    self.ids['download_progress_bar'].value = current_size / total_size
Run Code Online (Sandbox Code Playgroud)

其次,正如@KeyWeeUsr 建议的那样,通过使用threading模块更改解压缩方法,使其不会阻塞事件循环:

def unzip_content(self, req, result):
    threading.Thread(target=self.unzip_thread).start()

def unzip_thread(self):
    # ... (same as unzip_content method in question)
Run Code Online (Sandbox Code Playgroud)

这是完整的新版本:

from __future__ import division
import os
import zipfile
import threading
import time
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.network.urlrequest import UrlRequest

ZIP_URL = 'https://www.python.org/ftp/python/3.5.1/python-3.5.1-embed-win32.zip'
ZIP_FILENAME = 'Python351.zip'

kv_string = """
<RootWidget>
    BoxLayout:
        orientation: "vertical"
        Button:
            id: download_button
            text: "Download content"
            on_press: self.parent.parent.download_content()
        ProgressBar:
            id: download_progress_bar
            max: 1
            value: 0
"""

Builder.load_string(kv_string)


class RootWidget(BoxLayout):

    stop = threading.Event()    

    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)

    def download_content(self):
        self.ids["download_button"].disabled = True
        req = UrlRequest(ZIP_URL, on_progress=self.update_progress,
                         chunk_size=1024, on_success=self.unzip_content,
                         file_path=ZIP_FILENAME)

    def update_progress(self, request, current_size, total_size):
        self.ids['download_progress_bar'].value = current_size / total_size

    def unzip_content(self, req, result):
        threading.Thread(target=self.unzip_thread).start()

    def unzip_thread(self):
        print("Unzipping file")
        fh = open(ZIP_FILENAME, 'rb')
        z = zipfile.ZipFile(fh)
        ZIP_EXTRACT_FOLDER = ZIP_FILENAME + '_extracted'
        if not os.path.exists(ZIP_EXTRACT_FOLDER):
            os.makedirs(ZIP_EXTRACT_FOLDER)
        z.extractall(ZIP_EXTRACT_FOLDER)
        fh.close()
        os.remove(ZIP_FILENAME)
        time.sleep(4) # DEBUG: stretch out the unzip method to test threading

        print("Done")


class MyApp(App):

    def on_stop(self):
        # The Kivy event loop is about to stop, set a stop signal;
        # otherwise the app window will close, but the Python process will
        # keep running until all secondary threads exit.
        self.root.stop.set()        

    def build(self):
        return RootWidget()


if __name__ == '__main__':
    MyApp().run()
Run Code Online (Sandbox Code Playgroud)