Creating A Word Clock With Python And Tkinter

10th October 2021 - 17 minutes read time

I recently saw a design of a physical clock that inspired me to go about creating one using Python and Tkinter. The clock was essentially a wall of letters with lights behind that light up depending on what time it is. Without the lights the clock looks like a jumble of letters, it is only when the light is on that the relevant time is displayed.

The original clock was of a proprietary design so I set about creating one that was based more on an open source clock design that I found. That clock design I found didn't display the AM or PM of the time, so I ended up tweaking that design a little anyway.

Using Tkinter to create the clock as a GUI application seemed like a nice little project to get used ti Tkinter and Python. This ties in quite closely to my article on converting the current time into a sentence in Python, so I already had quite a bit of the logic figured out.

There are a few things to figure out here, but the first task is to create the words. This is the final collection of letters that I came up with to make up the clock.

ITRISUHALFTEN
QUARTERTWENTY
FIVEQMINUTEST
PASTMTOSAMOPM
ONENTWOZTHREE
FOURFIVESEVEN
SIXEIGHTYNINE
TENELEVENPHIL
TWELVELOCLOCK

The idea is that when we want to display the time we just change the font style on the letters we need, rather than turning on or off lights. For example, for the time "IT IS FIVE TO NINE AM" we would alter the colour of the letters to make that particular set of characters stand out.

Setting Things Up

I thought for a while how to go about creating the clock. There are a few different avenues we could go down here, for example we could do one of the following.

  • Print out the text as one big string and then use a find replace method to inject a different colour of text into the output
  • Print out the time and the surrounding letters as a continuous set of characters, changing the colour of the text as it is rendered out.
  • Since the letters are actually in a grid, treat them like a grid and reference them as coordinates. The letters themselves can then be labels that will be updated with different formatting depending on them being part of the time or not.

I ended up going for the letters in a grid option. This made sense from the point of view of creating a Tkinter application as each letter could be stored in a label. It would then just be a case of referencing the letters we need to change at a given time and changing the format of those letters (eg, bold font, different colour). Also, if we wanted to create this as a physical project, then creating the letters as a grid would allow us to create it without changing too much code.

To that end, I set out the letters so that I could reference them in a grid. 

  0123456789ABC
0 ITRISUHALFTEN
1 QUARTERTWENTY
2 FIVEQMINUTEST
3 PASTMTOSAMOPM
4 ONENTWOZTHREE
5 FOURFIVESEVEN
6 SIXEIGHTYNINE
7 TENELEVENPHIL
8 TWELVELOCLOCK

As an example, if we wanted to reference the "IT" we would do 0,0 for the I and 0,1 for the "T". Changing the labels at these coordinates would highlight those letters.

Let's first create the interface so that we can see it printed out on the screen.

To do this I just created a simple SentenceClock class that extends the Tkinter class. This way we get access to the Tkinter classes and objects encapsulated within a single class. The __init__() dunder is used to setup the letters as a grid and then create those letters as Labels.

This is the initial code written that adds a SentenceClock class and uses the __init__() method to transform all of the letters into a grid of Label objects. We kick everything off at the bottom using the 'if __name__ == "__main__":' comparison that allows us to run this code only if this script is called directly. Within this construct we create the SentenceClock object and use the mainloop() method from the Tkinter object, which we have access to since the SentenceClock extends the main Tkinter class.

import tkinter as tk

