Reliable button presses captured in polling program - python

I'm Newbie (sorry... but thanks in advance)
I've spent the weekend reading questions and looking at examples in so many places that I've lost my mind.
I'm building a polling program that reads I/O from various sources.
While I'm polling, I need the user to be able to click on part of the screen to cause an action or go to another GUI page for a while before returning here. If he leaves this page, I can suspend the polling required by this root page.
My issue is that I cannot get reliable mouse clicks or mouse position from the bind. I've depopulated most of the code to get to basics shown below. I'm totally missing how to accomplish this, which should be a typical use case?
from tkinter import *
## Set the key variables
myTanks = [20, 30, 50, 80]
activeTanks = len(myTanks)
## Functions Defined Below
def motion(event):
print("(%s %s)" % (event.x, event.y))
def click(event):
print("clicked")
def readTankLevels(activeTanks):
global myTanks
print (myTanks)
for i in range(0,activeTanks):
print("Reading Tank ", str(i))
## I inserted the following line to emulate the network activity and processing in the real program
root.after(500, print("Waiting "+str(i)))
def drawTank():
print("Drawing the Tanks in GUI")
def updateLabels():
print("Updating the tank labels")
t1Holder=Canvas(root, width=(100), height=50, bg="black", highlightbackground="black")
t1Holder.bind('<Button-1>',motion)
t1Holder.grid(row=0, column=0, padx=5, pady=0)
t2Holder=Canvas(root, width=(50), height=75, bg="blue", highlightbackground="black")
t2Holder.bind('<Motion>',motion)
t2Holder.grid(row=0, column=1, padx=5, pady=0)
## Open GUI
root = Tk()
root.wm_title("Tank Monitor")
root.config(width=300, height=200, padx=0, pady=0, background = "black")
## Body of program
while True:
## Continuous poll of the tanks being monitored
readTankLevels(activeTanks)
drawTank
updateLabels()
root.update()
print ("You should never get here!")
mainloop()

I ended up learning about how 'after' really works via experimentation and wanted to post the answer for others that must have similar issues.
The 'After' command places the function call on the queue of things to do, with a timestamp on when to do it.
While that interim period ensues, the interpreter is basically sitting at the mainloop() command (in my case i do this by design vs having other code running during this period).
In doing it this way, the interpreter can service all mouse movements and button presses very quickly.
I'm now able to have a truly effective GUI. I was not able to find this specific answer anywhere I looked, as most posts just say to use this instead of sleep under Tkinter, but don't provide the details of what it actually does.
Note that it is important to re-establish the 'after' command within the loop that is called to function. In my case I wanted to read sensors every 10 seconds, so the line before mainloop() is the after command, and in the def, i copy the same line of code to recall the 'after' every 10 seconds. (showing as 2 seconds below to make demo quicker). This should help anyone in the boat I was in...
from tkinter import *
root = Tk() #Makes the window
root.wm_title("Remote Sensor Dashboard")
## initialize variables
## def's piled in form many functions below
def readSensors(): ## Get sensor data from web connected devices
print("sensors have been read")
updateGUI()
root.after(2000, readSensors)
def updateGUI(): ## redraw each affected frame or canvase
print("GUI has been updated")
def updateMousePosition(event): ## Print the coordinates of the mouse (can also bind to clicks)
print("(%s %s)" % (event.x, event.y))
#Create window that has your mouse targets (use buttons, color wheels etc)
mouseHouse = Frame(root, width=200, height = 200, bg="black")
mouseHouse.bind('<Motion>',updateMousePosition)
mouseHouse.grid()
## Initial Read of Sensor Network
root.after(2000, readSensors)
## Peform Initial GUI draw with the Sensor Data
print ("GUI has been drawn")
mainloop()

Related

Tkinter window not displaying until after program is run

