Beginning Java ME: A Simple Mandelbrot Viewer

Version 3.0 of the Java ME SDK from Sun makes it very easy to start developing Java applications for mobile devices. The SDK comes with a full-fledged (and surprisingly usable) IDE, as well as a suite of emulators and example applications, all of which allows you to pick up and start writing your own apps in no time. I’ve always wanted to develop a few small applications that I could run from my phone, so I thought this might be a good time to start.

My first idea was to develop a very basic Mandelbrot set renderer and zoomer. Sure, it’s not very "useful," but it’s definitely pretty, and you’ll know that you’ll always have one of the defining symbols of modern mathematics right at your fingertips.

And, of course, it’s an awesome way to pick up chicks at the bar or dance club. I mean, come on, flash ’em the old Mandelbrot set, and they’ll be all over you, am I right? Anyone?

Just as a refresher, the Mandelbrot set is plotted by iterating through the complex quadratic polynomial zn+1 = zn2 + c, where c is each “pixel” within the range of the complex plane we want to plot. If the sequence is bounded for a given c (within a certain number of iterations), the point is considered part of the Mandelbrot set, and the pixel is colored black. If the sequence is unbounded for a given c, the color of the pixel is determined by how “fast” the sequence diverges. The more iterations we take for our calculation, the more “precise” the set’s boundary will be.

Here are some fundamental requirements for the application:

  • Render the Mandelbrot set using a predefined color palette
  • Allow the user to "move" the drawing by pressing the L, R, U, and D keys
  • Allow the user to "zoom" in and out of the Set by pressing, say, the 1 and 3 keys
  • Allow the user to decrease/increase iterations by pressing, oh I don’t know, the 7 and 9 keys, respectively

Creating a New Project

Now that we have some requirements, let’s get down to business. We’ll create a brand new project in the Java ME SDK, and we’ll call it something original, like Mandelbrot:

screenshot1.png

Using all of the defaults for the purposes of this project is just fine. The SDK should generate a project that targets CLDC 1.1 (Connected Limited Device Configuration) and MIDP 2.0 (Mobile Information Device Profile). It will also automatically create a MIDlet class that represents our new application.

The automatically-generated class descends from the base MIDlet class, and implements the CommandListener interface, which enables our app to "listen" to commands that we can assign to buttons on your phone. The class will be called something like HelloMIDlet, but we can easily rename it to something more pertinent like MandelbrotApp, and erase the "hello world" code so we have a totally clean class.

Theoretically, however, in addition to a class that extends MIDlet, we’ll also need a class that extends the base class Canvas (we’ll call it MandelbrotCanvas), so that we can perform any kind of graphics operations we need. This class will also be responsible for capturing keystrokes from the phone’s keypad (this is different from capturing "commands" which the MIDlet class does).

The Code

Let’s first go over the MandelbrotApp class. Here are the first few lines of the class, followed by some explanation:

public class MandelbrotApp extends MIDlet implements CommandListener {
    private Display myDisplay;
    private MandelbrotCanvas myCanvas;
    private Command exit = new Command ("Exit", Command.EXIT, 1);
    private Command about = new Command ("About", Command.ITEM, 2);

    private int[] colorPalette;
    private int[] scanlineBuffer;
    private int screenWidth = 0, screenHeight = 0;

    public float rangex1 = -2F, rangex2 = 0.5F, rangey1 = -1.4F, rangey2 = 1.4F;
    public int numIterations = 24;


