Tkinter events outside of the mainloop? - python

The program I am writing has a tkinter window that is constantly being fed with data manually rather than being part of a mainloop. It also needs to track mouse location. I havn't found a workaround for tracking the mouse outside of mainloop yet, but if you have one please do tell.
from Tkinter import *
import random
import time
def getCoords(event):
xm, ym = event.x, event.y
str1 = "mouse at x=%d y=%d" % (xm, ym)
print str1
class iciclePhysics(object):
def __init__(self, fallrange, speed=5):
self.speed = speed
self.xpos = random.choice(range(0,fallrange))
self.ypos = 0
def draw(self,canvas):
try:
self.id = canvas.create_polygon(self.xpos-10, self.ypos, self.xpos+10, self.ypos, self.xpos, self.ypos+25, fill = 'lightblue')
except:
pass
def fall(self,canvas):
self.ypos+=self.speed
canvas.move(self.id, 0, self.ypos)
root = Tk()
mainFrame = Frame(root, bg= 'yellow', width=300, height=200)
mainFrame.pack()
mainCanvas = Canvas(mainFrame, bg = 'black', height = 500, width = 500, cursor = 'circle')
mainCanvas.bind("<Motion>", getCoords)
mainCanvas.pack()
root.resizable(0, 0)
difficulty = 1500
#root.mainloop()
currentIcicles = [iciclePhysics(difficulty)]
root.update()
currentIcicles[0].draw(mainCanvas)
root.update_idletasks()
time.sleep(0.1)
currentIcicles[0].fall(mainCanvas)
root.update_idletasks()
tracker = 0
sleeptime = 0.04
while True:
tracker+=1
time.sleep(sleeptime)
if tracker % 3 == 0 and difficulty > 500:
difficulty -= 1
elif difficulty <= 500:
sleeptime-=.00002
currentIcicles.append(iciclePhysics(difficulty))
currentIcicles[len(currentIcicles)-1].draw(mainCanvas)
for i in range(len(currentIcicles)):
currentIcicles[i].fall(mainCanvas)
root.update_idletasks()
for i in currentIcicles:
if i.ypos >= 90:
currentIcicles.remove(i)
root.update_idletasks()

There is no way. Mouse movement is presented to the GUI as a series of events. In order to process events, the event loop must be running.
Also, you should pretty much never do a sleep inside a GUI application. All that does is freeze the GUI during the sleep.
Another hint: you only need to create an icicle once; to make it fall you can use the move method of the canvas.
If you are having problems understanding event based programming, the solution isn't to avoid the event loop, the solution is to learn how event loops work. You pretty much can't create a GUI without it.

Related

How to make a tkInter overlay UI clickthrough (non-clickable)?

I got the source code of a timer that synchronizes with the timer of a videogame bomb but the problem is, when I'm playing the game and move my mouse and click it, I accidentally click the Timer UI and it makes the command prompt where the script is running pop up and it's really gamebreaking.
I was wondering if there's any way to make the UI clicktrough so even if I click it accidently the application does not pop up.
Here's the UI's code:
import tkinter as tkr
from python_imagesearch.imagesearch import imagesearch
root = tkr.Tk()
root.geometry("+0+0")
root.overrideredirect(True)
root.wm_attributes("-topmost", True)
root.wm_attributes("-alpha", 0.01)
root.resizable(0, 0)
timer_display = tkr.Label(root, font=('Trebuchet MS', 30, 'bold'), bg='black')
timer_display.pack()
seconds = 44
//in this space i print a lot of lines creating a box with text explaining what the script does in it.
def countdown(time):
if time > 0:
mins, secs = divmod(time, 60)
ID = timer_display.bind('<ButtonRelease-1>', on_click)
timer_display.unbind('<ButtonRelease-1>', ID)
def color_change(t_time):
if t_time > 10:
return 'green'
elif 7 <= t_time <= 10:
return 'yellow'
elif t_time < 7:
return 'red'
timer_display.config(text="{:02d}:{:02d}".format(mins, secs),
fg=color_change(time)), root.after(1000, countdown, time - 1)
else:
root.wm_attributes('-alpha', 0.01)
search_image()
def start_countdown():
root.wm_attributes('-alpha', 0.7)
countdown(seconds)
def search_image():
pos = imagesearch("./github.png")
pos1 = imagesearch("./github1.png")
if pos[0] & pos1[0] != -1:
start_countdown()
else:
root.after(100, search_image)
root.after(100, search_image)
root.mainloop()
If the UI you are talking about is timer_display then I suppose you are using bind function for the clicking part you were talking about. Here is what you can try:
# Save the binding ID
ID = timer_display.bind('<ButtonRelease-1>', on_click)
Then use it to unbind <ButtonRelease-1>
timer_display.unbind('<ButtonRelease-1>', ID)
Maybe this question might be of some help.

