tkinter: event binding remains after application terminates? - python

I'm running tkinter in Python 3.4. A button event seems to remain bound to a command even after the application terminates. Code snippet:
# application class
class DataSel:
def __init__(self,parent):
self.parent = parent
<...>
self.button_sel = tk.Button(self.parent,text='Select')
self.button_sel.grid(row=1,sticky='nesw')
self.button_sel.bind('<Button-1>',self.sel_click)
self.button_quit = tk.Button(self.parent,text='Quit')
self.button_quit.grid(row=2,sticky='nesw')
self.button_quit.bind('<Button-1>',self.quit_click)
def sel_click(self,event):
self.filename = askopenfilename(parent=self.parent)
<...>
def quit_click(self,event):
self.parent.destroy()
# main part of application
root = tk.Tk()
root.lift()
sel = DataSel(root)
root.lift()
root.mainloop()
When I restart the interpreter from scratch and run this application, there is no error message. However, the button_sel button remains pressed (in low relief) after the sel_click method is finished. Then, if I quit the application and rerun it, I get the following message in the shell:
invalid command name ".94227256"
while executing
"$w cget -state"
(procedure "tk::ButtonDown" line 12)
invoked from within
"tk::ButtonDown .94227256"
(command bound to event)
where the number .94227256 changes each time I rerun.
Apart from this message, and the fact that the button remains in low relief, all other functionality is OK. But it seems like the button event somehow stays bound to a stale command!

What is happening is that your binding happens before the button widget is able to process the same event. You are doing this during the processing of the events and you aren't telling Tkinter to stop processing the events further. Therefore, when Tkinter gets around to having the widget process the click event, the window no longer exists and tkinter throws an error.
The root of the problem is that you are putting bindings on a button. You shouldn't do that. If you want to call a function from a button you need to use the command attribute of the button.
If you really think you need to do this via a binding (rather than via the command attribute), you need your function to return '"break"` to tell tkinter to stop any further processing of the event.

Related

Python method strangely on pause until tkinter root is closed