    public MandelbrotApp () {
        super ();

        myDisplay = Display.getDisplay (this);
        myCanvas = new MandelbrotCanvas (this);
        myCanvas.setCommandListener (this);
        myCanvas.addCommand (exit);
        myCanvas.addCommand(about);

        scanlineBuffer = null;
        colorPalette = new int[256];

        for(int i=0; i<64; i++)
            colorPalette[i] = (((i * 4) << 8) | ((63 - i) * 4));
        for(int i=64; i<128; i++)
            colorPalette[i] = ((((i - 64) * 4) << 16) | (((127 - i) * 4) << 8));
        for(int i=128; i<192; i++)
            colorPalette[i] = (((255) << 16) | ((i - 128) * 4));
        for(int i=192; i<256; i++)
            colorPalette[i] = ((((255 - i) * 4) << 16) | (255));

    }

In the above code, we perform some initialization of things we'll use later on. First we create an instance of our canvas object (MandelbrotCanvas), and assign two commands ("exit" and "about") to the canvas. This means that when the canvas becomes visible, these two commands will become available from the two top buttons of your handset's keypad. Also, by setting the canvas' CommandListener to this, we're saying that this class will handle the commands issued while the canvas is displayed. The variables rangex1, rangex2, rangey1, and rangey2 represent the initial boundaries for our Mandelbrot calculation. By manipulating these variables we can pan and zoom in and out of the image.

We also define a color palette that we'll use when rendering the Mandelbrot set. The color palette contains 256 entries, and is simply a gradient of colors from red, to green, to blue, and back to red. Additionally, we define a variable called scanlineBuffer which will actually contain the screen contents before they're painted onto the screen. We leave this initialized to null, because we'll dynamically allocate this buffer the first time our paint event is called.

Next, we'll create a function that actually renders the Mandelbrot set onto our screen buffer. This function uses the standard Mandelbrot algorithm without any fancy attempts at optimization, so it may be slower for some phones than others (on some phones it will be god-awful slow; sorry!).

    public void RenderMandelbrot(){
        if((myCanvas.getWidth() != screenWidth) || (myCanvas.getHeight() != screenHeight)){
            screenWidth = myCanvas.getWidth();
            screenHeight = myCanvas.getHeight();
            scanlineBuffer = new int[screenWidth * screenHeight];
        }

        float bmpWidth = (float)screenWidth;
        float bmpHeight = (float)screenHeight;

        float x, y, xsquare, ysquare, dx, dy, bail = 4, j, p;
        int i, mul, col;
        int xpos, ypos;
        float[] q = null;

        if(screenWidth > screenHeight) q = new float[screenWidth + 1];
        else q = new float[screenHeight + 1];

        mul = 255 / numIterations;
        dx = (rangex2 - rangex1) / bmpWidth;
        dy = (rangey2 - rangey1) / bmpHeight;

        q[0] = rangey2;
        for(i=1; i < q.length; i++) q[i] = q[i - 1] - dy;
        xpos = 0; ypos = 0;

        for(p = rangex1; p <= rangex2; p += dx){
            i = 0;
            for(j = rangey1; j <= rangey2; j += dy){
                x = 0; y = 0; xsquare = 0; ysquare = 0; col = 1;
                while(true){
                    if(col > numIterations){
                        scanlineBuffer[ypos*screenWidth + xpos] = 0;
                        break;
                    }
                    if((xsquare + ysquare) > bail){
                        scanlineBuffer[ypos*screenWidth + xpos] = colorPalette[(col*mul)%255];
                        break;
                    }
                    xsquare = x * x;
                    ysquare = y * y;
                    y *= x;
                    y += (y + q[i]);
                    x = xsquare - ysquare + p;
                    col++;
                }
                i++;
                ypos++;
                if(ypos >= screenHeight) break;
            }
            myCanvas.repaint();
            myCanvas.serviceRepaints();
            xpos++;
            if(xpos >= screenWidth) break;
            ypos = 0;
        }
    }

Notice in the above function that, inside the outer loop, we force a repaint of the canvas, so that the user gets a sense of the graphic actually being drawn in real time. If we didn't do this, the app would be totally unresponsive until all of the image is rendered.

When the app is started, the following function is called, where we set the canvas to be the currently-displayed object, and call the Mandelbrot rendering function:

    public void startApp () throws MIDletStateChangeException {
        myDisplay.setCurrent (myCanvas);
        RenderMandelbrot();
    }

Another point of interest in this class is the paint handler. This function actually gets called from the Canvas class (see lower), but I put the paint code in this class for convenience.

