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

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

Related

Customizing Button Commands in Python Tkinter

I've recently started learning how to use Tkinter. I'm working on the front-end for a small project to design an Enigma Machine, and a part of it involves creating a keyboard-like layout of buttons to simulate the plugboard. When clicking one button, I want it to go to a method I've created to handle all button presses and tell the method which letter the button corresponds to, and act accordingly. The main task right now is to make it so that pressing one button and then pressing another button after that draws a line between both buttons.
What I've added below is a scuffed version of the actual program to cut out repeated code like the button declarations.
from tkinter import *
from tkinter import ttk
from PIL import ImageTk, Image
class Steckerbrett:
root = Tk()
frm = ttk.Frame(root, padding=10)
frm.grid(row=0, column=0)
ttk.Label(frm, text="Enigma Machine").grid(column=0, row=0)
ttk.Label(frm, text="Steckerbrett").grid(column=0, row=1)
ttk.Label(frm, text="").grid(column=0, row=2)
# plugboard keys row 1
frm1 = ttk.Frame(root, padding=10)
frm1.grid(row=1, column=0)
ttk.Button(frm1, text="Q", command=connection).grid(column=0, row=0)
ttk.Button(frm1, text="W", command=connection).grid(column=1, row=0)
# i have declarations like this for all 26 letters
root.mainloop()
def connection(self, letter):
ttk.Label(self.frm, text=letter).grid(column=0, row=2)
sb = Steckerbrett()
The issue here is that since 'command' doesn't allow entering a function with arguments, I can't pass the letter to the function, and I can not make a custom method for every single button, because I would need to process first button presses and second button presses differently to make the connections. Is there a way to use a single function in the command but customize it to each button? Alternatively, if there isn't, are there any other solutions I can try? I am willing to try using other graphics packages as well.

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.

Reject events during function execution in TKinter

I have a TKinter application (Python) that reacts to button presses and keyboard input. However, it is possible that a function that is called after one of these events may take some time to execute. I would like for the GUI to reject all user input while the function is executing. [EDIT: Just want to clarify: all user input refers to any and all buttons/key bindings that exist in the app - I am not just trying to reject the event from a particular button.]
Here is a very simple example to show that TKinter currently seems to add events to a queue that gets executed after each event is handled. Each time the button is pressed, the text in the button has a zero appended to it. However, if the button is pressed when the text reads 00, the function takes a while to execute (I put in a self.after(3000)), and if the button is pressed while that function is executing, then each of those presses will register. So if I press the button 5 times in under the 3 seconds that I have it "stall", then each of those clicks registers, and I end up seeing 0000000 on the button.
import tkinter as tk
# To demonstrate that keystrokes/button clicks do register while a function is executing (and then subsequently fire)
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
# Build the app
self.text = tk.StringVar()
self.pack()
self.create_widgets()
def create_widgets(self):
self.btn = tk.Button(self, textvariable=self.text, command=self.DoSomething, padx=30, pady=30)
self.btn.grid(row=0, column=0)
self.text.set('0')
def DoSomething(self):
# self.RejectEventsWhileThisFuncExecutes()
old_msg = self.text.get()
if old_msg == '00':
self.after(3000)
self.text.set(old_msg + '0')
# self.BeginAcceptingEventsAgain()
root = tk.Tk()
app = Application(master=root)
app.mainloop()
Basically, what I'd like is maybe something I can call in DoSomething() at the beginning and end of the function, say self.RejectEventsWhileThisFuncExecutes() and self.BeginAcceptingEventsAgain(), that will make sure that no clicks are registered while the function is executing.
A common strategy is to create an invisible widget (eg: a 1x1 frame in the corner of the window), and do a grab (ie: call grab_set) on that widget. That causes all events to be funneled to that widget. As long as there are no bindings on that window, the net effect is that the events get ignored. You just need to flush the event queue (call the update method) before removing the grab.
You can remove the command from the button at the start of DoSomething:
self.btn.config(command=lambda: None)
And then reset it at the end. Just make sure to update to process all queued events before rebinding:
self.update()
self.btn.config(command=self.DoSomething)

Reliable button presses captured in polling program

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

Flashing Tkinter Labels

I'm a beginner programmer in python and have recently began using tkinter though I have come across a problem which I can't solve.
Basically I have two entry boxes.
Entry1 = message
Entry2 = no. of flashes
(This is just an example of what I need.)
All I need is a for loop for a label to pop up and flash entry1 as many times as entry2, yes I realize how to get the entry inputs but I have no idea how to get the label to continuously flash, I have tried pack_forget and .destroy methods for the label in a loop, but unfortunately it does not display as it almost instantly clears it from the screen again.
The basic idea is to create a function that does the flash (or half of a flash), and then use after to repeatedly call the function for as long as you want the flash to occur.
Here's an example that switches the background and foreground colors. It runs forever, simply because I wanted to keep the example short. You can easily add a counter, or a stop button, or anything else you want. The thing to take away from this is the concept of having a function that does one frame of an animation (in this case, switching colors), and then scheduling itself to run again after some amount of time.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.label = tk.Label(self, text="Hello, world",
background="black", foreground="white")
self.label.pack(side="top", fill="both", expand=True)
self.flash()
def flash(self):
bg = self.label.cget("background")
fg = self.label.cget("foreground")
self.label.configure(background=fg, foreground=bg)
self.after(250, self.flash)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()

Categories