7

I want to have a toplevel window/dialog box with a progress bar and a few entry, label and button widgets. I want the dialog box to get updated from the main_window window. The main_window does the work and I need this to be reflected in the dialog box. I want the main window to remain active so you can stop the process. I also want to be able to stop the process in the dialog box.

I could not get this to work without using multiprocessing and threading. It seems like I'm going about this the wrong way or am I? Also I'm new to multiprocessing and threading so I hope I did that correctly anyways. If anyone knows of a better way to do this please let me know.

Below is my attempt at doing what I want, it works but is it the correct way to do it?

My First Attempt:

import tkinter as tk
import tkinter.ttk as ttk

from time import sleep
from queue import Empty
from threading import Thread
from multiprocessing import Process, Queue


HIDE = -1
STOP = -2
BREAK = -3
PAUSE = -4
RESUME = -5


class App(tk.Tk):
    def __init__(self, **kwargs):
        title = kwargs.pop('title', '')
        theme = kwargs.pop('theme', 'clam')
        geometry = kwargs.pop('geometry', None)
        exit_callback = kwargs.pop('exit_callback', None)
        super().__init__(**kwargs)

        self.title(title)
        self.style = ttk.Style()
        self.style.theme_use(theme)

        if geometry:
            self.geometry(geometry)

        if exit_callback:
            self.protocol('WM_DELETE_WINDOW', exit_callback)


def main_window(out_que, in_que, maximum):
    def worker():
        if app.running:
            return

        app.running = True
        app.finished = False
        for count in range(0, maximum + 1):
            try:
                message = in_que.get_nowait()
                if message:
                    if message == PAUSE:
                        message = in_que.get()

                    if message == BREAK:
                        break
                    elif message == STOP:
                        app.destroy()
            except Empty:
                pass

            sleep(0.1)  # Simulate work.
            out_que.put(count)

        app.running = False
        app.finished = True
        start_btn.config(state=tk.NORMAL)

    def app_stop():
        out_que.put(STOP)
        app.destroy()

    def test_stop():
        if app.running:
            out_que.put(HIDE)
        elif app.finished:
            out_que.put(HIDE)
            in_que.get()

        stop_btn.config(state=tk.DISABLED)
        start_btn.config(state=tk.NORMAL)

    def test_start():
        while not in_que.empty():
            in_que.get()

        stop_btn.config(state=tk.NORMAL)
        start_btn.config(state=tk.DISABLED)

        thread = Thread(target=worker, daemon=True)
        thread.daemon = True
        thread.start()

    app = App(title='Main Window', theme='alt', geometry='350x150', exit_callback=app_stop)
    app.running = False
    app.finished = True
    app.rowconfigure(0, weight=1)
    app.rowconfigure(1, weight=1)
    app.columnconfigure(0, weight=1)

    start_btn = ttk.Button(app, text='Start Test', command=test_start)
    start_btn.grid(padx=10, pady=5, sticky=tk.NSEW)
    stop_btn = ttk.Button(app, text='Stop Test', state=tk.DISABLED, command=test_stop)
    stop_btn.grid(padx=10, pady=5, sticky=tk.NSEW)

    app.mainloop()


def progress_window(in_que, out_que, maximum):
    def hide():
        out_que.put(BREAK)
        pause_btn.config(text='Pause')
        app.withdraw()

    def pause():
        if progress_bar['value'] < progress_bar['maximum']:
            text = pause_btn.cget('text')
            text = 'Resume' if text == 'Pause' else 'Pause'
            pause_btn.config(text=text)
            out_que.put(PAUSE)
        else:
            pause_btn.config(text='Pause')

    def worker():
        while True:
            data = in_que.get()
            print(data)
            if data == HIDE:
                hide()
            elif data == STOP:
                app.destroy()
                out_que.put(STOP)
                break
            elif not data:
                app.deiconify()
                progress_bar["value"] = 0
            else:
                progress_bar["value"] = data
                app.update_idletasks()

    app = App(title='Progress', theme='clam', geometry='350x150', exit_callback=hide)

    app.rowconfigure(0, weight=1)
    app.rowconfigure(1, weight=1)
    app.columnconfigure(0, weight=1)
    app.columnconfigure(1, weight=1)

    progress_bar = ttk.Progressbar(app, orient=tk.HORIZONTAL, mode='determinate')
    progress_bar["maximum"] = maximum
    progress_bar.grid(padx=10, sticky=tk.EW, columnspan=1000)

    pause_btn = ttk.Button(app, text='Pause', command=pause)
    pause_btn.grid()
    cancel_btn = ttk.Button(app, text='Cancel', command=hide)
    cancel_btn.grid(row=1, column=1)

    thread = Thread(target=worker)
    thread.daemon = True
    thread.start()

    app.withdraw()
    app.mainloop()


