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

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.

Related

Tkinter lag at the "update" command

I made a timer just to test something out. and for some reason it starts lagging, here is the timer:
from tkinter import *
from time import sleep
main = Tk()
main.title("Timer")
c = Canvas(main,width=1000,height=400)
c.pack()
c.config(bg="black")
hours = -1
while True:
hours += 1
for minutes in range(60):
for seconds in range(60):
c.create_rectangle(0,0,1000,400,fill="black")
c.create_text(500,200,text=(str(hours)+":"+str(minutes)+":"+str(seconds)),font=("",200),fill="white")
c.update()
sleep(1)
Can someone figure out where it happens? I noticed I can run the timer without tkinter and just with print, but I need to use tkinter for other projects too.
You are creating text widgets covered by black rectangles in succession, without ever removing them - in essence, every second, you are piling two more canvas items on top of all the previous ones!
The correct approach is to use the tkinter.mainloop in conjunction with the tkinter.after method, and steer clear from using a while loop, and tkinter.update. You can change the text displayed by a canvas text item using itemconfigure.
The use of time.sleep in a GUI is a recipe to have your GUI stop responding to interactions - don't do that!
Maybe do this instead:
import tkinter as tk
def update_clock():
clock_text = f'{hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}'
canvas.itemconfigure(clock, text=clock_text)
main.after(1000, _increment_time)
def _increment_time():
global clock, hours, minutes, seconds
seconds += 1
if seconds == 60:
seconds = 0
minutes += 1
if minutes == 60:
minutes = 0
hours += 1
update_clock()
main = tk.Tk()
main.title('Timer')
canvas = tk.Canvas(main, width=1000, height=400, bg='black')
canvas.pack()
hours, minutes, seconds = 0, 0, 0
clock = canvas.create_text(500, 200, font=('', 200), fill='white')
update_clock()
main.mainloop()

time function always gives me 0.0 output

Im trying to make a CPS counter, and when I reach 100 clicks, its supposed to print "test" and also print the time it took to get to 100 clicks. But it always gives 0.0 as the time output.
import tkinter
import time
counter = tkinter.Tk()
clicks = 0
def addClick():
global clicks
clicks = clicks + 1
lbl.configure(text=clicks)
start = time.time()
if clicks == 100:
print("test")
end = time.time()
print(start - end)
lbl = tkinter.Label(counter, text = clicks)
lbl.pack()
btn = tkinter.Button(counter, text="Click here", command=addClick)
btn.pack()
counter.mainloop()
...
start = time.time()
if clicks == 100:
print("test")
end = time.time()
print(start - end)
You keep restarting start after every click. A possible solution would be to start it only after the first click. This will require start to be a global variable as well.
Also note that you should do end - start, not start - end.
clicks = 0
start = None
...
global clicks
global start
...
if clicks == 1:
# instantiating 'start' only if it was the first click
start = time.time()
elif clicks == 100:
print("test")
end = time.time()
print(end - start)
However, using global variables is quite a code-smell and an anti-pattern, and we already have 2 of them in such a tiny program.
You can try to wrap them in a data-structure such as a dict:
import tkinter
import time
counter = tkinter.Tk()
data = {'clicks': 0, 'start': None}
def addClick():
data['clicks'] += 1
lbl.configure(text=data['clicks'])
if data['clicks'] == 1:
# instantiating 'start' only if it was the first click
data['start'] = time.time()
elif data['clicks'] == 100:
print("test")
end = time.time()
print(end - data['start'])
lbl = tkinter.Label(counter, text=data['clicks'])
lbl.pack()
btn = tkinter.Button(counter, text="Click here", command=addClick)
btn.pack()
counter.mainloop()
Another, real-world fitting solution would be to wrap the entire tkinter app in a class, that can keep track of its own state.

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

Python tkinter canvas transparent