class SentenceClock(tk.Tk):

    def __init__(self):
        super().__init__()

        self.title("What Time Is It?")
        
        self.letters = [
            ['I','T','R','I','S','U','H','A','L','F','T','E','N'],
            ['Q','U','A','R','T','E','R','T','W','E','N','T','Y'],
            ['F','I','V','E','Q','M','I','N','U','T','E','S','T'],
            ['P','A','S','T','M','T','O','S','A','M','O','P','M'],
            ['O','N','E','N','T','W','O','Z','T','H','R','E','E'],
            ['F','O','U','R','F','I','V','E','S','E','V','E','N'],
            ['S','I','X','E','I','G','H','T','Y','N','I','N','E'],
            ['T','E','N','E','L','E','V','E','N','P','H','I','L'],
            ['T','W','E','L','V','E','L','O','C','L','O','C','K'],
        ]

        self.labels = {}

        for i in range(0, len(self.letters)):
            for j in range(0, len(self.letters[i])):
                self.labels['label_' + str(i) + '_' + str(j)] = tk.Label(self, fg="gray", text=self.letters[i][j], font="Helvetica 16")
                self.labels['label_' + str(i) + '_' + str(j)].grid(column=j, row=i)

if __name__ == "__main__":
    sentence_clock = SentenceClock()
    sentence_clock.mainloop()

Using the grid() method as a way of placing the Label objects into the application, we can place the Label in exactly the right place, creating the grid letters. What we have now is an object that contains all the letters in our grid, coloured grey and in a 16 point font.

Running this code we produce the following.

A Paython and Tkinter application showing the words needed to create the time, but without any time set.

This looks great! All we need to do now is highlight the letters that represent the time.

Working Out The Time

As I mentioned before, this code pulls upon the code written in my previous post on transforming the current time into a sentence. The difference here is that instead of returning the words we instead return an array that contains the letter coordinates on our grid.

Translating the "to" or "past" part of the time is done as follows, we just accept the minute value and translate this to the relevant letters. I have added comments to show what word is being returned.

    def translate_to_or_past(self, minute):
        to_or_past = []
        if 3 <= minute < 33:
            to_or_past = [[3,0],[3,1],[3,2],[3,3]] # PAST
        elif 33 <= minute <= 57:
            to_or_past = [[3,5],[3,6]] # TO
        return to_or_past

Next is to translate the minute into a set of coordinates. Again, we just need to create an array of coordinates that will be used to change the labels in the relevant positions. As what this method is doing isn't clear I will reiterate the explanation of the code from the pervious post.

The translate_minute() method will convert the number of minutes into a text value by performing the following actions.

  • The number of minutes is normalised to be between 0 and 30 by taking away the number of minutes from 60 if the value is over 30. This converts values like 45 to 15, which we can convert to be "a quarter".
  • If the number of minutes is below 3 (which would mean a value around the hour mark) then return a blank string.
  • We then define an array that contains the minute values we need. This is an array of coordinate arrays, each one representing the time labels we need to alter.
  • Next, we map the number of minutes between 3 and 28 to the number of items in the array (0 to 5). This value is bumped down by 0.4 in order to better fit the values we want and then rounded so that it is an integer value and can be used to select an item from the array. The result of this will be a number between 0 and 5.
  • We then use the integer value we just worked out to pick out the correct value from the minute values array.

With that translated value as an array of coordinates we then return the value.

    def translate_minute(self, minute):
        if (minute > 30):
            minute = 60 - minute

        if minute >= 3:
            minute_blocks = [
                [[2,0],[2,1],[2,2],[2,3],[2,5],[2,6],[2,7],[2,8],[2,9],[2,10],[2,11]], # FIVE
                [[0,10],[0,11],[0,12],[2,5],[2,6],[2,7],[2,8],[2,9],[2,10],[2,11]], # TEN
                [[0,7],[1,0],[1,1],[1,2],[1,3],[1,4],[1,5],[1,6]], # A QUARTER
                [[1,7],[1,8],[1,9],[1,10],[1,11],[1,12],[2,5],[2,6],[2,7],[2,8],[2,9],[2,10],[2,11]], # TWENTY
                [[1,7],[1,8],[1,9],[1,10],[1,11],[1,12],[2,0],[2,1],[2,2],[2,3],[2,5],[2,6],[2,7],[2,8],[2,9],[2,10],[2,11]], # TWENTYFIVE
                [[0,6],[0,7],[0,8],[0,9]], # HALF
            ]
            mapped_minute_value = round((0 + (5 - 0) * ((minute - 3) / (28 - 3))) - 0.4)
            minute_name = minute_blocks[mapped_minute_value]
        else:
            minute_name = ''
        return minute_name

