How can I stop raising event in Tkinter? - python

I have some code like this
from Tkinter import *
master = Tk()
def oval_mouse_click(event):
print "in oval"
def canvas_mouse_click(event):
print "in canvas"
w = Canvas(master, width = 800, height = 600)
uid = w.create_oval(390, 290, 410, 310, fill='blue')
w.tag_bind(uid, "<Button-1>", lambda x: oval_mouse_click(x))
w.bind("<Button-1>" , canvas_mouse_click)
w.pack()
mainloop()
When I click on Canvas I have "in canvas" message in console.
When I click] on Oval I have two messages "in oval" and "in canvas", but I want to have only first message. Is there any way to stop event raising?
I can do this task with some global flag but I think there should be more natural way for Tkl.

I have just posted an improved solution on the similar problem there Python tkinter: stopping event propagation in text widgets tags.
The core idea is the same as presented in the previous solutions: hijack the Canvas widget by binding it with the same event sequence as tag_bind. The improved solution I came up with enables now to simulate the expected return "break" behaviour of Tk's other bind+callback pairs. In short:
create a wrapper around the wished callback, i.e. a callable class instance
when the class instance is called, run callback and check its result.
if the result is "break", temporarily hijack the event propagation: bind the Canvas widget to the same event bound to tag_bind, with an empty callback. Then, after an idle time, unbind.
if the result is not "break": do nothing, the event will propagate to Canvas automatically
The link above lists a full working example with the Text widget, but it is immediately transferable to the Canvas widget.

Here is the simplest example to handle your issue:
import Tkinter
def oval_mouse_click(event):
print "in oval"
event.widget.tag_click = True
def canvas_mouse_click(event):
if event.widget.tag_click:
event.widget.tag_click = False
return
print "in canvas"
root = Tkinter.Tk()
canvas = Tkinter.Canvas(width=400, height=300)
oid = canvas.create_oval(400/2-10, 300/2-10, 400/2+10, 300/2+10, fill='blue')
canvas.tag_click = False
canvas.tag_bind(oid, "<Button-1>", oval_mouse_click)
canvas.bind("<Button-1>" , canvas_mouse_click)
canvas.pack()
root.mainloop()
There is no other easier way to handle this under Canvas.

Related

Is there a way to reopen a window after closing it using destroy() in tkinter?

