Tkinter Keyboard Binds - python

I'm working on an interface using Tkinter and the canvas widget, and so far have found answers to issues I have had from others questions and the answers posted, but I am stumped on this one.
I have several keyboard binds in the class where my GUI elements are created, and they all work fine when the program is started. The binds looks something like this:
self.canvas.get_tk_widget().bind("<Control-o>",self.flash_open)
and are within the __init__ function of the class. As of yesterday, I initialized this class
to start the program, then waited for the user to select open from a menu, which then opened (among other things) a tkmessagebox
self.specfilename =askopenfilename(filetypes=[("spec", "")],initialdir= self.pathname)
With this filename I am able to retrieve my required variable names from a certain filetype (inconsequential to the problem). Today I modified the __init__ function to call the open function when the program starts. Since nothing else can be done until this file is opened, it would make sense to open it first thing. Once the file is selected and the Tkmessagebox is closed, the root window is active, but none of the keyboard binds work. My functions still work using the menu/buttons assigned to them, just not the binds. I have tried binding the shortcuts to the root, with the same result, and am now thinking it may be an issue with the order I am calling them
def __init__(self):
...
self.openfile() #calls the tkmessagebox
self.root.mainloop() #starts gui
I had actually run into this issue before, where a toplevel() instance was closed/destroyed and disabled the binds of the parent window. There isn't any error message to speak of, the binds just don't do anything. I should also mention I have tried to focus on the root window again using
self.openfile()
self.root.mainloop()
self.root.focus_set()
I got around it before by using the wm_withdraw() and wm_deiconify() functions to simply hide the child window, then close it after the program is complete. This fix is a little more difficult to apply in this case however. If anyone can shed some light on the cause of the problem I'd appreciate it.
Edit:
I've written up a runable code segment to show exactly what my issue is.
import os
from tkFileDialog import askopenfilename
from Tkinter import *
class Start:
def __init__(self):
self.root = Tk()
self.root.title('Binding Troubles')
menubar = Menu(self.root)
#add items and their commands to the menubar
filemenu = Menu(menubar, tearoff=0)
filemenu.add_command(label="Do work", command=self.do_work)
filemenu.add_command(label="Open File",command=self.openfile)
menubar.add_cascade(label="File", menu=filemenu)
#bind control-o to perform the do work function
self.root.bind("<Control-o>",self.flash_do_work)
self.root.bind("<Control-O>",self.flash_do_work)
#add the menubar to the GUI
self.root.config(menu=menubar)
#initially open a tkdialog to open a file
self.openfile()#comment out this line to make the bind work
self.root.focus()#also tried self.root.focus_set()
self.root.mainloop()
def flash_do_work(self,event):
#indirect tie to the do_work() function, I'm don't know a
#proper way to make functions handle calls from both events and non-events
self.do_work()
def openfile(self):
#gets current path
self.pathname = os.getcwd()
#Requests filename using a tkdialog
self.filename =askopenfilename(initialdir= self.pathname)
print self.filename
def do_work(self):
#placeholder for actual function; shows whether the bind is working or not
print "work"
Start()
The bind will work if self.openfile() is removed from __init__, and used only from the menu
Another Edit: I've updated the example again, giving a menu option to run the openfile() function. I noticed that if openfile() is called in __init__, the bind will not work. But if next the openfile function is called again, this time manually from the menu, the bind will start working again. Not exactly sure what to take from this. Also, my apologies for the post getting so long.

Change
self.openfile()
to
self.root.after(1, self.openfile)
This moves the call to askopenfilename into the main event loop. Having it outside the main event loop is somehow clobbering your event bindings.

I had this kind of problem a couple of times and it took quite a while until I found a solution I was comfortable with. As #Steven Rumbalski suggests I tried with delaying the application, which works but seems shaky.
Then I found the functions for waiting until something is complete, in this case wait_visibility(widget). This will delay execution until the widget is visible, which seems to be the thing to be waiting for. Try this:
self.root.wait_visibility(self.root) # Wait for root to be displayed
self.openfile()
Now; I'm not sure why this is so, and it seems that there may be differences depending on platform: Tkinter window event . This has nevertheless worked for me on Windows10 and Python 3.10.5.