if __name__ == '__main__':
    jobs = []
    que1 = Queue()
    que2 = Queue()
    process = 50  # The maximum amount of work to process, # items.

    for target in (main_window, progress_window):
        p = Process(target=target, args=(que1, que2, process))
        jobs.append(p)
        p.start()

    for j in jobs:
        j.join()

Here is my second attempt, without multiprocessing just threading.

I have updated the code to not use multiprocessing, just threading. Is threading necessary or can the be done with out it and accomplish the same thing?

The code seems to work fine but am I doing it right? I'm new to threading and just want to make sure I'm doing things right before I continue with my project.

import tkinter as tk
import tkinter.ttk as ttk

from time import sleep
from queue import Empty
from threading import Thread
from multiprocessing import Queue


HIDE = -1
STOP = -2
DONE = -3
BREAK = -4
PAUSE = -5


class App(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.running = False
        self.finished = True
        self.app_que = Queue()
        self.dialog_que = Queue()
        self.process_items = 50

        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)

        self.title('Main Window')
        self.geometry('350x150')

        self.style = ttk.Style()
        self.style.theme_use('clam')

        wdg = self.start_btn = ttk.Button(self, text='Start Test', command=self.test_start)
        wdg.grid(padx=10, pady=5, sticky=tk.NSEW)
        wdg = self.stop_btn = ttk.Button(self, text='Stop Test', state=tk.DISABLED, command=self.test_stop)
        wdg.grid(padx=10, pady=5, sticky=tk.NSEW)

        self.dlg = ProgressDialog(self, title='Progress', geometry='350x150', process=self.process_items)
        self.dlg.app_que = self.app_que
        self.dlg.dialog_que = self.dialog_que

        self.protocol('WM_DELETE_WINDOW', self.app_stop)

        thread = Thread(target=self.dlg.worker, daemon=True)
        thread.start()

    def worker(self):
        self.dlg.cancel_btn.config(text='Cancel')
        self.dlg.pause_btn.config(state=tk.NORMAL)

        for count in range(0, self.process_items + 1):
            try:
                message = self.app_que.get_nowait()
                if message:
                    if message == PAUSE:
                        message = self.app_que.get()

                    if message == BREAK:
                        self.stop_btn.config(state=tk.DISABLED)
                        break
                    elif message == STOP:
                        self.destroy()
            except Empty:
                pass

            sleep(0.1)  # Simulate work.
            self.dialog_que.put(count)

        self.dialog_que.put(DONE)
        self.dlg.cancel_btn.config(text='Close')

        self.finished = True
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)

    def app_stop(self):
        self.dialog_que.put(STOP)
        self.destroy()

    def test_stop(self):
        if self.running or self.finished:
            self.dialog_que.put(HIDE)

        self.stop_btn.config(state=tk.DISABLED)
        self.start_btn.config(state=tk.NORMAL)

    def test_start(self):
        while not self.app_que.empty():
            self.app_que.get()

        thread = Thread(target=self.worker, daemon=True)
        thread.start()

        self.stop_btn.config(state=tk.NORMAL)
        self.start_btn.config(state=tk.DISABLED)

        self.dlg.deiconify()


