Title: Draw an analog clock in tkinter and Python
This program draws an analog clock (for those who still remember how to read one). It has three main parts that set up the window, create the clock widgets, and update the clock's hands to show the time.
Setting Up the Window
The program uses the following constructor to prepare the tkinter window.
def __init__(self):
# Make the main interface.
self.window = tk.Tk()
self.window.title('')
self.window.protocol('WM_DELETE_WINDOW', self.kill_callback)
method = 1
# Remove all window decorations. Cannot minimize, maximize, resize,
# or move. Doesn't appear in taskbar. Kill with Alt+F4.
if method == 1:
self.window.overrideredirect(True)
# In Windows, tool window style. Cannot minimize or maximize. Can
# resize. Doesn't appear in taskbar. Kill with Alt+F4 or click X.
if method == 2:
self.window.wm_attributes('-toolwindow', 'True')
# Prevent ressizing. Can minimize and restore. Appears in taskbar.
# Kill with Alt+F4 or click X.
if method == 3:
self.window.resizable(False, False)
# Make the clock widgets.
self.make_clock()
# Display the window.
self.window.focus_force()
self.window.mainloop()
This is mostly straightforward, but there is a slightly tricky bit in the middle that tries to make the clock look nice by removing the title bar. Unfortunately, if you do that, there are some side effects like being unable to move the window. (You can fix that with drag and drop. I may add that later.)
The code uses three methods, two comments out, to minimize the window decorations. These are OS-specific so they may not work well with your operating system. (I'm using Windows 11.)
The first method calls self.window.overrideredirect(True) to remove all window decorations including the title bar and borders as shown in the picture on the right. This works well in that it prevents the user from resizing the window, but it also means you cannot move, minimize, or maximize the window. It also doesn't appear in the taskbar. You can still kill the window by pressing Alt+F4 or stopping the kernel. You could also add a user interface method to stop the program like adding a context menu.
The second method calls self.window.wm_attributes('-toolwindow', 'True') to give the window a tool window style, which apparently only works on Windows. Now you can move the window and click the X button to close it, but you still cannot resize, minimize, or maximize it. It also doesn't appear in the taskbar or the Windows task switcher so it can be hard to find. If you lose it, it seems that you can make it reappear if you minimize all windows and then bring any window back.
The third method calls self.window.resizable(False, False) to make the window non-resizable. You can move the window, click its X button to close it, and minimize and restore the window, but you cannot maximize it or resize it. For now, this is probably the best option.
Creating Clock Widgets
The following code shows how the program builds the clock's widgets.
def make_clock(self):
self.canvas = tk.Canvas(self.window, width=CLOCK_WID, height=CLOCK_HGT,
bg='light green')
self.canvas.pack()
# Face
THICKNESS = 4
xmin = THICKNESS / 2 + 1
ymin = xmin
xmax = CLOCK_WID - THICKNESS / 2 + 1
ymax = xmax
self.canvas.create_oval(xmin, ymin, xmax, ymax,
outline='blue', fill='lightblue', width=THICKNESS)
# Tick marks.
radius = CLOCK_WID / 2
r1 = radius - THICKNESS - 2
r2 = r1 - THICKNESS
r3 = r2 - THICKNESS
self.cx = (xmin + xmax) / 2
self.cy = (ymin + ymax) / 2
for tick in range(0, 60):
radians = tick / 60 * (2 * math.pi)
if tick % 5 == 0:
x1 = self.cx + math.cos(radians) * r1
y1 = self.cy + math.sin(radians) * r1
x2 = self.cx + math.cos(radians) * r3
y2 = self.cy + math.sin(radians) * r3
thickness = 3
else:
x1 = self.cx + math.cos(radians) * r1
y1 = self.cy + math.sin(radians) * r1
x2 = self.cx + math.cos(radians) * r2
y2 = self.cy + math.sin(radians) * r2
thickness = 1
self.canvas.create_line(x1, y1, x2, y2, fill='blue', width=thickness)
# Second hand.
self.second_radius = r3
self.second_hand = self.canvas.create_line(
0, 0, 10, 10, fill='red', width=1)
# Minute hand.
self.minute_radius = self.second_radius - 3 * THICKNESS
self.minute_hand = self.canvas.create_line(
0, 0, 10, 10, fill='green', width=3)
# Hour hand.
self.hour_radius = self.minute_radius - 3 * THICKNESS
self.hour_hand = self.canvas.create_line(
0, 0, 10, 10, fill='black', width=6)
# Center.
x1 = self.cx - radius * 0.05
y1 = self.cy - radius * 0.05
x2 = self.cx + radius * 0.05
y2 = self.cy + radius * 0.05
self.canvas.create_oval(x1, y1, x2, y2, fill='black', outline='')
# Start the clock.
self.tick()
This code first creates a Canvas widget to hold the clock. It then creates the circular clock face. I used a thick border for the face so you need to fiddle with the circle's coordinates a bit to make it fill the Canvas. I've seen documentation that states the pixels for a thick circle go inside the circle, but they actually seem to center around the circle's edge. That means the outer edge of the circle is half the line thickness larger. The code sets xmin, xmax, ymin, and ymax to account for that and make the circle fill the Canvas.
Next, the code loops through the 60 tick marks. It makes every fifth tick mark longer to highlight the hour positions.
The code then creates the second, minute, and hour hands. It saves the IDs of the lines it creates so the program can update the hands' positions every second. Initially the three hands run from (0, 0) to (10, 10) because we will update them shortly.
Finally, this method fills a small circle in the center of the clock and calls self.tick to update the hands' positions.
Updating Clock Hands
The following code shows the program's tick method, which updates the clock's hands.
def tick(self):
'''Update the clock hand positions.'''
now = datetime.now()
# Seconds.
seconds = now.second
second_radians = seconds / 60 * (2 * math.pi) - math.pi / 2
x = self.cx + math.cos(second_radians) * self.second_radius
y = self.cy + math.sin(second_radians) * self.second_radius
self.canvas.coords(self.second_hand, self.cx, self.cy, x, y)
# Minutes.
minutes = now.minute + seconds / 60
minute_radians = minutes / 60 * (2 * math.pi) - math.pi / 2
x = self.cx + math.cos(minute_radians) * self.minute_radius
y = self.cy + math.sin(minute_radians) * self.minute_radius
self.canvas.coords(self.minute_hand, self.cx, self.cy, x, y)
# Hours.
hours = now.hour + minutes / 60
hour_radians = hours / 12 * (2 * math.pi) - math.pi / 2
x = self.cx + math.cos(hour_radians) * self.hour_radius
y = self.cy + math.sin(hour_radians) * self.hour_radius
self.canvas.coords(self.hour_hand, self.cx, self.cy, x, y)
# Try again in a second.
DELAY = 1000 # Update every 1000 milliseconds.
self.canvas.after(DELAY, self.tick)
This code gets the current time. It then gets the angle at which the second hand should be pointed. This is the current seconds divided by 60 times the number of radians in a full circle: 2π. The - math.pi / 2 adjusts for the fact that angles start pointing to the right in tkinter. The program uses the angle to find the end points for the second hand and changes that coordinate of the second hand's line to position it properly.
Next, the program repeats those steps to draw the minute hand. This time the angle is defined by the current number of minutes much as the previous code used the number of seconds. In addition, the code adds a small amount for the current number of seconds. A total of 60 seconds would move the minute hand 1/60th of a circle, so the code adds seconds / 60 to the angle. After calculating the angle, the code adjusts the minute hand as before.
The calculation for the hour hand is similar. It calculates the angle for the current hour and adds 1/60th of a circle for the minutes. (Notice that the minutes value includes 1/60th of a circle for the seconds, so the hour includes 1/360th of a circle for the seconds.) This time it divides the hours angle by 12 instead of 60 because there are 12 hours on the clock face in contrast to the 60 minutes or seconds.
Having updated the clock's hand positions, the code calls self.canvas.after(DELAY, self.tick) to make the tick event run again in 60 seconds.
Conclusion
This example draws an analog clock, in case you remember how to read one. Its two biggest challenges are drawing the clock itself and figuring out how to minimize the window's decorations. Unfortunately the later seems to be operating system dependent so your mileage may vary.
In my next posts, I'll look at other ways to hide window decoration and ways to move a window without a title bar.
Meanwhile, download the example to experiment with it and to see additional details.
|