Why does Tkinter hang when I call tkSimpleDialog.askstring from a lambda? - python

I'm developing a GUI application that models an essay. Among other things, the user can create a new topic and then populate that topic with notes. At the moment, I have two ways of creating new topics: through a dropdown option in the menu (the menu command) and through a button on the main screen (the button command). The button starts life with the text "New Topic". When the user presses the button, the program makes a new topic, asks the user to name the topic using tkSimpleDialog.askstring, and then sets the button's text to be the name of the topic and the number of notes in that topic. The button's command then changes to be adding a note to that topic.
While developing the program, I first verified that the menu command worked. It calls askstring successfully, creating a new popup window that handles input in the way I wanted. However, as soon as I added the button command, the call to askstring failed, even when called via the menu command. The window that should have the askstring dialog is whited out and the program hangs. If I comment out the button command, it works again. If I comment out the menu command, it hangs.
Here's the code where I add the command to the menu:
TopicBtn.menu.add_command(label="New Topic", underline=0,
command=self.newTopic)
Here's the code for newTopic():
def newTopic(self, button=None):
""" Create a new topic. If a Button object is passed, associate that Button
with the new topic. Otherwise, create a new Button for the topic. """
topicPrompt = "What would you like to call your new topic?"
topicName = tkSimpleDialog.askstring("New Topic", topicPrompt)
if topicName in self.topics.keys():
print "Error: topic already exists"
else:
newTopic = {}
newTopic["name"] = topicName
newTopic["notes"] = []
newTopic["button"] = self.newTopicButton(newTopic, button)
self.topics[topicName] = newTopic
self.addToTopicLists(newTopic)
Here's the code for newTopicButton():
def newTopicButton(self, topic, button=None):
""" If a Button object is passed, change its text to display the topic name.
Otherwise, create and grid a new Button with the topic name. """
if button is None:
button = Button(self.topicFrame)
index = len(self.topics)
button.grid(row=index/self.TOPICS_PER_ROW, column=(index %
self.TOPICS_PER_ROW), sticky=NSEW, padx=10, pady=10)
else:
button.unbind("<Button-1>")
buttonText = "%s\n0 notes" % topic["name"]
button.config(text=buttonText)
button.config(command=(lambda s=self, t=topic: s.addNoteToTopic(t)))
return button
And, finally, here's the code for the button command:
for col in range(self.TOPICS_PER_ROW):
button = Button(self.topicFrame, text="New Topic")
button.bind("<Button-1>", (lambda e, s=self: s.newTopic(e.widget)))
button.grid(row=0, column=col, sticky=NSEW, padx=10, pady=10)
Anybody have any idea why binding the lambda expression to the button makes askstring hang?
Edit: Thanks for the comments. Here's a minimal example that exhibits the behavior:
from Tkinter import *
import tkSimpleDialog
class Min():
def __init__(self, master=None):
root = master
frame = Frame(root)
frame.pack()
button = Button(frame, text="askstring")
button.bind("<Button-1>", (lambda e, s=self: s.newLabel()))
button.grid()
def newLabel(self):
label = tkSimpleDialog.askstring("New Label", "What should the label be?")
print label
root = Tk()
m = Min(root)
root.mainloop()
Note that switching from button.bind("<Button-1>", (lambda e, s=self: s.newLabel())) to button = Button(frame, text="askstring", command=(lambda s=self: s.newLabel())) fixes the bug (but doesn't give me a reference to the button that was pressed). I think the problem has something to do with capturing the event as one of the inputs to the lambda.

The problem you encountered here is due to the call to wait_window in the dialog you are using (you never call it yourself, but the code that implement the dialog does). For instance, the following code replicates the problem after (likely) two button clicks:
import Tkinter
def test(event=None):
tl = Tkinter.Toplevel()
tl.wait_window(tl)
root = Tkinter.Tk()
btn = Tkinter.Button(text=u'hi')
btn.bind('<Button-1>', test)
btn.pack(padx=10, pady=10)
root.mainloop()
This call to wait_window effectively does what the update command does, and is a typical example of why calling update is a bad thing to do. It enters in conflict with the <Button-1> event being handled, and hangs. The problem is that you will have to live with wait_window being used, since it belongs to the dialog's code. Apparently, if you bind to <ButtonRelease-1> then this conflict never happens. You could also use the command parameter in the button, which works fine too.
Lastly, I suggest the following to create the buttons in a cleaner manner based on what you want to achieve:
for i in range(X):
btn = Tkinter.Button(text=u'%d' % i)
btn['command'] = lambda button=btn: some_callback(button)

I figured out a workaround. From the minimum-example testing, it appears that the problem comes from making a separate call to bind and thereby accepting the event as an input to the lambda. If anyone can explain why that might be happening, I'll accept their answer over mine, but I'll accept this one for now.
The workaround is not to use a separate bind function but to create an array of buttons and then pass the correct entry in the array as the parameter to the lambda function (you can't pass the button itself, since it's being created in the line that has the lambda function).
Here's the code:
from Tkinter import *
import tkSimpleDialog
class Min():
def __init__(self, master=None):
root = master
frame = Frame(root)
frame.pack()
buttons = [None] * 2
for i in range (2):
buttons[i] = Button(frame, text="askstring",
command=(lambda s=self, var=i: s.newLabel(buttons[var])))
buttons[i].grid()
def newLabel(self, button):
label = tkSimpleDialog.askstring("New Label", "What should the label be?")
button.config(text=label)
print label
root = Tk()
m = Min(root)
root.mainloop()

Related

Threading in Tkinter with Button Command

I'm building a tkinter app and as a part of it the user has to upload a file, write a message and then press a Button whose command runs in another thread.
self.sidebar_button = Button(self.sidebar_frame, text="send your message",
command=threading.Thread(target=send_msg).start)
If the Button is pressed in the right condition then everything is fine.
However, if the user doesn't upload the file and write the message before pressing the Button then i show the user an error message. The problem here is that since the first Button press has started the thread, it can't start again.
Can you think of a workaround for this problem?
Is it possible to disable the Button before the right conditions are met?
Look at this:
import tkinter as tk
# This is called each time there is a change in the entry
def check_conditions(*args):
message_text = message_var.get()
# Here we check the conditions:
if message_text == "":
button.config(state="disabled")
else:
button.config(state="normal")
def send():
message_text = message_var.get()
print(f"Send: {message_text!r}")
root = tk.Tk()
message_var = tk.StringVar(root)
# When the value of the `message_var` has changed, call `check_conditions`
message_var.trace("w", check_conditions)
# Create the entry and attach `message_var`
message = tk.Entry(root, textvariable=message_var)
message.pack()
button = tk.Button(root, text="Send", command=send, state="disabled")
button.pack()
root.mainloop()
It uses <tk.StringVar>.trace("w", <function>) to call check_conditions each time the user changes the entry. For more info, read this.
tkinter doesn't always like being called from other threads, so avoid using threading when using tkinter.

Button.wait_variable usage in Python/Tkinter

There have already been several topics on Python/Tkinter, but I did not find an answer in them for the issue described below.
The two Python scripts below are reduced to the bare essentials to keep it simple. The first one is a simple Tkinter window with a button, and the script needs to wait till the button is clicked:
from tkinter import *
windowItem1 = Tk()
windowItem1.title("Item1")
WaitState = IntVar()
def submit():
WaitState.set(1)
print("submitted")
button = Button(windowItem1, text="Submit", command=submit)
button.grid(column=0, row=1)
print("waiting...")
button.wait_variable(WaitState)
print("done waiting.")
windowItem1.mainloop()
This works fine, and we see the printout “done waiting” when the button is clicked.
The second script adds one level: we first have a menu window, and when clicking the select button of the first presented item, we have a new window opening with the same as above. However, when clicking the submit button, I don’t get the “Done waiting”. I’m stuck on the wait_variable.
from tkinter import *
windowMenu = Tk()
windowMenu.title("Menu")
def SelectItem1():
windowItem1 = Tk()
windowItem1.title("Item1")
WaitState = IntVar()
def submit():
WaitState.set(1)
print("submitted")
button = Button(windowItem1, text="Submit", command=submit)
button.grid(column=0, row=1)
print("waiting...")
button.wait_variable(WaitState)
print("done waiting")
lblItem1 = Label(windowMenu, text="Item 1 : ")
lblItem1.grid(column=0, row=0)
btnItem1 = Button(windowMenu, text="Select", command=SelectItem1)
btnItem1.grid(column=1, row=0)
windowMenu.mainloop()
Can you explain it?
Inside your SelectItem1 function, you do windowItem1 = Tk(). You shouldn't use Tk() to initialize multiple windows in your application, the way to think about Tk() is that it creates a specialized tkinter.Toplevel window that is considered to be the main window of your entire application. Creating multiple windows using Tk() means multiple main windows, and each one would need its own mainloop() invokation, which is... yikes.
Try this instead:
windowItem1 = Toplevel()

In tkinter for Python, can someone identify why <<ListBoxSelect>> is not firing?

I am trying to develop a small database application using tkinter in python. I have a small database which has inventory numbers for various assets. I have successfully constructed the listbox using tkinter and managed to populate it using sqlite3's library.
I've been trying to get the << ListBoxSelect >> event bound to which item is selected in the listbox and to output the asset's information. Several hours later, all I'm trying to do now is get the bind event to fire at all and print out a simple string until I can figure it out.
Before I post my code, I used This code snippet to see if I could get ANY binding event to work. This code snippet does in fact work however trying to integrate the way its written into my own code has not been successful. The event binder simply does not fire in my code at all. Can anyone please identify why this is or what I'm doing incorrectly?
Please see relevant code below (please note the indenting is not correct when trying to paste it into this forum):
class MainWindow(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
# Create Widgets
self.invListbox = tk.Listbox(self, height=20, width=10, selectmode=tk.SINGLE)
self.assetDescLabel1 = tk.Label(self, text="Asset Description")
self.assetDescEntry1 = tk.Entry(self)
self.exitButton = tk.Button(self, text="Exit", command=exit)
# Configure Widgets
# Updates assetDescEntry based on invListbox selection
self.invListbox.bind('<<ListboxSelect>>', self.invOnSelect())
# Place Widgets
self.invListbox.grid(row=1, column=1)
self.assetDescLabel1.grid(row=1, column=2)
self.assetDescEntry1.grid(row=2, column=2)
self.exitButton.grid(row=2, column=1)
# Populate invListbox
self.InvList = main.sqlInventoryNumberList()
for row in self.InvList:
item = row[0]
self.invListbox.insert(1, item)
def invOnSelect(self):
selection = "This is just a regular old string"
print(selection)
I can't get the code above to run as too much is missing. You must bind a function in the bind statement not the result of a function. So .bind( ... , func ) NOT .bind(..., func() ). Your function must take an event object too. Below is a minimal working example.
import tkinter as tk
def invOnSelect(event):
selection = "This is just a regular old string"
print( event, selection )
root = tk.Tk()
invListbox = tk.Listbox( root, height=20, width=10, selectmode=tk.SINGLE )
invListbox.grid()
invListbox.bind( '<<ListboxSelect>>', invOnSelect ) # invOnSelect has no brackets here.
InvList = [ '1', '2', '3' ]
for item in InvList:
invListbox.insert(tk.END, item)
root.mainloop()
The ideas should integrate into your application.

Is there a way to make a Button perform multiple commands

I'm doing a school project. I designed a welcome page for it on tkinter and put a button 'ok' which when pressed moves the code forward but the welcome page doesnt close itself once pressed.
i have tried defining another function to close it but that does not work.
welcome = Tk()
okbutton = Button(welcome, text='ok', command=R)
okbutton.pack()
welcome.mainloop()
and the code moves forward but welcome page remains open...Is there a method to resolve this?
Window never closes automatically when you create new window. You have to use welcome.destroy() for this. You can run it in function which creates new window.
import tkinter as tk
def welcome_page():
global welcome
welcome = tk.Tk()
tk.Label(welcome, text='Welcome').pack()
button = tk.Button(welcome, text='OK', command=other_page)
button.pack()
welcome.mainloop()
def other_page():
global welcome
global other
welcome.destroy() # close previous window
other = tk.Tk()
tk.Label(other, text='Other').pack()
button = tk.Button(other, text='OK', command=end)
button.pack()
welcome.mainloop()
def end():
global other
other.destroy() # close previous window
welcome_page()
To perform the two commands call one command inside the other (sounds like it should be towards the end) and assign the first command to the button.
A button can only call a single function, but that single function can do anything you want.
def do_ok():
print("hello!")
welcome.destroy()
welcome = Tk()
okbutton = Button(welcome, text='ok', command=do_ok)
okbutton.pack()
welcome.mainloop()

Python tkinter label won't change at beginning of function

I'm using tkinter with Python to create a user interface for a program that converts Excel files to CSV.
I created a label to act as a status bar, and set statusBarText as a StringVar() as the textvariable. inputFileEntry and outputFileEntry are textvariables that contain the input and output file paths.
def convertButtonClick():
statusBarText.set('Converting...')
if inputFileEntry.get() == '' or outputFileEntry.get() == '':
statusBarText.set('Invalid Parameters.')
return
retcode = subprocess.('Program.exe' ,shell=true)
if retcode == 0:
statusBarText.set('Conversion Successful!')
else:
statusBarText.set('Conversion Failed!')
This function gets called when you click the convert button, and everything is working fine EXCEPT that the status bar never changes to say 'Converting...'.
The status bar text will get changed to invalid parameters if either the input or output are empty, and it will change to success or failure depending on the return code. The problem is it never changes to 'Converting...'
I've copied and pasted that exact line into the if statements and it works fine, but for some reason it just never changes before the subprocess runs when its at the top of the function. Any help would be greatly appreciated.
Since you're doing all of this in a single method call, the GUI never gets a chance to update before you start your sub process. Check out update_idletasks() call...
from http://infohost.nmt.edu/tcc/help/pubs/tkinter/universal.html
w.update_idletasks()
Some tasks in updating the display, such as resizing and redrawing widgets, are called idle tasks because they are usually deferred until the application has finished handling events and has gone back to the main loop to wait for new events.
If you want to force the display to be updated before the application next idles, call the w.update_idletasks() method on any widget.
How are you creating your Label?
I have this little test setup:
from Tkinter import *
class LabelTest:
def __init__(self, master):
self.test = StringVar()
self.button = Button(master, text="Change Label", command=self.change)
self.button.grid(row=0, column=0, sticky=W)
self.test.set("spam")
self.testlabel = Label(master, textvariable = self.test).grid(row = 0,column = 1)
def change(self):
self.test.set("eggs")
root = Tk()
root.title("Label tester")
calc = LabelTest(root)
root.mainloop()
And it works.
Did you make sure to use "textvariable = StatusBarText" instead of "text=StatusBarText.get()"?

Categories