Related

Unable to exit tkinter app when using "wait_variable()"

I have a python code that includes tkinter window and other running tasks.
I've been trying to bind "WM_DELETE_WINDOW" event to a function that exits my python code when I close the window but can't achieve that.
This is what I try:
def on_exit():
root.destroy()
sys.exit()
root.protocol('WM_DELETE_WINDOW', on_exit)
The window is destroyed successfully but the python code doesn't exit. Any possible reason for sys.exit() not to work?
What am I doing wrong? any alternative approach should I try?
Doing some testing I figured out what can be the problem.
Here's a small code that summarizes my code which is much bigger.
import tkinter as tk
import sys
root = tk.Tk()
submitted = tk.IntVar()
def on_exit():
root.destroy()
sys.exit()
root.protocol('WM_DELETE_WINDOW', on_exit)
def submit():
submitted.set(1)
print("submitted")
button= tk.Button(root, text="Submit",command=submit)
button.pack()
button.wait_variable(submitted)
root.mainloop()
I believe now that wait_variable is the source of the problem.
And the code actually exits when I added submitted.set(1) to on_exit() ( or if I clicked the button first before closing the window ) but if I tried closing the window without pressing the button, the code won't exit.
So does this mean that wait_variable not only makes tkinter app wait, but also prevents python code exiting?!
I tried os._exit(1) and it worked, but I think it's not clean.
As your updated question points out the problem is wait_variable(). Going off the documentation for this method wait_variable() enters a local event loop that wont interrupt the mainloop however it appears that until that local event loop is terminated (the variable is updated in some way) it will prevent the python instance from terminating as there is still an active loop. So in order to prevent this you have also correctly pointed out you need to update this variable right before you terminate the tk instance.
This might seam odd but it is the behavior I would expect. It is my understanding that an active loop needs to be terminated before a python instance can exit.
As Bryan has pointed out in the comments the wait_variable() method is "a function which calls the vwait command inside the embedded tcl interpreter. This tcl interpreter knows nothing about python exceptions which is likely why it doesn't recognize the python exception raised by sys.exit()"
Link to relevant documentation:
wait_variable()
Relevant text from link:
wait_variable(name)
Waits for the given Tkinter variable to
change. This method enters a local event loop, so other parts of the
application will still be responsive. The local event loop is
terminated when the variable is updated (setting it to it’s current
value also counts).
You can also set the variable to whatever it is currently set as to terminate this event loop.
This line should work for you:
submitted.set(submitted.get())
That said you do not actually need sys.exit(). You can simply use root.destroy().
You new function should look like this:
def on_exit():
submitted.set(submitted.get())
root.destroy()
The python instance will automatically close if there is no more code after the mainloop.

Tkinter add Widgets (a Button) to a new second Frame