I am looking to make the background of the tkinter canvas transparent, but still have Mouse events of the canvas, here is my code, I am on Windows 10, Python 3.6:
from tkinter import *
import time
WIDTH = 500
HEIGHT = 500
LINEWIDTH = 1
TRANSCOLOUR = 'gray'
global old
old = ()
tk = Tk()
tk.title('Virtual whiteboard')
tk.wm_attributes('-transparentcolor', TRANSCOLOUR)
canvas = Canvas(tk, width=WIDTH, height=HEIGHT)
canvas.pack()
canvas.config(cursor='tcross')
canvas.create_rectangle(0, 0, WIDTH, HEIGHT, fill=TRANSCOLOUR, outline=TRANSCOLOUR)
def buttonmotion(evt):
global old
if old == ():
old = (evt.x, evt.y)
return
else:
canvas.create_line(old[0], old[1], evt.x, evt.y, width=LINEWIDTH)
old = (evt.x, evt.y)
def buttonclick(evt):
global old
canvas.create_line(evt.x-1, evt.y-1, evt.x, evt.y, width=LINEWIDTH)
old = (evt.x, evt.y)
canvas.bind('<Button-1>', buttonmotion)
canvas.bind('<B1-Motion>', buttonclick)
while True:
tk.update()
time.sleep(0.01)
When run the code, it makes a transparent background, but I select the things under, instead of the canvas.
I build a little workaround with the help of the win api, here is my suggestion:
from tkinter import *
import time
import win32gui
import win32api
WIDTH = 500
HEIGHT = 500
LINEWIDTH = 1
TRANSCOLOUR = 'gray'
title = 'Virtual whiteboard'
global old
old = ()
global HWND_t
HWND_t = 0
tk = Tk()
tk.title(title)
tk.lift()
tk.wm_attributes("-topmost", True)
tk.wm_attributes("-transparentcolor", TRANSCOLOUR)
state_left = win32api.GetKeyState(0x01) # Left button down = 0 or 1. Button up = -127 or -128
canvas = Canvas(tk, width=WIDTH, height=HEIGHT)
canvas.pack()
canvas.config(cursor='tcross')
canvas.create_rectangle(0, 0, WIDTH, HEIGHT, fill=TRANSCOLOUR, outline=TRANSCOLOUR)
def putOnTop(event):
event.widget.unbind('<Visibility>')
event.widget.update()
event.widget.lift()
event.widget.bind('<Visibility>', putOnTop)
def drawline(data):
global old
if old !=():
canvas.create_line(old[0], old[1], data[0], data[1], width=LINEWIDTH)
old = (data[0], data[1])
def enumHandler(hwnd, lParam):
global HWND_t
if win32gui.IsWindowVisible(hwnd):
if title in win32gui.GetWindowText(hwnd):
HWND_t = hwnd
win32gui.EnumWindows(enumHandler, None)
tk.bind('<Visibility>', putOnTop)
tk.focus()
running = 1
while running == 1:
try:
tk.update()
time.sleep(0.01)
if HWND_t != 0:
windowborder = win32gui.GetWindowRect(HWND_t)
cur_pos = win32api.GetCursorPos()
state_left_new = win32api.GetKeyState(0x01)
if state_left_new != state_left:
if windowborder[0] < cur_pos[0] and windowborder[2] > cur_pos[0] and windowborder[1] < cur_pos[1] and windowborder[3] > cur_pos[1]:
drawline((cur_pos[0] - windowborder[0] - 5, cur_pos[1] - windowborder[1] - 30))
else:
old = ()
except Exception as e:
running = 0
print("error %r" % (e))
Shot explanation of the new code bits:
tk.lift()
tk.wm_attributes("-topmost", True)
...
def putOnTop(event):
event.widget.unbind('<Visibility>')
event.widget.update()
event.widget.lift()
event.widget.bind('<Visibility>', putOnTop)
...
tk.bind('<Visibility>', putOnTop)
tk.focus()
These lines ensure, that the window will be always be on top of all other windows.
global HWND_t
HWND_t = 0
...
def enumHandler(hwnd, lParam):
global HWND_t
if win32gui.IsWindowVisible(hwnd):
if title in win32gui.GetWindowText(hwnd):
HWND_t = hwnd
win32gui.EnumWindows(enumHandler, None)
This code bit will go through all the windows currently displayed and catches the handle of the whiteboard window (make sure the title is unique, or this could capture the wrong handle).
state_left = win32api.GetKeyState(0x01)
...
if HWND_t != 0:
windowborder = win32gui.GetWindowRect(HWND_t)
cur_pos = win32api.GetCursorPos()
state_left_new = win32api.GetKeyState(0x01)
if state_left_new != state_left:
if windowborder[0] < cur_pos[0] and windowborder[2] > cur_pos[0] and windowborder[1] < cur_pos[1] and windowborder[3] > cur_pos[1]:
drawline((cur_pos[0] - windowborder[0] - 5, cur_pos[1] - windowborder[1] - 30))
else:
old = ()
This
Checks, if the handle is found
Checks, if mouse button 1 is clicked or not
Checks, if mouse is inside the window
if all is true, it takes the mouse data and draws the line
the current mode is, that it doesn't draw anything till the button is clicked and then draws until the button is clicked again.
I'm sure you've thought of this, but have you tried setting the hexadecimal colour as ""?
i.e
canvas = tk.Canvas(width, height, bg = "")
That works for my version of python.

Tkinter events outside of the mainloop?

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.

Categories