    public void paint (Graphics g) {

        g.drawRGB(scanlineBuffer, 0, screenWidth, 0, 0, screenWidth, screenHeight, false);

        g.setColor(0xFFFFFF);
        int fontHeight = g.getFont().getHeight();
        int strY = 4;
        g.drawString("(C) Dmitry Brant", 4, strY, 0); strY += fontHeight;
        g.drawString("Iterations: " + Integer.toString(numIterations), 4, strY, 0); strY += fontHeight;
    }

In the above function, all we do is draw our screen buffer to the screen, then write some text over the image, which includes a little copyright message and the current number of iterations used in the Mandelbrot calculation. Note that we dynamically get the height of the phone's font (and adjust accordingly), since the font varies greatly between different phone models.

The other interesting function in this class is the command handler. This function will be called when either the "Exit" or "About" commands are pressed while our app is running. If "Exit" is pressed, we'll destroy the application. If "About" is pressed, we'll display a simple Alert message:

    public void commandAction (Command cmd, Displayable disp) {
        if (cmd == exit) {
            destroyApp (true);
        }
        else if(cmd == about){
            Alert alert = new Alert ("About...");
            alert.setType (AlertType.INFO);
            alert.setTimeout (Alert.FOREVER);
            alert.setString ("Copyright 2009 Dmitry Brant.\nhttp://dmitrybrant.com");
            myDisplay.setCurrent (alert);
        }
    }

Finally, let's have a quick look at the MandelbrotCanvas class:

class MandelbrotCanvas extends Canvas {
    MandelbrotApp myApp;

    MandelbrotCanvas (MandelbrotApp mandelTestlet) {
        myApp = mandelTestlet;
    }

    void init () {
    }

    void destroy () {
    }

    protected void paint (Graphics g) {
        myApp.paint (g);
    }

    protected void keyPressed (int key) {
        int action = getGameAction (key);

        float xScale = (myApp.rangex2 - myApp.rangex1);
        float yScale = (myApp.rangey2 - myApp.rangey1);

        boolean gotAction = true, gotKey = true;
        switch (action) {
        case LEFT:
            myApp.rangex1 += (xScale / 16.0F);
            myApp.rangex2 += (xScale / 16.0F);
            break;
        case RIGHT:
            myApp.rangex1 -= (xScale / 16.0F);
            myApp.rangex2 -= (xScale / 16.0F);
            break;
        case UP:
            myApp.rangey1 -= (yScale / 16.0F);
            myApp.rangey2 -= (yScale / 16.0F);
            break;
        case DOWN:
            myApp.rangey1 += (yScale / 16.0F);
            myApp.rangey2 += (yScale / 16.0F);
            break;
        case FIRE:
        default:
            gotAction = false;
        }

        if(!gotAction){
            switch (key){
            case KEY_NUM1:
                myApp.rangex1 -= (xScale / 4.0F);
                myApp.rangex2 += (xScale / 4.0F);
                myApp.rangey1 -= (yScale / 4.0F);
                myApp.rangey2 += (yScale / 4.0F);
                break;
            case KEY_NUM3:
                myApp.rangex1 += (xScale / 4.0F);
                myApp.rangex2 -= (xScale / 4.0F);
                myApp.rangey1 += (yScale / 4.0F);
                myApp.rangey2 -= (yScale / 4.0F);
                break;
            case KEY_NUM7:
                myApp.numIterations-=4; if(myApp.numIterations < 2) myApp.numIterations = 2;
                break;
            case KEY_NUM9:
                myApp.numIterations+=4;
                break;
            default:
                gotKey = false;
            }
        }

        if(gotAction || gotKey)
            myApp.RenderMandelbrot();
    }

The only relevant functions in the above class are the paint function, which is called automatically whenever the screen needs repainting, and the keyPressed function, which gets called when the user presses any of the keys on the keypad.

Notice how pressing the Up/Down/Left/Right buttons causes the x- and y-ranges to be panned to simulate the effect of scrolling, and the "1" and "3" keys have the effect of zooming. Keys "7" and "9" are also programmed to decrease and increase the number of iterations by 4. After any key is pressed, the graphic is redrawn due to RenderMandelbrot() being called again.

Testing the App

That's about all there is to it. The next step is to test how the app works. If we run the app from the IDE of the Java ME SDK, it automatically launches a default emulator and loads the app onto it:

screenshot2.png

Seems to work fine on the emulator! Now how about getting it onto an actual phone? Building the app produces two files: a .JAR file, which is the actual app, and a .JAD file, which is a text file that contains certain descriptions about the app (such as the author, copyright, and URL). But what do we do with these files?

Loading a Java app onto a phone can be done in a few different ways:

