GhostBotBoy
GhostBotBoy

Reputation: 21

Pause function execution while awaiting input in entry (tkinter)

Let's say I've got a function that requests input. How can I pause the function that calls this function while I am waiting for user input in an entry widget. I tried it with a while loop and time.sleep(sec). Furthermore I executed the function within another thread (so the main thread should not be interrupted), but the problem that always occurs is that the whole program freezes (typing in entry impossible)!

Because I do not have that much experience with Python I am truly stuck.

PS: I am coding on a mac.

The code I used:

    import time
    _input = []

    def get_input()
      try:
        return _input[0]
      except IndexError:
        return None

    def req():
      while get_input() == None:
        time.sleep(1)
      return get_input()

The function req() is always called within a function which is called via 'getattr()' in a function which parses the input in the entry widget. The variable '_input' automatically gets the user input from the entry. The input I then successfully got from the '_input' variable is then discarded.

Maybe the problem is that the function is running and that is why another function cannot be executed... but shouldn't that be irrelevant if I was using a distinct thread? Why didn't that work...?

Upvotes: 1

Views: 5003

Answers (3)

Bryan Oakley
Bryan Oakley

Reputation: 385980

The way to wait for user input is to open up a dialog. A modal dialog will force the user to dismiss the dialog, a non-modal will allow the user to continue to use the main application.

In your case, you can create a dialog using a Toplevel and fill it with any widgets that you want, then use the wait_window function to wait for that window to be destroyed. To make it modal you can create a "grab" on the toplevel. To keep this simple, I have not done that in the following example.

Here is a basic example. The key is the call to wait_window which will not return until the dialog is destroyed.

import tkinter as tk

class CustomDialog(object):
    def __init__(self, parent, prompt="", default=""):
        self.popup = tk.Toplevel(parent)
        self.popup.title(prompt)
        self.popup.transient(parent)

        self.var = tk.StringVar(value=default)

        label = tk.Label(self.popup, text=prompt)
        entry = tk.Entry(self.popup, textvariable=self.var)
        buttons = tk.Frame(self.popup)

        buttons.pack(side="bottom", fill="x")
        label.pack(side="top", fill="x", padx=20, pady=10)
        entry.pack(side="top", fill="x", padx=20, pady=10)

        ok = tk.Button(buttons, text="Ok", command=self.popup.destroy)
        ok.pack(side="top")

        self.entry = entry

    def show(self):
        self.entry.focus_force()
        root.wait_window(self.popup)
        return self.var.get()

To use it, call the show method:

dialog = CustomDialog(root, prompt="Enter your name:")
result = dialog.show()

With the above, result will have the string that you entered.

For more information about creating dialogs, see Dialog Windows on the effbot site,

Upvotes: 2

Roland Smith
Roland Smith

Reputation: 43495

GUI programming is quite different from normal python scripts.

When you see the GUI pop up, it is already running in the mainloop. That means that your code is only invoked from the mainloop as a callback attached to some event or as a timeout function. Your code actually interrupts the flow of events in the mainloop.

So to keep the GUI responsive, callbacks and timeouts have to finish quickly (say in 0.1 second max). This is why you should not run long loops in a callback; the GUI will freeze.

So the canonical way to do a long calculation in a GUI program is to split it up into small pieces. Instead of e.g. looping over a long list of items in a for loop, you create a global variable that holds the current position in the list. You then create a timeout function (scheduled for running by the after method) that takes e.g. the next 10 items from the list, processes them, updates the current position and reschedules itself using after.

The proper way to get input for a function is to get the necessary input before starting the function. Alternatively, you could use a messagebox in the function to get the input. But in general it is considered good design to keep the "guts" of your program separate from the GUI. (Consider that you might want to switch from Tkinter to the GTK+ or QT toolkits in the future.)

Now onto threads. You might think that using threads can make long-running tasks easier. But that is not necessarily the case. For one thing, the standard Python implementation (we shall call it CPython) has a Global Interpreter Lock that ensures that only one thread at a time can be running Python bytecode. So every time your long-running calculation thread is running, the other thread containing the mainloop is halted. In Python 3 the thread scheduling is improved w.r.t. Python 2 as to try and not starve threads. But there is no guarantee that the GUI thread gets enough runtime when the other thread is doing a ton of work.

Another restriction is that the Tkinter GUI toolkit is not thread safe. So the second thread should not use Tkinter calls. It will have to communicate with the GUI thread by e.g. setting variables or using semaphores. Furthermore, data structures that are used by both threads might have to be protected by Locks, especially if both threads try to modify them.

In short, using threads is not as simple as it seems. Also multithreaded programs are notoriously difficult to debug.

Upvotes: 1

PM 2Ring
PM 2Ring

Reputation: 55479

Here's a function that creates a simple Tkinter GUI that allows the user to input data into an Entry widget. When the user hits Enter the function gets the current value from the Entry, closes the GUI and returns the value to the calling code. The calling code will block until tkinter_input returns. If the user closes the GUI window with the close button the contents of the Entry are ignored, and None is returned.

import tkinter as tk

def tkinter_input(prompt=""):
    root = tk.Tk()
    tk.Label(root, text=prompt).pack()
    entry = tk.Entry(root)
    entry.pack()
    result = None
    def callback(event):
        nonlocal result
        result = entry.get()
        root.destroy()
    entry.bind("<Return>", callback)
    root.mainloop()
    return result

result = tkinter_input("Enter data")
print(result)

Upvotes: 2

Related Questions