tkinker unbind doesn't seem to unbind - python

I have this code:
def on_click(event=None):
c.unbind('<Button-1>')
c.config(background="red")
print ("You clicked the square")
time.sleep(delay)
c.config(background="green")
c.bind('<Button-1>', on_click)
root.update()
root = tk.Tk()
c = tk.Canvas(root, width=200, height=200, background="green")
c.pack()
c.bind('<Button-1>', on_click)
root.mainloop()
And when I click the canvas while it is red (unbound) it prints "You clicked the square" when the sleep is done.
I already tried the approach here: Deleting and changing a tkinter event binding
but got no results because I'm still able to click the square and obtain a print from it when it's red

You're calling unbind, then freezing the app. While it is frozen, events continue to get added to the queue without being processed. Immediately after the sleep is finished you re-establish the binding before the queue has a chance to process the events. By the time the events are handled, the binding will have already been re-established.
As a general rule of thumb you should never call sleep in a GUI program, and this is one good illustration why.
If you want to cancel the binding for a short period of time and then reset it, cancel the binding and then use after to reset it after the given time period.
def on_click(event=None):
c.unbind('<Button-1>')
c.config(background="red")
c.after(delay, enable_binding)
def enable_binding():
c.config(background="green")
c.bind('<Button-1>', on_click)
When you click, your function is called and you change the color and unbind the event. Then, the event loop has a chance to process the color change and process additional events. Once the time has elapsed, your function will be called and the event will be re-bound.

Related

Is tkwait wait_variable/wait_window/wait_visibility broken?

I recently started to use tkwait casually and noticed that some functionality only works under special conditions. For example:
import tkinter as tk
def w(seconds):
dummy = tk.Toplevel(root)
dummy.title(seconds)
dummy.after(seconds*1000, lambda x=dummy: x.destroy())
dummy.wait_window(dummy)
print(seconds)
root = tk.Tk()
for i in [5,2,10]:
w(i)
root.mainloop()
The code above works just fine and as expected:
The for loop calls the function
The function runs and blocks the code for x seconds
The window gets destroyed and the for loop continues
But in a more event driven environment these tkwait calls gets tricky. The documentation states quote:
If an event handler invokes tkwait again, the nested call to tkwait
must complete before the outer call can complete.
Instead of an output of >>5 >>2 >>10 you will get >>10 >>2 >>5 because the nested call blocks the inner and the outer will block the inner. I suspect a nested event loop or an equivalent of the mainloop processes events in the normal fashion while waiting.
Am I doing something wrong by using this feature? Because if you think about it, nearly all tkinter dialog windows are using this feature and I've never read about this behavior before.
An event driven example might be:
import tkinter as tk
def w(seconds):
dummy = tk.Toplevel(root)
dummy.title(seconds)
dummy.after(seconds*1000, lambda x=dummy: x.destroy())
dummy.wait_window(dummy)
print(seconds)
root = tk.Tk()
btn1 = tk.Button(
root, command=lambda : w(5), text = '5 seconds')
btn2 = tk.Button(
root, command=lambda : w(2), text = '2 seconds')
btn3 = tk.Button(
root, command=lambda : w(10), text = '10 seconds')
btn1.pack()
btn2.pack()
btn3.pack()
root.mainloop()
As an additional problem that raises with wait_something is that it will prevent your process to finish if the wait_something never was released.
Basically, you need great care if you're using an inner event loop because:
Conditions that would terminate the outer event loop aren't checked for until the inner event loop(s) are finished.
It's really quite easy to end up recursively entering an inner event loop by accident.
The recursive entry problem is usually most easily handled by disabling the path that enters the event loop while the inner event loop runs. There's often an obvious way to do this, such as disabling the button that you'd click.
The condition handling is rather more difficult. In Tcl, you'd handle it by restructuring things slightly using a coroutine so that the thing that looks like an inner event loop isn't, but rather is just parking things until the condition is satisfied. That option is... rather more difficult to do in Python as the language implementation isn't fully non-recursive (and I'm not sure that Tkinter is set up to handle the mess of async function coloring). Fortunately, provided you're careful, it's not too difficult.
It helps if you know that wait_window is waiting for a <Destroy> event where the target window is the toplevel (and not one of the inner components) and that destroying the main window will trigger it as all the other windows are also destroyed when you do that. In short, as long as you avoid reentrancy you'll be fine with it. You just need to arrange for the button that was clicked to be disabled while the wait is ongoing; that's good from a UX perspective too (the user can't do it, so don't provide the visual hint that they can).
def w(seconds, button):
dummy = tk.Toplevel(root)
dummy.title(seconds)
dummy.after(seconds*1000, lambda x=dummy: x.destroy())
button["state"] = "disabled" # <<< This, before the wait
dummy.wait_window(dummy)
button["state"] = "normal" # <<< This, after the wait
print(seconds)
btn1 = tk.Button(root, text = '5 seconds')
# Have to set the command after creation to bind the button handle to the callback
btn1["command"] = (lambda : w(5, btn1))
This all omits little things like error handling.