  • Download from the Web
  • Download over Bluetooth
  • Download with Java App Loader (Motorola phones only)
  • Load from a memory card
  • Hack right into the phone

I'm fortunate enough to have a Motorola RAZR V3xx, which has a microSD slot. I was amazed how easy it was to get it up and running on this phone. Here's all that's required to install a Java app onto this phone (and probably similar Motorola models that accept a microSD card):

  • With the microSD card in the phone, connect a USB cable from your computer to the phone (it will map as a disk drive)
  • Copy your app's .JAR and .JAD files to the /mobile/kjava directory
  • Disconnect the USB cable from the phone
  • On the phone, go to Menu -> My Stuff -> Games & Apps -> [Install New], and select your app from the list
  • That's it! The app will now be installed on the phone!

Installing over the Web

screenshot3.jpg

If you don't have a phone that lets you install apps from a microSD card, you can download the app from the Web using your phone. This is, of course, only if your phone has an active account and supports Web access. You'll also probably be paying a few cents for the data transfer, depending on the wireless plan you purchased from your carrier. Oh, and of course, your phone must support CLDC 1.1 (1.0 doesn't have floating-point support), and MIDP 2.0, which most modern phones do.

Now then, if you want to load an app that you wrote onto your phone over the Web, you need to have access to a web server where you can place your app to be downloaded. For my example, I'll use my web server, dmitrybrant.com, and I'll put the app in a subdirectory, like so:

I put both the .JAR and .JAD files in the "jme" subdirectory. To download the app, you only need to link to the .JAD file. But wait! There's one more crucial step to take before our app can be downloaded from a phone. We need to edit the .htaccess file in this directory, and add the proper MIME types for our files:

AddType text/vnd.sun.j2me.app-descriptor jad
AddType application/java-archive jar

Once this is done, we can try downloading the app to our phone. After a minute of fat-fingering the URL, agreeing to install an unsigned app, and waiting for it to finish installing... lo and behold:

For the record, the above screen took about 20 seconds to draw on my V3xx. Your phone may be significantly slower (or faster, but probably not). So be prepared to wait a bit for the drawing to complete. Or go ahead and optimize the code to use integer-only math! (Let me know if you do!)

Browse the source code for this project on GitHub.

Remembering a dear friend

Today marks the death of one of our most beloved friends, Robin Klauss, at the heartbreaking age of 29. I remember clearly when, during one of our countless karaoke nights which were usually filled with joy and laughter, she revealed the news of her cancer diagnosis. During the previous weeks, she had noticed a small bump on the back of her neck, which she thought might be a spider bite. But the bump wouldn’t go away, and eventually she started noticing a decrease in mobility in her neck and arm. After a visit to the doctor, she was told that it’s not a spider bite, but cancer  – a tumor in her neck that had already begun to invade her vertebrae and spinal cord.

During her experience with cancer, she wrote a few brief posts on her LiveJournal account, which I thought I’d re-post here, since that account is obviously no longer maintained, and will likely be shut down eventually. Here are the posts in chronological order (take note of the startlingly fast progression of the disease):

Jun. 18th, 2008
I’ve been diagnosed with adenocarcinoma of an unknown primary. Being an unknown primary it’s automatically labeled stage IV. Thought you might like to know.

Jun. 19th, 2008
My throat hurts so bad from the radiation. I feel like there’s this giant scab on the back of my throat and that I’m swallowing steel wool. The radiation oncologist that is on-call tried to call in a prescription for some kind of numbing something or-other but the pharmacist doesn’t have the lidocaine or whatever to mix it. 🙁 I tried to use chloroseptic spray but I couldn’t get it far enough back (and it tastes like shit). I so fucking hate this.

Jul. 2nd, 2008
So, today I had my 1 month follow up after the radiation surgery. On the bright side, all three of the docs I saw today were ecstatic about my progress. The neuro-oncologist about fell out of her chair when she was my arm movement was back. She said I am “the poster child for stereo-tactic radio-surgery.” 🙂 No chemo yet. 🙂 I have another follow up in 2 months to see how the tumor is doing. So, if it’s still there, then maybe chemo? No clue. I am having some pain at the top of my head on the right side. My medical oncologist said to keep an eye on it and let him know if it gets worse or if I notice any new pain. The main goal is to keep an eye out for any new metastases. However, the pain I’m having on my left side may be due to how the tumor destroyed the C3 and C4 vertebrae. There may be instability in my neck and the ligaments may have relaxed. Whatever that means. My neuro-oncologist ordered 6 x-rays of my neck and will be presenting my case to a spinal tumor board tomorrow to see if other doctors and neurosurgeons think I should have surgery on my neck to help stabilize it. The last thing I want is surgery and to miss work, however, I am also completely terrified that I will crack my neck because of how fragile it is. I doubt I’ll find out what she has to say tomorrow, hopefully on Monday.

Jul. 15th, 2008
Yesterday I had an appointment with a neurosurgeon regarding strengthening my neck. First, and most importantly, he was really hot. His PA wasn’t bad looking, either. Lucky me! Second, I need to have neck surgery next Friday, July 25. He said the when the cancer invaded my C3 and C4 vertebrae, it replaced bone with cancer. So now my neck is slowly collapsing to the right side and fracturing because there’s nothing there to support it. Surgery doesn’t have to be done ASAP but much sooner rather than later. Basically, if I don’t have the surgery, my neck will break. He said something about putting mesh cages and bone graft or something in the destroyed vertebrae. There will be two incisions, one in the front, then they’ll flip my fat ass over and then another in the back. I don’t know how big the front incision will be but he said the back will be about 4″. I’ll spend 5-6 days in the hospital and about a week in rehab and then recover for 2-4 more weeks. Yup. I’m a lucky girl!

Aug. 1st, 2008
I had the surgery last Friday. The surgery itself was over 11 hours long. They wheeled me away at 7:30am and my parents didn’t get to see me again until around 9:00 that night. My mom said, for some reason, the surgery didn’t start until 9:30ish but I only remember them wheeling me away, asking me a couple of questions and being in the operating room for about 3 minutes and I can’t remember anything else. For some reason hot doc didn’t do the mesh cage thing he was talking about and instead did metal plates and screws. This will make it a blast to travel. I’ll be setting off metal detectors like a freakin’ terrorist. Okay, it won’t be that bad. He did say that the plates are not supposed to set off metal detectors but I will have some card to say that I have this stuff in my neck to get me out of jail. After surgery they checked my breathing and I wasn’t breathing on my own so the intubated me with a different/smaller kind of breathing tube and I was unconscious in ICU until they woke me up to extubate me on Sunday morning. They didn’t move me into a regular room until Monday afternoon and I was finally discharged Wednesday afternoon. I didn’t have to go to rehab because 1.) my pain is negligible and 2.) I’m moving surprisingly well. Hot doc took a bone graft from my right hip and, right now, that’s what hurts and makes life difficult. I freakin’ walk like Frankenstein’s monster because I have to take it easy. My throat is also killing me, not only because of the intubation, but also because of the surgery. Hot doc said he would have to move my esophagus aside to be able to get to the vertebrae and the pain is normal and should go away soon. It definitely is getting better every day but it still hurts like hell. I have to wear a neck brace at all times except when I shower or eat. I can’t bend or twist my head and no lifting anything over 10 pounds. I’m recovering at my parent’s house, where I’ll be for the next 5 weeks. 5 weeks! Agh! And I can’t drive because I can’t turn my head so I am literally depending on them for EVERYTHING! I feel like a 2 year old. My mom makes my food and does my laundry and they’re both there when I go for a walk because they need to be there in case I fall or something. Because the biopsy was so unclear, hot doc removed some of the tumor and sent it off to pathology. Hopefully it will tell us more about what kind of cancer this is and the best way to treat it. He definitely thinks chemo is a good idea, so that will be starting soon. Everyone keeps saying I shouldn’t be so sure that it will be the kind where I’ll lose my hair, but I’m pretty damn sure it will be. I wonder if bald is is this season? Also, I have to renew my driver’s license in September… What will it say under hair color? Bald?

Aug. 24th, 2008
So nothing new has been happening. I was hoping to be back home this weekend but I’m still at my parent’s house. Hopefully I will be back in C-bus next weekend. The incision on the back of my head isn’t healing well. It seems like a couple of stitches popped before it was healed so now I have a hole in the back of my head. My dad said it’s slightly smaller than a dime but it’s not really healing. I saw my hot neurosurgeon on Tuesday because of it not healing. He had us put packing in the hole for a few days and now we’re doing this wet/dry thing where my mom has to clean the hole out with saline and then put dry gauze on it. We’re supposed to do it 4 times a day. He wants to see me again tomorrow and said that he didn’t want me to go back to work yet. I’ll find out tomorrow if he’ll let me go back to work next week. I really don’t mind seeing him again. He’s so freakin’ hot! He called me at my parent’s house yesterday morning to see how it’s healing. He really had to do some searching because I changed all of my phone numbers in my chart to be my cell, house and work numbers. I really have no clue where he got there number. And he said he wants to see me when I have my next oncologist appointment in September. Then I’ll also see him when I have my next follow up with him in October. I think he’s just smitten with me and he’s too shy to make his move. Okay. I’m delusional but it could happen, right?

Sep. 8th, 2008
I went in for my three month follow up today. The tumor has grown 1 cm since the beginning of July and it looks like some new lymph nodes are involved. My oncologist wants me to have a PET scan to see if the lymph nodes are really involved with the cancer or if they’re just inflamed from the surgery. He offered me three options to deal with the cancer: 1) do nothing, which is not an option at this point, 2) some kind of clinical trial that involves one type of chemo, given once every three weeks, and a daily pill that is used to treat TB, 3) a chemo cocktail that is three different kinds of chemo given at once. The clinical trial will waste too much of my FMLA time. I only have about 5 weeks left. The trial is divided into 2 different phases for a total of 37 days straight. Out of those 37 days, I would need to be at the hospital for at least 13 of the days. That’s not including days that I feel too tired or whatever to go to work. Following the 37 days, it would be just chemo once every three weeks until my doc says it’s done. I don’t like the idea of wasting all of those FMLA days. That will leave me with about 12 days for chemo after the first 37 days and for days I don’t feel well or am too tired. My mom thinks I’m choosing work over my health but she fails to remember that it’s because of work that I have health insurance to pay for all of this. If I lose my job, I lose my health insurance. Also the chemo is only given on Mondays. That leave no time for me to recover before I have have to drive 2 1/2 hours back to Columbus. So I’m opting for the third choice, the three chemo cocktail. It’s also once every 3 weeks but it doesn’t require the time commitment the trail does. I am going to wait to get a second opinion. I want to see a doc at the James but I’m having a hell of a time getting in to see someone. I would much rather have chemo here and recoup at my place. My job this week is to be a pain in the ass and call every doc I know at the James to get someone to refer me to a medical oncologist. Hopefully I won’t be dicked around.

