Running a loop alongside a Tkinter application - python

I am a high school programming student and I have a small question. I have been tasked with writing a simple game in Tkinter where an icicle falls from the ceiling and you have to avoid it with your mouse. Simple enough. However, I have hit an issue. Whenever I run a loop in a Tkinter application, it won't open. I've tried with a for loop that pauses every .5 seconds using time.sleep() and the window opens as soon as the loop finishes. Is there some special thing I need to do to make loops work in Tkinter?
from Tkinter import *
import time
import random
class App:
def __init__(self, parent):
self.frame = Frame(root, bg= '#1987DF', width=800, height=800)
self.frame.bind("<Motion>", self.motionevent)
self.frame.pack()
#self.run()
def randhex(self):
b = "#"
for i in range(1, 7):
a = random.randint(0, 15)
if a == 10:
a = "A"
elif a == 11:
a = "B"
elif a == 12:
a = "C"
elif a == 13:
a = "D"
elif a == 14:
a = "E"
elif a == 15:
a = "F"
b = b+str(a)
return b
def motionevent(self, event):
xpos, ypos, bg = event.x, event.y, self.randhex()
str1 = "X : %d Y : %d BG : %s" % (xpos, ypos, bg)
root.title(str1)
x,y, delta = 100, 100, 10
self.frame.config(bg=bg)
def run(self):
for i in range(0, 10):
time.sleep(.5)
print 'i'
self.frame.config(bg=self.randhex())
root = Tk()
app = App(root)
root.mainloop()
Currently all it is supposed to do is change the background when the mouse moves. When the line in init that says self.run() is uncommented it will print 'i' 10 times then the window will open. Help?

Writing an event based program is not the same as writing traditional programs. There is already an infinite loop running, and like any infinite loop, if you place another loop inside, the outer loop can't continue until the inner loop finishes. Since the outer loop is what causes the screen to refresh and events to be processed, inner loops effectively freeze your app until they are done.
Since there is already a loop running you don't need to create another loop. All you need to do is add little jobs to the event queue one at a time. Each job is, in effect, one iteration of your inner loop.
For example, if you wanted to write an inner loop like this:
for i in range(10):
print "i:", i
... you would instead add an event to the event queue and each time the event loop iterates (or more precisely, each time it finishes processing any other events) it will do one iteration of your loop. You do it like this:
def do_one_iteration(i):
print "i:", i
if i < 9:
root.after_idle(do_one_iteration, i+1)
Then, after the first time you call do_one_iteration, it will place the next iteration of itself on the event queue, and continue to do so until it decides it is done.
Typically you would call do_one_iteration when the user presses a button (eg: the "start" button). Call it once, then it does one bit of work (ie: moving the icicle down a couple of pixels) and then reschedules itself.
In game development you might have a function called update_display which is in charge of redrawing everything. It could, for example, subtract 1 from the Y coordinate of each icicle. You can add bindings for the left and right arrows to move the player (by incrementing or decrementing the X coordinate), and your update function would use these new coordinates to redraw the player.
By the way, you can slow down your program by using after instead of after_idle to call the function after a slight delay. This delay can be used to control the frame rate. For example, assuming the update is nearly instantaneous (and it probably will be in your case), calling after with an argument of 41 (milliseconds) yields a framerate of approximately 24 fps (24 frames times 41 milliseconds equals 984 milliseconds, or roughly 24 frames per second)
It may sound complicated but it's really pretty easy in practice once you do it once or twice.

Related

Python tkinter buttons are not working during a -while loop- condition

i am doing some measurement with raspberry pi. I made a GUI with python tkinter. measurement is done every seconds. when the measured value is higher than a particular value, i want to set some pin high for 30 seconds (counter). but measurement should continue. and during this time, if there is another high value, again reset the counter to 30 seconds. So i used a while loop. I am displaying measured values in GUI. I am calling the main function (readSensor) every seconds using .after method. During while loop condition, GUI is frozen and value is not updated on screen and buttons are not working. Shall i remove while loop and use .after? but how to use the counter there?
status = False
def start():
global status
status = True
def stop():
global status
status = False
def readSensor():
# there is a start and stop button in GUI
if status:
measuredValues = []
pi.write(23,0) # pin 23 is low now
# code to do measurement is here
#it works fine
#result is a list with name measuredValues
#then checking the conditions below
if((measuredValues[0] > Limit_1) or (measuredValues[1] > Limit_2) or (measuredValues[2] > Limit_3) or (measuredValues[3] > Limit_4)):
counter = 0
print(measuredValues)
while (counter <=30):
# this while is the problematic part. if condition is true, i will set the pin 23 high.
# i would like to set the pin high for 30 seconds
# no any buttons seems working during this time. why?
# stop button is also not working.
pi.write(23,1)
time.sleep(1.0) #shall i avoid this sleep?
print(counter)
measuredValues = []
#measurement will continue here
#same code to do measurement as before
#output is the same list with name measuredValues
print(measuredValues)
# if any of the new measuring values is/are higher than limit, want to reset the counter to 30
# shall I use .after method here? and avoid while loop?
# how to use it with a condition like maiximum 30s
if ((measuredValues[0] > Limit_1) or (measuredValues[1] > Limit_2) or (measuredValues[2] > Limit_3) or (measuredValues[3] > Limit_4) ):
counter = 0
else:
counter +=1
else:
pi.write(23,0)
print(measuredValues)
win.after(1000, readSensor) # new measurement every second
win = Tk()
# code for GUI here
win.after(1000, readSensor)
win.mainloop()
You should consider placing your function in a thread. So your start button in your GUI should call the function using:
import threading
start_button = Button(YourFrame, command = lambda:threading.Thread(target = readSensor).start()) #YourFrame is the Frame widget where you placed the start button
That should make the UI responsive while doing the calculations inside your funtion.