global window
window = Tk()
window.geometry('300x200')
window.minsize(300,200)
window.maxsize(300,200)
window.configure(bg='black')
window.title("testupdate")
global outputtext
outputtext = tk.StringVar()
outputtext.set("starting window...")
my_label = tk.Label(window, textvariable = outputtext, bg = 'black', fg = 'white', font = 'terminal')
my_label.pack()
class ChangeLabel():
def __init__(self, text):
outputtext = tk.StringVar()
outputtext.set(text)
my_label.config(textvariable = outputtext)
Here's the main code: https://replit.com/#YourPetFinch/textadventure#main.py
I've been trying to make a text adventure game with Tkinter so I can package it all nicely as an app. I created a function that I can call from another file to update a label as the game output so I can keep the GUI setup simple, but I've been having a problem where the window won't show up until the code has finished running. I'm new to tkinter (and honestly not very good at Python in general) so this is probably a stupid question.
That's not how global is used. The global statement is only used inside a function, to state that you're going to modify a global. You can't actually make a global that crosses files, but your from gui import * will handle that.
The issue here is understanding event-driven programming. When you create a window, nothing gets drawn. All that does is send a number of messages to signal the core. The messages will not get fetched and dispatched until it gets into the .mainloop(). The main loop processes all the messages and does the drawing, which might queue up more messages.
So, you cannot use time.sleep(2) like that. As you saw, that will interrupt the process and prevent any drawing from being done. Instead, you will have to use window.after to request a callback after some number of seconds. The .mainloop monitors the timers, and will give you a call when time runs out. You cannot use time.sleep inside an event handler either, for the same reason; your GUI will freeze until your event handler returns.

Tkinter scale gets stuck each time a function is called

I have a problem where the tkinter Scale widget seems to get stuck whenever I run a seemingly big function.
This is the code:
from tkinter import Tk, Button, Frame, Scale
root = Tk()
slider = Scale(root, orient='horizontal')
slider.pack()
frame = Frame(root)
frame.pack()
num = 0
def buttons():
for widget in frame.winfo_children():
widget.destroy()
for i in range(50):
Button(frame, text='Button' + str(i)).pack()
def basic():
global num
slider.set(num)
num += 1
print(num)
if num <= 100:
slider.after(100, basic)
if __name__ == '__main__':
buttons()
basic()
root.bind('<space>', lambda x: buttons())
root.mainloop()
What I want my program to do is update the slider normally even when I press 'Space' (meaning calling the buttons() function)
If you watch closely each time you press Space the slider will get stuck a little.
Since I'm using the slider for an Mp3 player in order to show time elapsed, this loss of time is extremely important for example for audio files of 10 or so seconds since the slider falls behind a lot making it seem as if it's working wrong \
I'd also like to point out that destroying the buttons and then repacking them is necessary for me.
I suspect that this happens because the program has to go over the buttons() function something that takes time since it's creating 50 buttons. Or am I mistaken?
Can I avoid that lag?
PS: As I mentioned in my comment:
I normally have a button that renames a (button) which is a song and in order for them to alphabetically ordered after renaming i need to recall the function that draws them. If I only configure tha name of the button (and not redraw them), it will stay in place and not move down or up depending on its name, while on the actual directory the order will change leading to inappropriate behavior such as playing the same song
Here are some images for better understanding:
Thanks in advance!
Look at this code:
import tkinter as tk
def config_buttons():
# Get the `text` of the first button
starting_value = int(buttons[0].cget("text")) + 1
# Iterate over all of the buttons
for i, button in enumerate(buttons, start=starting_value):
# Change the button's `text` and `command` atributes
button.config(text=i, command=lambda i=i:print("Clicked %i"%i))
root = tk.Tk()
buttons = []
add_button = tk.Button(root, text="+1 on all buttons", command=config_buttons)
add_button.pack()
for i in range(50):
button = tk.Button(root, text=i, command=lambda i=i:print("Clicked %i"%i))
button.pack()
buttons.append(button)
root.mainloop()
When the add_button buttons is pressed, I iterate over all of the buttons and change their text and command attributes. As I am not creating new buttons, the function runs very fast.
You can implement something similar in your code. Basically, avoid creating new buttons and just update the ones you already have on the screen.

Need help finding a solution to tkinter with gpio and after method