Dynamically updating polygon shape in tkinter window

I'm writing a program for my raspberry pi4 for my Master's project which sounded so simple at first but for some reason Python is causing me grief.
In short - I have created a tkinter canvas with a polygon centred on the canvas. The shape of this polygon will depend upon the value of a counter.
The count is controlled by a blink event from a neurosky mindwave headset - this count is working (mostly).
What I want to then do is update the canvas to put the new points for the polygon into the pack but nothing I have tried seems to work. The closest I got was trying a .redraw() command which drew an infinite number of windows before I pulled the plug.
I am not a complete novice to coding having taught many languages in my time but have never used python before and am clearly missing a very simple step which will cause everything to fall out.
I will try to modify the code to use a keyboard press rather than a headset and add it below later if folk think it will help.
import keyboard
import time
from tkinter import *
count = 0
points = [250,250,350,250,350,350,250,350]
root = Tk()
while True:
# set window to middle of screen
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
xcoord = screen_width/2-300
ycoord = screen_height/2 - 300
root.geometry("%dx%d+%d+%d" % (600,600,xcoord,ycoord))
#set up canvas size and background colour
canvas1 = Canvas(root, relief = FLAT,width = 600, height = 600, background = "blue")
#set up buttons shape and colour
button = canvas1.create_polygon(points, fill="darkgreen", outline="yellow")
canvas1.pack()
if keyboard.is_pressed("f"):
if count < 4:
count += 1
elif count == 4:
count = 0
time.sleep(0.1)
if count == 0:
points = [250,250,350,250,350,350,250,350]
elif count == 1:
points = [300,100,500,500,100,500]
elif count == 2:
points = [200,100,400,100,300,500]
elif count == 3:
points = [100,300,500,100,500,500]
elif count == 4:
points = [100,100,100,500,500,300]
print(count)
root.update()
You need to delete the old polygon and create new one. Also don't use while loop in tkinter application. For your case, you can bind a callback on <Key> event and update the polygon in the callback:
import tkinter as tk
count = 0
points = [
[250,250,350,250,350,350,250,350],
[300,100,500,500,100,500],
[200,100,400,100,300,500],
[100,300,500,100,500,500],
[100,100,100,500,500,300],
]
root = tk.Tk()
# set window to middle of screen
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
xcoord = screen_width//2 - 300
ycoord = screen_height//2 - 300
root.geometry("%dx%d+%d+%d" % (600,600,xcoord,ycoord))
#set up canvas
canvas1 = tk.Canvas(root, relief=tk.FLAT, background="blue")
canvas1.pack(fill=tk.BOTH, expand=1)
# create the polygon with tag "button"
canvas1.create_polygon(points[count], fill="darkgreen", outline="yellow", tag="button")
def on_key(event):
global count
if event.char == 'f':
count = (count + 1) % len(points)
print(count)
canvas1.delete("button") # delete the old polygon
canvas1.create_polygon(points[count], fill="darkgreen", outline="yellow", tag="button")
root.bind("<Key>", on_key)
root.mainloop()
You can update any parameters of a shape.
canvas.itemconfigure(shape1_id_or_tag, fill="green")
canvas.itemconfigure(shape2_id_or_tag, fill="#900", outline="red", width=3)
But for your situation try '.coords()':
canvas1.coords("Button", points[count])
Source: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/canvas-methods.html

Updating location of circle with mouse movement breaks on fast mouse movement, tkinter-python