window6.after(1,lambda:window6.destroy())
is what I've been using to close my windows, is there any way to get them back after doing this?
basically, is there something that is the opposite of this?
ps. these are the libraries that I've imported, if it helps in any way
import tkinter as tk
from tkinter import *
import time
from tkinter import ttk
Is there a way to reopen a window after closing it using destroy() in tkinter?
The short answer is "no". Once it has been destroyed, it is impossible to get back. You should either create the window via a frame or class so that it's easy to recreate, or hide the window by calling .withdraw() rather than .destroy().
If you put the window code into a class or a function then after destroying it you can create a new instance of it by
1: creating a new instance of the class with the window code in the init function
2: call the function the has the code for the window
By doing this you are essentially creating a new instance of the program, but without initiating the script.
from tkinter impot *
from tkinter import ttk
#creating window function, not class
def main_window():
#window code here
root = Tk()
Label(root, text = "Hello World").pack()
#destroying main window
root.destroy()
root.mainloop()
main_window()
Of course, there are a few hurdles such as the window shutting down as soon as it opens, but this is to show that you can create a new instance of a window from your program.
You can wait for user input to see whether or not the window will open or close.
If you took an OOP approach, you can pass a reference to the Parent Widget as argument to the New_Window and store it in a class attribute.
You´ll have a two way reference: Parent knows child and child knows parent.
Then you can set the Parent Reference to the New_Window to None, from within the child Widget self.parent.new_window = None in a close_me() method right after you call self.destroy() on the New_Window:
1st Bonus: this code prevents the opening of more than 1 instance of a Window at a time. You won´t get more than 1 New_Window on the screen. I don´t think having two loggin windows opened or two equal options window makes sense.
2nd Bonus: It is possible to close the window from other parts of the code, as in a MVC patter, the Controller can close the window after doing some processing.
Here´s a working example:
import tkinter as tk
class Toolbar(tk.Frame):
'''Toolbar '''
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
# to store the New Window reference
self.new_window = None
self.button_new_window = tk.Button(self, text = 'New Window', command = lambda : self.get_window(self))
self.configure_grid()
def configure_grid(self):
'''Configures the Grid layout'''
self.grid(row=1, column=0, columnspan=3, sticky=(tk.N,tk.S,tk.E,tk.W))
self.button_new_window.grid(row = 2, column = 2, padx=5, pady=5)
def get_window(self, parent):
''' If window exists, return it, else, create it'''
self.new_window = self.new_window if self.new_window else Window(parent)
return self.new_window
class Window(tk.Toplevel):
'''Opens a new Window.
#param parent -- tk.Widget that opens/reference this window
'''
def __init__ (self, parent : tk.Widget):
# Stores reference to the Parent Widget, so you can set parent.new_window = None
self.parent = parent
super().__init__(master = parent.master)
self.title('New Window')
self.button_dummy = tk.Button(self, text = 'Do the thing', width = 25, command = lambda : print("Button pressed on window!"))
self.button_close = tk.Button(self, text = 'Close', width = 25, command = self.close_me)
self.configure_grid()
def configure_grid(self):
'''Grid'''
self.button_dummy.grid(row = 1, column = 0)
self.button_close.grid(row = 2, column = 0)
def close_me(self):
'''Tkinter widgets are made of two parts. 1. The python Object and 2. The GUI Widget.
The destroy() method gets rid of the widget part, but leaves the object in memory.
To also destroy the object, you need to set all of its references count to ZERO on
the Parent Widget that created the new Window, so the Garbage Collector can collect it.
'''
# Destroys the Widget
self.destroy()
# Decreasses the reference count on the Parent Widget so the Garbage Collector can destroy the python object
self.parent.new_window = None
if __name__ == '__main__':
root = tk.Tk()
toolbar = Toolbar(root)
root.mainloop()
I don´t know if this: .destroy() and re-instantiate approach is more efficient than the .withdraw() and .deiconify(). Maybe if you have a program that runs for long periods of time and opens a lot of windows it can be handy to avoid stackoverflow or heapoverflow.
It sure frees up the object reference from memory, but it has the additional cost of the re-instantiation, and that is processing time.
But as David J. Malan would say on CS50, “There´s always a tradeoff”.

Prevent multiple toplevels from opening?

I have a object oriented tkinter program set up.
I have initialized a variable to store Toplevel() in as
self.toplevel = None
Then when I create the actual Toplevel window I simply assign it to the variable:
self.toplevel = Toplevel()
The thing is...when the Toplevel() window is closed, the value still remains in the variable self.toplevel. How can I reset the variable back to None after closing the window so that I can perform a check:
if (self.toplevel == None):
self.toplevel = Toplevel()
Or are there any other methods to prevent multiple Toplevel Windows from opening?
Check this How do I handle the window close event in Tkinter?
Assign the value None to self.toplevel after the Toplevelcloses usign a callback function TopCloses. For this, write a method within the GUI class to access the toplevel attribute and set it's value to None inside the callback function.
In your main program,
def TopCloses():
top.destroy()
#Call the setTopLevel method and assign the attribute toplevel value None
guiObject.setTopLevel(None)
top.protocol("WM_DELETE_WINDOW", TopCloses)
root.mainloop()
Here is my solution:
#somewhere in __init__ make
self.window = None
#I took this piece of code from my bigger app and I have a function
#self.makevariables(), which is called in init, which contains the line above.
def instructions(self):
if self.window == None: #here I check whether it exists, if not make it, else give focus to ok button which can close it
self.window = Toplevel(takefocus = True)
#some optional options lol
self.window.geometry("200x200")
self.window.resizable(0, 0)
#widgets in the toplevel
Label(self.window, text = "NOPE").pack()
self.window.protocol("WM_DELETE_WINDOW", self.windowclosed) #this overrides the default behavior when you press the X in windows and calls a function
self.okbutton = Button(self.window, text = "Ok", command = self.windowclosed, padx = 25, pady = 5)
self.okbutton.pack()
self.okbutton.focus()
self.okbutton.bind("<Return>", lambda event = None:self.windowclosed())
else:
self.okbutton.focus() #You dont need to give focus to a widget in the TopLevel, you can give the focus to the TopLevel, depending how you want it
#self.window.focus() works too
def windowclosed(self): #function to call when TopLevel is removed
self.window.destroy()
self.window = None
These are all overly complicated solutions imo.
I just use win32gui as such:
toplevel_hwid = win32gui.FindWindow(None, '<Top Level Window Title>')
if toplevel_hwid:
print(f'Top level window already open with HWID: {toplevel_hwid}')
win32gui.SetForegroundWindow(toplevel_hwid)
return
else:
<create new top level>
Simple, easy, and gives you the flexibility to close, move, focus, etc.