Sep. 10th, 2008
I was able to find an oncologist at the James! I called someone, who talked to someone else and said, “Oh, yes, I remember talking to Carrie about you before. You mean no one ever called you to schedule an appointment?” *head desk* So I’m totally not going to go back to see that guy in Cleveland. It works out so much better for me to see the oncologist here. I have to take a whole day off work for a 30 minute appointment, versus just a couple of hours. And I’ve already seen a radiation oncologist and a neurosurgeon at the James who have said in the past that they would take care of my 3 month check-ups, if I want them to. What the Cleveland oncologist said is that this thing will never go away. We can only hope to decrease or maintain the size of it before it spreads further. Fantastic. With my birthday coming up it makes me wonder, how many more birthdays will I have?

Sep. 24th, 2008
I finally got to see the new oncologist at the James today. He wants me to have a PET scan tomorrow and start chemo on Friday. When I told my mom and my co-workers everyone had the same look of “holy crap! so soon!” on their faces (well, in the case of my mom, she said it). It’s about time. I’m in so much pain. The pain killers I have stopped working so well last week. I hate taking them and I’m totally afraid I’m going to get addicted to them but I really do need them to function.

She didn’t do any more writing after that, likely because of the effects of the pain killers or the chemo. And of course we hardly saw her anymore because the medications and the pain kept her virtually immobile at home, and cared for by her family.