I am making a draggable point(a circle). The code works, however, while dragging, if the mouse motion is quick the point stops moving. I have taken help from this code for making this program. I will be using this point later on for other purposes. Here is my full code,
from tkinter import *
import sys,os,string,time
class Point():
def __init__(self,canvas,x,y):
self.canvas = canvas
# It could be that we start dragging a widget
# And release it while its on another
# Hence when we land on a widget we set self.loc to 1
# And when we start dragging it we set self.dragged to 1
self.loc = self.dragged = 0
self.x = x
self.y = y
self.radius = 5
self.point = canvas.create_oval(self.x-self.radius,self.y-self.radius,self.x+self.radius,self.y+self.radius,fill="green",tag="Point")
canvas.tag_bind("Point","<ButtonPress-1>",self.down)
canvas.tag_bind("Point","<ButtonRelease-1>",self.chkup)
canvas.tag_bind("Point","<Enter>",self.enter)
canvas.tag_bind("Point","<Leave>",self.leave)
def down(self,event):
self.loc = 1
self.dragged = 0
event.widget.bind("<Motion>",self.motion)
canvas.itemconfigure(self.point,fill = "red")
def motion(self,event):
root.config(cursor = "exchange")
cnv = event.widget
cnv.itemconfigure(self.point,fill = "red")
self.x,self.y = cnv.canvasx(event.x), cnv.canvasy(event.y)
got = canvas.coords(self.point,self.x-self.radius,self.y-self.radius,self.x+self.radius,self.y+self.radius)
def enter(self,event):
canvas.itemconfigure(self.point,fill="blue")
self.loc = 1
if self.dragged == event.time:
self.up(event)
def up(self,event):
event.widget.unbind("<Motion>")
canvas.itemconfigure(self.point,fill="green")
self.canvas.update()
def chkup(self,event):
event.widget.unbind("<Motion>")
root.config(cursor = "")
canvas.itemconfigure(self.point,fill="green")
if self.loc: # is button released in the same widget as pressed
self.up(event)
else:
self.dragged = event.time
def leave(self,event):
self.up(event)
root = Tk()
root.title("Drag and Drop")
canvas = Canvas(root,width = 256, height = 256, borderwidth = 1)
point = Point(canvas,128,128)
canvas.pack()
root.mainloop()
Your problem is that your <Leave> binding can fire if you move the mouse outside of the tiny circle faster than you can process the move. That causes the binding for <Motion> to be disabled.
My recommendation is to a) don't bind on <Leave> to disable the binding, and b) bind on <B1-Motion> so that the binding is active only while the button is pressed.

Tkinter .after() not responding to keypress fast enough

I am trying to animate a ball, that is be moving forward. In addition, if the user presses up or down button, it should move respectively.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class Movement:
def __init__(self, _frame, _canvas):
self.frame = _frame
self.canvas = _canvas
self.x = 10
self.y = 150
self.count = 0
self.update()
self.frame.bind_all("<Up>", lambda event: self.move_up())
self.frame.bind_all("<Down>", lambda event: self.move_down())
pass
def move_up(self):
self.y -= 10
pass
def move_down(self):
self.y += 10
pass
def update(self):
canvas.delete("obj")
self.count += 10
x2 = self.x + 50
y2 = self.y + 50
self.canvas.create_oval(self.x + self.count, self.y, x2 + self.count, y2, fill="red", tag="obj")
root.after(1000, self.update)
pass
root = tk.Tk()
width = 400
height = 400
canvas = tk.Canvas(root, width=width, height=height)
frame = tk.Frame(root, width=width, height=height)
if __name__ == '__main__':
Movement(frame, canvas)
canvas.grid()
root.mainloop()
There's an issue, though. Given the fact that the ball is moving every 1 second, it doesn't respond to the key press fast enough. As a result, if you click the up kick, the screen is updating after one second.
I know there's a .move() function inside Tkinter. However, due to many reason I cannot use that.
My main concern is the interval it take for the screen to be updated.
From what I understand you want to be able to keep the timer moving forward consistent but also be able to move up and down at any point even outside of the timer.
to accomplish this I moved the timer to its own function and allowed the up down functions to call the object destroy/create function.
I have updated you code to reflect that.
Take a look at this code and let me know if it helps.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class Movement:
def __init__(self, _frame, _canvas):
self.frame = _frame
self.canvas = _canvas
self.x = 10
self.y = 150
self.count = 0
self.object_timer()
self.frame.bind_all("<Up>",lambda event: self.move_up())
self.frame.bind_all("<Down>",lambda event: self.move_down())
def move_up(self):
self.y -= 10
self.update()
def move_down(self):
self.y += 10
self.update()
def update(self):
canvas.delete("obj")
x2 = self.x + 50
y2 = self.y + 50
self.canvas.create_oval(self.x + self.count, self.y, x2 + self.count, y2, fill="red", tag="obj")
def object_timer(self):
self.update()
self.count += 10
root.after(1000, self.object_timer)
root = tk.Tk()
width = 400
height = 400
canvas = tk.Canvas(root, width=width, height=height)
frame = tk.Frame(root, width=width, height=height)
if __name__ == '__main__':
Movement(frame, canvas)
canvas.grid()
root.mainloop()
It's all just math. You can call update twice as much, and move the ball half as much if you want to keep the ball moving at the same speed while reacting to keypresses faster.
For example, instead of moving 10 pixels every second, have it move 1 pixel every tenth of a second. The end result is still 10 pixels every second. The animation will be smoother, and it will react to keypresses much faster.
By the way, there's no need to delete and recreate the oval every iteration. It's possible to move items on the canvas. Deleting and recreating will eventually cause performance problems do to how the canvas is implemented internally. Each object you create gets a new unique id, and the canvas will slow down when it has a large number of ids. The canvas does not re-use an id.
I would also recommend against using lambda. Unless you need it, it adds complexity without adding any value. Just add an extra parameter to move_up and move_down:
self.frame.bind_all("<Up>",self.move_up)
self.frame.bind_all("<Down>", self.move_down)
...
def move_up(self, event=None):
...
def move_down(self, event=None):
...

