Tkinter: invoke event in main loop - python

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

Related

Python and Tkinter: object oriented programming query

I am trying to learn python, Tkinter and oop. Below is the code that I wrote after following tutorial on effbot.org
from Tkinter import Tk, Frame, Label
class Nexus(object):
"""Top level object which represents entire app"""
def __init__(self, main_window):
self.nexus_frame = Frame(main_window)
self.nexus_frame.pack()
self.label = Label(main_window, text="Tkinter")
self.label.pack()
def main():
main_window = Tk()
nexus_app = Nexus(main_window)
main_window.wm_title("Hello World Window")
width = main_window.winfo_screenwidth()
height = main_window.winfo_screenheight()
main_window.wm_minsize(width=width-100, height=height-100)
main_window.mainloop()
if __name__ == "__main__":
main()
Here a top level window is created first and it is passed as argument to Nexus class where I am adding a frame and a label to the frame. Then I am setting the size of top level window relative to current screen size back in the main function.
My question is why was the top level window create in main function?
Could it not be created inside __init__ of Nexus class itself?
What difference would it make if main_window was create inside __init__ of Nexus class and mainloop() was started therein?
Once Tk.mainloop is entered, no further code will be executed. Instead, the Tk event loop will take over (hence the name).
What that means is that if you, eg, did something like this:
def main():
...
main_window.mainloop()
print 'Hello world!'
then that print statement would never be executed (or, at least, not while the GUI is running).
So, with that in mind, why is there a problem with creating the root window and executing main loop within the constructor (the __init__ statement)? Well, two basic reasons:
It would mean that the constructor never returns, which is unexpected. If a programmer sees this:
def main():
Nexus()
print 'Hello world!'
then he or she will expect that print statement to be executed. As a rule, you don't expect creating an instance of a class to be the kind of thing which will cause an infinite loop (as the event loop is).
Related to that is the second reason: it would not be possible to create more than one instance of Nexus, because as soon as you create one, Tk.mainloop will take over. Again, that's unexpected: a class is a description of a type of object, and you would normally expect to be able to instantiate more than one object like that.
At the moment, if you write:
def main():
...
Nexus(main_window)
Nexus(main_window)
then you'll get two copies of your Nexus window on the screen. That's expected, and sensible. The alternative would not be.
So what's the take-away message?
When you're dealing with GUI programs, entering the event loop is the last thing you want to do. Your setup might involve creating one object (as now), or it might involve creating many objects (eg, a complex GUI app might have two or three windows).
Because we want to be able to write similar code in both cases, the usual approach is to create the root window (the Tk object) once, and then pass it in as a reference to any classes that need to know about it.

Tkinter's event_generate command ignored

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>')

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.

disable tkinter keyboard shortcut (2)

I'm proposing a continuation of the discussion in disable tkinter keyboard shortcut: I have an event handler for an event that Tkinter also uses, so that my prog & Tkinter interact badly.
Since it is a problem that I've been unable to solve I'm re-proposing here, where I tried to boil it down to the simplest form in the following code:
#!/usr/bin/env python
from Tkinter import *
import tkFont
def init():
global root,text
root = Tk()
root.geometry("500x500+0+0")
dFont=tkFont.Font(family="Arial", size=10)
text=Text(root, width=16, height=5, font=dFont)
text.pack(side=LEFT, fill=BOTH, expand = YES)
root.bind("<Control-b>", setbold)
text.tag_config("b",font=('Verdana', '10', 'bold' ))
text.tag_config("i",font=('Verdana', '10', 'italic' ))
def removeformat(event=None):
text.tag_remove('b',SEL_FIRST,SEL_LAST)
text.tag_remove('i',SEL_FIRST,SEL_LAST)
def setbold(event=None):
removeformat()
text.tag_add('b', SEL_FIRST,SEL_LAST)
text.edit_modified(True)
def main():
init()
mainloop()
if __name__ == '__main__':
main()
What it should do is simply to produce a text window where you write into.
Selecting some text and pressing Ctrl+B the program should remove any preexisting tag, then assign to it the 'b' tag that sets the text to bold.
What instead happens is an exception at the first tag_remove, telling me that text doesn't contain any characters tagged with "sel".
The suggestion to use a return 'break' is of no use, since the selection disappears before setbold() has any chance to act...
Set your binding on the text widget, not on the root. (Whole toplevel bindings are processed after widget class bindings – where the standard <Control-Key-b> binding is – and those are processed after the widget instance bindings, which is what you want to use here.) And you need to do that 'break'; it inhibits the subsequent bindings. (If you're having any problems after that, it's probably that the focus is wrong by default, but that's easy to fix.)
The only other alternative is to reconfigure the bindtags so that class bindings are processed after toplevel bindings, but the consequences of doing that are very subtle and far-reaching; you should use the simpler approach from my first paragraph instead as that's the normal way of handling these things.
Bindings are handled in a specific order, defined by the bindtags of that widget. By default this order is:
The specific widget
The widget class
The toplevel window
The special class "all"
If there are conflicting bindings -- for example, a control-b binding on both the widget and class -- they both will fire (in the described order) unless you break the chain by returning "break".
In the case of the code you posted, however, you are binding to the toplevel window (ie: the root window), and the conflicting binding is on the class. Therefore, the binding will fire for the class before it is processed by the toplevel, so even if your binding returned "break" it wouldn't matter since the class binding happens first.
The most straight-forward solution is to move your binding to the actual widget and return "break". That will guarantee your binding fires first, and the return "break" guarantees that the class binding does not fire.
If you really want your binding on the root window, you can remove the binding for the class using the bind_class method with the value of "Text" for the class.
You might find the Events and Bindings page on effbot.org to be useful.

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)

Categories

Resources