I created a Frame, gave it a menubar. Works just fine. The purpose of the entry in the menubar is to open a new frame, in which u can change some settings. The creation of the new Window works also. However I can't create widgets on the new created window. I tried it with a Button and got a
TclError: can't invoke "button" command: application has been destroyed
I tried to google it and found Cannot invoke button command: application has been destroyed which didn't quite helped me.
Further I found a solution were u have to create a parent class (which inherrits from Frame) and than create all other Frames within it, but on the first view it looked pretty complicated. Especially because the creation of the second window seems to work in the first place.
I know this is probably a really basic question, so thanks in advance for your time
def perfSettings():
perfFrame = Tk(className=" Performanz Einstellungen")
perfFrame.configure(bg='#F2F2F2')
perfFrame.geometry("300x300")
perfFrame.mainloop()
btn = Button(master=perfFrame, text='Speichern', command=myPerfSettingValue.getValues, width=37)
btn.pack()
# Button(perfFrame, text='Abbrechen', command=perfFrame.destroy, width=37).grid(row=0 ,column=1 )
class perfSettingsValue:
def __init__(self):
self.bvhSteps = 0
def getValues(self):
pass
#Hauptfenster
root = Tk(className="BoneMapping & SkeletonEstimation")
root.configure(bg='#F2F2F2')
root.geometry("1300x600")
myPerfSettingValue = perfSettingsValue()
menubar = Menu(root)
sdmenu = Menu(menubar, tearoff=0)
sdmenu.add_command(label="Performanz", command=perfSettings)
menubar.add_cascade(label='Einstellungen',menu=sdmenu)
root.config(menu=menubar)
The key problem here is that you are trying to add a button after starting the mainloop which effectively blocks the execution of the program. The error you are getting is because the line that adds the button gets executed after the window has been closed.
Your problem will be solved if you modify your function like this:
def perfSettings():
perfFrame = Tk(className=" Performanz Einstellungen")
perfFrame.configure(bg='#F2F2F2')
perfFrame.geometry("300x300")
btn = Button(master=perfFrame, text='Speichern', command=myPerfSettingValue.getValues, width=37)
btn.pack()
perfFrame.mainloop()
This is not the only problem though. Instead of creating a new instance of Tk, you should create a new Toplevel instance, which will, in your case, act just as a Tk instance, but have a lot less tendency to cause trouble.
Finally, you should consider reading on the object oriented approach to designing tkinter applications. There are far too many variants of that to be appropriately elaborated here but I certainly recommend you take the effort to learn to use one of them. It will make your code more comprehensible and maintainable. My usual approach is to create a class that inherits from Toplevel or Tk for every type of window I am going to use.

Button behaviour

I work with Python 3.5 and TKinter.
I defined a label and file dialog that updates this label.
A button is responsible to launch this dialog.
self.sel_folder_val = the label that will be updated.
The code:
self.sel_folder_val['text']=filedialog.askdirectory()
After pressing the button in order to launch this dialog, the button stays pressed. Any dialog that a button is responsible to open cause the button to stay low (pressed) after closing this dialog.
I have tried this also with no help...:
self.select_folder_btn.config(relief=RAISED)
Code example:
self.select_folder_btn = Button(self.top)
self.select_folder_btn.place(relx=0.07, rely=0.57, height=34, width=187)
self.select_folder_btn.configure(activebackground="#d9d9d9")
self.select_folder_btn.configure(activeforeground="#000000")
self.select_folder_btn.configure(background="#d9d9d9")
self.select_folder_btn.configure(disabledforeground="#a3a3a3")
self.select_folder_btn.configure(font=self.font3)
self.select_folder_btn.configure(foreground="#000000")
self.select_folder_btn.configure(highlightbackground="#d9d9d9")
self.select_folder_btn.configure(highlightcolor="black")
self.select_folder_btn.configure(pady="0")
self.select_folder_btn.configure(text='''Select destination folder''')
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
def update_folder_value(self,event):
self.sel_folder_val['text']=filedialog.askdirectory()
return
After executing update_folder_value() function, self.select_folder_btn stays down.
I used the command:
self.select_folder_btn.configure(command=self.update_folder_value)
Instead of bind:
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
It solved my problem.
Thanks
First for future reference this is a minimal working example:
from Tkinter import *
import tkFileDialog as filedialog
class app:
def __init__(self):
self.top = Tk()
self.select_folder_btn = Button(self.top)
self.select_folder_btn.place(relx=0.07, rely=0.57, height=34, width=187)
self.select_folder_btn.configure(activebackground="#d9d9d9")
self.select_folder_btn.configure(activeforeground="#000000")
self.select_folder_btn.configure(background="#d9d9d9")
self.select_folder_btn.configure(disabledforeground="#a3a3a3")
#self.select_folder_btn.configure(font=self.font3)
self.select_folder_btn.configure(foreground="#000000")
self.select_folder_btn.configure(highlightbackground="#d9d9d9")
self.select_folder_btn.configure(highlightcolor="black")
self.select_folder_btn.configure(pady="0")
self.select_folder_btn.configure(text='''Select destination folder''')
self.select_folder_btn.configure(command=self.update_folder_value)
self.sel_folder_val = {}
self.top.mainloop()
def update_folder_value(self):
self.sel_folder_val['text']=filedialog.askdirectory()
self.top.update_idletasks()
app()
and even that's not minimal. Second your problem is hard to find since this isn't minimal- you're doing something really weird - binding the button to a click. You're overriding the built-in binding, and apparently it still affects the state of the button on press, but not going back. What you wanted is:
self.select_folder_btn.configure(command=self.update_folder_value)
instead of your:
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
You could also define that in the Button command. What you did is bypassed the button mechanism, so apparently only half of it is executed, and the relief is not raised. Note you have to remove the event parameter your method accepts.

