Tkinter After Loop Only Runs to Time if Mouse Moving - python

I'm using after() for a Tkinter animation loop:
from tkinter import Tk, Canvas
from time import time
root = Tk()
root.configure(width=1920, height=1080)
root.resizable(True, True)
main = Canvas(root, width=1600, height=900)
ball = main.create_oval(80, 80, 120, 120, fill="#FF0000")
main.pack()
frametarget = 17
lasttime = time()
frametimes = []
def move():
global lasttime
frametimes.append(time() - lasttime)
lasttime = time()
main.move(ball, 1, 1)
root.after(frametarget, move)
root.after(0, move)
root.mainloop()
print(sum(frametimes[60:]) / len(frametimes[60:]) * 1000)
This generally seems to be updating in the region of ~30ms and looks visually slow when left unattended, but when the mouse is continuously moving over the window it hits 17ms consistently and runs smooth, but reverts back once this stops.
I'm using Python 3.7, Windows 10
Tests:
Average frame time where frametarget = 17 (for ~60fps)
Without mouse movement = 29.28ms
With mouse movement = 17.08ms
Average frame time where frametarget = 50 (for 20fps)
Without mouse movement = 62.50ms
With mouse movement = 50.04ms
Update:
The same issue is not present on Linux.

Upgrading to Python 3.8.6 (from 3.7.4) solved the issue.
It appeared to be unique to my own environment as the problem did not reproduce on OSX, Linux or other Windows environments.

I had almost the same problem except in my case I didn't have to move the mouse, just hover with it on the window, but I think this solution will work for both cases.
Add this at the start of your code:
from ctypes import windll
timeBeginPeriod = windll.winmm.timeBeginPeriod
timeBeginPeriod(1)

Related

How Do I make sure my pygame window stays at the top

I am made a little window that helps me play a game. But when I click somewhere else the window just minimizes or goes to the back. How do I make sure that my pygame window stays on top of the screen?
My answer is taken from Bring a pygame window to front
from os import environ
environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame # import after disabling environ prompt
from win32gui import SetWindowPos
import tkinter as tk
root = tk.Tk() # create only one instance for Tk()
root.withdraw() # keep the root window from appearing
screen_w, screen_h = root.winfo_screenwidth(), root.winfo_screenheight()
win_w = 250
win_h = 300
x = round((screen_w - win_w) / 2)
y = round((screen_h - win_h) / 2 * 0.8) # 80 % of the actual height
# pygame screen parameter for further use in code
screen = pygame.display.set_mode((win_w, win_h))
# Set window position center-screen and on top of other windows
# Here 2nd parameter (-1) is essential for putting window on top
SetWindowPos(pygame.display.get_wm_info()['window'], -1, x, y, 0, 0, 1)
# regular pygame loop
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
done = True
Hope it helps!☺

Bring a pygame window to front

from os import environ
environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame # import after disabling prompt
screen = pygame.display.set_mode((800, 800))
screen.fill((50, 50, 50)) # Dark gray color
pygame.display.update()
Yes, I did my research already, and couldn't find anything helpful: hence this question.
Every time I run the program the pygame window opens below other windows. I want it to behave in 2 ways based on code: Pin the window on top and spawn on top but no pin.
Here is the simplest solution I found:
(It also requires tkinter to get system screen metrics)
from os import environ
environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame # import after disabling environ prompt
from win32gui import SetWindowPos
import tkinter as tk
root = tk.Tk() # create only one instance for Tk()
root.withdraw() # keep the root window from appearing
screen_w, screen_h = root.winfo_screenwidth(), root.winfo_screenheight()
win_w = 250
win_h = 300
x = round((screen_w - win_w) / 2)
y = round((screen_h - win_h) / 2 * 0.8) # 80 % of the actual height
# pygame screen parameter for further use in code
screen = pygame.display.set_mode((win_w, win_h))
# Set window position center-screen and on top of other windows
# Here 2nd parameter (-1) is essential for putting window on top
SetWindowPos(pygame.display.get_wm_info()['window'], -1, x, y, 0, 0, 1)
# regular pygame loop
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
done = True

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

Clunky/slow dragging functionality on my analog clock project

I just finished my first Python project, an analog clock. I managed to reach a state where I implemented everything I had in mind and fixed the issues that followed. The last thing I added in is a dragging capability for my clock and while it works it seems slow or at least just unsmooth. Right now I lack the knowledge and experience to understand why its happening and let alone fix it.. Any help or ideas would be appreciated, Thanks in advance. :)
Here is my code:
# Imported tkinter for its methods
import tkinter as tk
# Imported turtle for drawing the clock's hands
import turtle
# Imported time to handle setting and updating the time
import time
# Declared and set the color used for transparency
transparent_clr = '#FB00FF'
# Setup a borderless window with transparent background
# and always on top flag
root = tk.Tk()
root.overrideredirect(True)
root.wm_attributes('-topmost', 1)
root.deiconify()
root.attributes('-transparentcolor', transparent_clr)
# Setup the clock's face using an image
Clock_bg = tk.PhotoImage(file='Clock_bg.png')
canvas = tk.Canvas(width=300, height=300, highlightthickness=0)
screen = turtle.TurtleScreen(canvas)
canvas.create_image(0, 0, image=Clock_bg)
canvas.pack()
screen.tracer(0)
screen.bgcolor(transparent_clr)
# Configure the pen used for the clock's hands
draw = turtle.RawTurtle(screen)
draw.hideturtle()
draw.speed(0)
draw.pensize(3)
# Retain Windows TaskBar visibility and function such as exiting
# the app
wn = tk.Toplevel(root)
wn.iconify()
wn.iconbitmap('Clock_icon.ico')
wn.attributes('-alpha', 0.0)
def wn_destroy():
wn.protocol('WM_DELETE_WINDOW', exit_func)
def exit_func():
root.destroy()
wn_destroy()
# Make the clock draggable
def draggable():
root._offsetx = 0
root._offsety = 0
root.bind('<Button-1>', winclick)
root.bind('<B1-Motion>', windrag)
def windrag(event):
x = root.winfo_pointerx() - root._offsetx
y = root.winfo_pointery() - root._offsety
root.geometry('+{x}+{y}'.format(x=x, y=y))
def winclick(event):
root._offsetx = event.x
root._offsety = event.y
draggable()
# Draw the clock and its hands
def draw_clock(h, m, s, draw):
# Draw the hours hand
draw.penup()
draw.goto(0, 0)
draw.color('black')
draw.setheading(90)
angle = (h / 12) * 360 + (m / 60) * 30
draw.rt(angle)
draw.pendown()
draw.fd(70)
# Draw the minutes hand
draw.penup()
draw.goto(0, 0)
draw.color('black')
draw.setheading(90)
angle = (m / 60) * 360 + (s / 60) * 6
draw.rt(angle)
draw.pendown()
draw.fd(100)
# Draw the seconds hand
draw.penup()
draw.goto(0, 0)
draw.color('red')
draw.setheading(90)
angle = (s / 60) * 360
draw.rt(angle)
draw.pendown()
draw.fd(60)
# Update the time in real time
while True:
# Declared and set the hour, minutes and seconds
h = int(time.strftime('%I'))
m = int(time.strftime('%M'))
s = int(time.strftime('%S'))
draw_clock(h, m, s, draw)
screen.update()
time.sleep(1)
draw.clear()
"You are using your own mainloop with time.sleep(1), therefore root.geometry( gets only called every second. –stovfl"
Thanks a billion, stovf1!

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