Search code examples
pythonpython-3.xkivykivy-languagepython-3.9

How to update kivy progress bar value from new thread?


I have this image downloader which works as new thread and a popup which contains progress bar. Progress bar does not update during download but after(downloader is written with requests, gui app was made with kivy). Any ideas how to fix this stuff?

Downloader: It is separated in another file

class Downloader(threading.Thread):

    def __init__(self, url: str, download_monitor):
        super(Downloader, self).__init__(daemon=True)  # daemon dies when main die
        self.url = url
        self.download_monitor = download_monitor  # url popup

    def run(self) -> None:
        # Reset
        self.download_monitor.reset()

        file_name = self.url.split('/')[-1]

        # Less RAM usage
        with requests.get(self.url, stream=True) as req:  # stream=True not to read at once
            req.raise_for_status()
            with open('temp/'+file_name, 'wb') as file:
                chunks = list(enumerate(req.iter_content(chunk_size=8192)))
                self.download_monitor.downloading_progress.max = chunks[-1][0]  # last element
                for progress, chunk in chunks:
                    self.download_monitor.downloading_progress.value = progress
                    file.write(chunk)

PopUp .py: It is separated in another file

class UrlPopup(Popup):
    url_input = ObjectProperty()
    downloading_progress = ObjectProperty()

    def __init__(self, **kwargs):
        super(UrlPopup, self).__init__(**kwargs)
        
    def download(self):
        # https://www.nasa.gov/sites/default/files/thumbnails/image/hubble_ngc2903_potw2143a.jpg.jpg
        if self.url_input.text.startswith('https://'):  # if it is url address
            download(self.url_input.text, self)

    def on_dismiss(self):
        self.reset()
        self.url_input.text = ''

    def reset(self):
        self.downloading_progress.max = 0
        self.downloading_progress.value = 0

PopUp .kv: It is separated in another file

<UrlPopup>:
    url_input: url_input
    downloading_progress: downloading_progress

    id: downloader
    title: 'URL address'
    size_hint: .25, None
    height: 157

    BoxLayout:
        orientation: 'vertical'
        size_hint_y: None
        height: 64

        TextInput:
            id: url_input

            multiline: False
            size_hint_y: None
            height: 32
            font_size: 16

        ProgressBar:
            id: downloading_progress

            size_hint_y: None
            height: 32

        BoxLayout:
            orientation: 'horizontal'
            size_hint_y: None
            height: 32

            Button:
                text: 'Download'
                on_press: root.download()
            Button:
                text: 'Close'
                on_press: root.dismiss()

EDIT1 ApuCoder I did as you wrote but progress still updates after download. Any other ideas? PopUP .py:

class UrlPopup(Popup):
    url_input = ObjectProperty()
    downloading_progress = ObjectProperty()
    progress_value = NumericProperty()

    def update_progress(self, dt):
        self.progress_value += 1

Downloader .py:

 with requests.get(self.url, stream=True) as req:  # stream=True not to read at once
            req.raise_for_status()
            with open('temp/'+file_name, 'wb') as file:
                chunks = list(enumerate(req.iter_content(chunk_size=8192)))
                self.download_monitor.downloading_progress.max = chunks[-1][0]  # last element
                Clock.schedule_interval(self.download_monitor.update_progress, .1)
                for progress, chunk in chunks:
                    #self.download_monitor.downloading_progress.value = progress
                    file.write(chunk)

PopUp .kv:

ProgressBar:
            id: downloading_progress

            value: root.progress_value
            size_hint_y: None
            height: 32

EDIT2 This is in same file as class Downloader. I call this function when a button is pressed

def download(url: str, download_monitor):
    """Other thread"""
    downloader = Downloader(url, download_monitor)
    downloader.start()

Solution

  • Assuming that you want to download some content and to show the ongoing process (or current status) in kivy, I updated and modified some of your code to make a minimal example.

    In this case there is no need to create a new Thread class, instead create a new thread object each time and set the target to some method (here, start_download) for fetching and writing the binary data in disk. Thus the progress can be controlled within this method, thereby no scheduling is required.

    from threading import Thread
    import requests
    
    from kivy.app import runTouchApp
    from kivy.lang import Builder
    from kivy.properties import (
        BooleanProperty,
        NumericProperty,
        ObjectProperty,
    )
    from kivy.uix.popup import Popup
    from kivy.uix.screenmanager import Screen
    
    
    
    Builder.load_string("""
    
    <DownLoadScreen>:
    
        Button:
            text: "Open Downloader"
            on_release: root.open_downloader()
    
    
    <UrlPopup>:
        url_input: url_input
        title: 'URL address'
        size_hint: .75, None
        height: "450dp"
    
        BoxLayout:
            orientation: "vertical"
    
            TextInput:
                id: url_input
                text: "https://www.nasa.gov/sites/default/files/thumbnails/image/hubble_ngc2903_potw2143a.jpg.jpg"
                multiline: False
                size_hint_y: None
                height: "64dp"
                font_size: "16sp"
    
            ProgressBar:
                pos_hint: {"center_x" : 0.5}
                value: root.prog_val
                max: root.tot_size
    
            Label:
                id: lbl
                text: "Downloading file...({:.0%})".format(root.prog_val/root.tot_size) if root.has_started else ""
    
            BoxLayout:
                size_hint_y: None
                height: dp(48)
    
                Button:
                    text: 'Download'
                    on_release: root.download()
    
                Button:
                    text: 'Close'
                    on_press: root.dismiss()
    """)
    
    
    
    class UrlPopup(Popup):
    
        url_input = ObjectProperty()
        prog_val = NumericProperty(0) # To capture the current progress.
        tot_size = NumericProperty(1) # Total size of the file/content. Setting the default value to 1 to avoid ZeroDivisionError, though will not affect anyhow.
        has_started = BooleanProperty(False) # Just to manipulate the label text.
    
        def start_download(self):
            self.has_started = True
            self.url = self.url_input.text
    #       file_name = self.url.split('/')[-1]
            with requests.get(self.url, stream=True) as req:
                if req.status_code == 200: # Here, you can create the binary file.
    #               chunks = list(enumerate(req.iter_content(chunk_size=8192))) # This may take more memory for larger file.
                    self.tot_size = int(req.headers["Content-Length"])
                    item_size = 2048 # Reducing the chunk size increases writing time and so needs more time in progress.
                    for i, chunk in enumerate(req.iter_content(chunk_size = item_size)):
                        self.prog_val = i*item_size
    #                   file.write(chunk)
                    self.ids.lbl.text = "Download completed." # A confirmation message.
    
        def download(self):
            """A new thread object will be created each time this method is revoked. But be careful about the threads already created."""
            Thread(target = self.start_download).start()
    
        def on_dismiss(self):
            self.url_input.text = ""
            self.has_started = False
    
    
    
    class DownLoadScreen(Screen):
    
        def open_downloader(self):
            UrlPopup().open()
    
    
    runTouchApp(DownLoadScreen())
    

    Let me know if it fits your need.