Python forget and destroy frame not working

I have a program where I check if the user has an active network connection or not. If not, the program displays a frame that says to turn on the internet connection. The the program checks to see if the state has changed, if so, the login screen is shown. But I can't get rid of the noNetworkConnectionScreen.
The loadFrame is the frame that shows the 'splashscreen'.
class AppUI(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.parent.title("Redux")
self.loadFrame = Frame(self)
self.initLogin()
def initLogin(self):
internet_is_on = False
#check 3 times to be sure
for i in range(3):
#check the internet connection by pinging to google (returns True or False)
if db.internetOn():
internet_is_on = True
if internet_is_on:
self.initLoginScreen()
else:
self.initLoadScreen()
thread1 = threading.Thread(target=self.checkNetwork)
thread1.start()
I also tried grid_forget, which crashes:
def checkNetwork(self):
internet_is_off = True
while internet_is_off:
if db.internetOn():
internet_is_off = False
self.loadFrame.pack_forget()
self.loadFrame.destroy()
self.initLoginScreen()
else:
time.sleep(2)
And here I initialize the loadscreen:
def initLoadScreen(self):
self.loadFrame.grid(row=0, column=0, pady=(150,0))
self.lblUser = Label(self.loadFrame, text="HI", font=('Arial', 60), foreground="#666666")
self.lblUser.grid(row=0, column=0)
self.canvas = Canvas(self.loadFrame, width = 121, height = 81)
self.canvas.grid(row=1, column=0)
self.loader = PhotoImage(file = 'loader.gif')
self.canvas.create_image(121, 81, image = self.loader, anchor = NW)
self.lblNetwork = Label(self.loadFrame, text="please make sure you have an active network connection", font=('Arial', 16), foreground="#666666")
self.lblNetwork.grid(row=2, column=0)
I can see that the letters from lblNetwork are cut off at the sides, but the frame doesn't disappear. Any ideas?
Destroying a widget and/or "forgetting" it are the right ways to remove a widget from the screen. Almost certainly, the problem has to do with your use of threading. Tkinter isn't thread safe, and should only be used in one thread. If you create widgets in one thread, you shouldn't try to use them from any other thread. In this case you're trying to destroy a frame from a thread other than where it was created.
What you'll need to do is set up some sort of communication between the threads -- a queue or a shared (non-Tkinter) variable. Since you are simply checking a boolean flag, a simple shared variable will work. In your main program you can check for this variable using a simple after-based loop in your main thread:
def check_network(self):
if the_network_is_down:
<display a message>
else:
<remove the message>
# check once a second
self.after(1000, self.check_network)
Also, if you call destroy on a widget, there's no need to call grid_forget or pack_forget -- once the widget is destroyed there's nothing else you need to do.
Could it be that you have to force the screen refresh ?
pyGTK sometimes needs this (but are you using pyGTK ?) :
while gtk.events_pending(): # this forces the refresh of the screen
gtk.main_iteration()

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

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

tkinter: stopping event propagation in text widgets tags

I'm currently writing a color scheme editor. For the preview of the scheme, I use a text widget, where I insert text with the corresponding color tags (which I generate programmatically).
What I want is the following behaviour:
click anywhere on the text widget where no text is: change background color
click on text inserted with a tag: change tags corresponding foreground color
Now here's my problem:
When I click on a tagged text, the callback of the tag is called. So far so good. But then, the callback of the text widget is called as well, although I return "break" in the tags callback method (which should stop further event handling). How can I stop this?
To illustrate this specific problem, I wrote this working example (for Python 2 & 3):
#!/usr/bin/env python
try:
from Tkinter import *
from tkMessageBox import showinfo
except ImportError:
from tkinter import *
from tkinter.messagebox import showinfo
def on_click(event, widget_origin='?'):
showinfo('Click', '"{}"" clicked'.format(widget_origin))
return 'break'
root = Tk()
text = Text(root)
text.pack()
text.insert(CURRENT, 'Some untagged text...\n')
text.bind('<Button-1>', lambda e, w='textwidget': on_click(e, w))
for i in range(5):
tag_name = 'tag_{}'.format(i)
text.tag_config(tag_name)
text.tag_bind(tag_name, '<Button-1>',
lambda e, w=tag_name: on_click(e, w))
text.insert(CURRENT, tag_name + ' ', tag_name)
root.mainloop()
Any help is appreciated!
Edit: Tried Python 2 as well.
Thanks for posting this question and for providing a solution. I can't count how many hours I lost trying to fix up the symptoms created by this behaviour. Weird Tk design decision that tag_bind is insenstive to return "break".
Following your idea to hijack the Text widget by binding it with the same event sequence as tag_bind, I have improved the solution, which enables now to simulate the expected return "break" behaviour of Tk's other bind+callback pairs. The idea is the following (full source below):
create a wrapper around the wished callback, i.e. a callable class instance
when the class instance is called, run callback and check its result.
if the result is "break", temporarily hijack the event propagation: bind the Text widget to the same event bound to tag_bind, with an empty callback. Then, after an idle time, unbind.
if the result is not "break": do nothing, the event will propagate to Text automatically
Here is a full working example. My specific problem was to get some sort of hyper text behaviour: ctrl-clicking on a hyper-text should not move the insertion point to the click's location. The example below shows that within the same callback wrapped in tag_bind, we can propagate or not the event to the Text widget, simply by returning "break" or another value.
try:
# Python2
import Tkinter as tk
except ImportError:
# Python3
import tkinter as tk
class TagBindWrapper:
def __init__(self, sequence, callback):
self.callback=callback
self.sequence=sequence
def __call__(self, event):
if "break" == self.callback(event):
global text
self.bind_id=text.bind(self.sequence, self.break_tag_bind)
return "break"
else:
return
def break_tag_bind(self, event):
global text
# text.after(100, text.unbind(self.sequence, self.bind_id))
text.after_idle(text.unbind, self.sequence, self.bind_id)
return "break"
def callback_normal(event):
print "normal text clicked"
return "break"
def callback_hyper(event):
print "hyper text clicked"
if event.state & 0x004: # ctrl modifier
return "break" # will not be passed on to text widget
else:
return # will be passed on to text widget
# setup Text widget
root=tk.Tk()
text = tk.Text(root)
text.pack()
text.tag_config("normal", foreground="black")
text.tag_config("hyper", foreground="blue")
text.tag_bind("hyper", "<Button-1>", TagBindWrapper("<Button-1>", callback_hyper))
text.tag_bind("normal", "<Button-1>", callback_normal)
# write some normal text and some hyper text
text.insert(tk.END, "normal text, ", "normal")
text.insert(tk.END, "hyper text (try normal-click and ctrl-click).", "hyper")
root.mainloop()
There is one simplification I couldn't find how to do: replace the wrapper call TagBindWrapper("<Button-1>", callback_hyper) by TagBindWrapper(callback_hyper), i.e. get the information of the event 'sequence' string ("<Button-1>") simply from the event object passed to __call__. Is it possible?
Okay, after tkint tinkering around a bit, I was able to figure out a working solution. I think the problem is that tags are probably not subclassed from BaseWidget.
My workaround:
Make a seperate callback for the tags; set a variable there which keeps track of which tag was clicked
Let the event handler of the text widget decide what to do depending on the content of this variable
The workaround in code (sorry for using global here, but I just modified my questions simple example...):
#!/usr/bin/env python
try:
from Tkinter import *
from tkMessageBox import showinfo
except ImportError:
from tkinter import *
from tkinter.messagebox import showinfo
tag_to_handle = ''
def on_click(event, widget_origin='?'):
global tag_to_handle
if tag_to_handle:
showinfo('Click', '"{}" clicked'.format(tag_to_handle))
tag_to_handle = ''
else:
showinfo('Click', '"{} " clicked'.format(widget_origin))
def on_tag_click(event, tag):
global tag_to_handle
tag_to_handle = tag
root = Tk()
text = Text(root)
text.pack()
text.insert(CURRENT, 'Some untagged text...\n')
text.bind('<Button-1>', lambda e, w='textwidget': on_click(e, w))
for i in range(5):
tag_name = 'tag_{}'.format(i)
text.tag_config(tag_name)
text.tag_bind(tag_name, '<Button-1>',
lambda e, w=tag_name: on_tag_click(e, w))
text.insert(CURRENT, tag_name + ' ', tag_name)
root.mainloop()
I hope this is helpful for people having the same problem.
I'm still open to nicer solutions of course!

Categories