class ProgressDialog(tk.Toplevel):
    def __init__(self, parent, *args, **kwargs):
        title = kwargs.pop('title', '')
        process = kwargs.pop('process', 0)
        geometry = kwargs.pop('geometry', None)
        super().__init__(parent, *args, **kwargs)
        self.withdraw()

        self.app_que = None
        self.dialog_que = None

        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)

        self.title(title)

        if geometry:
            self.geometry(geometry)

        wdg = self.progress_bar = ttk.Progressbar(self, orient=tk.HORIZONTAL, mode='determinate')
        wdg["value"] = 0
        wdg["maximum"] = process
        wdg.grid(padx=10, sticky=tk.EW, columnspan=1000)

        wdg = self.pause_btn = ttk.Button(self, text='Pause', command=self.pause)
        wdg.grid()
        wdg = self.cancel_btn = ttk.Button(self, text='Cancel', command=self.hide)
        wdg.grid(row=1, column=1)

        self.protocol('WM_DELETE_WINDOW', self.hide)

    def worker(self):
        while True:
            message = self.dialog_que.get()
            print(message)
            if message == HIDE:
                self.hide()
            elif message == STOP:
                self.app_que.put(DONE)
                break
            elif message == DONE:
                self.pause_btn.config(state=tk.DISABLED)
            else:
                self.progress_bar["value"] = message

    def hide(self):
        self.app_que.put(BREAK)
        self.pause_btn.config(text='Pause')
        self.withdraw()

    def pause(self):
        if self.progress_bar['value'] < self.progress_bar['maximum']:
            text = self.pause_btn.cget('text')
            text = 'Resume' if text == 'Pause' else 'Pause'
            self.pause_btn.config(text=text)
            self.app_que.put(PAUSE)
        else:
            self.pause_btn.config(text='Pause')


if __name__ == '__main__':
    app = App()
    app.mainloop()
Daniel Huckson
  • 1,157
  • 1
  • 13
  • 35
  • It's generally a bad idea to do UI from different threads, use your main thread for UI and make worker threads if necessary. – Mark Ransom May 19 '20 at 19:06
  • @Mark Ransom, I be leave it is necessary in this case for what I want to achieve. I have tried to do this without threads and processes and I can't make it work for me in tkinter any other way. – Daniel Huckson May 19 '20 at 19:13
  • 1
    I gave you that advice for a reason. Most GUI frameworks break terribly if you try to use them from multiple threads; I don't know `tkinter` specifically but there's no reason it would be different. – Mark Ransom May 19 '20 at 19:33
  • @Mark Ransom, yes I understand that but I still need to achieve my goals. What differences does it make if I can't do what I want any other way I know of. This is the point, I need to know the correct way for making a working modal, an example for instance. – Daniel Huckson May 19 '20 at 19:44
  • If your goals are inconsistent with the way the world works, don't expect a lot of useful help. My advice still stands, use threads if you need to but don't let them touch `tkinter`; use some other mechanism to pass information into and out of the threads. – Mark Ransom May 19 '20 at 20:29
  • 1
    With tkinter Mark is correct. You have to run tkinter in the main thread. Bad things happen if you don't. Anything you need to do outside of the GUI itself can be in a thread as long as you are careful on how you interact with the GUI elements from the thread. – Mike - SMT May 22 '20 at 15:57
  • @TheMaker, Thank you now I understand! – Daniel Huckson May 23 '20 at 01:12

1 Answers1

5

If you didn't want to use thread,maybe you could try asyncio.I don't know whether my code is correct,but it works fine on my PC.

Welcome to point out the fault in my code, I really don't know whether it is a good practice.

import tkinter as tk
from tkinter import ttk
import asyncio, time
import warnings