How can i make this code do what it's supposed to do?

So, what I am trying to do is when you open the window, it starts a process in which every 0.2 seconds it changes the first and 3rd value of the color (in which it converts the elements of the range into a hex value and then a string) to go from rgb( 86, 32, 86) to rgb(126, 32, 126). Although I thought this might just work, it doesn't. I only get a background of the first color and that's all.
from tkinter import *
import time
root = Tk()
for i in range(86,126):
h = hex(i)
h = str(h)
h = h[2] + h[3]
root.configure(background=("#" + h + "32" + h ))
time.sleep(0.2)
root.mainloop()
You must use the after function to give the window system time to process updates. Calling window update functions in a loop like that on the main thread will lock up the window until the loop terminates.
Try moving the code in the for loop into a new function, say updateBackground, and making it call itself recursively using after:
def updateBackground(i):
# ...
if i < 126:
root.after(200, lambda: updateBackground(i + 1))
Note that I used a lambda in order to increment i.
Credit: https://stackoverflow.com/a/36670519/1757964
Your main issue with this code is the use of sleep(). Because Tkinter is a single thread application and is event driven what ends up happening when you use sleep() the entire Tkinter instance freezes.
To work around this Tkinter provides a method called After() that is designed to schedule an event to happen after a time. So to get the same affect you are trying to get with sleep we can instead create a function that can call itself after 0.2 sec and provide it with the starting number and ending number.
from tkinter import *
root = Tk()
def do_something(start_number, end_number):
if start_number <= end_number:
h = str(hex(start_number))[2:] # combined all your work on h to one line
root.configure(background=("#{}32{}".format(h, h)))
start_number += 1
root.after(200, do_something, start_number, end_number)
do_something(86, 126)
root.mainloop()
Note that the color change is mild and hard to see. If you increase the `end_number thought it will become more obvious.

python appjar function doesn't thread

having issues trying to get threading working in python using the awesome Appjar package.
The following program needs to count through a list, and update a progress bar simultaneously. I've followed the appjar documentation for threading, but it's returning NameError: name 'percent_complete' is not defined in the app.thread (line 35), in which you're meant to insert function params - my code is below:
from appJar import gui
import time
# define method the counts through a list of numbers, and updates the progress meter
def press(btn):
objects = [1,3,6]
total = len(objects)
current_object = 0
for i in objects:
print(i)
current_object += 1
current_percent_complete = (current_object / total) * 100
updateMeter(current_percent_complete)
time.sleep(1)
def updateMeter(percent_complete):
app.queueFunction(app.setMeter, "progress", percent_complete)
# create a GUI variable called app
app = gui("Login Window")
app.setBg("orange")
app.setFont(18)
# add GUI elements : a label, a meter, & a button
app.addLabel("title", "COUNTER")
app.setLabelBg("title", "blue")
app.setLabelFg("title", "orange")
app.addMeter("progress")
app.setMeterFill("progress", "green")
app.addButton("START COUNTING", press)
# put the updateMeter function in its own thread
app.thread(updateMeter, percent_complete)
# start the GUI
app.go()
I can get rid of the error by defining percent_complete like so:
from appJar import gui
import time
# define method the counts through a list of numbers, and updates the progress meter
percent_complete = 0
def press(btn):
...
However, when GUI loads and button is pressed it doesn't thread. Instead it iterates through the list, then updates the progress bar afterwards.
Has anyone come across the same issue? any insight would be awesomely appreciated!
Thanks!
There are a couple of issues here:
First, I'm not sure your maths result in good percentages to update the meter with, so you might not see much change - should you be using i?
Second, the GUI won't be updated until the loop (and the sleeps inside it) all complete. Instead, you should try counting how many items to process, and iterating through them with an after() function, see here: http://appjar.info/pythonLoopsAndSleeps/#conditional-loops
Third, the call to app.thread() at the end doesn't achieve much - it calls the update_meter() function with a parameter that doesn't exist, it can be removed.
Fourth, the actual update_meter() function isn't necessary, as you're not really using a thread - that can be removed as well...
Give this a try, once you've had a look at the maths:
current_object = 0
def press(btn):
global current_object
current_object = 0
processList()
def processList():
global current_object
objects = [1,3,6]
total = len(objects)
if current_object < total:
i = objects[current_object]
print(i)
current_object += 1
current_percent_complete = (current_object / total) * 100
app.setMeter("progress", current_percent_complete)
app.after(1000, processList)
UPDATE: just to clarify on the maths issue, you're dividing one integer by another: 0/3, 1/3, 2/3, 3/3 and so on. In python2 this will result in 0, in python3 you'll get fractions.

Python: Keyboard input without repeat delay

For those who don't know, repeat delay is the slight pause between, when holding down a key, the letter first appearing and it repeating. This might be a useful feature when typing, however when you start to write games, it becomes very annoying. An example is when you need to move a character; it will move a tiny bit, pause, and then start to move again. Tkinter code:
ball = canvas.create_rectangle(50, 50, 100, 100)
def move():
canvas.move(ball, 0, 3)
canvas.bind_all("<space>", move)
If space is pressed, the ball will move down 3 pixels, pause, and then start moving normally. I was wondering if there is any way to avoid that pause, for example a module that reads directly from the keyboard, and not the windows-processed keyboard. I know that it is possible to "cheat" by, for example, automatically running the function when you expect the delay to occur; sadly that is inaccurate and can result in choppy movement. Thanks in advance
Make a recursion loop for while it is pressed. Or that's how I do it, at least.
moving = False
def move():
global moving
moving = True
def stop_moving():
global moving
moving = False
def myloop():
global moving
if moving == True:
canvas.move(ball, 0, 3)
root.after(1, myloop)
root.bind('<space>', lambda e: move())
root.bind('<KeyRelease-space>', lambda e: stop_moving())
just make sure you call your loop once before root.mainloop()
#like this
root.after(1, myloop)
root.mainloop()

pygame.time.set_timer confusion?

So, I have a problem, I don't fully understand the event that is needed to be given to a timer command anyway, it doesn't say anywhere online, to where I searched for hours. So I just used what most people seemed to be using 'USEREVENT + 1'. I'm not sure if it is correct, but my timer is not working. Am I using it correctly? Here is my code:
nyansecond=462346
nyanint=0
spin=0
aftin=452345
def nyanmusic(nyansecond,nyanint,spin):
if nyanint == 0:
nyansound.play()
nyanint= 1
elif nyanint == 1:
nyansecond = pygame.time.set_timer(USEREVENT+1,7000)
if nyansecond < 200 and spin == 1:
spin = 0
nyansecond = pygame.time.set_timer(USEREVENT+1,7000)
elif nyansecond > 6500 and nyansecond < 100000 and spin == 0:
spin = 1
nyansoundm.play()
return nyansecond,nyanint,spin
I then def it into my code on the second page I implemented (which works fine). It runs the nyansound, but doesn't run nyansoundm after 6.5 seconds (6500 milliseconds). I'm making this program to help me learn the basics of python and pygame, before moving on to more complex stuff. I can also use it when I want to listen to nyan cat or other looped songs without having to go on youtube and waste precious bandwidth. Don't worry about that, though.
Oh, and here is the code I have put into my loop, although I do not think this matters too much:
#music
nyansecond,nyanint,spin = nyanmusic(nyansecond,nyanint,spin)
Let's recap what pygame.time.set_timer does:
pygame.time.set_timer(eventid, milliseconds): return None
Set an event type to appear on the event queue every given number of milliseconds. The first event will not appear until the amount of time has passed.
Every event type can have a separate timer attached to it. It is best to use the value between pygame.USEREVENT and pygame.NUMEVENTS.
pygame.USEREVENT and pygame.NUMEVENTS are constants (24 and 32), so the argument eventid you pass to pygame.time.set_timer should be any integer between 24 and 32.
pygame.USEREVENT+1 is 25, so it's fine to use.
When you call pygame.time.set_timer(USEREVENT+1,7000), the event with eventid 25 will appear in the event queue every 7000ms. You didn't show your event handling code, but I guess you do not check for this event, which you should do.
As you can see, pygame.time.set_timer returns None, so your line
nyansecond = pygame.time.set_timer(USEREVENT+1,7000)
doesn't make sense since nyansecond will always be None, and hence comparing it against an integer
if nyansecond < 200 ...
is pointless.
If you want to play a sound every 6.5 seconds using the event queue, simpy call pygame.time.set_timer once(!):
PLAYSOUNDEVENT = USEREVENT + 1
...
pygame.time.set_timer(PLAYSOUNDEVENT, 6500)
and check the event queue for this event in your main loop:
while whatever: # main loop
...
# event handling
if pygame.event.get(PLAYSOUNDEVENT): # check event queue contains PLAYSOUNDEVENT
nyansoundm.play() # play the sound

Categories