[C# Helper]
Index Books FAQ Contact About Rod
[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: Draw LED-style letters in C#

[Draw LED-style letters in C#]

Every now and then I've had the urge to write a digital clock program, and to do that, I need to be able to draw LED-style letters. (Some things are practically a right of passage for programmers. Writing a program that can reproduce its own code, prime factoring your Social Security Number, writing clocks...)

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 some of it is rather long.

Most of the code consists of methods that draw the LEDs. Other pieces use those methods to the letters. The following sections describe the main pieces of the LedText class that does most of the interesting work.

The LedText Class

The LedText class draws LED-style letters. The following code shows the class's declaration and constructor.

class LedText { public float CellWidth, CellHeight, LedThickness, Gap; public Dictionary<char, bool[]> LetterLeds = null; public LedText(float cell_width, float cell_height, float led_thickness, float gap) { CellWidth = cell_width; CellHeight = cell_height; LedThickness = led_thickness; Gap = gap; // Define the functions that draw the LEDs. DefineLedFuncs(); // Define the letters. DefineLetters(); } ... }

[Draw LED-style letters in C#]

The class defines several variables that define the text's geometry. The picture on the right shows how those variables determine the size of the letters' pieces.

The LetterLeds dictionary holds an array of Boolean variables for each letter. Those values indicate which of the LEDs should be turned on and off for the corresponding letter. The dictionary uses the letter as a key, so it's easy to find the Boolean values that define the letter.

The class's constructor saves the geometry values and then calls the DefineLedFuncs and DefineLetters methods. The next section describes DefineLedFuncs. I'll describe the DefineLetters method a bit later.

DefineLedFuncs

The DefineLedFuncs method shown in the following code builds an array containing references to the methods that draw the LEDs.

// Make an array to hold the LED-drawing functions. private delegate PointF[] LedFunc(PointF point); private LedFunc[] LedFuncs = null; private void DefineLedFuncs() { LedFuncs = new LedFunc[] { MakeLed0, MakeLed1, MakeLed2, MakeLed3, MakeLed4, MakeLed5, MakeLed6, MakeLed7, MakeLed8, MakeLed9, MakeLed10, MakeLed11, MakeLed12, MakeLed13, }; }

[Draw LED-style letters in C#]

This code first declares a delegate data type named LedFunc. That type refers to a method that takes as a parameter a PointF and returns an array of PointF. The parameter will be the coordinate of the letter's upper left corner, and the returned array will hold the points that define that LED's polygon.

After it declares the LedFunc data type, the code declares an array of those named LedFuncs. The picture on the right shows how the LEDs are numbered. For example, the first entry in the LedFuncs array will hold a reference to the method that builds a polygon for LED number 0, which is a short, wide hexagon.

The DefineLedFuncs method creates the LedFuncs array, filling it with the LED-drawing methods.

Later the drawing methods will use the LedFuncs array to find the methods that they need to draw the LED-style letters.

LED-Drawing Methods

Most of the LED-drawing methods are relatively straightforward. They use fairly simple offsets from the letter's upper left corner to figure out where each polygon's vertices must be.

For example, starting at the letter's upper left corner, the leftmost tip of LED 0 is shifted right and down by half of the LED thickness. The code also adds in the Gap value to provide some separation from the nearby LEDs. (Actually it should probably add the Gap divided by the square root of two or something, but I like the current result so I'm not going to mess with it.)

Several of the LEDs have this basic horizontal hexagon shape, so I wrote a helper method named MakeHLed to define them. I'll show you that shortly, but first here's the MakeLed0 method.

public PointF[] MakeLed0(PointF position) { PointF p1 = new PointF( position.X + LedThickness / 2f + Gap, position.Y + LedThickness / 2f); PointF p2 = new PointF( position.X + CellWidth - LedThickness / 2f - Gap, p1.Y); return MakeHLed(p1, p2); }

This method calculates the coordinates of the hexagon's leftmost and rightmost points p1 and p2. It then calls the following MakeHLed method to create the LED's hexagon. (The "H" in the middle of the name stands for "horizontal.")

public PointF[] MakeHLed(PointF p1, PointF p2) { PointF[] points = { new PointF(p1.X, p1.Y), new PointF(p1.X + LedThickness / 2f, p1.Y + LedThickness / 2f), new PointF(p2.X - LedThickness / 2f, p2.Y + LedThickness / 2f), new PointF(p2.X, p2.Y), new PointF(p2.X - LedThickness / 2f, p2.Y - LedThickness / 2f), new PointF(p1.X + LedThickness / 2f, p1.Y - LedThickness / 2f), }; return points; }

This adds and subtracts halves of the LED thickness to make the hexagon shape starting at the leftmost point p1, moving over to rightmost point p2, and returning to point p1. The code is straightforward, although you do need to be careful while figuring out how to generate the points.

The methods that draw 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 MakeVLed 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, MakeCtLed ("Ct" for "center top") and MakeCbLed ("Cb" for "center bottom").

So far these methods are fairly long but straightforward. The methods that draw the diagonal LEDs are more complicated.

Drawing Diagonal LEDs

[Draw LED-style letters in C#]

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, the 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.)

[Draw LED-style letters in C#]

The picture 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.

public PointF[] MakeLed2(PointF position) { float sqrt2 = (float)Math.Sqrt(2.0); float dx = LedThickness / sqrt2; float dy = dx; PointF u_diag_pt1 = new PointF( position.X + dx, position.Y); PointF u_diag_pt2 = new PointF( position.X + CellWidth, position.Y + CellHeight - dy); PointF l_diag_pt1 = new PointF( position.X, position.Y + dy); PointF l_diag_pt2 = new PointF( position.X + CellWidth - dx, position.Y + CellHeight); PointF u_horz_pt1 = new PointF( position.X, position.Y + LedThickness + Gap); PointF u_horz_pt2 = new PointF( position.X + CellWidth, position.Y + LedThickness + Gap); PointF l_horz_pt1 = new PointF( position.X, position.Y + CellHeight / 2f - LedThickness / 2f - Gap); PointF l_horz_pt2 = new PointF( position.X + CellWidth, position.Y + CellHeight / 2f - LedThickness / 2f - Gap); PointF l_vert_pt1 = new PointF( position.X + LedThickness + Gap, position.Y); PointF l_vert_pt2 = new PointF( position.X + LedThickness + Gap, position.Y + CellHeight); PointF r_vert_pt1 = new PointF( position.X + CellWidth / 2f - LedThickness / 2f - Gap, position.Y); PointF r_vert_pt2 = new PointF( position.X + CellWidth / 2f - LedThickness / 2f - Gap, position.Y + CellHeight); PointF[][] segs = { new PointF[] { l_vert_pt1, l_vert_pt2, u_horz_pt1, u_horz_pt2 }, new PointF[] { u_horz_pt1, u_horz_pt2, u_diag_pt1, u_diag_pt2 }, new PointF[] { u_diag_pt1, u_diag_pt2, r_vert_pt1, r_vert_pt2 }, new PointF[] { r_vert_pt1, r_vert_pt2, l_horz_pt1, l_horz_pt2 }, new PointF[] { l_horz_pt1, l_horz_pt2, l_diag_pt1, l_diag_pt2 }, new PointF[] { l_diag_pt1, l_diag_pt2, l_vert_pt1, l_vert_pt2 }, }; return MakeIntersectionLed(segs); }

This method calculates the locations of points that it can use to define the segments. It then creates an array of arrays named segs. Each entry in the segs array is an array 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 MakeLed2 method passes the array into the following MakeIntersectionLed method.

public PointF[] MakeIntersectionLed(PointF[][] segs) { List points = new List(); foreach (PointF[] seg in segs) { PointF a1 = seg[0]; PointF a2 = seg[1]; PointF b1 = seg[2]; PointF b2 = seg[3]; bool lines_intersect, segs_intersect; PointF intersection, close_pt1, close_pt2; FindIntersection(a1, a2, b1, b2, out lines_intersect, out segs_intersect, out intersection, out close_pt1, out close_pt2); points.Add(intersection); } return points.ToArray(); }

After all that setup, this method is fairly simple. It loops through the segments in the segs array. Each entry in the array contains four points that define two line segments. The method simply calls the FindIntersection method 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.

That's the end of the code that creates polygons to draw the LEDs. The methods that I haven't shown here are similar to the others. You can always download the example if you want to see all of their details.

DefineLetters

Recall that the LetterLeds dictionary maps letters as keys to arrays of Boolean values that indicate which LEDs should be drawn. The following code shows the DefineLetters method that fills the LetterLeds dictionary.

// Define the LED methods used to draw letters. public void DefineLetters() { if (LetterLeds != null) return; LetterLeds = new Dictionary(); LetterLeds.Add((char)0, StringToBool("11111111111111")); LetterLeds.Add('0', StringToBool("11000100100011")); LetterLeds.Add('1', StringToBool("00001100000010")); LetterLeds.Add('2', StringToBool("10000111100001")); LetterLeds.Add('3', StringToBool("10000101000011")); LetterLeds.Add('4', StringToBool("01000111000010")); LetterLeds.Add('5', StringToBool("11000011000011")); LetterLeds.Add('6', StringToBool("11000011100011")); LetterLeds.Add('7', StringToBool("10000100000010")); LetterLeds.Add('8', StringToBool("11000111100011")); LetterLeds.Add('9', StringToBool("11000111000011")); LetterLeds.Add('A', StringToBool("11000111100010")); LetterLeds.Add('B', StringToBool("10010101001011")); LetterLeds.Add('C', StringToBool("11000000100001")); LetterLeds.Add('D', StringToBool("10010100001011")); LetterLeds.Add('E', StringToBool("11000010100001")); LetterLeds.Add('F', StringToBool("11000010100000")); LetterLeds.Add('G', StringToBool("11000001100011")); LetterLeds.Add('H', StringToBool("01000111100010")); LetterLeds.Add('I', StringToBool("10010000001001")); LetterLeds.Add('J', StringToBool("00000100100011")); LetterLeds.Add('K', StringToBool("01001010100100")); LetterLeds.Add('L', StringToBool("01000000100001")); LetterLeds.Add('M', StringToBool("01101100100010")); LetterLeds.Add('N', StringToBool("01100100100110")); LetterLeds.Add('O', StringToBool("11000100100011")); LetterLeds.Add('P', StringToBool("11000111100000")); LetterLeds.Add('Q', StringToBool("11000100100111")); LetterLeds.Add('R', StringToBool("11000111100100")); LetterLeds.Add('S', StringToBool("11000011000011")); LetterLeds.Add('T', StringToBool("10010000001000")); LetterLeds.Add('U', StringToBool("01000100100011")); LetterLeds.Add('V', StringToBool("01001000110000")); LetterLeds.Add('W', StringToBool("01000100110110")); LetterLeds.Add('X', StringToBool("00101000010100")); LetterLeds.Add('Y', StringToBool("00101000001000")); LetterLeds.Add('Z', StringToBool("10001000010001")); LetterLeds.Add(' ', StringToBool("00000000000000")); // -|\|/|--|/|\|- }

This method makes a new LetterLeds dictionary and then adds items to that list.

Each item that it adds has the form of a letter and the result of a call to the StringToBool helper method described shortly. That method converts a string of zeros and ones into an array of corresponding Boolean values.

The DefineLetters method starts by creating a special entry for the letter (char)0. That entry has every LED turned on so it creates a result similar to several of the pictures shown earlier in this post. A bit later you'll see how the program uses that entry when you try to draw a letter that is undefined.

After it defines "letter 0," the code defines entries for the digits 0 through 9, the letters A through Z, and the space character. Feel free to add others if you like.

If you study the entries, you can verify that 0s and 1s identify each letter's LEDs. For example, the Zs entries activate the top and bottom LEDs, plus the upper-right to lower-left diagonal LEDs.

The comment at the end of the method makes it a little easier to create new entries. The -|\|/|--|/|\|- characters show the direction in which the corresponding LEDs point.

The following code shows the StringToBool helper method.

// Convert a string of the form 10100110... // into an array of bool. private bool[] StringToBool(string values) { bool[] result = new bool[values.Length]; for (int i = 0; i < values.Length; i++) result[i] = (values[i] == '1'); return result; }

This method creates an array of bool that has the same length as its input string. It then loops through the string's characters and sets the corresponding array entries to true if the character is 1. When it is finished, the method returns the array.

Drawing Letters

The class does a lot of work to set up the LetterLeds dictionary and the LedFuncs array. Using them is a lot easier.

The following MakeLetterPgons method returns list of polygons needed to draw a letter.

// Make the polygons that represent a letter. public void MakeLetterPgons(char letter, PointF position, out List<PointF[]> used_pgons, out List<PointF[]> unused_pgons) { used_pgons = new List<PointF[]>(); unused_pgons = new List<PointF[]>(); bool[] used; if (LetterLeds.ContainsKey(letter)) used = LetterLeds[letter]; else used = LetterLeds[(char)0]; for (int i = 0; i < used.Length; i++) { if (used[i]) used_pgons.Add(LedFuncs[i](position)); else unused_pgons.Add(LedFuncs[i](position)); } }

This method uses output parameters to return polygons for the LEDs that should and should not be used to draw the letter.

The method first creates the two lists. It then uses the LetterLeds dictionary's ContainsKey method to see if the dictionary contains an entry for the letter. If the letter is in the dictionary, then the code fetches its Boolean values and saves them in the used array. If the letter is not in the dictionary, then the code uses the "letter 0" entry's Boolean values so the letter will be drawn with all LEDs active as a sort of placeholder. (Alternatively you could use the space character to make a blank result.)

The code then loops through the Boolean values in the used array. If an entry is true, the program calls the corresponding LedFuncs method to generate the LED and adds it to the used_pgons list. If the used array entry is false, the program calls the LedFuncs method and saves the result in the unused_pgons list.

The following DrawLetter method uses the MakeLetterPgons method to draw a letter.

// Draw a letter. public void DrawLetter(Graphics gr, Brush bg_brush, Brush used_brush, Pen used_pen, Brush unused_brush, Pen unused_pen, PointF position, char letter) { // Clear the background. gr.FillRectangle(bg_brush, position.X, position.Y, CellWidth, CellHeight); // Draw the polygons. List<PointF[]> used_pgons, unused_pgons; MakeLetterPgons(letter, position, out used_pgons, out unused_pgons); foreach (PointF[] pgon in unused_pgons) { gr.FillPolygon(unused_brush, pgon); gr.DrawPolygon(unused_pen, pgon); } foreach (PointF[] pgon in used_pgons) { gr.FillPolygon(used_brush, pgon); gr.DrawPolygon(used_pen, pgon); } }

This method fills the letter's background area with the brush bg_brush. Normally you would probably want the whole drawing area to have this same background color. In that case, this step isn't really necessary. In fact, if you have some sort of drawing already on the drawing area and you want to draw the letter on top, you might need to not do this. You can pass the brush SolidBrushes.Transparent in as the bg_brush parameter to leave the background unchanged.

Next the code calls the MakeLetterPgons method to get the letter's polygons. It then loops through the polygons representing inactive LEDs and fills and then outlines them. As with the background, you can pass a transparent brush or pen for the unused_brush and unused_pen parameters if you don't want to draw one of those items.

The method repeats those steps for the active LEDs and is done. Simple!

Drawing Text

The LedText class has one more method: DrawText. This method uses the following code to draw a string of letters.

// Draw a sequence of letters. public void DrawText(Graphics gr, Brush bg_brush, Brush used_brush, Pen used_pen, Brush unused_brush, Pen unused_pen, PointF position, float h_spacing, string text) { foreach (char ch in text) { DrawLetter(gr, bg_brush, used_brush, used_pen, unused_brush, unused_pen, position, ch); position.X += CellWidth * h_spacing; } }

The code loops through the input string's letters. The code calls DrawLetter to draw each letter and then adds h_spacing times the letters' cell width to the X position to draw the next letter shifted to the right.

Normally h_spacing should be something like 1.2 to allow a little space between the letters. You can make that space wider, narrower, or even negative if you like.

The Main Program

The main program includes several test methods. The following method is initially not commented out so the program draws the digits 0 through 9 and the letters A through Z.

private void TestLetters(Graphics gr) { gr.Clear(Color.Black); gr.SmoothingMode = SmoothingMode.AntiAlias; const float margin = 10; const float cell_width = 50; const float cell_height = 80; const float led_thickness = 7; const float gap = 1.5f; LedText letter = new LedText( cell_width, cell_height, led_thickness, gap); Brush bg_brush = Brushes.Black; Brush used_brush = Brushes.Lime; Pen used_pen = Pens.Transparent; Brush unused_brush = new SolidBrush(Color.FromArgb(0, 40, 0)); Pen unused_pen = Pens.Transparent; PointF position = new PointF(margin, margin); letter.DrawText(gr, bg_brush, used_brush, used_pen, unused_brush, unused_pen, position, 1.2f, "ABCDEFGHI"); position.Y += letter.CellHeight * 1.2f; letter.DrawText(gr, bg_brush, used_brush, used_pen, unused_brush, unused_pen, position, 1.2f, "JKLMNOPQR"); position.Y += letter.CellHeight * 1.2f; letter.DrawText(gr, bg_brush, used_brush, used_pen, unused_brush, unused_pen, position, 1.2f, "STUVWXYZ0"); position.Y += letter.CellHeight * 1.2f; letter.DrawText(gr, bg_brush, used_brush, used_pen, unused_brush, unused_pen, position, 1.2f, "123456789"); }

This method defines some geometry values and creates a LedText object. It then makes a PointF variable named position to keep track of where the text should be drawn.

The code then calls the DrawText method to draw the string "ABCDEFGHI." Nxet it adds 1.2 times the letters' cell height to the position variable's Y coordinate to move down a line. It repeats those steps, drawing a string of letters and then moving down to a new line, until it has drawn the letters and digits.

Conclusion

This is not a font. You can go online and download LED fonts, but I wanted to build LED-style letters myself and I still don't know how to create a font from scratch in C#. For now, this program is pretty fun.

Download the example to see all of the details and to experiment with it. Try changing the letter geometry, colors, and brushes. For example, the picture below draws "3" with yellow LEDs outlined in red and "5" with LEDs filled with a green and lime brick pattern.

[Draw LED-style letters in C#]

Download the example to experiment with it and to see additional details.

© 2009-2023 Rocky Mountain Computer Consulting, Inc. All rights reserved.