0

About the Application:

  • There is a main application which allows you to choose what set of images you would like to download.
  • After the user chooses the set (multiple sets can be chosen at a time), a download window is displayed with progress bar.
  • The main application is the root window and the download windows are toplevel windows with parent as root.
  • The downloading of set of images uses multiprocessing.pool.ThreadPool

Problem:

  • When there is a single download i.e., only one toplevel window, no problem arises, but if there are simultaneous downloads i.e., multiple toplevel windows, then these windows affect each other.
  • By affecting each other I mean, the percentage label gets corrupted and get mixed with other percentage labels, also the actual download process (writing content to file) also gets mixed up.

What I have tried:

  • Creating toplevel windows with root as parent e.g. top = tk.Toplevel(root).
    Outcome: Same problem as before.
  • Creating toplevel windows without a parent e.g. top = tk.Toplevel().
    Outcome: Same problem as before.
  • Using ThreadPoolExecutor from concurrent.futures with map() and submit() methods.
    Outcome: Same problem as before.
  • Removing ThreadPool and using a simple for loop.
    Outcome: The main application freezes until the loop is completed and also, the downloading gets slower as one download at a time.

Code:

# imports done already


# running main application
def start_gui():
    root = tk.Tk()
    root.wm_iconbitmap('logo.ico')
    MainWindow(root)
    root.mainloop()