The translate hour method takes the hour and the minute value as we need to use the next hour value if the minute value is over 33. Doing this allows us to print out the "past the current hour" and and "to the next hour" values correctly. 

    def translate_hour(self, hour, minute):
        hours = [
            [[4,0],[4,1],[4,2]], #ONE
            [[4,4],[4,5],[4,6]], # TWO
            [[4,8],[4,9],[4,10],[4,11]], # THREE
            [[5,0],[5,1],[5,2],[5,3]], # FOUR
            [[5,4],[5,5],[5,6],[5,7]], # FIVE
            [[6,0],[6,1],[6,2]], # SIX
            [[5,8],[5,9],[5,10],[5,11],[5,12]], # SEVEN
            [[6,3],[6,4],[6,5],[6,6],[6,7]], # EIGHT
            [[6,9],[6,10],[6,11],[6,12]], # NINE
            [[7,0],[7,1],[7,2]], # TEN
            [[7,3],[7,4],[7,5],[7,6],[7,7],[7,8]], # ELEVEN
            [[8,0],[8,1],[8,2],[8,3],[8,4],[8,5]], # TWELVE'
            [[4,0],[4,1],[4,2]], #ONE
        ]
        if minute > 33:
            return hours[hour]
        else:
            return hours[hour - 1]

Finally, we pull this all together into a method that takes in the current time as an hour, minute and uses the above methods to create a list of coordinates for the letters. The letters "IT IS" are added to the start and "OCLOCK" is added to the end (if the time is around the hour mark) to complete the array.

    def translate_time(self, hour, minute, am_or_pm):
        letters = [
            [0,0], [0,1], [0,3], [0,4] # IT IS
        ]

        letters.extend(self.translate_hour(hour, minute))
        letters.extend(self.translate_to_or_past(minute))
        letters.extend(self.translate_minute(minute))

        if (am_or_pm == 'PM'):
            letters.extend([[3,11],[3,12]]) # PM
        else:
            letters.extend([[3,8],[3,9]]) # AM

        if (0 <= minute < 3) or (57 < minute <= 60):
            letters.extend([[8,7],[8,8],[8,9],[8,10],[8,11],[8,12]]) # OCLOCK

        return letters

Whilst this is technically all we need to do in order to translate the time into the letter coordinates I thought it would be best to have the clock update in real time.

Updating The Time

The first thing to do here is to add a method called update_time() that will convert the current time into the letter coordinates and then update the labels in our application.

Before that can happen we need to reset the letters to their original font. This means looping through the original letters grid and making sure all of the letters are grey.

Once that is done we work out the time using the time module. The import time line at the top is important as this is needed to get access to the time module. The time values are then passed into the translate_time() method to convert them to an array of letter coordinates.

With the letter coordinates in hand we can then loop through the coordinates, find the label associated with that position, and change it to a highlighted colour. In this case we set them to yellow and a bold font.

Finally, we use a method built into Tkinter called after(), which we have direct access to since we extended the Tkinter class to create the SentenceClock class. This is a timer that will trigger the update_time() method after a period of milliseconds. Essentially, the final line in this method will cause the program to wait for 1 second before calling the same method again.

import tkinter as tk
import time

class SentenceClock(tk.Tk):

    def update_time(self):
        for i in range(0, len(self.letters)):
            for j in range(0, len(self.letters[i])):
                self.labels['label_' + str(i) + '_' + str(j)].config(fg="gray", font="Helvetica 16")

        current_time = time.localtime()

        hour = int(time.strftime("%I", current_time))
        minute = int(time.strftime("%M", current_time))
        am_or_pm = time.strftime("%p", current_time)

        letters = self.translate_time(hour, minute, am_or_pm)

        for letter in letters:
            self.labels['label_' + str(letter[0]) + '_' + str(letter[1])].config(fg="yellow", font="Helvetica 16 bold")
        
        self.after(1000, self.update_time)

