1

I'm new to python GUI programming, I have a button that when you click on it, takes time to run because of calculation time. all I want is to show a 'waiting' word in the middle of a label when my main function is running.

Here is my code where Macula(file_path): function takes time to run and I try to put a Label before calling M.makula_detection(image, thresh, n, Size) but as I know, we can't show label features until the function ends.

I also tried to use two functions in on command but it doesn't answer too because they run together.

Here is my code:

from tkinter import *
from PIL import ImageTk, Image
from tkinter import filedialog
import cv2 as cv
import MaskGeneration as mg
import MakulaDetection as M

# =================================== statics and configuration ===================================
color = '#20536C'
root = Tk()
root.title('Opticdisk and Macula detector')
root.configure(bg= color)
root.geometry('1070x700')
root.resizable(width=False, height=False)
root.iconbitmap('J:\Projects\Bachelor Project\download.ico')


# =================================== functions and body ===================================

filename_path = {}
# label = Label(LEFT,text = 'fff')
def open_image(file_path):
   global select_lbl
   bigIm_path = {}# the new path of resized big image
   file_path['image'] = filedialog.askopenfilename(initialdir="J://uni//final project//Data set",
                                              title="select an image",
                                              filetypes=(('all files', '*.*'), ('jpg files', '*.jpg'), ('tif file','*.tif')))

   IM = cv.imread(file_path['image'])
   im_wid = len(IM)
   im_len =len(IM[1])
   if (im_len > 749 and im_wid > 630) or (im_len>749) or (im_wid>630):

       IM = cv.resize(IM, (629, 629))
       cv.imwrite('J://uni//final project//res_image//big_img.jpg', IM)
       bigIm_path['image'] = ('J://uni//final project//res_image//big_img.jpg')
       #top line save the new resized big image in bigIM_path{}
       mainImage = ImageTk.PhotoImage(Image.open(bigIm_path['image']))
       select_lbl = Label(left, image=mainImage,
                   width=749,
                   height=630,
                   bg='#020101')  # .place(x=20, y=0)
       select_lbl.image = mainImage  # keep a reference! to show the image
       select_lbl.place(x=0, y=0)

   else:
       mainImage = ImageTk.PhotoImage(Image.open(filename_path['image']))
       select_lbl = Label(left, image=mainImage,
                   width= 749,
                   height=630,
                   bg='#020101')#.place(x=20, y=0)
       select_lbl.image = mainImage  # keep a reference! to show the image
       select_lbl.place(x=0, y=0)

def Macula(file_path):
   path={}#
   # messagebox = Message(left).place(x=20,y=10)
   try:
       # select_lbl.pack_forget()
       image = cv.imread(file_path['image'])
       image = cv.cvtColor(image, cv.COLOR_BGR2RGB)
       # top line solve the problem of blue background color by changing color space  from BGR to RGB
       Size = 0.2
       thresh = 25
       n = 80

       M.makula_detection(image, thresh, n, Size)
       path['image'] = ('J://uni//final project//res_image//marked_M.png')
       ''' this line is a tec for dispaly the file '''
       mainImage = ImageTk.PhotoImage(Image.open(path['image']))
       lbl = Label(left, image=mainImage,
                   width=749,
                   height=630,
                   bg='#020101')  # .place(x=20, y=0)
       lbl.image = mainImage  # keep a reference! to show the image
       lbl.place(x=0, y=0)

   except:
       print('error')
       lbl = Label(left,text = "No image file selected !!!",
                   font = ('Times', 25, 'italic', 'bold'),
                   fg = '#ffe874',
                   bg = color)
       lbl.place(x=200,y=270)

def Mask(file_path):
   path = {}
   # messagebox = Message(left).place(x=20,y=10)
   try:
       Im = cv.imread(file_path['image'])
       mask = mg.mask_generation(Im)
       # dispaly function
       I = cv.resize(mask, (400, 400))
       cv.imwrite('J://uni//final project//res_image//finalresult.jpg', I)
       path['image'] = ('J://uni//final project//res_image//finalresult.jpg')
       ''' this line is a tec for dispaly the file '''
       mainImage = ImageTk.PhotoImage(Image.open(path['image']))
       lbl = Label(left, image=mainImage,
                   width=749,
                   height=630,
                   bg='#020101')  # .place(x=20, y=0)
       lbl.image = mainImage  # keep a reference! to show the image
       lbl.place(x=0, y=0)

   except:
       print('error')
       lbl = Label(left, text="No image file selected !!!",
                   font=('Times', 25, 'italic', 'bold'),
                   fg='#03283a',
                   bg=color)
       lbl.place(x=200, y=270)
       # lbl.grid(row=5,column=10)

# =================================== Buttons ===================================

btnBrowse = Button(top, width=93,
                  text='select file',
                  fg='#58859a',
                  font=('Times', 15, 'italic', 'bold'),
                  bg='#03283a',
                  command = lambda :open_image(filename_path))
btnBrowse.pack(side=BOTTOM)

btnMask = Button(right, text='Image Mask',
                fg= '#58859a',
                font=('Times', 20, 'italic', 'bold'),
                bg="#03283a",
                width=19,
                height=6,
                command=lambda:Mask(filename_path) )
btnMask.pack(side=TOP)

btnMacula = Button(right, text='Macula',
                  fg= '#58859a',
                  font=('Times', 20, 'italic', 'bold'),
                  bg="#03283a",
                  width=19,
                  height=6,
                  command=lambda :Macula(filename_path))
btnMacula.pack(side=TOP)