Tkinter gets slow when resizing the window. Can I have the resize command activate when the user settles instead of activating it continuously? [duplicate]

I am trying to write a function to dynamically resize an image displayed in a tkinter window.
Therefore I bound this function to the Configure event:
connroot.bind( "<Configure>", connresiz)
My problems are:
That the connresiz() function gets called 3 times (why 3?) at program start, and
More troublesome, that dynamically resizing the window calls the function continuously as I drag the mouse! How can avoid this?
I thought about checking at the simultaneous presence of a <Configure> and <ButtonRelease-1> events, but I don't know how to code it.
1) We don't know that, since we can't see your code...
2) Short answer is: you can't, because that's exactly what <Configure> event does! Long answer, you can, with a little trick/hack. Since anytime the window is changing, it will call all the binded functions to <Configure>, and the same happens anytime as the mouse button released (right after the last <Configure> call) we can create a flag/switch which will tell us, if the window was "configured" then we can check that switch anytime the mouse button is released, and switch it back to the default value after we ran some actions.
So if you want the image to resized only, when the mouse was released and the window was changed this is the code you need:
from tkinter import *
class Run:
def __init__(self):
self.root = Tk()
self.clicked = False
self.root.bind('<ButtonRelease-1>', self.image_resize)
self.root.bind('<Configure>', lambda e: self.click(True))
def image_resize(self, event):
if self.clicked:
print("I'm printed after <Configure>.") # the action goes here!
self.click(False)
def click(self, value):
self.clicked = value
app = Run()
app.root.mainloop()
According to the official tk documentation, <Configure> events fire "whenever its size, position, or border width changes, and sometimes when it has changed position in the stacking order." This can happen several times during startup.
It is called continuously while you resize the window because the size of the widget is changing. That's what it's defined to do. You can't prevent it from being called, though you can certainly modify what you do in the callback. For example, you could delay resizing the image until you've not received another <Configure> event for a second or two -- which likely means the user has stopped interactive resizing.

Tkinter .after runs forever and .mainloop never runs

I'm creating a python program on my RPi3 that changes the frequency of a GPIO pin depending on a Tkinter scale.
Here is my code:
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
from Tkinter import *
import time
freq = 1.0
master = Tk()
def update():
period = 1.0/float(freq)
GPIO.output(8, GPIO.HIGH)
time.sleep(period/2.0)
GPIO.output(8, GPIO.LOW)
time.sleep(period/2.0)
master.after(0, update)
scale = Scale(master, from_=1, to=20000, orient=HORIZONTAL, variable=freq)
scale.pack()
GPIO.setup(8, GPIO.OUT)
master.after(0, update)
master.mainloop()
GPIO.cleanup()
For some reason, master.after(0, update) runs forever and master.mainloop() never runs. I can tell because the scale never shows up and pin 8 turns on for half a second, then turns off for half a second, and the cycle repeats. If I press Ctrl+C then master.after(0, update) stops running and master.mainloop() starts running, the scale appears, but nothing happens when I drag the slider left and right.
I ran the program by typing sudo python tone.py in the terminal then pressing enter.
Fix/Alternative?
Processing events
You are making two somewhat common mistakes: you shouldn't do after(0, ...), and you shouldn't call sleep.
after(0, ...) means that every time you process the event, you immediately add another event. The event loop never has a chance to process other events in the queue, including events to handle the slider, screen updates, etc.
When you call sleep, the GUI does exactly that: it sleeps. While it is sleeping it can't process any events.
The solution is to use only after with a reasonable time span, and not call sleep at all.
For example:
def update():
...
# set the pin high
GPIO.output(8, GPIO.HIGH)
# set the pin low in half the period
master.after(period/2, GPIO.output, 8, GPIO.LOW)
# do this again based on the period
master.after(period, update)
Another way, if you continually want to toggle the pin every half second, would be this:
def update(value=GPIO.HIGH):
GPIO.output(8, value)
next_value = GPIO.LOW if value == GPIO.HIGH else GPIO.HIGH
master.after(500, update, next_value)
Using the slider
When you use the variable attribute of the slider, the variable must be an instance of one of the special tkinter variables, such as IntVar. You will then need to call get or set to get or set the value.
For example:
freq = IntVar()
freq.set(1)
def update(value=GPIO.HIGH):
period = 1.0/float(freq.get())
...
mainloop() is running, but Tkinter won't update views unless it is idle. KeyboardInterrupt will tell it to cleanup, at which point it finishes its event queue (which includes updating interface) and exits.
Solution: Give mainloop time to be idle- you really just need to change your after(0, update) to have some milliseconds inside OR tell master to .update_idletasks() to update the GUI.
A slightly better solution would be to make your high/low parts into their own functions which call each after the needed delay- sleeping in the mainloop gui is a bad idea, as your GUI cannot update whatsoever if the program is sleeping. (nor can it take input et al.) You would have millisecond frames to change input before it updated again, while using two functions that call each other after the chosen milliseconds would let you adjust the timings et al while it's waiting to flip to the other on/off.