I'm working on a project in tkinter that is a questionnaire. If you answer correctly it takes you to a end page where it prompts you to press a button. Once You hit that button I need the GPIO pin to set to high and hold for a duration and then switch back to low. After that it then takes you back to the main page to start the questionnaire over again.
I started with the time.sleep function to hold the pin high which I've learned is not good to use for GUI's. It does work for me despite that but through testing it I found that while it's sleeping for the duration the button will still take button presses and seems to buffer them which seems to stack on after the first button press is made.
After doing some searches I found the after method and I've tried implementing that and it seems to do something very similar. I'd like to make the program as foolproof as possible so if someone is impatient and hits the button twice it won't extend the duration and make it lock up.
I've also looked in to trying to disable the button after a press but I can't seem to get it to work right.
Here is the window where It prompts you to press the button and then triggers the gpio to go high, wait for a duration then go low. It then switches it to the main page. I also have it moving the mouse so it doesn't hover over the button on the next page
class PleasePass(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Thank you \n Please press the button then proceed to tempature reading",
font=('Helvetica', 30))
label.grid(column=0, row=0, padx=110, pady=200)
button1 = tk.Button(self, text="Ready to Proceed", height=3, width=50, bg="lightgreen",
fg="black", font=('Helvetica', 20, "bold"),
command=lambda: [GPIO.output(26, GPIO.HIGH), self.after(2000),
GPIO.output(26, GPIO.LOW),
controller.show_frame(StartPage),
self.event_generate('<Motion>', warp=True, x=50, y=50)])
button1.grid(column=0, row=100)
I'd appreciate some help with this one. I'm just starting to learn how to use python and tkinter so my code is very copy paste and i'm sure very sloppy.
Don't put so much functionality into a lambda function directly. Write an additional method in your class:
def clicked(self):
self.button1.config(state="disabled") # disable button
GPIO.output(26, GPIO.HIGH)
self.after(2000, self.proceed) # call method proceed after 2 seconds
def proceed(self):
GPIO.output(26, GPIO.LOW)
# additional stuff
Now, in your __init__ method make button1 an instance variable with self.button1 = tk.Button(.... Finally, you can simply set the command parameter of your Button to your new method: command=self.clicked (without brackets).

Pygame, Tkinter or something else for TUI and responding to keyboard input?

I'm quite new to programming and trying to write a fairly simple program that records the times in between different keyboard button presses (a bit like multiple reaction time tests) and prints these times on screen in an array, then terminates and saves the array after a certain period of time is up.
I've already written most of the program in pygame after giving up on Tkinter because it seemed to be the best thing for responding to keyboard input in real time. However, now that I'm wanting the text to scroll automatically once the screen fills up, add more columns to the array, and export to Excel, I'm starting to wonder whether I'd be better off with a module more suited to text handling.
Can anyone advise me on whether I'm making a mistake attempting this in pygame and whether responding immediately to multiple keyboard inputs in Tkinter is possible? I can provide more detail if necessary.
Using Tkinter, you can bind to <Any-KeyPress>. The function that is called is passed an event object that has a timestamp. You can use that to compute the time between events.
Here's a quick example that shows how to display the time between keypresses. You can of course add your own logic to count and track and display however you want.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
self.last_event = None
tk.Frame.__init__(self, parent)
self.label = tk.Label(self, text="")
self.text = tk.Text(self, wrap="word")
self.label.pack(side="top", fill="x")
self.text.pack(fill="both", expand=True)
self.text.bind("<Any-KeyRelease>", self.on_key_release)
def on_key_release(self, event):
if self.last_event is not None:
delta = event.time - self.last_event.time
self.label.configure(text="time since last event: %s ms" % delta)
self.last_event = event
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()

python label not changing dynamically

I want to see continuously changing value of label in tkinter window. But I'm not seeing any of it unless I make a keyboard interrupt in MS-CMD while running which shows me the latest assigned value to label. Plz tell me ..What's going on & what's the correct code ??
import random
from Tkinter import *
def server() :
while True:
x= random.random()
print x
asensor.set(x)
app=Tk()
app.title("Server")
app.geometry('400x300+200+100')
b1=Button(app,text="Start Server",width=12,height=2,command=server)
b1.pack()
asensor=StringVar()
l=Label(app,textvariable=asensor,height=3)
l.pack()
app.mainloop()
The function server is called when you click the button, but that function contains an infinite loop. It just keep generating random numbers and sending these to asensor. You are probably not seeing any of it because the server function is run in the same thread as the GUI and it never gives the label a chance to update.
If you remove the while True bit from your code, a new number will be generate each time you click the button. Is that what you wanted to do?
Edit after comment by OP:
I see. In that case your code should be changed as follows:
import random
from Tkinter import Tk, Button, Label, StringVar
def server():
x = random.random()
print x
asensor.set(x)
def slowmotion():
server()
app.after(500, slowmotion)
app = Tk()
app.title("Server")
app.geometry('400x300+200+100')
b1 = Button(app, text="Start Server", width=12, height=2, command=slowmotion)
b1.pack()
asensor = StringVar()
asensor.set('initial value')
l = Label(app, textvariable=asensor, height=3)
l.pack()
app.mainloop()
I also introduced a new function, slowmotion, which does two things: 1) calls server, which updates displays the value, and 2) schedules itself to be executed again in 500ms. slowmotion is first ran when you first click the button.
The problem with your code was that it runs an infinite loop in the main GUI thread. This means once server is running, the GUI will not stop and will not get a chance to display the text you asked it to display.

Categories