class App(tk.Tk):
    def __init__(self):
        super(App, self).__init__()

        self.start_btn = ttk.Button(self, text="Start Test", command=self.test_start)
        self.start_btn.pack(padx=10, pady=5, fill="both", expand=True)

        self.stop_btn = ttk.Button(self, text="Stop Test", command=self.test_stop, state=tk.DISABLED)
        self.stop_btn.pack(padx=10, pady=5, fill="both", expand=True)

        self.test_window = tk.Toplevel()
        self.test_window.progressbar = ttk.Progressbar(self.test_window, orient=tk.HORIZONTAL)
        self.test_window.progressbar.grid(padx=10, pady=5, sticky=tk.NSEW, columnspan=2, column=0, row=0)

        self.test_window.switch_btn = ttk.Button(self.test_window, text="Pause", command=self.switch)
        self.test_window.switch_btn.grid(padx=10, pady=5, sticky=tk.NSEW, column=0, row=1)
        self.test_window.cancel_btn = ttk.Button(self.test_window, text="Cancel", command=self.test_cancel)
        self.test_window.cancel_btn.grid(padx=10, pady=5, sticky=tk.NSEW, column=1, row=1)

        self.test_window.withdraw()

    def test_start(self):
        self.stop_btn['state'] = tk.NORMAL
        self.test_window.deiconify()
        self.test_window.after(0, self.work)

    def work(self):
        async def async_work(): # define a async task
            try:
                await asyncio.sleep(3)  # could be another async work.
            except asyncio.CancelledError:
                print("cancel or stop")
                raise  # if don't raise the error ,it won't cancel

        async def progressbar_add():
            self.task = asyncio.create_task(async_work())
            timeout = 0
            while True: # wait the async task finish
                done, pending = await asyncio.wait({self.task}, timeout=timeout)
                self.test_window.update()
                if self.task in done:
                    self.test_window.progressbar['value'] += 10  # if finished, value += 10
                    print(self.test_window.progressbar['value'])
                    await self.task
                    break

        if self.test_window.progressbar['value'] >= 100:
            return
        asyncio.run(progressbar_add())
        self.test_window.after(0, self.work)

    def test_stop(self):
        self.test_window.progressbar['value'] = 0
        self.stop_btn['state'] = tk.DISABLED
        try:
            all_tasks = asyncio.Task.all_tasks()
            for task in all_tasks:
                task.cancel()
        except RuntimeError:  # if you have cancel the task it will raise RuntimeError
            pass

    def switch(self):
        if self.test_window.switch_btn['text'] == 'Pause':
            self.test_window.switch_btn['text'] = 'Resume'
            try:
                all_tasks = asyncio.Task.all_tasks()
                for task in all_tasks:
                    task.cancel()
            except RuntimeError:  # if you have cancel the task it will raise RuntimeError
                pass
        else:
            self.test_window.switch_btn['text'] = 'Pause'
            return self.work()

    def test_cancel(self):
        # self.test_window.progressbar['value'] = 0
        print(self.test_window.progressbar['value'])
        self.test_window.withdraw()
        self.task.cancel()


app = App()
app.mainloop()

Below Python 3.7,you couldn't use asyncio.run(async).It was added in Python 3.7.Need to use get_event_loop() and run_until_complete().(Point out By @Saad)

jizhihaoSAMA
  • 12,336
  • 9
  • 27
  • 49
  • 2
    Your code won't work below python 3.7 without modification as some methods of `asyncio` has changed in python 3.7. [See the changes here](https://docs.python.org/3/whatsnew/3.7.html). – Saad May 23 '20 at 09:23
  • @jizhihaoSAMA, this code you suggested does not work on my machine. – Daniel Huckson May 23 '20 at 17:33
  • what will happened on your machine?Is it linux? – jizhihaoSAMA May 23 '20 at 18:00
  • @jizhihaoSAMA, Yes I run Linux. When I click the pause button the program exception errors. – Daniel Huckson May 23 '20 at 20:14
  • Correct,CancelledError.It should be,else it won't stop the async task. – jizhihaoSAMA May 24 '20 at 03:35
  • @jizhihaoSAMA, What way is better? The way you did it or the way I have done it. I think the way I have done it because asyncio needs at least Python 3.7 to work. – Daniel Huckson May 24 '20 at 15:33
  • @DanielHuckson `asyncio` could work after Python 3.4.Before Python 3.7,just need some changes(change the `asyncio.run()`).I am not sure which is better.My answer just a idea that use only one thread.But I think if you could use multithread correctly, I think that will be better. – jizhihaoSAMA May 24 '20 at 15:41
  • @jizhihaoSAMA, yes I agree with that. – Daniel Huckson May 24 '20 at 16:22