label.configure works sometimes why?

Part of my code is as follows:
def get_songs():
label6.configure(text='Wait')
os.system('/home/norman/my-startups/grabsongs')
label6.configure(text='Done')
The label is not updated at the first .configure() but is at the second one.
Except if I cause a deliberate error immediately after the first one at which point it is updated and then the program terminates.
The system call takes about 2 minutes to complete so it isn't as if there isn't time to display the first one.
I am using Python 2.7.6
Does anyone know why please?
I'm going to guess you're using Tkinter. If so, as #albert just suggested, you'll want to call label.update_idletasks() or label.update() to tell Tkinter to refresh the display.
As a very crude example to reproduce your problem, let's make a program that will:
Wait 1 second
Do something (sleep for 2 seconds) and update the text to "wait"
Display "done" afterwards
For example:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
Notice that "Wait" will never be displayed.
To fix that, let's call update_idletasks() after initially setting the text:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
label.update_idletasks()
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
As far as why this happens, it actually is because Tkinter doesn't have time to update the label.
Calling configure doesn't automatically force a refresh of the display, it just queues one the next time things are idle. Because you immediately call something that will halt execution of the mainloop (calling an executable and forcing python to halt until it finishes), Tkinter never gets a chance to process the changes to the label.
Notice that while the gui displays "Wait" (while your process/sleep is running) it won't respond to resizing, etc. Python has halted execution until the other process finishes running.
To get around this, consider using subprocess.Popen (or something similar) instead of os.system. You'll then need to perodically poll the returned pipe to see if the subprocess has finished.
As an example (I'm also moving this into a class to keep the scoping from getting excessively confusing):
import Tkinter as tk
import subprocess
class Application(object):
def __init__(self, parent):
self.parent = parent
self.label = tk.Label(parent, text='Not waiting yet')
self.label.pack()
self.parent.after(1000, self.do_stuff)
def do_stuff(self):
self.label.configure(text='Wait')
self._pipe = subprocess.Popen(['/bin/sleep', '2'])
self.poll()
def poll(self):
if self._pipe.poll() is None:
self.label.after(100, self.poll)
else:
self.label.configure(text='Done')
root = tk.Tk()
app = Application(root)
tk.mainloop()
The key difference here is that we can resize/move/interact with the window while we're waiting for the external process to finish. Also note that we never needed to call update_idletasks/update, as Tkinter now does have idle time to update the display.

Tkinter: one or more mainloops?

I have an already large Tkinter program, so that I have an init file, where the root = Tk() window is defined (containing basically a Text widget and a few other things), some more code, and last the call to mainloop() function.
Everything works, until I needed to call a procedure before the mainloop, and I wanted to raise a wait window at the begin, to be destroyed at procedure's end.
I wrote something like:
msg = Message(root, text='wait a few seconds...')
msg.pack()
But it doesn't and cannot work, since mainloop() has not been called yet!
If I instead do:
msg = Message(root, text='wait a few seconds...')
msg.pack()
mainloop()
The program stops at this first mainloop, doesn't finish the procedure call.
mainloop() should be used as your last program line, after which the Tkinter program works by a logic driven by user clicks and interactions, etc.
Here, I need a sequence of raise window > do stuff > destroy window > mainloop
You are correct that mainloop needs to be called once, after your program has initialized. This is necessary to start the event loop, which is necessary for windows to draw themselves, respond to events, and so on.
What you can do is break your initialization into two parts. The first -- creating the wait window -- happens prior to starting the event loop. The second -- doing the rest of the initialization -- happens once the event loop has started. You can do this by scheduling the second phase via the after method.
Here's a simple example:
import Tkinter as tk
import time
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
# initialize Tkinter
tk.Tk.__init__(self, *args, **kwargs)
# hide main window
self.wm_withdraw()
# show "please wait..." window
self.wait = tk.Toplevel(self)
label = tk.Label(self.wait, text="Please wait...")
label.pack()
# schedule the rest of the initialization to happen
# after the event loop has started
self.after(100, self.init_phase_2)
def init_phase_2(self):
# simulate doing something...
time.sleep(10)
# we're done. Close the wait window, show the main window
self.wait.destroy()
self.wm_deiconify()
app = SampleApp()
app.mainloop()
You should use Tkinter's method to run asyncore's loop function, but you should use asyncore.poll(0) instead of asyncore.loop(). If you call function asyncore.poll(0) every x ms, it has no longer an effect on Tkinter's main loop.

Categories

Resources