Clunky/slow dragging functionality on my analog clock project - python

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!

Related

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

How to start threads at the same time in Python [duplicate]

This question already has an answer here:
thread starts running before calling Thread.start
(1 answer)
Closed 3 months ago.
I want to make a small program which plays a song and an image pops up while the mouse cursor is moved. I have 3 functions for 3 actions and I want to run them at the same time but I can't accomplish it. Could you help me?
import random
import threading
import pyautogui
import pygame
from tkinter import *
def play_song():
file = 'Troll_Song.ogg'
pygame.mixer.init()
pygame.mixer.music.load(file)
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
pygame.time.Clock().tick(10)
def create_window():
while True:
root = Tk()
root.title('Trololo...')
photo = PhotoImage(file='trollface.gif')
label = Label(root, image=photo)
label.pack()
w = 620 # width for the Tk root
h = 620 # height for the Tk root
# get screen width and height
ws = root.winfo_screenwidth() # width of the screen
hs = root.winfo_screenheight() # height of the screen
# random positions of the window
x = random.randint(0, ws - 620)
y = random.randint(0, hs - 620)
# set the dimensions of the screen
# and where it is placed
root.geometry('%dx%d+%d+%d' % (w, h, x, y))
root.mainloop()
def mouse_move():
width, height = pyautogui.size()
while True:
x = random.randint(0, width)
y = random.randint(0, height)
pyautogui.moveTo(x, y, duration=0.3)
if __name__ == '__main__':
t1 = threading.Thread(target=create_window())
t2 = threading.Thread(target=play_song())
t3 = threading.Thread(target=mouse_move())
t1.start()
t2.start()
t3.start()
I don't know if it's the only problem with your code, but I can tell about threading -- target has to be a function, instead you call functions, making them run in main thread. So if first function is an infinite loop -- program will not create any threads, because it will stuck executing the first function. Here is how you do it:
t1 = threading.Thread(target=create_window)
t2 = threading.Thread(target=play_song)
t3 = threading.Thread(target=mouse_move)

python 3 tkinter: how to add .after() to graphics

i am trying to make a circle that slowly gets bigger and to do so i need to add a delay. I tried using time.sleep() but found out that won't work and that i need to use .after(). i have tried making a function that makes the circle but that didn't work. i have tried adding a lambda before the oval is created but that didn't work either. is there anyway i can add a delay before the new circle is made?
Thanks.
My code:-
from tkinter import *
root = Tk()
c = Canvas(root, width = 500, height = 500)
c.pack()
oval = c.create_oval(0, 0, 1, 1)
for x in range(2, 200, 5):
c.delete(oval)
root.after(100, oval = c.create_oval(0, 0, x, x))
root.after() takes a ms delay and a function to run after that delay. What you are trying to do is after that delay run a function with a parameter which is x (the radius of the circle). The problem with this is that you cannot ask it to run a function with an input.
To get around this, you could use an update function and then global variables for the radius of the circle etc.
This worked for me:
from tkinter import *
root = Tk()
c = Canvas(root, width = 500, height = 500, highlightthickness = 0)
c.pack()
oval = c.create_oval(0, 0, 1, 1)
r = 2
def update():
global r, oval
if x < 200:
x += 5
c.delete(oval)
oval = c.create_oval(0, 0, r, r)
root.after(100, update)
update()
root.mainloop()
I also added the option highlightthickness = 0 to your canvas because otherwise the coordinate system does not match up and your circles will overflow out of the canvas.

Tkinker circumference of a circle coords handling python 3

I'm making a little alien project to help to learn graphics in tkinter and I have come across a problem. I am trying to make the aliens eyeball stay inside the eye but still move around however that requires me to detect the edge of the eyeball which is a circle. Not really sure how coords work in tkinter (other than the basics) so any help appreciated. Thanks!
from tkinter import *
from threading import Timer
import random
import time
global canvas, root
root = Tk()
root.title("Alien")
root.attributes("-topmost", 1)
canvas = Canvas(root, height=300, width =400)
canvas.pack()
Blinking = False
class Alien:
def __init__(self):
global canvas, root
self.body = canvas.create_oval(100,150,300,250, fill = "green")
self.eye = canvas.create_oval(170,70,230,130, fill = "white")
self.eyeball = canvas.create_oval(190,90,210,110, fill = "black")
self.mouth = canvas.create_oval(150,220,250,240, fill = "red")
self.neck = canvas.create_line(200,150,200,130)
self.hat = canvas.create_polygon(180,75,220,75,200,20, fill = "blue")
self.words = canvas.create_text(200,800, text = "I'm an alien!", anchor="nw")
root.update()
def openMouth(self):
global canvas, root
canvas.itemconfig(self.mouth, fill = "black")
root.update()
def closeMouth(self):
global canvas, root
canvas.itemconfig(self.mouth, fill = "red")
root.update()
def burp(self, event):
self.openMouth()
canvas.itemconfig(self.words, text = "BURRRRP!")
time.sleep(0.5)
self.closeMouth()
def moveEye(self,event):
global root, canvas
canvas.move(alien.eyeball , random.randint(-1,1) , random.randint(-1,1))
root.update()
def blink(self,event):
canvas.itemconfig(self.eye, fill = "green")
canvas.itemconfig(self.eyeball, state=HIDDEN)
Blinking = True
root.update()
def unblink(self,event):
canvas.itemconfig(self.eye, fill = "white")
canvas.itemconfig(self.eyeball, state=NORMAL)
Blinking = False
root.update()
alien = Alien()
alien.openMouth()
time.sleep(1)
alien.closeMouth()
canvas.bind_all("<Button-1>", alien.burp)
canvas.bind_all("<KeyPress-a>", alien.blink)
Timer(2, alien.moveEye).start()
while not Blinking:
alien.moveEye(event)
if alien.moveEye.event.x > 190:
canvas.move(alien.eyeball, -1 , 0)
Small circle with radius r lies inside big circle with radius R, if
(BigCenter.X - SmallCenter.x)^2 + (BigCenter.Y - SmallCenter.Y)^2 < (R - r)^2
^2 denotes squared value

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