In her second-to-last post, Robin asks how many more birthdays she would get to celebrate (she had just turned 29). The answer turned out to be zero. The cancer would claim her life just over a year after the initial diagnosis. That’s the cruel, cold, indiscriminate nature of this disease. Once we get past the bewilderment of contemplating that this could happen to any of us, all we can do is cherish every moment we get with our friends and loved ones, and remember the happiness and warmth that Robin brought to our lives.

Here is Robin during a random night out, expressing what would later become her response to cancer:
(taken from my crappy Motorola phone at the time)

Seven-Segment Display for .NET

Seven-segment displayI’m usually not a big fan of custom controls except in the most extreme circumstances. From the point of view of usability, it should always make the most sense to use the controls that are shipped with the Operating System. Your user base is already familiar with the OS’s native controls, so creating custom controls would only add to the learning curve for your application. But I digress. Sometimes, there are certain controls that just beg to be written, whether they’re useful or not.

That’s why I decided to write this seven-segment LED control: not because it’s any more "useful" than a standard Label control, but because it looks freakin’ sweet. I also wrote the control to become more familiar with the internals of C# and .NET in general. And, if you like the control and are able to use it, or learn from it, so much the better.

Even if you haven’t heard the name "seven-segment display" before, you’ve probably seen quite a few in your lifetime. They appear on pretty much every piece of electronic equipment that needs to display numbers for any reason, like the timer on a microwave oven, the display on a CD player, or the time on your digital wristwatch.

