1

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...?

halfer
  • 19,824
  • 17
  • 99
  • 186
GhostBotBoy
  • 21
  • 1
  • 3
  • Possible duplicate of [Asking the user for input until they give a valid response](https://stackoverflow.com/questions/23294658/asking-the-user-for-input-until-they-give-a-valid-response) – derHugo Dec 30 '17 at 11:12
  • That is not what I mean. I want to accomplish the task 'input()' does in the console (pausing the program and waiting for input) on a tkinter window. – GhostBotBoy Dec 30 '17 at 11:20
  • @derHugo That question is about console input, it's not relevant to Tkinter. – PM 2Ring Dec 30 '17 at 11:21
  • 1
    This should not be an issue in a normal Tkinter program. The GUI should just sit there waiting for events to respond to. Can you show us a [mcve]? – PM 2Ring Dec 30 '17 at 11:24
  • Um how can I show code in a comment I cannot get it to work... :/ – GhostBotBoy Dec 30 '17 at 11:25
  • 2
    better show code in question - it will be more useful. – furas Dec 30 '17 at 11:27
  • 2
    All relevant info needs to go into the question itself, not in comments. Besides, comments don't preserve indentation, so they're virtually useless for multi-line Python code. – PM 2Ring Dec 30 '17 at 11:32
  • I thought you were using Tkinter. I can't see anything related to Tkinter in the code you just posted. – PM 2Ring Dec 30 '17 at 11:40
  • 1
    If you _are_ using Tkinter you should **not** be using `time.sleep`: that will just freeze the whole program, as you've already discovered. There _are_ ways to create time delays in Tkinter that won't freeze the program, but you may not need to do that. I suspect you just need to organize your code differently. – PM 2Ring Dec 30 '17 at 11:42
  • Well these are just the methods I am using to get the input from the '_input' variable the tkinter code is in another python file which appends the input to the '_input' variable. – GhostBotBoy Dec 30 '17 at 11:43
  • Ahh ok, but shouldn't it still work with a thread? I do know that I can use .after() in tkinter. The problem is that these functions are executed in a module which does not import tkinter. (Just the main file does + the main file also imports said module) – GhostBotBoy Dec 30 '17 at 11:44
  • You don't normally need to use threads with Tkinter, unless you're doing something fairly complicated. And if you _do_ use threads with Tkinter you have to be careful because Tkinter may behave oddly if it's not running in the main thread. – PM 2Ring Dec 30 '17 at 11:47
  • Well ok. Do you have an idea what i could do to request user input (from a tkinter entry) within a function call and pause the function until I receive the input? – GhostBotBoy Dec 30 '17 at 12:01
  • Yes, I do. But you still haven't shown us a MCVE, so maybe my simple solution is irrelevant for your program. – PM 2Ring Dec 30 '17 at 12:34
  • When you use this function, is it just a small part of a larger tkinter program, or is it a non-GUI program that needs a temporary GUI to get input? – Bryan Oakley Dec 30 '17 at 15:06

3 Answers3

2

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)
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
2

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,

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
1

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.

Roland Smith
  • 42,427
  • 3
  • 64
  • 94