What is causing this cascade menu failure with tkinter in python?

The following code exhibits a problem I do not understand:
from Tkinter import *
root = Tk()
cheese_var = IntVar()
parrot_var = IntVar(value=1)
check_menu = Menu(tearoff=0)
check_menu.add_checkbutton(label="Cheese", variable=cheese_var)
check_menu.add_checkbutton(label="Parrot", variable=parrot_var)
count = 0
class Top():
def __init__(self):
global count
count += 1
self.tl = Toplevel(root)
Label(self.tl, text="Window " + str(count)).pack()
self.mb = Menubutton(self.tl, text="Push Me", bg='pink')
self.menu = Menu(self.mb, tearoff=0)
self.menu.add_cascade(label="Choices", menu=check_menu)
self.menu.add_command(label="New Window", command=new_top)
self.mb.config(menu=self.menu)
self.mb.pack()
def new_top():
Top()
Top()
root.mainloop()
The menu brought up by the menu button in the created top level window initially behaves as expected. Clicking on the New Window command there creates a new such window, which also behaves as expected. Indeed, as long as you keep creating new top level windows, everything continues to work as expected. However, once you delete (close) any one of those windows, then, in a subsequently created new window, the Choices cascade on the new menu is not functional. (It is still OK in the windows created before the closing of one.)
The situation in which I initially encountered this symptom was much more complex, but I was able to simplify it down to the above example which exhibits the issue. I have discovered that I can avoid the problem by having each instance of Top create its own check_menu as an attribute; but I do not understand why this should be necessary. Please point me the way if there is one to avoid the problem without such replication of a cascade menu used in multiple windows.
Unfortunately, I don't think it is possible to do what you want. I'll try to explain as best as I can.
When you first run the script, check_menu is created and works fine for the first window. As you create more windows, check_menu is simply shared between them. However, when you close one of them, check_menu (and everything under it) is destroyed. So, when you create a new window after that, check_menu no longer exists and it doesn't show.
However, the script doesn't throw an error because, for some reason, Tkinter allows you to assign menus to things that aren't menus. Believe it or not, none of the following code:
self.menu.add_cascade(label="Choices", menu=None)
self.menu.add_cascade(label="Choices", menu=1)
self.menu.add_cascade(label="Choices", menu="")
will break the script. Each line simply does nothing but create an empty cascade "Choices".
That is basically what is happening. After closing one window, check_menu and everything under it is destroyed. Yet, Tkinter doesn't throw an error but instead assigns a menu to something that is no longer a menu (as far as what it is assigning the menu to, I believe it is using the old instance of check_menu, which was destroyed).
To solve this problem, recreate check_menu and everything under it each time you call Top. In other words, put the code for check_menu (and its options) in the __init__ method of Top. That way, each time Top is called, check_menu will exist.
Hope this helps (and that I explained it sufficiently :)

Creating Toplevel widgets that are thread safe