They’re called seven-segment displays because they’re actually made up of seven "segments" — seven individual lights (LEDs or otherwise) that light up in different patterns that represent any of the ten digits (0 – 9).

So, what are you waiting for? Download the test application which the screen shot is from. (Or browse the source code on GitHub)

Using the code

This custom control can be built into your application by simply including the "SevenSegment.cs" file in your project. Rebuild your project, and you’ll be able to select the SevenSegment control from your tool palette and drop it right onto your forms.

To replicate the look of a seven-segment display, I draw seven polygons that precisely match the physical layout of a real display. To model the polygons, I drew them out on graph paper, and recorded the coordinates of each point in each polygon. To draw the polygons on the control, I use the FillPolygon function, passing it the array of points that represent the polygon. Let’s examine the control’s Paint event to see exactly what’s going on:

private void SevenSegment_Paint(object sender, PaintEventArgs e)
{
	//this will be the bit pattern that gets shown on the segments,
	//bits 0 through 6 corresponding to each segment.
	int useValue = customPattern;
	
	//create brushes that represent the lit and unlit states of the segments
	Brush brushLight = new SolidBrush(colorLight);
	Brush brushDark = new SolidBrush(colorDark);

	//Define transformation for our container...
	RectangleF srcRect = new RectangleF(0.0F, 0.0F, gridWidth, gridHeight);
	RectangleF destRect = new RectangleF(Padding.Left, Padding.Top, this.Width - Padding.Left - Padding.Right, this.Height - Padding.Top - Padding.Bottom);
	
	//Begin graphics container that remaps coordinates for our convenience
	GraphicsContainer containerState = e.Graphics.BeginContainer(destRect, srcRect, GraphicsUnit.Pixel);

	//apply a shear transformation based on our "italics" coefficient
	Matrix trans = new Matrix();
	trans.Shear(italicFactor, 0.0F);
	e.Graphics.Transform = trans;

	//apply antialiasing
	e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
	e.Graphics.PixelOffsetMode = PixelOffsetMode.Default;

	// Draw elements based on whether the corresponding bit is high!
	// "segPoints" is a 2D array of points that contains the segment coordinates to draw
	e.Graphics.FillPolygon((useValue & 0x1) == 0x1 ? brushLight : brushDark, segPoints[0]);
	e.Graphics.FillPolygon((useValue & 0x2) == 0x2 ? brushLight : brushDark, segPoints[1]);
	e.Graphics.FillPolygon((useValue & 0x4) == 0x4 ? brushLight : brushDark, segPoints[2]);
	e.Graphics.FillPolygon((useValue & 0x8) == 0x8 ? brushLight : brushDark, segPoints[3]);
	e.Graphics.FillPolygon((useValue & 0x10) == 0x10 ? brushLight : brushDark, segPoints[4]);
	e.Graphics.FillPolygon((useValue & 0x20) == 0x20 ? brushLight : brushDark, segPoints[5]);
	e.Graphics.FillPolygon((useValue & 0x40) == 0x40 ? brushLight : brushDark, segPoints[6]);

	//draw the decimal point, if it's enabled
	if (showDot)
		e.Graphics.FillEllipse(dotOn ? brushLight : brushDark, gridWidth - 1, gridHeight - elementWidth + 1, elementWidth, elementWidth);

	//finished with coordinate container
	e.Graphics.EndContainer(containerState);
}