# class for download window
class DownloadBox:
    def __init__(self, root, urls, name, download_path):
        # creating toplevel window without parent
        top = tk.Toplevel()
        top.protocol('WM_DELETE_WINDOW', lambda: None)
        top.geometry('400x120+{}+{}'.format((root.winfo_rootx() + root.winfo_width() - top.winfo_width()) // 2,
                                            (root.winfo_rooty() + root.winfo_height() - top.winfo_height()) // 2))
        top.resizable(0, 0)
        top.title(name)
        top.wm_iconbitmap('logo.ico')
        top.configure(background='#d9d9d9')

        # ... adding widgets to window

        # progressbar widget
        self.style = ttk.Style(top)
        self.style.layout('text.Horizontal.TProgressbar',
            [('Horizontal.Progressbar.trough',
                {'children': [('Horizontal.Progressbar.pbar',
                    {'side': 'left', 'sticky': 'ns'})],
                'sticky': 'nswe'}), 
            ('Horizontal.Progressbar.label', {'sticky': ''})])
        self.style.configure('text.Horizontal.TProgressbar', text='0%')

        self.progress_bar = ttk.Progressbar(top)
        self.progress_bar.place(relx=0.025, rely=0.450, height=20, width=380)
        self.progress_bar.configure(orient='horizontal')
        self.progress_bar.configure(length=len(urls))
        self.progress_bar.configure(mode='determinate')
        self.progress_bar.configure(style='text.Horizontal.TProgressbar')
        self.progress_bar.configure(value=0)

        # ThreadPool implementation
        ThreadPool(10).imap_unordered(self.download, self.urls)

        # tried this
        # for url in self.urls:
            # self.download(url)


    # progress bar increment function
    def increment_progressbar(self):
        self.current_value += 1
        self.progress_bar['value'] = self.current_value * 100 / len(self.urls)

        # this percentage label gets mixed up with other windows
        self.style.configure('text.Horizontal.TProgressbar', text='{:0.0%}'.format(self.current_value / len(self.urls)))

        if self.current_value == len(self.urls):
            self.top.after(1000, self.top.destroy)


    # ThreadPool calls this function
    def download(self, url):
        if not self.cancel_download:
            try:
                response = requests.get(url, stream=True)

                with open(self.download_path + '/' + url.split('/')[-1], 'wb') as f:
                    for chunk in response:
                        f.write(chunk)

                if self.top.winfo_exists():
                    self.increment_progressbar()
            except:
                messagebox.showinfo(title='Connection Timed Out', message='{} Couldn\'t be Downloaded... Check Internet Connection or Try Again Later'.format(self.name))
                self.cancel()


    # cancel button functionality
    def cancel(self):
        self.cancel_download = True
        self.top.destroy()


# class for main applicaton
class MainWindow:
    def __init__(self, top):
        top.geometry('810x600+300+80')
        top.resizable(0, 0)
        top.configure(background='#d9d9d9')

        self.top = top

        # initializing on startup
        self.init_func()


    def init_func(self):
        # doing somework
        # pseudocode for checking for button click
        if button.cliked():
            self.download(args)


    def download(self, args):
        urls, name, download_path = args
        # passing parent window only to get dimensions
        DownloadBox(self.top, urls, name, download_path)


if __name__ == '__main__':
    start_gui()

What I expect:

  • Make the download windows independent of each other, i.e. the download should process independently and also the percentage labels
  • Make use of ThreadPool while downloading

Questions:

  • Are there any other threading means to download?
  • Should I be using ProcessPool, if so, how?

Update:

# inside __init__ method of DownloadBox class
self.style.layout('{}.text.Horizontal.TProgressbar'.format(self.name),
            [('Horizontal.Progressbar.trough',
                {'children': [('Horizontal.Progressbar.pbar',
                    {'side': 'left', 'sticky': 'ns'})],
                'sticky': 'nswe'}), 
            ('Horizontal.Progressbar.label', {'sticky': ''})])
        self.style.configure('{}.text.Horizontal.TProgressbar'.format(self.name), text='0%')

# inside increment_progressbar method
# here the variable name is passed as parameter to increment_progressbar method
self.style.configure('{}.text.Horizontal.TProgressbar'.format(name), text='{:0.0%}'.format(self.current_value / len(self.urls)))

Outcome: progress_bar is working fine... But an issue still exists with ThreadPool

  • 1
    ***this percentage label gets mixed up with other windows ... `style.configure('text.Horizontal.TProgressbar'...`***: This is due to use the same, `text.`, layout identification for every `Progressbar`. Keep in mind, `layout` is global and you configure that single instance concurrently. – stovfl Apr 25 '20 at 07:51
  • @stovfl, Thanks for pointing that out. I tried creating a dictionary of styles (each progressbar with its own style) with the list of urls as the keys and the `ttk.Style(top)` as value. Then, I modified these values in the `increment_progressbar()` using the url as reference. But... when I checked, I found out that only 10 jobs that were passed as `ThreadPool` parameter were completed and the gui freezes – Arihant Bedagkar Apr 25 '20 at 08:22
  • In general you have to follow [All Tcl commands need to originate from the same thread](https://stackoverflow.com/a/26703844/7414759) – stovfl Apr 25 '20 at 08:41
  • @stovfl Previously, you said that I am using same layout identifier for every `progress_bar`. So, how can I use different layout identifiers for different `progress_bar`. Also, I have to look on that link you mentioned. I guess it is for `Thread` implementation in my program. – Arihant Bedagkar Apr 25 '20 at 09:04
  • ***how can I use different layout identifiers***: Follow this answer, part [**`Custom.TButton`**](https://stackoverflow.com/a/52210978/7414759) – stovfl Apr 25 '20 at 09:18
  • @stovfl Okay... `progress_bar` is working fine now. But, there is still a problem with the thread, if I use number of workers = cpu_counts, download works perfect, but if number of workers = any number greater than cpu_counts, some downloads are skipped (files are created, but are corrupted) – Arihant Bedagkar Apr 25 '20 at 10:03
  • Your end of tasks condition, `if self.current_value == len(self.urls):`, looks wrong. This doesn't take into account that other threaded tasks are still alive. I also missing the initialisation of `self.urls` and`self.current_value`. Furthermore you `destroy` at the first call to `self.cancel()` which leads probably to terminate `ThreadPool` before all urls are done. – stovfl Apr 25 '20 at 12:12

0 Answers0