I'm trying to learn how to use the thread module. I followed along with the instructions here: http://effbot.org/zone/tkinter-threads.htm
My hope is the test script will:
Print out the "count" every two seconds
Show a pop-up dialog window (also every 2 seconds)
The pop-ups should be allowed to accumulate (if I don't click "OK" for a while, there should be
multiple pop-ups)
However, when I run this script it will freeze the main window and after a while crash. I think I'm not implementing the thread module correctly.
Could someone please have a look and point out what I'm doing wrong?
Here is what I've tried so far:
from Tkinter import *
import thread
import Queue
import time
class TestApp:
def __init__(self, parent):
self.super_Parent = parent
self.main_container = Frame(parent)
self.main_container.pack()
self.top_frame = Frame(self.main_container)
self.top_frame.pack(side=TOP)
self.bottom_frame = Frame(self.main_container)
self.bottom_frame.pack(side=TOP)
self.text_box = Text(self.top_frame)
self.text_box.config(height=20, width=20)
self.text_box.pack()
self.queue = Queue.Queue()
self.update_me()
def show_popup(self):
self.my_popup = Toplevel(self.main_container)
self.my_popup.geometry('100x100')
self.popup_label = Label(self.my_popup, text="Hello!")
self.popup_label.pack(side=TOP)
self.pop_button = Button(self.my_popup, text="OK", command=self.my_popup.destroy)
self.pop_button.pack(side=TOP)
def write(self, line):
self.queue.put(line)
def update_me(self):
try:
while 1:
line = self.queue.get_nowait()
if line is None:
self.text_box.delete(1.0, END)
else:
self.text_box.insert(END, str(line))
self.text_box.see(END)
self.text_box.update_idletasks()
except Queue.Empty:
pass
self.text_box.after(100, self.update_me)
def pipeToWidget(input, widget):
widget.write(input)
def start_thread():
thread.start_new(start_test, (widget,))
def start_test(widget):
count = 0
while True:
pipeToWidget(str(count) + "\n", widget)
count += 1
time.sleep(2)
widget.show_popup()
root = Tk()
widget = TestApp(root)
start_button = Button(widget.bottom_frame, command=start_thread)
start_button.configure(text="Start Test")
start_button.pack(side=LEFT)
root.title("Testing Thread Module")
root.mainloop()
I can't reproduce your problem, but I can see why it would happen.
You're using the queue to pass messages from the background thread to the main thread for updating text_box, which is correct. But you're also calling widget.show_popup() from the background thread, which means it creates and displays a new Toplevel in the background thread. That's not correct.
All UI code must run in the same thread—not all UI code for each top-level window, all UI code period. On some platforms, you may get away with running each window in its own thread (or even free-threading everything), but that isn't supposed to work, and definitely will crash or do improper things on some platforms. (Also, that single UI thread has to be the initial thread on some platforms, but that isn't relevant here.)
So, to fix this, you need to do the same dance for creating the popups that you do for updating the textbox.
The obvious way to do that is to move the widget.show_popup() to the loop in update_me(). If you want it to happen 2 seconds after the textbox updates, just add self.top_frame.after(2000, self.show_popup) to the method.
But I'm guessing you're trying to teach yourself how to have multiple independent updating mechanisms, so telling you "just use a single update queue for everything" may not be a good answer. In that case, just create two queues, and a separate update method servicing each queue. Then, do your pipeToWidget, sleep 2 seconds, then pipeToPopup.
Another way around this is to use mtTkinter. It basically does exactly what you're doing, but makes it automatic, pushing each actual Tk GUI call onto a queue to be run later by the main loop. Of course your objects themselves have to be thread-safe, and this also means that you have to deal with the GUI calls from one thread getting interleaved with calls from another thread. But as long as neither of those is a problem (and they don't seem to be in your case), it's like magic.
If you want to know why this is freezing and/or crashing for you on Win7 and not for me on OS X 10.8… well, you really need to look into a mess of Tcl, C, and Python code, and also at how each thing is built. And, unless it's something simple (like your Tk build isn't free-threaded), it wouldn't tell you much anyway. The code isn't supposed to work, and if it seems to work for me… that probably just means it would work every time until the most important demo of my career, at which point it would fail.

Categories

Resources