GUI wait until BooleanVar() changes - python

how can I make my Tkinter GUI wait for a change of a BooleanVar()? The BooleanVar is controlled by a distance sensor. The GUI should wait until the variable changes to False and the move on.
I tried to use a while True - loop, but as expected it disturbed the mainloop and the programm crashed.
I've also considered to use one if the methods to wait for user-input, but I can't figure out how.
Is there any way to solve this?
Thanks!

I don't understand what you mean by "wait" here, because a GUI is always in a constant state of "wait". It waits for events, and then it acts on events.
If you have a BooleanVar that is set somehow, you can set a trace on that variable. When the value changes, the trace will call a callback of your choice. In that callback your code can do whatever you want.
self.sensor = tk.BooleanVar()
self.sensor.trace("w", self.on_sensor_change)
...
def on_sensor_change(self, *args):
print "the sensor changed:", self.sensor.get()

Related

A function is running despite me changing a variable that should stop it

I was hoping someone could help me with this issue. I'm hoping it's fairly simple to fix, but I have been trying for a while to figure this out. I have trimmed my larger code to this, as I believe the issue here is the crux of the problem.
I have a raspberry pi and an external button. This is on python3 on Linux. I am using GPIOZero for the button. The code below I believe is simple to understand, but basically, I want a function to loop at all times. When I press a button I want another function to run, but only if a variable is a certain number. I describe what I ultimately want to happen in a comment below, but my code is unfinished and simplified just for this problem.
I only want button.when_pressed to work when timer = 0. The problem is, once the code naturally gets into the button.when_pressed function, it never "lets go" of the function again. When I successfully redefine the variable to timer = 1, it still prints button is pressed when I press the button. I don't know why. To me, it seems like it should only work once timer = 0.
Any suggestions? I think I have a misunderstanding of global variables I will plan on researching. I'm not sure if that's the issue. I have also tried using break and continue to try to get it "back on its loop" but that hasn't worked either. Also I want to use button.when_pressed instead of btn.is_pressed, because I want the program to only do something when I'm holding the button down once, and not loop when I'm holding it down. In this case, I want button is pressed to print a single time. If I did btn.is_pressed it would print button is pressed every two seconds, which I dont want.
Thanks for any help. I'm happy to learn.
#!/usr/bin/python3
from gpiozero import Button
from time import sleep
import time
button = Button(4)
timer = 0
def press():
print("Button is pressed")
global timer
timer = 1
def loop():
global timer
timer = 1
while True:
if timer == 0:
button.when_pressed = press
else:
loop()
sleep(2)
If you want to disable the callback you had set to button.when_pressed, you need to do another assignment with button.when_pressed = None. This is listed in the documentation:
when_released:
[...] Set this property to None (the default) to disable the event.
It's not exactly clear what behavior you want from your current code. If you want the button to be active for 2 seconds, then be deactivated indefinitely, you can use:
button.when_pressed = press
sleep(2)
button.when_pressed = None
There's no need for a loop, since you don't want to repeat anything.
If you only want the button to be active for a single button press, that needs to happen within 2 seconds, you could instead call button.wait_for_press(2). I hesitate to write a full block of code for that though, as the docs don't specify how a timeout is signaled (it might be by return value, or via an exception). I don't have a Raspberry Pi so I can't test myself, but you could try it out and see what happens.
Treat your whole piece of code as one "black box", ask yourself, what is the input/output? button press or timer mode? (because I don't quite understand what does timer variable mean in your code)
Your code implies timer mode is the top level input to control the flow,
while True:
if timer == 0:
button.when_pressed = press
else:
loop()
sleep(2)
Is it expected?
If you allow user to press the button at any time, suggest you make button press to be your top level input, change the logic to keep when_pressed callback always on, set flag once triggered, and then check if the button has been pressed and still is_pressed in your while loop.
pressed = False
def play_video_1():
pass
def play_video_2():
pass
def press():
print("Button is pressed")
global pressed
pressed = True
button.when_pressed = press
while True:
if pressed and not_playing_video2:
if is_pressed:
play_video_1()
else:
pressed = False
play_video_2()
else:
play_video_2()

How to queue a task AFTER all widget resizing operations?

I want to query a widget's size after changing its contents. Here's a demonstration:
import tkinter as tk
win = tk.Tk()
label = tk.Label(win)
label.pack()
def callback1():
label['text'] = 'hello world'
win.after_idle(callback2)
def callback2():
print('label width:', label.winfo_width())
win.after(0, callback1)
win.mainloop()
According to the documentation, callbacks queued with after_idle should only be executed when there's nothing else to do:
Registers a callback that is called when the system is idle. The callback will be called there are no more events to process in the mainloop.
And yet, callback2 is clearly executed before the label is resized, because the output of the program is this:
label width: 1
Even adding a call to update_idletasks() doesn't change this output. Only if win.update_idletasks() is called in both callback1 and callback2, the correct size is printed. I really don't understand why it's necessary to call it twice.
Question
Why is callback2 being executed before the label is resized? How can I ensure that label.winfo_width() returns the correct size?
Limitations
The main goal of this question is to understand how/when tkinter executes (idle) tasks. I want to find out how to correctly queue tasks so that they're executed only after the GUI has updated itself. I'm not really interested in workarounds such as these:
I'd prefer to avoid using update() because I don't understand how it causes race conditions or when it's safe to use. (In my real code, all of this would be executed inside an event handler, which the documentation explicitly states should be avoided.)
I also want to avoid using the <Configure> event. This is because there might be an arbitrary number of widgets changing size, and I cannot reasonably be expected to bind event handlers to all of their <Configure> events. I really just need a way to execute a callback function after all the resizing has taken place.

Trying to create a mouse recorder, but it keeps looping endlessly?

I'm trying my hand at Pynput, and I'm starting off with creating a simple program to record the movements of a mouse, and then replay those movements once a button is clicked.
However, every time I click the mouse, it just starts to freak out and endlessly loop. I think it's going through the movements at a super high speed, but I eventually have to Alt-F4 the shell to stop it.
Any help would be appreciated.
import pynput
arr = []
from pynput import mouse
mou = pynput.mouse.Controller()
def on_move(x,y):
Pos = mou.position
arr.append(Pos)
def on_click(x, y, button, pressed):
listener.stop()
for i in arr:
mou.position = i
print("Done")
listener = mouse.Listener(on_move = on_move, on_click=on_click)
listener.start()
You have to be careful when using multiple threads (which is the case here, since mouse.Listener runs in its own thread). Apparently, as long as you are in the callback function, all events are still processed, even after you have called listener.stop(). So when replaying, for each mouse position you set, the on_move callback function is called, so that mouse position is added to your list again, which causes the endless loop.
In general, it's bad practice to implement too much functionality (in this case the "replaying") in a callback function. A better solution would be to use an event to signal another thread that the mouse button has been clicked. See the following example code. A few remarks:
I've added a few print statements to see what's happening.
I've added a small delay between the mouse positions to really see the playback. (NB: This also might make breaking out of the application a bit easier in case it hangs!)
I've changed a few variable names to make more sense. Calling an array "arr" is not a good idea. Try to use names that really describe the variable. In this case it is a list of positions, so I choose to call it positions.
I'm using return False to stop the mouse controller. The documentation states "Call pynput.mouse.Listener.stop from anywhere, raise StopException or return False from a callback to stop the listener.", but personally, I think returning False is the cleanest and safest solution.
import threading
import time
import pynput
positions = []
clicked = threading.Event()
controller = pynput.mouse.Controller()
def on_move(x, y):
print(f'on_move({x}, {y})')
positions.append((x, y))
def on_click(x, y, button, pressed):
print(f'on_move({x}, {y}, {button}, {pressed})')
# Tell the main thread that the mouse is clicked
clicked.set()
return False
listener = pynput.mouse.Listener(on_move=on_move, on_click=on_click)
listener.start()
try:
listener.wait()
# Wait for the signal from the listener thread
clicked.wait()
finally:
listener.stop()
print('*REPLAYING*')
for position in positions:
controller.position = position
time.sleep(0.01)
Note that when you run this in a Windows command prompt, the application might hang because you have pressed the mouse button and are then starting to send mouse positions. This causes a "drag" movement, which pauses the terminal. If this happens, you can just press Escape and the program will continue to run.
You got yourself an infinite loop. I think the listener you referred to in the on_click method might be null or undefined. Also according to some documentation I found you need to return false for the on_click method to stop listening
This is what I was looking at:
https://pythonhosted.org/pynput/mouse.html

How is tkinter code executed?

I am writing a program using tkinter, but I do not understand how it works. Normally, code is executed top-down, but with tkinter it obviously does not.
For example, I have bound a function to the left mouse button, and this function is executed every time I click the button. But how is the other code around that treated? My problem is that I in the start of my program initialize a variable that is used as an argument in the bound function, and then it is changed in the function and returned. But every time the function is called, the variable seems to be reset to its initial value.
Does anyone know why this is?
I have it written like this:
var = "black"
var = c.bind("<Button-1>", lambda event: func(event, arg=var))
The function "func" changes var and returns it, but the next time I press the button the variable is always "black".
Thanks in advance!
Tkinter does indeed run top down. What makes tkinter different is what happens when it gets to the bottom.
Typically, the last executable statement in a tkinter program is a call to the mainloop method of the root window. Roughtly speaking, tkinter programs look like this:
# top of the program logic
root = tkinter.Tk()
...
def some_function(): ...
...
some_widget.bind("<1>", some_function)
...
# bottom of the program logic
root.mainloop()
mainloop is just a relatively simple infinite loop. You can think of it as having the following structure:
while the_window_has_not_been_destroyed():
event = wait_for_next_event()
process_event(event)
The program is in a constant state of waiting. It waits for an event such as a button click or key click, and then processes that event. Conceptually, it processes the event by scanning a table to find if that event has been associated with the widget that caught the event. If it finds a match, it runs the command that is bound to that widget+event combination.
When you set up a binding or associate a command with a button, you are adding something to that table. You are telling tkinter "if event X happens on widget Y, run function Z".
You can't use a return result because it's not your code that is calling this function. The code that calls the function is mainloop, and it doesn't care what the function returns. Anything that gets returned is simply ignored.

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.

Categories