btnClear = Button(right, text='exit',
                 fg= '#58859a',
                 font=('Times', 20, 'italic', 'bold'),
                 bg="#03283a",
                 width=19,
                 height=6,
                 command=root.quit)
btnClear.pack(side=TOP)

root.mainloop()

Is this even possible?

martineau
  • 119,623
  • 25
  • 170
  • 301
  • Take a look at [Introduction to GUI programming with `tkinter`](https://python-textbok.readthedocs.io/en/1.0/Introduction_to_GUI_Programming.html). – martineau Oct 22 '20 at 21:48
  • As see question [Tkinter — executing functions over time](https://stackoverflow.com/questions/9342757/tkinter-executing-functions-over-time) on this site. – martineau Oct 22 '20 at 21:53

1 Answers1

2

It is absolutely possible it's just a matter of how clean the solution can be.

There are 4 primary options here ordered in increasing complexity:

  1. have Macula update a label synchronously before starting the bulk of the work. This is done with root.update() etc.
  2. have the button trigger a callback which updates a label then schedules Makula to run soon in the event loop. This would use .after(0, Makula)
  3. run the complex code in a separate thread and have the main thread poll for completion.
  4. run the heavy code in chunks, this isn't really possible when the long running code is only 1 line from your prospective. (I consider this more complicated than using a thread because creating a thread to run a function is very straight forward and the code to do polling is pretty generic)

Depending on your comfort level with programming you may want any one of these so I'll give an overview of each,

The first method the long running function just updates the label then calls .update() to push the updates to GUI before running. so the code would look something like this:

def Macula(file_path=None):
   # probably want to have a status label already existing
   # maybe create it here? makes cleaning it and deciding where to put it harder.
   status_label.config(text = "loading")
   root.update()

   # long running code here
   # for i in range(1000000):
   #     print("A", end="")

   # after function, clear label (or delete)
   status_label.config(text = "ready")

This does work but the application will still hang while the function is running and the GUI updates end up getting coupled to the application logic which is undesirable in the long run.

A slightly better solution is to make heavier use of .after() to schedule executions instead of relying on .update() to do synchronous updates, in this case you would have a function to update the ui:

def handle_long_running_function(callback):
    # update label
    status_label.config(text="loading")
    # define a wrapper to run the long function and then update the label again
    def run_func():
        try:
            callback()
        finally:
            status_label.config(text="ready")
    # number here is millisecond delay, setting to 0 or 1 doesn't make much difference
    root.after(1,run_func) 

def Macula(file_path=None):
   # long running code here
   for i in range(1000000):
       print("A", end="")


button = tk.Button(root, text="run func", 
              command=lambda: handle_long_running_function(Macula))

This way the callback is scheduled to execute instead of happening totally synchronously, this gives a bit more power to the event loop to try to handle any other outstanding events if possible although running long code in the same thread as the event loop still causes the program to hang while it runs. This approach to wrapping the callback in a handling function also de-couples the logic from UI updating which is desirable. It also provides a place to handle simultaneous updates to status_label, possibly allowing you to at least consider race conditions.

note this approach could be just as easily used to decouple the update in the use of .update() but I'm trying to offer different solutions with different complexities here.


In order to have a solution that doesn't involve your GUI to hang while the expensive functions are running we need a different approach, one that doesn't run long functions on the same thread as the event loop.

Assuming we want our application to work without multiple threads you can imagine breaking up long execution code by using yield or some form of await to indicate the code should be paused to let the event loop catch up before resuming the code, then we can create a wrapper or decorator to handle scheduling of this type of function:

import functools

def tkinter_managed_long_running_task(gen_func):
    """wraps a generator function to pause execution at each yield and wait a bit
    before resuming, allowing the mainloop to update gui.
    the function can yield a number of milliseconds to delay until it resumes,
    otherwise the next chunk is run almost immidiately."""
    @functools.wraps(gen_func)
    def wrapper(*args, **kw):
        # call original function and schedule
        gen = gen_func()
        def run_next_chunk():
            try:
                delay_time = next(gen)
            except StopIteration as e:
                # could potentially resolve a Future here if we were doing asyncio stuff.
                # this grabs the value in `return value` inside a generator,
                # most cases this is not used for this kind of application
                returned_value = e.value
                return # no schedule
            else:
                # hit a yield, will schedule next execution
                if delay_time is None:
                    delay_time = 10 # 10 milliseconds is pretty small, but probably reasonable default?
                root.after(delay_time, run_next_chunk)
        # run the first chunk immidiately
        run_next_chunk()
        
    return wrapper

@tkinter_managed_long_running_task
def Macula(file_path=None):
   # long running code here
   for i in range(1000000):
       yield 1 # have yields along with millisecond delay.
       print("A", end="")

This lets us simply add yield statements (in this case specifying the delay in ms until it is resumed) and it works pretty seamlessly. This of course only works when the long execution time can be split up, your case however the long task is a single call to a library so there isn't really anywhere to insert these types of yields to slow things down. In this case the only option to maintain GUI while it runs is to run it on a separate thread.

A solution using threads would use the technique above to essentially create a new thread to do heavy lifting in a separate thread then have some code using the tkinter_managed_long_running_task or other techniques to periodically check for completion from the main thread so that it can trigger UI updates accordingly.

At this point in complexity though you are probably either not using tkinter because it's relatively slow for GUI applications or you are thinking "why use yield, why not just implement proper async and await functions to run in parallel to tkinter?" I highly doubt anyone is actually thinking that, but at one point I did but didn't have much idea what I was doing and ended up abandoning it because it was far too slow to do anything useful with it.

Anyway this answer has gotten really long and ramble-y. I hope something here is helpful. Cheers :)

Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59