Through this self referencing timer we are essentially updating the clock once every second.

The update_time() method needs a way to get going though, so we add the same after() call to the __init__() method so that once the letter grid is in place we start the update process. Here is the updated __init__() method.

    def __init__(self):
        super().__init__()

        self.title("What Time Is It?")
        
        self.letters = [
            ['I','T','R','I','S','U','H','A','L','F','T','E','N'],
            ['Q','U','A','R','T','E','R','T','W','E','N','T','Y'],
            ['F','I','V','E','Q','M','I','N','U','T','E','S','T'],
            ['P','A','S','T','M','T','O','S','A','M','O','P','M'],
            ['O','N','E','N','T','W','O','Z','T','H','R','E','E'],
            ['F','O','U','R','F','I','V','E','S','E','V','E','N'],
            ['S','I','X','E','I','G','H','T','Y','N','I','N','E'],
            ['T','E','N','E','L','E','V','E','N','P','H','I','L'],
            ['T','W','E','L','V','E','L','O','C','L','O','C','K'],
        ]

        self.labels = {}

        for i in range(0, len(self.letters)):
            for j in range(0, len(self.letters[i])):
                self.labels['label_' + str(i) + '_' + str(j)] = tk.Label(self, fg="gray", text=self.letters[i][j], font="Helvetica 16")
                self.labels['label_' + str(i) + '_' + str(j)].grid(column=j, row=i)

        self.after(1000, self.update_time)

Starting the application renders the letter labels, works out the time, and formats them correctly. Updating every second.

Here is a screenshot of the application in action. As you can see from the screenshot the time it twenty minutes to eleven PM.

A Python and Tkinter application showing the words needed to create the time, with the time set to 20 to eleven.

This was sat on my screen for a few hours and it worked very well. The clock kept time very well, even when I put my computer to sleep as the time is worked out based on the system time, rather than an internal chronometer.

Conclusion

Overall I was very pleased with how this clock turned out. By segmenting the working out of the time into parts it is also possible to unit test this clock, much in the same way as was done in the previous article. This means that if the clock was to be transformed into a physical device then there wouldn't be any problems as everything would be tested before hand.

The blanking and formatting of the letters every second is a bit of a cheap trick, but the overall effect works. I wasn't able to spot any flicker on the application window at all and the transition of the letters happens without any delays. In fact, one downside of the clock is that it takes a couple of seconds to read, and a few times I was half way through reading it when the time updated. Even then, I wasn't actually aware of the update until I read through it a second time.

If you want to run this clock for yourself then you can do using the Github Gist that puts together all the code seen here. Let me know if you have used the code on any of your projects.

Comments

Permalink

This is very cool.  I get error messages after the clock displays. 

Not sure where they are coming from??

tkinter_time_wall.py', current_namespace=True)

invalid command name "2209094932352update_time"
    while executing
"2209094932352update_time"
    ("after" script)
invalid command name "2209094932288update_time"
    while executing
"2209094932288update_time"
    ("after" script)
invalid command name "2209100506752update_time"
    while executing
"2209100506752update_time"
    ("after" script)
invalid command name "2209099901248update_time"
    while executing
"2209099901248update_time"
    ("after" script)

Rich Lysakowski (Wed, 10/20/2021 - 06:04)

Permalink

That error is caused when an element is deleted from the GUI but still being referenced. Since I'm not deleting anything in the script it seems strange that it would do this.

I have tested this script on a few machines and I've not seen this issue. Maybe you are using an older version of Python/Tkinter that has a bug?

Add new comment

The content of this field is kept private and will not be shown publicly.