Python tkinter Canvas root.after() maximum recursion depth exceeded

from tkinter import *
root = Tk()
canvas = Canvas(root, width=400, height=400, bg="white")
canvas.pack()
rect = canvas.create_rectangle(100, 100, 110, 110, fill='blue')
def move_down(event):
canvas.move(rect, 0, 10)
root.after(1, move_down(event))
root.bind('<Down>', move_down)
root.mainloop()
I can't seem to figure out how to make root.after() work. How can I fix this so the rectangle keeps moving down?
Short version: you can't put parentheses on the function you pass to after.
root.after(1,move_down(event))
This line does not register the function move_down as the callback of the after event. Instead, it calls move_down immediately, and would register the return value of move_down as the callback, if you didn't enter an infinite recursion.
To solve this, use just move_down without actually calling it, and make event an optional variable because after isn't going to supply a value. You should probably also use a time larger than 1 ms, or else your rectangle will zip off the screen in the blink of an eye.
from tkinter import *
root = Tk()
canvas = Canvas(root, width=400, height= 400, bg="white")
canvas.pack()
rect = canvas.create_rectangle(100, 100, 110, 110, fill='blue')
def move_down(event=None):
canvas.move(rect, 0, 10)
root.after(100,move_down)
root.bind('<Enter>', move_down) #or whatever you're binding it to
root.mainloop()
Bonus info: If you're about to ask "ok, now how do I get the rectangle to stop moving when I release the key? And how do I make it move in each other direction when I press the other arrow keys?" That requires a more sophisticated design. You need the function registered to root.after to move a variable number of pixels depending the rectangle's velocity, which gets changed based on key events happening independently. Sample implementation:
from tkinter import *
root = Tk()
canvas = Canvas(root, width=400, height= 400, bg="white")
canvas.pack()
rect = canvas.create_rectangle(100, 100, 110, 110, fill='blue')
x_velocity = 0
y_velocity = 0
keys_being_held_down = set()
key_accelerations = {
"Up": (0, -10),
"Down": (0, 10),
"Left": (-10, 0),
"Right": (10, 0)
}
def key_pressed(event):
global x_velocity, y_velocity
#ignore autorepeat events
if event.keysym in keys_being_held_down:
return
keys_being_held_down.add(event.keysym)
acceleration = key_accelerations[event.keysym]
x_velocity += acceleration[0]
y_velocity += acceleration[1]
def key_released(event):
global x_velocity, y_velocity
keys_being_held_down.remove(event.keysym)
acceleration = key_accelerations[event.keysym]
x_velocity -= acceleration[0]
y_velocity -= acceleration[1]
def tick():
canvas.move(rect, x_velocity, y_velocity)
print(x_velocity, y_velocity)
root.after(100,tick)
for key in key_accelerations:
root.bind("<{}>".format(key), key_pressed)
root.bind("<KeyRelease-{}>".format(key), key_released)
root.after(100, tick)
root.mainloop()
(This isn't necessarily the best way to do it, but it demonstrates the basic approach)
I would recommend not using root.after(), so it will move when you click, and not move when you stop clicking

Categories