I am creating a tkinter application using Python 3.4 that collects posts from an API, filter them and allow the user to review them and make a decision for each one (ignore, delete, share, etc.)
The user is expected to pick a date and some pages and then click on the 'Collect' button. The program then fetch the posts from the pages and stock them in 'wholeList'.
When the user clicks on the second button 'Review', the posts must be filtered and passed to the Reviewer.
My problem is that the Reviewer receives no posts at all, and neither does the Filterer. I have added some debugging print() statements at some places, notably to handlerCollect(), and the result baffled me, hence this post.
Instead of finishing the handlerCollect() callback method when I click on 'Collect', the program puts it on hold somewhere between "DEBUG->1" and "DEBUG->2". The main window does not freezes or anything, for I can click on 'Review' and have it print "DEBUG->4" and open up the Reviewer. When I close the main window, "DEBUG->0" "DEBUG->2" and "DEBUG->3" finaly print, along with the rest of the handlerCollect() method executing.
The same behavior happens with handlerChoosePage(), with "DEBUG->0" being delayed until the tkinter root (TK()) is destroyed. My knowledge of structural programming tells me it should be the very first one printed. Instead, it is the very last. My best conclusion is that I must not be ending my Toplevel mainloop()s correctly. I have to admit I have never encountered something like this before. I thought the proper way of ending mainloop()s on Toplevels was with destroy() and I am very confused as to why methods calling mainloop()s get put on hold until the Tk root is destroyed; not really practical.
from GUICollector import GUICollector as Collector
class Launcher(tk.Frame):
def __init__(self, *args, **kwargs):
...
self.allPagesCB = Checkbutton(self.dateFrame, text="Collect for all pages",
variable = self.allPagesVar, command=self.handlerChoosePage)
self.collectBtn = Button(self, text="Collect", command=self.handlerCollect)
self.reviewBtn = Button(self, text="Review", command=self.handlerReview)
def handlerChoosePage(self):
if self.allPagesVar.get() == 0:
child = tk.Toplevel(self)
selector = PageSelector(self.toCollect, child)
selector.pack(side="top", fill="both", expand=True)
selector.mainloop()
print("DEBUG->0")
def handlerCollect(self):
print("DEBUG->1")
self.collect()
print("DEBUG->4")
for post in self.collector.getPosts():
if post not in self.wholeList:
print("...")
self.wholeList.append(post.copy())
self.collector = None
print(len(self.wholeList), "posts in wholeList")
def collect(self):
window = tk.Toplevel(self)
self.collector = Collector(self.toCollect, self.sinceEpoch, window)
self.collector.grid(row=0,column=0)
self.collector.after(500, self.collector.start)
print("DEBUG->2")
self.collector.mainloop() # This is what seems to hang indefinetly
print("DEBUG->3")
def handlerReview(self):
print("DEBUG->5")
print(len(self.wholeList), "posts in wholeList")
filterer = Filterer(self.wholeList)
self.wholeList = filterer.done[:]
window = tk.Toplevel()
reviewer = Reviewer(self.wholeList[:], window)
reviewer.grid(row=0,column=0)
reviewer.mainloop()
The GUICollector module requires no interaction from the user at all.
This module seems to work perfectly: doing its job, displaying it is done and then closing after the specified delay.
Since the GuiCollector mainloop() seems to be the culprit of the hanging, here is how I end it:
class GUICollector(tk.Frame):
def __init__(self, pagesList, since, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
def start(self, event=None):
if some_logic:
self.after(250,self.start)
else:
self.done() # Does get called.
def done(self):
# Some StringVar update to display we are done on screen
self.after(1250, self.validate)
def validate(self):
self.master.destroy()
The PageSelector module is destroyed with the same call on the press of a button: self.master.destroy()
Here is the revelant output of the program:
DEBUG->1
DEBUG->2
=> collected data of page [PageName]
=> Found 3 posts in page
DEBUG->5
0 posts in wholeList
[The main window (Launcher) is manually closed at this point]
DEBUG->3
DEBUG->4
...
...
...
3 posts in wholeList
DEBUG->0
The concept of mainloop assumes that you first create and initialize objects (well, at least these that are required at application start, i.e. not used dynamically), set event handlers (implement interface logic) and then go into infinite event handling (what User Interface essentially is), i.e. main loop. So, that is why you see it as it "hangs". This is called event-driven programming
And the important thing is that this event handling is done in one single place, like that:
class GUIApp(tk.Tk):
...
app = GUIApp()
app.mainloop()
So, the mainloop returns when the window dies.
Until I have some time to refactor my code, I solved the problem by adding the following line to my destroy() calls:
self.quit() # Ends mainloop
self.master.destroy() # Destroys master (window)
I understand this doesn't not solve the bad structure of my code, but it answers my specific question. destroy() doesn't end the mainloop of TopLevels, but quit() does. Adding this line makes my code execute in a predictable way.
I will be accepting #pmod 's answer as soon as I have refactored my code and verified his claim that the Tk() mainloop will cover all child TopLevels.

Tkinter's overrideredirect prevents certain events in Mac and Linux

I am writing a program in Python with a Tkinter UI. I want to have a small window with no title bar. This window must receive keyboard input. I am not picky whether this is in the form of an Entry widget or just binding to KeyPress. overrideredirect(True) is typically how the title bar is disabled. Unfortunately, (except in Windows), this seems to prevent many events from being received. I wrote this code to illustrate the problem:
#!/usr/bin/env python
from __future__ import print_function
import Tkinter
class AppWindow(Tkinter.Tk):
def __init__(self, *args, **kwargs):
Tkinter.Tk.__init__(self, *args, **kwargs)
self.overrideredirect(True)
self.geometry("400x25+100+300")
titleBar = Tkinter.Frame(self)
titleBar.pack(expand = 1, fill = Tkinter.BOTH)
closeButton = Tkinter.Label(titleBar, text = "x")
closeButton.pack(side = Tkinter.RIGHT)
closeButton.bind("<Button-1>", lambda event: self.destroy())
self.bind("<KeyPress>", lambda event: print("<KeyPress %s>" % event.char))
self.bind("<Button-1>", lambda event: print("<Button-1>"))
self.bind("<Enter>", lambda event: print("<Enter>"))
self.bind("<Leave>", lambda event: print("<Leave>"))
self.bind("<FocusIn>", lambda event: print("<FocusIn>"))
self.bind("<FocusOut>", lambda event: print("<FocusOut>"))
if __name__ == "__main__":
app = AppWindow()
app.mainloop()
This creates a little window (with no title bar) that prints the name of common events when it receives them. I have run this script on Windows 7, Mac OSX (El Capitan), and Ubuntu 14.04.1. I ran only Ubuntu in a virtual machine (VMWare).
In Windows, this seems to work as expected. All events that my code tests for can be received.
In Ubuntu, the Tkinter window receives <Enter>, <Leave>, and <Button-1> events as expected, but <KeyPress>, <FocusIn>, and <FocusOut> are never received. In fact, even after the window has been clicked on, the last window with focus continues to receive the key presses.
In OSX, the Tkinter window receives <Button-1> events as expected, but <KeyPress>, <FocusIn>, and <FocusOut> are never received. The last window with focus does not continue to receive key presses like in Ubuntu. The <Enter> and <Leave> events behave a little oddly. The <Enter> event is not received until the window is clicked. Then, once the <Leave> event occurs, the window needs to be clicked again to receive another <Enter> event.
I have also tried self.focus_force() just before the end of the __init__ function. This causes the window to receive a <FocusIn> event when the program starts, but no further <KeyPress>, <FocusIn>, or <FocusOut> events are never received.
Ultimately, my question is this: is there any way to hide the title bar but continue to receive keyboard input in OSX and Linux?
I am aware of some other questions dealing with this same problem. In these three questions:
python tkinter overrideredirect; cannot receive keystrokes (Linux)
root.overrideredirect and <Any-KeyPress> binding
How to bind Tkinter destroy() to a key in Debian?
The accepted answer is to use self.attributes('-fullscreen', True), which will not work for me as I want a tiny little window, not a fullscreen application.
There is one other question: Tkinter overrideredirect no longer receiving event bindings. This seems very close to my question, but provided less detail and has no answer.
Update: I have been attempting to investigate the underlying mechanism of my problem. I know that Tkinter is a wrapper around Tcl/Tk, so I thought I would try rewriting my code in Tcl. I don't really know Tcl, but I think I managed to (more or less) translate my Python:
#!/usr/bin/env wish
wm overrideredirect . True
wm geometry . "400x25+100+300"
bind . <KeyPress> {puts "<KeyPress %K>"}
bind . <Button-1> {puts "<Button-1>"}
bind . <Enter> {puts "<Enter>"}
bind . <Leave> {puts "<Leave>"}
bind . <FocusIn> {puts "<FocusIn>"}
bind . <FocusOut> {puts "<FocusOut>"}
I tried the resulting program in Windows and Mac OSX. In Windows I received <KeyPress> events, but in OSX I did not. Without the wm overrideredirect . True line, OSX does receive the <KeyPress> events. Therefore it looks like this problem is not with Python, but with Tcl/Tk.
I have submitted a bug report to Tk for this situation.
You can use the devilspie program to remove the decorations from your window. Use the wm title . myname command to give your window a specific name and use that name in the devilspie configuration fragment below. Remove the overrideredirect command from your program.
I have tested this (as a Tk program), and the undecorated window will still receive the keypress &etc. bindings.
Note that devilspie is written as a daemon process and stays active. The daemon can be killed after it is started and the window changes it made will still be in effect. Or it can be left running, and any time your window is activated, the devilspie configuration will be applied.
(if (is (application_name) "t.tcl")
(begin (undecorate)))

What does the"wait_window" method do?

It seems that the object that calls this method waits for the window passed as parameter to be destroyed before continue with its own loop...
From the doc strings of the Misc class, we can observe:
def wait_window(self, window=None):
"""Wait until a WIDGET is destroyed.
If no parameter is given self is used."""
At first glance, it seems like this method can make a Toplevel modal, but this is not true. To make a Toplevel modal, we have to use the grab_set() method.
I have see around other explanations:
wait_window seems to not return until the given widget passed as parameter
is not destroyed.
From another place:
wait_window(widget) - Creates a local event that waits for the given
widget to be destroyed. This loop doesn't affect the application's
mainloop.
From the effbot documentation, we have:
The wait_window enters a local event loop, and doesn’t return until the given window is destroyed (either via the destroy method, or
explicitly via the window manager):
widget.wait_window(window)
What exactly means for a window to wait for window (itself)?
It seems that the code that comes after the call to wait_window is not executed until the window passed to the same method is not destroyed.
In the following working example, we can see a proof on what just said:
from tkinter import *
def on_win_request(parent):
dialog = Toplevel()
parent.wait_window(dialog)
# executed only when "dialog" is destroyed
print("Mini-event loop finished!")
r = Tk()
b = Button(r, text='New Window', command=lambda: on_win_request(r))
b.pack()
b2 = Button(r, text='Hello!', command=lambda: print("hello"))
b2.pack()
r.mainloop()
"Mini-event loop finished!" will be printed only when the local Toplevel widget called dialog is destroyed.
So, in exactly what real circumstances should I use this method?
Like the documentation states, it waits until the given window is destroyed. It is mostly used for modal popups, though it doesn't itself make a window modal. The call to the function simply doesn't return until the target window is destroyed To make a modal window you have to do a grab as well.
The most common use is to create an instance of Toplevel, populate that window with widgets, then wait for the window to be dismissed before doing some other action. While it is waiting, tkinter is able to continue to process events as normal.
For instance, you can disable (or defer creation of) the main GUI, pop up a "terms of service" notice, and wait for the user to acknowledge the terms of service, copyright, license, etc. Once the window is destroyed you can then finish initialization, or enable some widgets, etc.
The standard file dialog is a perfect example: you pop up the dialog, then your code waits for the user to pick a file, then it uses the filename that was returned. Internally, the implementation of the dialog uses wait_window so that it doesn't return until the dialog is dismissed.

Python - Check for keypresses without using 100% cpu

I'm writing a GUI program in Python using Tkinter and I need a way to check if a keypress is happening without using all my cpu. Currently I'm using the threading module to start a thread that will check for the keypress without freezing the interface (Tkinter). I use win32api.GetKeyState() in a while loop inside my thread so that it constantly checks the status of the key because it needs to be able to tell if the key is being pressed even when the window doesnt have focus. The problem is the program uses 100% cpu the moment I start the thread. If I put a time.sleep() in the loop it cuts back the cpu usage dramatically BUT there is a delay between the actual keypress and the time that it knows that you are pressing a key.
Is there a way to capture a keypress the very moment it gets pressed even when the window is out of focus WITHOUT using so much cpu?
from Tkinter import *
import win32api
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
coords = StringVar()
Label(master=self, textvariable=coords).pack()
def GetCoords():
coords.set(str(win32api.GetCursorPos()))
root.bind_all("<Scroll_Lock>", self.GetCoords)
root = Tk()
app = Application(master=root)
#root.wm_iconbitmap(default='INSERT ICON HERE')
#root.wm_title("TITLE OF PROGRAM")
#app.master.maxsize(640, 480)
app.master.minsize(640, 480)
app.master.resizable(0, 0)
app.mainloop()
app.quit()
That script give me the following result:
AttributeError: Application instance has no attribute 'GetCoords'
You want to catch key events, instead of polling for the current keyboard state.
See Events and Bindings in the TkInter docs, which has a simple example that does exactly what you want (plus, it's cross-platform instead of Win32-only).
And this is generally an issue with all GUI programming (and network servers, for that matter), and the answer is always the same. You don't directly use non-blocking "check for current X status" calls usefully, with or without threads. In the main thread, you ask the event loop "call my function when X status changes", or you create a background thread and make a blocking "wait forever until X happens" call.
The Wikipedia page on Event loop actually has a pretty good description of this.
Looking at your edited version, you've got a completely new problem now:
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
coords = StringVar()
Label(master=self, textvariable=coords).pack()
def GetCoords():
coords.set(str(win32api.GetCursorPos()))
root.bind_all("<Scroll_Lock>", self.GetCoords)
GetCoords is a local function defined inside Application.__init__. But you're trying to use it as if it were a method of Application. You can't do self.GetCoords unless GetCoords is a method of self. That's exactly what the error message AttributeError: Application instance has no attribute 'GetCoords' means.
But you can just pass the local function GetCoords, just by taking out the self. prefix. I'm not sure this will do what you think (because I'm not sure whether you can close over a StringVar like that or not), but… try it and see.
Alternatively, you can make GetCoords a method just by moving it out of the def __init__ and giving it a self parameter. Then you can access self.GetCoords, and get a bound method, which you can pass exactly as you're trying to. However, in that case, it won't be able to access coords anymore, since that's a local variable inside __init__. To fix that, change that local variable into a member variable, by using self.coords everywhere in __init__ and GetCoords (and anywhere else) instead of coords.

tk destroy() and grid_forget() don't always work

i'm hoping anyone can help me out here. i'm having an issue with a tkinter gui i built. the issue only happens in windows. My GUI creates a results frame with some labels in it, when it's time to calculate something else, the user clicks on the "newPort" button and that button is supposed to remove the results frame and set to False some instance attributes internal to the calculation. The issue i'm having, which is apparent only in windows is that sometimes the results frame, and its descendant labels don't disappear every time. Sometimes they do, sometimes they don't. The instance variable is correctly set to False but the widgets are still visible on the main GUI. The GUI also contains a couple checkboxes and radiobuttons but they don't impact the creation of the results frame nor its expected destruction. I have not been able to pin point a pattern of actions the user takes before clicking on the newPort button which causes the frame and labels to not get destroyed. This happens when i freeze my app with py2exe, as well as running the app from the python interpreter within the eclipse IDE. I have not tried running the app from the python interpreter directly (i.e. without the IDE) and this problem does not happen on my Mac when i run the app using the eclipse python interpreter. Thanks very much all! My code looks like this:
import Tkinter as TK
class widget(object):
def __init__(self,parent=None):
self.parent = TK.Frame(parent)
self.parent.grid()
self.frame = TK.Frame(self.parent)
self.frame.grid()
newLedger = TK.Button(self.parent,command=self.newPort).grid()
self.calcButton = TK.Button(self.frame,command=self.showResults)
self.calcButton.grid()
self.calcVariable = True
def newPort(self):
self.calcVariable = False
try:
self.second.grid_forget()
self.first.grid_forget()
self.resultsFrame.grid_forget()
self.second.destroy()
self.first.destroy()
self.resultsFrame.destroy()
except:
raise
self.frame.update_idletasks()
def showResults(self):
self.resultsFrame = TK.Frame(self.frame)
self.resultsFrame.grid()
self.first = TK.Label(self.resultsFrame,text='first')
self.first.grid()
self.second = TK.Label(self.resultsFrame,text='second')
self.second.grid()
if __name__ == '__main__':
root = TK.Tk()
obj = widget(root)
root.mainloop()
You don't need to destroy or call grid_forget on the labels, and you don't need to call grid_forget on the resultsFrame; when you destroy the resultsFrame it will cause all off its children to be destroyed, and when these widgets are destroyed they will no longer be managed by grid.
The only way I can get widgets to not be destroyed is if I click on the "calc" button twice in a row without clicking on the "new" button in-between. I'm doing this by running your program from the command line.

Categories