Tkinter's event_generate command ignored - python

I am trying to figure out how to unittest a bind command in a dialog window. I'm attempting this with tkinter's event_generate. It is not working the way I expect. For this StackOverflow question I've set up some code with a single call to event_generate. Sometimes that line works and sometimes it is as if the line doesn't even exist.
The bind in the dialog's __init__ method looks like this:
self.bind('<BackSpace>', #Print "BackSpace event generated."
lambda event: print(event.keysym, 'event generated.'))
Any action in the dialog will call back to its terminate method (The dialog is based on Frederik Lundh's example Dialog in 'An Introduction to Tkinter'.)
def terminate(self, event=None):
print('terminate called') # Make sure we got here and the next line will be called
self.event_generate('<BackSpace>')
self.parent.focus_set()
self.destroy()
When the dialog is called using the code below any user action will end up calling terminate. In each case "terminate called" and "BackSpace event generated." are displayed. This proves that the call to event_generate is set up correctly.
parent = tk.Tk()
dialog = Dialog(parent)
dialog.wait_window()
In case it's relevant I ought to mention that I have moved Lundh's call to self.wait_window from his dialog's __init__ method to the caller. Whilst this breaks the neat encapsulation of his dialog it appears to be necessary for automated unittests. Otherwise the unittest will display the dialog and halt waiting for user input. I don't like this solution but I'm not aware of any alternative.
The problem I'm having is when wait_window is replaced with a direct call to the terminate method. This is the sort of thing that I'd expect to be able to do in unittesting which is to test my GUI code without running tkinter's mainloop or wait_window.
parent = tk.Tk()
dialog = Dialog(parent)
dialog.terminate()
This only prints "terminate called" and does not print "BackSpace event generated.". The call to event_generate appears to have no effect. If I follow the call in the debugger I can see that tkinter's event_generate() is being called with the correct arguments. self = {Dialog} .99999999, sequence = {str}'<BackSpace>', kw = {dict}{}
In view of the warning in the TkCmd man pages about window focus I have verified the dialog with the binding is given focus in its __init__ method.
Tkinter is not executing the callback. Why?
EDIT: This bare bones code shows update working. However, it only works if it is called in __init__ before event_generate is called by the main program. (This puzzle has been raised as a separate question)
class UpdWin(tk.Tk):
def __init__(self):
super().__init__()
self.bind('<BackSpace>',
lambda event: print(event.keysym, 'event generated.'))
self.update() # Update works if placed here
app = UpdWin()
app.event_generate('<BackSpace>')
# app.update() # Update doesn't work if placed here
Six Years On
4/12/2021. See Mark Roseman's excellent web site for a detailed explanation of why any use of update is a bad idea.
The problem posed by this six year old question is entirely avoided by better program design in which tkinter widget objects are never subclassed. Instead they should be created by composition where they can be easily monkey patched. (This advice is contrary to patterns shown in Frederik Lundh's example Dialog in 'An Introduction to Tkinter'.)
For unittest design, not only is there no need to start Tk/Tcl via tkinter but it is also unwise.

event_generate will by default process all event callbacks immediately. However, if you don't call update before calling event_generate, the window won't be visible and tkinter will likely ignore any events. You can control when the generated event is processed with the when attribute. By default the value is "now", but another choice is "tail" which means to append it to the event queue after any events (such as redraws) have been processed.
Full documentation on the when attribute is on the tcl/tk man page for event_generate: http://tcl.tk/man/tcl8.5/TkCmd/event.htm#M34

Don't know if this is relevant to your problem, but I got widget.event_generate() to work by calling widget.focus_set() first.

#lemi57ssss I know this is an old question, but I just want to highlight the point brought up by Bryan Oakley and to correct your last code to make it work. He said you have to update first before it can respond to the generated event. So if you switch the positions of update() and event_generate(), you will get the "BackSpace event generated." text printed out.
It worked when you put the update() in the __init__() was because of the same reason, i.e., it got called first before the event_generated().
See the amended code below:
class UpdWin(tk.Tk):
def __init__(self):
super().__init__()
self.bind('<BackSpace>',
lambda event: print(event.keysym, 'event generated.'))
#self.update() # Update works if placed here
app = UpdWin()
app.update() # Update also works if you placed it here
app.event_generate('<BackSpace>')

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 Keyboard Binds

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.

QWidget not showing after calling show()

In My App, I have a QWidget which is not showing after I call show(), even though isVisible returns true.
This widget is created from an event of the main application window. But when its started on its own, i.e., as the only widget on an app, it shows up normally.
Anyone knows what may cause this behavior?
Other widgets in my app show up normally only this one is giving me troubles. It actually use to work just fine under a previous version of Qt4 (don't remember which).
the code for the widget is here
update: windows seems to appear and is immediately destroyed.
The relevant code is in hidx/GUI/main.py:
#pyqtSignature("")
def on_actionScatterplot_Matrix_activated(self):
...
spm = scatmat.ScatMat(pars, self.currentdbname)
print "==>", spm.pw.isVisible()
spm.pw.hide()
spm.pw.showMaximized()
print spm.pw.size()
print "==>", spm.pw.isVisible()
#pyqtSignature("int")
def on_rowStart_valueChanged(self, p0):
...
In on_actionScatterplot_Matrix_activated, you create an instance of ScatMat, but don't keep a reference to it. So the window will be briefly shown, and then immediately garbage-collected once the function completes.

Update a Tkinter text widget as it's written rather than after the class is finished

I'm in a bind, since this is being written on a classified machine I am unable to copy+paste here. Being somewhat a novice, my approach is probably unorthodox.
I have a GUI written in Tkinter with several buttons. Each button is linked to a class that, in effect, runs a short script. When the button is clicked, I inititalize a class log_window which is simply a Tkinter text widget. I then create a global variable linking log to the log_window I just created, and as the script runs I pipe sys.stdout/stderr to log (I created a write method specifically for this). Everything is kosher, except that the log_window text widget doesn't update with my piped stdout until after the class calling it is finished. However, if I simply print within the class, it will print in the order it is called.
Example
import Tkinter
from Tkinter import *
import time
class log_window:
def __init__(self,master):
self.textframe = Tkinter.Frame(master)
self.text = Text(self.textframe)
self.text.pack()
self.textframe.pack()
def write(self,text):
self.text.insert(END,text)
class some_func1: # This effectively waits 5 seconds then prints both lines at once
def __init__(self,master):
log.write("some text")
time.sleep(5)
log.write("some text")
class some_func2: # This prints the first object, waits 5 seconds, then prints the second
def __init__(self,master):
print "some text"
time.sleep(5)
print "some text"
if __name__ == '__main__':
global log
root = Tk()
log = log_window(root)
root.after(100,some_func1, root)
root.after(100,some_func2, root)
root.mainloop()
Sorry if my example is a little bit muffed, but I think it makes the point. The piping I do is through Popen and some system calls, but they aren't part of the issue, so I only highlighted what, I presume, is the LCD of the issue.
I don't know the details of Tkinter's concurrency, but fiddling around reveals that if you put
master.update_idletasks()
after each call to log.write, it updates on cue. You could give log a .flush() method to do that (like file handles have), or you could just make log.write call it after writing.
When you call sleep it causes your whole GUI to freeze. You must remember that your GUI runs an event loop, which is an infinite loop that wraps all your code. The event loop is responsible for causing widgets to redraw when they are changed. When a binding is fired it calls your code from within that loop, so as long as your code is running, the event loop can't loop.
You have a couple of choices. One is to call update_idletasks after adding text to the widget. This lets the event loop service "on idle" events -- things that are schedule to run when the program isn't doing anything else. Redrawing the screen is one such event, and there are others as well.
The other option is to run your functions in a thread or separate process. Because Tkinter isn't thread safe, these other threads or processes can't directly communicate with the GUI. What they must do is push a message onto a queue, and then your main (GUI) thread must poll the queue and pull messages off. It would be easy to build this code into your log class, and polling the queue can be done using the event loop -- just write a method that pulls messages off the queue and inserts them into the widget, the calls itself using after a few hundred milliseconds later.
You have to update your widget content by adding self.text.update() after self.text.insert(END,text)

Tkinter: invoke event in main loop

How do you invoke a tkinter event from a separate object?
I'm looking for something like wxWidgets wx.CallAfter. For example, If I create an object, and pass to it my Tk root instance, and then try to call a method of that root window from my object, my app locks up.
The best I can come up with is to use the the after method and check the status from my separate object, but that seems wasteful.
To answer your specific question of "How do you invoke a TkInter event from a separate object", use the event_generate command. It allows you to inject events into the event queue of the root window. Combined with Tk's powerful virtual event mechanism it becomes a handy message passing mechanism.
For example:
from tkinter import *
def doFoo(*args):
print("Hello, world")
root = Tk()
root.bind("<<Foo>>", doFoo)
# some time later, inject the "<<Foo>>" virtual event at the
# tail of the event queue
root.event_generate("<<Foo>>", when="tail")
Note that the event_generate call will return immediately. It's not clear if that's what you want or not. Generally speaking you don't want an event based program to block waiting for a response to a specific event because it will freeze the GUI.
I'm not sure if this solves your problem though; without seeing your code I'm not sure what your real problem is. I can, for example, access methods of root in the constructor of an object where the root is passed in without the app locking up. This tells me there's something else going on in your code.
Here's an example of successfully accessing methods on a root window from some other object:
from tkinter import *
class myClass:
def __init__(self, root):
print("root background is %s" % root.cget("background"))
root = Tk()
newObj = myClass(root)
Here below just some doc and link to better understand Bryan's answer above.
function description from New Mexico Tech :
w.event_generate(sequence, **kw)
This method causes an event to trigger without any external stimulus. The handling of the event is the same as if it had been triggered by an external stimulus. The sequence argument describes the event to be triggered. You can set values for selected fields in the Event object by providing keyword=value arguments, where the keyword specifies the name of a field in the Event object.
list and description of tcl/tk event attributes here

Categories

Resources