You can set the value displayed in the control through two properties: Value and CustomPattern. The Value property is a string value that can be set to a single character such as "5" or "A". The character will be automatically translated into the seven-segment bit pattern that looks like the specified character.

If you want to display a custom pattern that may or may not look like any letter or number, you can use the CustomPattern property, and set it to any value from 0 to 127, which gives you full control over each segment, since bits 0 to 6 control the state of each of the corresponding segments.

The way it’s done in the code is as follows. I have an enumeration that encodes all the predefined values that represent digits and letters displayable on seven segments:

public enum ValuePattern
{
    None = 0x0, Zero = 0x77, One = 0x24, Two = 0x5D, Three = 0x6D,
    Four = 0x2E, Five = 0x6B, Six = 0x7B, Seven = 0x25,
    Eight = 0x7F, Nine = 0x6F, A = 0x3F, B = 0x7A, C = 0x53,
    D = 0x7C, E = 0x5B, F = 0x1B, G = 0x73, H = 0x3E,
    J = 0x74, L = 0x52, N = 0x38, O = 0x78, 
    P = 0x1F, Q = 0x2F, R = 0x18,
    T = 0x5A, U = 0x76, Y = 0x6E,
    Dash = 0x8, Equals = 0x48
}

Notice that each value is a bit map, with each bit corresponding to one of the seven segments. Now, in the setter of the Value property, I compare the given character against our known values, and use the corresponding enumeration as the currently displayed bit pattern:

//is it a digit?
int tempValue = Convert.ToInt32(value);
switch (tempValue)
{
	case 0: customPattern = (int)ValuePattern.Zero; break;
	case 1: customPattern = (int)ValuePattern.One; break;
	...
}
...
//is it a letter?
string tempString = Convert.ToString(value);
switch (tempString.ToLower()[0])
{
	case 'a': customPattern = (int)ValuePattern.A; break;
	case 'b': customPattern = (int)ValuePattern.B; break;
	...
}

Either way, the bit pattern to be displayed in the control ends up in the customPattern variable, which is then used in the Paint event as shown above.

You can also "italicize" the display by manipulating the ItalicFactor property. This value is simply a shear factor that gets applied when drawing the control, as seen in the Paint event. An italic factor of -0.1 makes the display look just slightly slanted, and a whole lot more professional.

If you begin noticing that the segments are being drawn outside the boundary of the control (perhaps from too much italicizing), you can use the Padding property and increase the left/right/top/bottom padding until all of the shapes are within the control’s client rectangle.

The control has several other convenient properties for you to play with, such as the background color, the enabled and disabled color for the segments, and the thickness of the segments.

Seven-segment array

In addition to the seven-segment control itself, I’m throwing in another control which is an array of seven-segment displays. This allows you to display entire strings on an array of 7-seg displays. Check out the demo application, and dig around the source code to see how it’s used; it’s really simple.

To use the array control, include the "SevenSegmentArray.cs" file in your project and rebuild. You’ll then be able to select the SevenSegmentArray control from the tool palette.

This control has an ArrayCount property that specifies the number of 7-seg displays in the array, as well as a Value property that takes any string to be displayed on the array. Easy, right?

Other thoughts

I must say I had a lot of fun writing this control, and .NET helped put a lot of the fun into it by making it incredibly easy to draw your own shapes, transform coordinates, and introduce truly powerful properties.

Also, coming from somewhat of an electronics background, for me, seeing this control brings a certain nostalgia for simpler times. I hope you enjoy it.