Title: Draw LED-style characters in tkinter and Python
Every now and then I get the urge to write a digital clock program (honestly, who hasn't?) and to do that I need to be able to draw LED-style letters. (Some things are practically a rite of passage for programmers. Writing a program that can reproduce its own code, prime factoring your Social Security Number, writing clock programs...)
This program draws LED-style letters so I can make a digital clock in a later post. That post will be a lot easier than this one, but even this one isn't terribly complicated, although the code is pretty long.
The following sections describe the main pieces of the Led class which draws LED-style characters.
The Led Class
The following code shows the Led class's declaration and constructor.
class Led:
def __init__(self, canvas, position, cell_width, cell_height,
led_thickness, gap, on_color, off_color, letter):
self.canvas = canvas
self.position = position
self.cell_width = cell_width
self.cell_height = cell_height
self.led_thickness = led_thickness
self.gap = gap
self.on_color = on_color
self.off_color = off_color
# Create the LED polygons.
pgons = [
self.led_pgon_0(position),
self.led_pgon_1(position),
self.led_pgon_2(position),
self.led_pgon_3(position),
self.led_pgon_4(position),
self.led_pgon_5(position),
self.led_pgon_6(position),
self.led_pgon_7(position),
self.led_pgon_8(position),
self.led_pgon_9(position),
self.led_pgon_10(position),
self.led_pgon_11(position),
self.led_pgon_12(position),
self.led_pgon_13(position),
]
self.polygons = [canvas.create_polygon(pgon, fill='yellow') for pgon in pgons]
# Define the letters.
self.define_letters()
# Set our initial letter.
self.set_letter(letter)
The class defines several variables that define the text's geometry. The picture on the right shows how those variables determine the dimensions of the LEDs' pieces.
After saving the parameters, the constructor calls 14 functions to create points defining polygons for the LED pieces. This program uses 14 pieces but some other LED styles use more. For example, one style breaks the top and bottom horizontal LEDs into two pieces.
The code then uses a list comprehension to turn the point lists returned by the functions into tkinter polygons.
The code then calls the define_letters method described shortly to determine which LED pieces should be turned on or off for a particular letter. It finishes by calling set_letter to set the Led object's initial letter.
I'll describe the define_letters and set_letter methods shortly, but first let's look at one of the LED polygon point methods.
LED Point Methods
The LED point methods return lists of points that define the cell's 14 LED polygons. The picture on the right shows how the polygons are numbered and point methods have corresponding names. For example, the method that generates the points for LED number 0 is led_pgon_0 and it's shown in the following code.
def led_pgon_0(self, position):
p1 = (
position[0] + self.led_thickness / 2 + self.gap,
position[1] + self.led_thickness / 2)
p2 = (
position[0] + self.cell_width - self.led_thickness / 2 - self.gap,
p1[1])
return self.make_h_led(p1, p2)
This code defines the points for LED polygon 0, which is horizontal LED that runs across the top of the character cell. It starts position, the upper left corner of the cell, and adds appropriate amounts to find the left and right corners of the polygon. It then calls the make_h_led method to find the points that define a horizontal LED polygon between those two points and returns the resulting points.
Here's the make_h_led method.
def make_h_led(self, p1, p2):
points = [
(p1[0], p1[1]),
(p1[0] + self.led_thickness / 2, p1[1] + self.led_thickness / 2),
(p2[0] - self.led_thickness / 2, p2[1] + self.led_thickness / 2),
(p2[0], p2[1]),
(p2[0] - self.led_thickness / 2, p2[1] - self.led_thickness / 2),
(p1[0] + self.led_thickness / 2, p1[1] - self.led_thickness / 2),
]
return points
This method finds the points that make up a horizontal LED from point p1 to point p2.
The methods that find points for LEDs 0, 6, 7, and 13 all follow this same basic approach.
The methods that draw LEDs 1, 5, 8, and 12 work similarly, although they call the make_v_led method (the "v" stands for "vertical") to draw their polygons.
The methods that draw LEDs 3 and 10 take a fairly similar approach, but they use two helper methods, make_ct_led ("ct" for "center top") and make_cb_led ("cb" for "center bottom"). You can see all of these if you download the example program.
So far these methods are fairly long but straightforward. The methods that draw the diagonal LEDs are more complicated.
Diagonal LEDs
Unfortunately it's not as easy to figure out where the vertices should go for the diagonal LEDs. You could use points that are the same distances vertically and horizontally from the inner corners of the existing LEDs, but then the LEDs along each diagonal wouldn't line up properly.
Instead this program uses points that are offset from the letter cell's outer corners. The offset is the LED thickness divided by the square root of two, as shown in the picture on the right. That makes the distance between the two points at each corner equal to the LED thickness.
(Note that this does not give the diagonal LEDs quite the same thickness as the others. If the letter cell were a square, that would be the case. When the cell is taller than it is wide, as it is in this example, then the result is slightly thinner. As was the case with diagonal gaps, I like the result so I'm not going to mess with it.)
The program then intersects those red diagonal lines with vertical and horizontal lines that lie on the inside edges of the outer polygons drawn so far. The picture on the right shows how the red diagonal lines intersect the blue vertical and horizontal lines to define two of the vertices for LED number 2.
The basic approach used to define the diagonal LEDs is to find intersecting pairs of line segments that define the polygon's vertices, and then pass them into a method that finds the intersections and uses the resulting points to define the polygon.
The following code shows the method that makes LED number 2.
def led_pgon_2(self, position):
sqrt2 = math.sqrt(2)
dx = self.led_thickness / sqrt2
dy = dx
u_diag_pt1 = (
position[0] + dx,
position[1])
u_diag_pt2 = (
position[0] + self.cell_width,
position[1] + self.cell_height - dy)
l_diag_pt1 = (
position[0],
position[1] + dy)
l_diag_pt2 = (
position[0] + self.cell_width - dx,
position[1] + self.cell_height)
u_horz_pt1 = (
position[0],
position[1] + self.led_thickness + self.gap)
u_horz_pt2 = (
position[0] + self.cell_width,
position[1] + self.led_thickness + self.gap)
l_horz_pt1 = (
position[0],
position[1] + self.cell_height / 2 - self.led_thickness / 2 - self.gap)
l_horz_pt2 = (
position[0] + self.cell_width,
position[1] + self.cell_height / 2 - self.led_thickness / 2 - self.gap)
l_vert_pt1 = (
position[0] + self.led_thickness + self.gap,
position[1])
l_vert_pt2 = (
position[0] + self.led_thickness + self.gap,
position[1] + self.cell_height)
r_vert_pt1 = (
position[0] + self.cell_width / 2 - self.led_thickness / 2 - self.gap,
position[1])
r_vert_pt2 = (
position[0] + self.cell_width / 2 - self.led_thickness / 2 - self.gap,
position[1] + self.cell_height)
segs = [
[ l_vert_pt1, l_vert_pt2, u_horz_pt1, u_horz_pt2 ],
[ u_horz_pt1, u_horz_pt2, u_diag_pt1, u_diag_pt2 ],
[ u_diag_pt1, u_diag_pt2, r_vert_pt1, r_vert_pt2 ],
[ r_vert_pt1, r_vert_pt2, l_horz_pt1, l_horz_pt2 ],
[ l_horz_pt1, l_horz_pt2, l_diag_pt1, l_diag_pt2 ],
[ l_diag_pt1, l_diag_pt2, l_vert_pt1, l_vert_pt2 ],
]
return self.make_intersection_led(segs)
This method calculates the locations of points that it can use to define the segments. It then creates a list of lists named segs. Each entry in the segs list is a list containing four points that define two line segments. The point where the segments intersect gives one of the polygon's vertices.
After it defines the segments, the led_pgon_2 method passes the list into the following make_intersection_led method.
def make_intersection_led(self, segs):
points = []
for seg in segs:
a1 = seg[0]
a2 = seg[1]
b1 = seg[2]
b2 = seg[3]
lines_intersect, segments_intersect, intersection, \
close_p1, close_p2 = find_line_intersection(a1, a2, b1, b2)
points.append(intersection)
return points
After all that setup, this method is fairly simple. It loops through the segments in the segs list. Each entry in the array contains four points that define two line segments. The method simply calls the find_line_intersection function to see where the two segments intersect and adds the resulting point to the points list. When it finishes processing all of the segment pairs, the method converts the list into an array and returns the result. (You can read about the find_line_intersection function in the post Determine where two lines intersect in Python.
That's the end of the code that creates polygons to draw the LEDs. The other methods are similar to those I've shown here. You can download the example if you want to see all of their details.
define_letters
Earlier I promised to describe the define_letters and set_letter methods. Here's define_letters.
The define_letters method determines which LEDs should be turned on for each letter. It's a long but straightforward method so the following code only shows part of it.
def define_letters(self):
'''Define the LED methods used to draw letters.'''
self.on_leds = {}
# All lines.
self.on_leds[0] = string_to_bool('11111111111111')
# 0 without diagonal slashes.
self.on_leds['0'] = string_to_bool('11000100100011')
# 0 with diagonal slashes.
self.on_leds['0/'] = string_to_bool('11001100110011')
self.on_leds['1'] = string_to_bool('00001100000010')
self.on_leds['2'] = string_to_bool('10000111100001')
self.on_leds['3'] = string_to_bool('10000101000011')
self.on_leds['4'] = string_to_bool('01000111000010')
...
''' -|\|/|--|/|\|-'''
The code first creates the self.on_leds dictionary. Then for each of the letters that the program defines, it calls string_to_bool to convert a string of 0s and 1s into a list of Booleans that indicate which LEDs should be turned on and off for that letter. The special entry 0 turns all of the LEDs on. You can look at the strings to see how the code defines the different letters.
The triple-quoted comment at the end holds a handy reference to help you remember which values correspond to which LEDs. For example, the first - character represents the horizontal LED number 0, the next | represents the vertical LED number 1, and so forth.
The following code shows the string_to_bool function.
def string_to_bool(string):
'''Convert a string of the form 10100110... into a list of Booleans.'''
return [ch == '1' for ch in string]
This function uses a list comprehension that compares each letter in the input string to "1". The result is a list or True and False values corresponding to the string's 0s and 1s. (Actually the function treats anything other than 1 as False so you could use strings with periods or underscores instead of 0s if that's easier to read.)
set_letter
The following set_letter method sets the LED's current letter.
def set_letter(self, letter):
'''Fill the polygons for this letter.'''
self.letter = letter
self.color_leds()
This code saves the new letter and then calls the following color_leds method to color the LEDs appropriately.
def color_leds(self):
'''Color the LEDs for our current letter.'''
for i, is_on in enumerate(self.on_leds[self.letter]):
if is_on:
self.canvas.itemconfig(self.polygons[i], fill=self.on_color)
else:
self.canvas.itemconfig(self.polygons[i], fill=self.off_color)
This method is also pretty simple. It uses the self.on_leds dictionary to get the list of True and False values for the current letter. It enumerates those values and sets the LED polygon colors to the saved on_color or off_color depending on whether its value is True or False.
The end result is that the LEDs are given the proper colors for this letter.
Main Program
To use the Led class, you create a tkinter Canvas widget and then create an Led object passing its constructor the Canvas, the position of the cell's upper left corner, and the other LED parameters like cell width and cell height.
The example program uses the following code to demonstrate the class.
def make_leds(self, wid):
'''Draw sample LEDs.'''
margin = 10
cell_width = 50
cell_height = 80
led_thickness = 7
gap = 1
on_color = 'lime'
off_color = '#004000'
x = y = margin
alphabet = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890') + \
['0/', 0] + list('|+-*$^')
for letter in alphabet:
Led(self.canvas, (x, y), cell_width, cell_height,
led_thickness, gap, on_color, off_color, letter)
x += cell_width + margin
if x + cell_width + margin > wid:
x = margin
y += cell_height + margin
After defining a few geometric constants, the code creates an alphabet holding the characters that it will display. Notice that the special value "0/" represents a zero with a diagonal slash through it. (Look at the define_letters method again.) The alphabet also contains the number 0, which is the special value that turns on all of the LEDs.
Having created the alphabet, the code loops through its contents and creates an Led object for each value, updating the cells' X and Y positions after each.
Conclusion
In my next post, I'll show how you can use the Led class to make a digital clock. Yes, you can easily make a digital clock that uses pretty fonts instead of these retro LED-style letters, but that would be too easy!
Download the example to experiment with it and to see additional details.
|