Dr. GUI .NET #7

 

July 31, 2002

Contents

Introduction
Where We've Been; Where We're Going
What's Life All About?
Life as a Windows Forms Application
Calculating New Boards
The Console UI
The Windows Forms UI
Give It a Shot!
What We've Done; What's Next

Tell us and the world what you think about this article on the Dr. GUI .NET message board at Dr. GUI .NET message board. There are some good discussions there, but they'll be better when YOU join in!

And check out the samples running as ASP.NET applications, with source code, at: http://coldrooster.com/DrGUIdotNet/. Life isn't up there as a Web application yet, but that's next!

Introduction

Welcome back to Dr. GUI's latest article on .NET Framework programming—this time, an application of two-dimensional arrays called Conway's Game of Life.

If you're looking for the earlier articles, check out the Dr. GUI .NET home page.

Visual Basic, Only for This Time

Dr. GUI decided to use Microsoft® Visual Basic® .NET this time without also doing the code in C#.

The good doctor is wondering what C#, Java, and C/C++ programmers think of this? Is Visual Basic easy enough to read that you can get the point? Or do you really miss having C# code? Let Dr. GUI know in this thread on the Dr. GUI .NET message board: https://www.gotdotnet.com/community/messageboard/Thread.aspx?id=29572.

Where We've Been; Where We're Going

Last time, we talked all about arrays in the Microsoft® .NET Framework.

This time, we're going to show an example of using two-dimensional arrays in a Microsoft® Windows® Forms application—Conway's Game of Life.

What's Life All About?

Conway's Game of Life was invented by John Conway, a mathematician, in 1970. It was described in Martin Gardner's Mathematical Games column in Scientific American that year.

The basic rules are that you start with a pattern of live cells on a grid, then apply a set of rules each generation to determine what cells will be on the grid for the next generation. These rules are quite simple:

If an empty spot on the grid has exactly three live neighbors (out of eight possible—diagonals count!), then a new life is born in that formerly empty spot.

A live cell continues to live if it has two or three neighbors. If it has fewer than two, it dies of loneliness; if it has more than three, it dies of overcrowding.

But from these simple rules, an astounding variety of patterns and automations can be formed.

For a much better, in-depth description of Life and the meaning of Life, see Paul Callahan's article on math.com at http://www.math.com/students/wonders/life/life.html and Alan Hensel's Life page at http://hensel.lifepatterns.net/.

Life, the Universe, and Everything

By the way, Life is a simple example of a class of programs known as "cellular automata." Physicist Stephen Wolfram is in the news nowadays for a new book, A New Kind of Science (http://www.amazon.com/exec/obidos/ASIN/1579550088/qid=1027637251/sr=8-1/ref=sr_8_1/104-0899262-1443926), which claims that cellular automata (only a little more complicated than Conway's Game of Life) can model the operation of the universe.

Life as a Windows Forms Application

Our version of Life is a very simple one: the board is fairly small, and the user interface is quite simple. The math.com article and Alan Hensel's site have links to more sophisticated versions of Life, including some containing many patterns you can load in and some that allow for huge universes. Ours has no way to save or load patterns. (I think the fact that we can't save or load patterns just about assures that we'll be revisiting Life when it comes time to talk about files.)

Here's what the UI looks like:

The Start button starts the generation process and becomes the Stop button once it's clicked—clicking it toggles, whether or not generations are calculated and displayed automatically. The slider control controls the speed of the simulation. The Single Step button causes the program to advance exactly one generation.

You can also edit the board directly by clicking on it. Or you can clear it with the Clear Board button, add a number of random organisms to an existing board with the "Add random" button, or clear the board and add a number of random organisms with the "Clear and add random" button.

When the program is running generations, the buttons that clear the board and add random life to the board are disabled.

Calculating New Boards

Before we describe how the UI works, let's look at how we actually calculate the next generation.

The first thing to note is that calculating the next generation requires a second array to hold the results. Since all the decisions about which cells are alive or not come at the same time, there's no way to temporarily store the data in order to calculate the rest. (Okay, well, no really good way.)

A simple-minded algorithm would be to, for each cell:

  • Visit the neighboring cells to calculate the number of cells for that neighbor.
  • Then mark the corresponding cell in the new array dead or alive depending on the original state of the cell and the number of neighbors.

The problem with this algorithm is that you waste a lot of time adding up empty cells (unless the board is very full). But if they board is very full, it will empty out quickly because most cells will have too many neighbors—so boards are only rarely anywhere near full, and never for more than a generation.

So this algorithm ends up being a double-nested loop (O n^2) where for each cell, we have to do eight memory accesses and additions, even if the board is blank in that area.

Realizing this inspires an optimization: Rather than counting neighbors for each cell, we can have each live cell increment the neighbor counts of all its neighbors. When we're done, each cell in the new array will contain the count of neighbors of that cell. With that information, a quick pass through the arrays allows us to finish calculating that generation.

We still have a double-nested loop to go through all the cells, but we don't do any additions/stores at all unless a cell is alive. The fact that we're avoiding visiting neighbors should more than make up for having to do a conditional test at each cell and having to store the incremented values back in memory—especially since many processors have a streamlined increment instruction.

The corners and the edges present some special cases, since corners have only three neighbors and edges have only five.

A simple-minded approach would be to add code to the test in the main loop to deal with the special cases. You could do this, but all the tests would slow down your program considerably. Instead, we do the special cases separately, using code that does precisely what's needed. For each of the four corners, the code looks like this:

    If inArray(0, 0) <> 0 Then
        outArray(0, 1) += 1
        outArray(1, 0) += 1
        outArray(1, 1) += 1
    End If

There's one of these if statements per corner—four in all. The others are similar. Click here to see it in a new window.

The next special cases are the edges (less the corners). We handle top/bottom and left/right in two loops. Here is the code that handles the top and bottom edges:

    ' then across top and bottom
    For j = 1 To width - 1
        If inArray(0, j) <> 0 Then
            outArray(0, j - 1) += 1
            outArray(1, j - 1) += 1
            outArray(1, j) += 1
            outArray(0, j + 1) += 1
            outArray(1, j + 1) += 1
        End If
        If inArray(height, j) <> 0 Then
            outArray(height, j - 1) += 1
            outArray(height - 1, j - 1) += 1
            outArray(height - 1, j) += 1
            outArray(height, j + 1) += 1
            outArray(height - 1, j + 1) += 1
        End If
    Next

The code to handle the left and right edges is similar. Click here to see it in a new window.

Finally, we handle the main case—the middle cells, all of which have eight neighbors:

   ' calculate number of neighbors in main part of inArray
   For i = 1 To height - 1
       For j = 1 To width - 1
           If inArray(i, j) <> 0 Then
               ' we have a life, so increment all neighbors' counts
               outArray(i - 1, j - 1) += 1
               outArray(i - 1, j) += 1
               outArray(i - 1, j + 1) += 1
               outArray(i, j - 1) += 1
               outArray(i, j + 1) += 1
               outArray(i + 1, j - 1) += 1
               outArray(i + 1, j) += 1
               outArray(i + 1, j + 1) += 1
           End If
       Next
   Next

Note that we did not use a loop to increment the neighbors. Doing so would have added overhead (for initializing and maintaining the loop counter and for testing when we're done with the loop) and made our program run slower. Instead, we just wrote the code to do what's needed directly. Note that we're relying on the compiler to remove common expressions such as i + 1 from the code above. This is pretty easy for modern compilers; but if yours won't do it, it's easy enough to create temporary variables to hold them.

Once outArray holds the counts of neighbors, we can calculate the new board by using outArray's counts and the original pattern in inArray:

    ' use neighbor data and original data to determine life
    ' 0 or 1 in outArray
    For i = 0 To height
        For j = 0 To width
            If outArray(i, j) = 3 Then
                outArray(i, j) = 1
            ElseIf inArray(i, j) = 1 And outArray(i, j) = 2 Then
                outArray(i, j) = 1
            Else
                outArray(i, j) = 0
            End If
        Next
    Next

We provided two overloads of CalcNext: one which you pass an array to, and it returns a new (and newly-allocated) array, and one which uses two existing arrays and therefore does no new-memory allocation.

The one that allocates an array is better for stateless scenarios such as Microsoft® ASP.NET programs or Web services. You don't want to keep the arrays around because doing so causes problems with scaling. And the overhead of creating the array each time is small compared with the time to transport the data over the wire and so on—not to mention small compared with the time to calculate the new array!

The simpler one looks like this:

    ' call this overload if you need to create array each time
    ' anyway, as in ASP.NET.
    Public Function CalcNext(ByVal inOutArray(,) As Integer) As Integer(,)
        Return CalcNext(inOutArray, Nothing)
    End Function

...and it calls the other (which allocates a new array if you pass null/Nothing as the second parameter):

    ' call this overload if you want to manage
    ' the input/output arrays yourself; returns output array
    Public Function CalcNext(ByVal inArray(,) As Integer, _
            ByVal outArray(,) As Integer) As Integer(,)

        ' In VB, lengths of dimensions are one more than max subscript,
        ' so we have to subtract one to get same size as original.
        ' We wouldn't have to in other languages.
        Dim width As Integer = inArray.GetLength(1) - 1 ' # cols
        Dim height As Integer = inArray.GetLength(0) - 1 ' # rows
        Dim i, j As Integer

        If (outArray Is Nothing) Then ' create array
            outArray = New Integer(height, width) {}
        Else
            CType(outArray, IList).Clear() ' clear existing array
        End If
      ' ...

Note that our neighbor-counting algorithm requires that the output array contain zeroes in all cells (because it increments the array elements rather than setting them). If we create the array anew, the .NET Framework guarantees this for us. But if we use an existing array (and avoid creating memory pressure by creating and throwing away arrays repeatedly), we need to zero it out.

Here we allocate a new array if null/Nothing was passed in as the second parameter. Note that Dr. GUI had considered (and actually written) these methods so that the single-parameter version would allocate the array and pass it in. However, the real (two-parameter) version needed to know whether the array had been zeroed or not (or it would waste time zeroing a newly-allocated array that was already zeroed), so the good doctor moved the allocation to the real method and used a null/Nothing reference to indicate whether a new array needed to be allocated or not.

The best way to clear an array is to use System.Array IList.Clear method (which is an explicit interface method implementation of the Clear method from IList). System.Array has a static/Shared Clear method as well, but it seems to be designed for one-dimensional arrays only—it takes a beginning and ending point of the range of elements to set to zero.

We use the system's method rather than writing our own doubly nested loop because the system's method is highly optimized and will execute as quickly as possible.

So our method has three important optimizations compared with the simplest possible algorithm:

  1. It only does additions for the cells that are live.
  2. We separate out our special cases so that the core loop can be written cleanly and so it'll execute fast.
  3. We unroll the loops for visiting all the neighbors to avoid loop overhead and so the compiler can optimize for us.

Not memory-efficient, but fast

You'll notice that we chose to use 32-bit integers rather than 8-bit bytes or single bits for each life. This is a huge waste of memory, but the good doctor did this for a reason: If we'd used something smaller than a machine word, the processor or the generated code would have to do something extra in order to access the individual bit(s) for each life. For individual bits, that would involve a lot of shifting and masking. For bytes, it would involve the processor internally doing some shifting and masking on byte-addressable systems, or, on machines that aren't byte-addressable, extra code would need to be generated in order to do this shifting and masking.

So the tradeoff we made was to choose speed over memory size by picking the fastest data size.

If we were planning to support a large board, it might be worthwhile to consider an array of bytes, since the time hit is likely to be pretty small on byte-addressable machines and the memory savings is a factor of four. Using a smaller data structure would also make it easier for the processor to keep the data in the cache. And a byte is large enough to keep track of the number of neighbors (maximum eight!), so that's not an issue.

It also might make sense to use individual bits if you can use some sort of lookup table scheme to do part of the calculation, as described below.

Not very object-oriented

You'll note that this algorithm isn't the least bit object-oriented. Object-oriented programming is very good for creating abstractions, but sometimes the overhead really gets in the way. This time is one of those times. Imagine how complicated (and slow) calculating a new generation would be were each cell an individual object, for instance.

One More Speedup

Dr. GUI hasn't tested this himself yet, but he's told, contrary to what he guessed in the last column, that arrays of arrays are actually FASTER than two-dimensional arrays, because the code that is generated to handle them is more highly optimized by the JIT compiler.

However, the arrays of arrays are a pain to allocate (you have to allocate each row separately in addition to the main array), so the good doctor chose not to use them this time around. Most of the code would look pretty much the same. Perhaps Dr. GUI will give it a shot next time, particularly if he does some benchmarks to compare performance.

Another Way of Doing It

For a much more sophisticated (and complicated) way of calculating Life generations, check out Alan Hensel's applet at http://hensel.lifepatterns.net/lifeapplet.html. Rather than using a two-dimensional array to store the board, he uses a list of building blocks, each 16 x 16. If there are no organisms in a given 16 x 16 block, the block is omitted from the list, so Hensel's algorithm not only doesn't waste time calculating the number of neighbors for blank areas, it doesn't even visit those blocks since they don't appear in the list.

He further optimizes by subdividing the 16 x 16 blocks into 8 x 8 blocks and finally 4 x 4 blocks. Each 4 x 4 block is stored in a 16-bit integer, and he uses a lookup table to calculate the results for the center 2 x 2 region—a huge time savings.

You can see that this algorithm is very highly optimized—both by not calculating empty regions and by using a lookup table rather than a set of if and assignment statements. In addition to the advantage of running very fast, it has the advantage of using far less memory than a matrix would use, since memory usage is proportional to the number of living cells, not to the total size of the board.

One thing you'll notice if you look at the code at all carefully: Because Java doesn't support unsigned types (and arithmetic), Alan has to spend a fair amount of code (and time) twiddling bits to compensate. Visual Basic .NET doesn't support unsigned types either, but both C# and the managed extensions to C++ do. If Alan were using either of those, his code would be a little simpler.

The Console UI

After writing the calculation engine, the first thing Dr. GUI did was to create a very simple console UI to test the life-generation methods. To see the entire console UI code in a new window, click here.

First, he asked the user to tell him the number of rows and columns, and created the array to specification:

        Console.Write("Enter number of rows: ")
        Dim heightM1 As Integer = Console.ReadLine()
        Console.Write("Enter number of columns: ")
        Dim widthM1 As Integer = Console.ReadLine()
        heightM1 -= 1
        widthM1 -= 1
        Dim lifeBoard(heightM1, widthM1) As Integer ' subtract after in C#

Then he read lines from the console to create lives in the array:

        Dim i, j As Integer
        Dim s As String
        Dim response As Char

        Do
            For i = 0 To heightM1
                Console.Write("Enter row {0}: ", i)
                s = Console.ReadLine()
                s = s.PadRight(widthM1 + 1)
                For j = 0 To widthM1
                    If s.Chars(j) <> " " Then
                        lifeBoard(i, j) = 1
                    Else
                        lifeBoard(i, j) = 0
                    End If
                Next
            Next
            PrintLifeBoard(lifeBoard)

Finally, he went into a loop to do each successive generation:

        Console.WriteLine("Next generation:")
        Do
            lifeBoard = LifeCalc.CalcNext(lifeBoard)
            PrintLifeBoard(lifeBoard)
            Console.Write("Enter to continue, ""n"" to play again," + _
                """x"" to stop: ")
            response = Console.ReadLine().ToLower()
        Loop While response <> "x" And response <> "n"
    Loop While response <> "x" ' matches with top Do...

The PrintLifeBoard method is quite simple:

    Sub PrintLifeBoard(ByVal inArray(,) As Integer)
        Dim i, j As Integer
        Dim heightM1 As Integer = inArray.GetLength(0) - 1 ' # rows
        Dim widthM1 As Integer = inArray.GetLength(1) - 1 ' # cols
        Dim sb As New StringBuilder(New String(" ", widthM1 + 1))

        For i = 0 To heightM1
            For j = 0 To widthM1
                sb(j) = IIf(inArray(i, j) <> 0, "*", " ")
            Next
            Console.WriteLine(sb)
        Next
    End Sub

Note that we used a StringBuilder to avoid creating and destroying multiple strings (as string concatenation would have done—ouch!), and we used the IIf function in Visual Basic, which is like the ? : operator in C#/C/C++.

The Windows Forms UI

A Windows Forms application is completely represented by the source code, but since you visually edit the form in design view (and the design view editor modifies the source) let's talk briefly about what's on the form and the properties we've set for those controls.

About the Form

The Windows Forms UI is pretty simple: it consists of a form containing a picture box control, several buttons, a textbox, and a slider.

Readers of Dr. GUI .NET will recall buttons and textboxes from previous columns. The picture box and slider are, however, new—as is the way we're using the group boxes. Finally, we're using a timer control on the form to provide periodic events so we can calculate and display a new generation for each timer "tick."

A picture box is worth a thousand words

A picture box is a control that contains an image and displays it. The advantage of using one is that you can use the forms designer to put it on the form and lay out the form—and it handles all the painting for you. All you need to do is set the Image property to point to an image and the PictureBox does the rest. (Were Dr. GUI rewriting the Windows Forms version of the drawing program that he did a couple of months ago, he'd use a PictureBox this time around.)

Picture boxes are also handy when the form is resized, because if you anchor it properly, the picture box will be resized with it. If you don't do any custom drawing, you don't need to do a thing. If you do, it's simple to make it work. We'll see how later.

Sliders: Not just from White Castle!

Those of you who are from the Midwest might know a restaurant called White Castle. They sell very small hamburgers that are incredibly greasy—so greasy, in fact, that they're often called "sliders" because they're small enough and greasy enough to slide right down your throat without even chewing. (Not really, but almost!)

The slider control we're talking about has no relationship to White Castle sliders.

The slider control represents a numeric value and provides a UI in the form of a slider to allow the user to change the value. It's easy to set the minimum and maximum values for the value the slider represents, as well as the RightToLeft property, which means that the value gets smaller (not larger) as you move the control to the right, as is required for this problem. Being able to set these properties makes it very easy to use the control, since you don't have to write code to do these tasks.

The slider control fires Scroll events when the slider is moved. In the old days (before Windows 95), there were no slider controls, so programmers used scroll bars instead. The name for the event has just kind of stuck from the old days.

Note that we have the minimum value for the slider set pretty high. That's because if the time to calculate a new generation and paint it on the screen is longer than the timer interval, the screen will never get a chance to update. This is a bigger problem as the screen gets larger. We'll look at fixing this later. One way would be to be sure to get the paint and click messages each time through; another would be to do the generation calculation and bitmap drawing in a background thread. Both are too complicated for this article, so we'll just make sure we don't try to do generations too quickly.

Because we're not running as fast as we can (since we run a generation only every 100ms), please don't use the Windows Forms application to make judgments about this program's speed. If you want to find out how fast the calculation engine is, set up a benchmark harness.

A group box for your children

The label, text box, and buttons in the top right group of controls are children of the group box in which they're contained. Because they're children of that group box, we can enable and disable them all at once by enabling or disabling the group box (which automatically enables or disables its children). In the designer, we make the other controls children of the group box by dragging the control into the group box (or creating it there in the first place). This causes the designer to add the control name to the groupBox.Controls.AddRange() call.

Your timer is ticking

The timer control is interesting. It's not visible on the form (the designer displays it in a small window just below the form), but in all other ways it behaves like a visible control: it has properties you can get and set, and it fires events. For Life, we need to get a regular event so that we can generate a new board each time. The timer fires those regular events, called Elapsed events, for us. Our event handler for the Elapsed event calculates a new generation and displays it.

While Dr. GUI was testing the Life program, he noticed a strange thing: the program came up running. So he added a statement to the form's Load event handler (we'll see this later) to stop the timer. This kept the game from running as soon as it started.

But then the good doctor noticed that the initial board was NOT random. Instead, it looked as though the game had been played for several generations. What was going on? The timer had been set off in the Load event handler for the form!

What was happening was that the timer was initialized as enabled, so it had fired the Elapsed event at least once before the Load event occurred, and the code in the handler shut the timer off. This event was queued up and handled after the initialization of the board to a random pattern (after the Load event handler returned). Remember that the Elapsed handler calculates a generation. So a generation was being calculated before the application was fully initialized and before the screen was first painted!

The proper solution to this is to go to the designer and set its Enabled property to False. This, as it turns out, is the default, so it appears that Dr. GUI had changed the property to True at some point while designing the form. Once the timer was disabled on startup, everything worked great!

By the way, you do NOT want to put custom code in the InitializeComponents method that appears inside the hidden region. (Look, but don't touch!) If you do, it may be lost when the designer edits this method.

Making the form resizable

There's one more thing we need to do to make this a cool application: we need to be able to resize the form and have it still look good.

As it turns out, Windows Forms makes this EXTREMELY easy. All we have to do is set the Anchor property on the controls correctly, and the resizing happens automatically. If we're not doing custom drawing, that's the end of it. (If you're used to trying to resize things in the Brand J system, you'll be amazed at how easy this is.)

Adding this to the program took about ten minutes. Dr. GUI remembers what a pain this was in MFC and Visual Basic and Windows in the past, so he thinks it totally rocks that this is so easy in the .NET Framework.

In order to understand what's going on here, you have to understand the concept of anchoring.

When you anchor a side of a control to its container, that side will stay at a fixed distance from the corresponding side of the container regardless of how the container is resized. Sides of controls that aren't anchored stay the same distance from the opposite side of the same control—they're not anchored to the container at all.

The key concept here is that controls are, by default, anchored to the top and left of their container. That means that as the container (in this case, the form) is resized, top and left sides of the controls stay in the same position relative to the upper left-hand corner of the container.

You could also anchor to the bottom and right only, in which case the controls would move relative to the bottom right corner of the container. (The top and left of the control would stay in the same position relative to the bottom and right of the control, so the whole control would move.)

Where it gets interesting is if you anchor to opposite sides, for instance, top and bottom, or left and right, or all four sides. In those cases, the control is resized as the container resizes, so that the appropriate side stays the same distance from the edge of the container as the container size changes.

It's very cool that you can see the results of your anchoring in the designer. Just set the anchors and resize the form in the designer.

In Life, we anchor the picture box to all four sides. That means that it resizes in both directions as the window is resized—exactly what we want. The label on top of the image is anchored to the top and left right so that it resizes with the image.

We anchor the other controls to the top right. That means they stay in the upper right-hand corner of the form, and that their size never changes (since they're not anchored on any of the opposite sides). By the way, only the group boxes and the stray button are anchored to the top right. Since the other controls are children of the group boxes, they're anchored to the group box, not to the form.

So the controls on the right are of fixed size and stick with the upper right-hand corner—exactly what we want. And the image resizes as the window resizes—again, exactly what we want.

If we had a static image, we could simply set the SizeMode property of the image to StretchImage or CenterImage. But we can't do that with our image because it would make it very hard to edit, since we're assuming integer sizes for the boxes, and the image could be scaled so that our clicks might not line up as we expect. This means, by the way, that the picture box might be up to almost a cell larger than the actual image.

Since we're doing some custom drawing, when the picture box is resized, we need to call a method to re-initialize the parameters that control the drawing of the image. We'll discuss that later on. Since we based the parameters on the picture box size, it's easy to call the same method, SetImageAndCellSizes, that we did when we initialized the application in the first place!

Note that one possible way of resizing the application is to minimize it—in essence, to resize it to zero. As it turns out, we'll need to handle that as a special case.

So with that, let's take a look at the code for the form class that declares member variables and event handlers.

The Form Class's Code

This form is a bit unusual in that we have a large number of fields. For efficiency's sake, we're keeping some large data structures—two matrices, a bitmap for drawing the image, and a Graphics object for drawing into the bitmap—as well as a number of variables we calculate just once (unless we resize). To see the entire code file in a new window, click here.

Fields

Here's the code with all our field declarations. Note that we initialize some fields—specifically, the arrays are created right here and now.

Public Class LifeForm
    Inherits System.Windows.Forms.Form

' [Windows Form Designer generated code] (hidden region)

    ' test to make sure you have rows/cols right by making
    ' rows different from columns
    Const rows As Integer = 32
    Const cols As Integer = 32
    ' have to subtract one because VB allocates 0...max, 
    ' not 0...max - 1 as in C#
    Dim currBoard = New Integer(rows - 1, cols - 1) {}
    Dim tempBoard = New Integer(rows - 1, cols - 1) {}
    Dim img As Image
    Dim g As Graphics
    Dim imgWidth, imgHeight As Integer
    Dim widthCell, heightCell As Integer

The number of rows and columns are created as constants so they can be used in the array declarations below. If this were C# or C++, we wouldn't have to subtract one from the subscript since in those languages, a 10-element array has its elements indexed by 0 through 9.

But Visual Basic has a legacy of being able to specify that the lower bound of the array is 1, not zero. That legacy didn't carry forward to Visual Basic .NET. To make porting code with the old OPTION BASE 1 statement easier, however, Visual Basic .NET allocates an extra array element. So if you specify 10 elements, you actually get 11, numbered 0 through 10.

If you actually only want 10 elements, just subtract one from the subscript when creating the array in Visual Basic, as we did here.

Next, we allocate references for the Image object we'll use to draw our life board, and a Graphics object we'll use to provide a drawing interface to the image. Finally, we're allocating integers to hold the height and width of the entire image and the width and height of an individual cell in the image.

The values for these variables depend on the size of the client area of the picture box (as well as the size of the array in the case of the cell sizes), so we can't initialize these variables until the picture box is created and initialized. The Load event handler for the form is the ideal time for such initializations, so we'll do ours there.

Initializing and Cleaning Up

Getting loaded

When the form is loaded, we need to do the initialization around the picture box and image. We can't do it earlier because we don't know how big the picture box is.

Here's the code:

    Private Sub LifeForm_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
        FillRandom(200, False)
        SetImageAndCellSizes()
    End Sub

As you see, we just fill the array with random life, then set up our image correctly. Here's how setting up the images works (we'll see FillRandom later):

    Private Sub SetImageAndCellSizes()
        ' set image and cell sizes
        imgWidth = BoardImage.ClientSize.Width
        imgHeight = BoardImage.ClientSize.Height
        widthCell = imgWidth \ (cols) ' integer division
        heightCell = imgHeight \ (rows)
        ' adjust virtual image size to exact maximum (might shrink),
        ' prevent(exceptions) on clicks on extreme right/bottom
        imgWidth = widthCell * cols
        imgHeight = heightCell * rows
        If Not (img Is Nothing) Then ' if we have one, we have both
            img.Dispose()
            g.Dispose()
        End If
        img = New Bitmap(imgWidth, imgHeight)
        g = Graphics.FromImage(img)
        BoardImage.Image = DrawBoard()
    End Sub

We get the size of the picture box's client (drawing) area; then we figure out how big each cell is in pixels, rounding down to an integer. We get the size of the bitmap by multiplying the array size by the size of each cell. That will be the same as or a little smaller than (by up to a cell size minus one) the picture box, but that's okay—it keeps the math simple.

Once we create the bitmap, we allocate a graphics object to go with it. We do the drawing using the graphics object, but the drawing appears on the associated bitmap.

In both cases, we call Dispose on the old bitmap and graphics if there is an old one. If your object has a Dispose method, it's important that you call it before you destroy the last reference to it. The garbage collector will eventually clean up after you, but it might not be fast enough to catch up with scarce non-memory resources. And if you end up with a lot of bitmaps and graphics objects sticking around, your system's performance can suffer. So clean up after yourself!

Drawing

The DrawBoard method does the actual drawing of the board. It returns a reference to the image on which it drew (the bitmap). We use that returned reference to set the Image property of our picture box. When that's set, the screen is updated.

Here's DrawBoard:

    Private Function DrawBoard() As Image
        ' black background
        g.FillRectangle(Brushes.Black, 0, 0, imgWidth, imgHeight)

        Dim i, j As Integer
        For i = 0 To rows - 1
            For j = 0 To cols - 1
                If currBoard(i, j) <> 0 Then
                    g.FillEllipse(Brushes.HotPink, _
                        j * widthCell, i * heightCell, _
                        widthCell, heightCell)
                End If
            Next
        Next
        Return img
    End Function

All it does is to clear the background to black, then draw an ellipse each place a cell is alive—very simple.

Another strategy for the graphics object—and perhaps for even the bitmap—would be to create and destroy it in the DrawBoard method. Since we're not using very many of them (only one if we don't resize), it seems to make more sense to create them once and keep them around until we're done with them.

But if we were a stateless application—a Web application or Web service—then we'd want to clean up after ourselves immediately. So we'd create the bitmap and graphics object in DrawBoard, then be sure to call Dispose on each before the method ends.

Getting unloaded

Since we're keeping the bitmap and graphics around, we should clean up the last graphics and bitmap when the application closes.

    Private Sub LifeForm_Closed(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles MyBase.Closed
        g.Dispose()
        img.Dispose()
    End Sub

Handling the Events

For the most part, the rest of the code is event handlers. There are a few methods that we could factor out, but most just handle events.

Editing the board

We handle editing the board by getting the mouse-down events generated by the picture box (yet another reason to use a picture box!), and by setting the appropriate position in the matrix. Note that the picture box might be larger than the image, so we need to check for clicks outside the image size and ignore them.

    Private Sub BoardImage_MouseDown(ByVal sender As System.Object, _
            ByVal e As System.Windows.Forms.MouseEventArgs) _
            Handles BoardImage.MouseDown
        If e.Y < imgHeight And e.X < imgWidth Then ' skip if outside image
            If IntervalTimer.Enabled Then
                StopTimer()
            End If
            ' integer division below!
            Dim row As Integer = e.Y \ heightCell
            Dim col As Integer = e.X \ widthCell
            currBoard(row, col) = currBoard(row, col) Xor 1
            BoardImage.Image = DrawBoard()
        End If
    End Sub

We handled the mouse-down event rather than another event for two reasons: The obvious event to handle would be Click. However, the click event doesn't give you a way to find out WHERE the click occurred—the EventArgs parameter passed to your handler has no data at all. So we needed to handle either mouse up or down.

Dr. GUI actually prefers to handle mouse up for this, but there's a subtle problem that forced him to use mouse down. Here's the problem: If you maximize the window by double-clicking on the title bar, and if the spot on which you double-clicked is in the image of the larger picture box, the mouse-up event will be sent to the picture box, causing the board to be edited. The good doctor thinks that Windows should be eating this mouse-up message when it maximizes, but it doesn't seem to do that. However, handling mouse down instead works quite well, so it's not the end of the world.

Calculating a new generation

The SingleStepButton_Click method causes a new generation to be calculated and displayed. First, it stops the timer if it's running. If the timer wasn't running, it calls NextGeneration.

    Private Sub SingleStepButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) _
            Handles SingleStepButton.Click
        If IntervalTimer.Enabled Then
            StopTimer() ' and otherwise ignore click
        Else
            NextGeneration()
        End If
    End Sub

NextGeneration calls CalcNext (seen before!), passing both boards. The new board is returned by reference from the method, so we save it—and then write the rest of the code to swap the two boards. Swapping the boards means that we never have to create a new board. Finally, we draw the contents of the board into a bitmap and set the Image property of the picture box to it so it's drawn on the screen.

    Private Sub NextGeneration()
        ' swap boards so we don't have to create a new one each time
        Dim newBoard As Integer(,) = _
            LifeCalc.CalcNext(currBoard, tempBoard)
        tempBoard = currBoard
        currBoard = newBoard
        ' skip drawing, save CPU if minimized
        If Not BoardImage.Size.IsEmpty Then
            BoardImage.Image = DrawBoard()
        End If    End Sub

Note that we skip drawing the board if our window is minimized. If the timer is enabled, we'll still calculate a new generation automatically on each timer tick, but we just won't write it into a bitmap if it won't be displayed.

The top groupbox

The buttons in the top groupbox clear the board and/or put a random pattern in it. They're quite simple:

    Private Sub ClearButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles ClearButton.Click
        CType(currBoard, IList).Clear()
        BoardImage.Image = DrawBoard()
    End Sub

    Private Sub ClearRandomButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles ClearRandomButton.Click
        FillRandom(NumRandom.Text, True)
        BoardImage.Image = DrawBoard()
    End Sub

    Private Sub AddRandomButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles AddRandomButton.Click
        FillRandom(NumRandom.Text, False)
        BoardImage.Image = DrawBoard()
    End Sub

The FillRandom method is also quite simple. Note that you're not guaranteed to get the number of cells you ask for; it only attempts to set that number of cells. It's possible (likely, in fact) that some of the cells will be set more than once. Given that we'd have to search for empty cells to fix this, the good doctor decided to keep it simple:

    Private Sub FillRandom(ByVal maxCells As Integer, _
            ByVal clear As Boolean)
        Dim i, j As Integer
        Dim r As New Random()
        Dim height As Integer = currBoard.GetLength(0) ' # rows
        Dim width As Integer = currBoard.GetLength(1) ' # cols
        If (clear) Then
            CType(currBoard, IList).Clear()
        End If
        For i = 1 To maxCells ' can fill spots > once, that's OK
            currBoard(r.Next(height), r.Next(width)) = 1
        Next
    End Sub

Resizing

Resizing is actually very simple in code. The .NET Framework does almost all the work for us.

The one thing it can't do for us is to reset the data we're using to generate the properly sized image. But we can do that quite easily by calling SetImageAndCellSizes:

    Private Sub BoardImage_Layout(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.LayoutEventArgs) _
            Handles BoardImage.Layout
        ' just skip if image minimized
        If Not BoardImage.Size.IsEmpty Then
            SetImageAndCellSizes()
        End If    End Sub

Note that we skip setting the image sizes if the form is minimized. (When the form is minimized, the picture box will be resized to 0 x 0. If we don't skip the function call, we'll get an exception from trying to create a 0 x 0-sized bitmap.

Last but not least: the timer

We have a few methods that deal with the timer. The simplest is the Elapsed event handler. All it does is to call NextGeneration to calculate and display the next generation:

    Private Sub IntervalTimer_Elapsed(ByVal sender As System.Object, _
            ByVal e As System.Timers.ElapsedEventArgs) _
            Handles IntervalTimer.Elapsed
        NextGeneration()
    End Sub

We turn the timer on and off with this set of three methods. TimerToggleButton_Click just calls the right method depending on the current state of the time (enabled or not):

    Private Sub TimerToggleButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles TimerToggleButton.Click
        If Not IntervalTimer.Enabled Then
            StartTimer()
        Else
            StopTimer()
        End If
    End Sub

StartTimer and StopTimer enable and disable the UI and change the button label in addition to starting and stopping the timer:

    Private Sub StartTimer()
        AddGroupBox.Enabled = False
        TimerToggleButton.Text = "Stop"
        IntervalTimer.Interval = SpeedSlider.Value
        IntervalTimer.Start()
    End Sub

    Private Sub StopTimer()
        IntervalTimer.Stop()
        TimerToggleButton.Text = "Start"
        AddGroupBox.Enabled = True
    End Sub

And the handler for the slider's Scroll event couldn't be simpler. Since the value of the slider is already scaled correctly (because we set the properties correctly), all we need to do is copy that value to the timer:

    Private Sub SpeedSlider_Scroll(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles SpeedSlider.Scroll
        IntervalTimer.Interval = SpeedSlider.Value
    End Sub

And, believe it or not, that's it!

Thank you!

Dr. GUI needs to thank some folks who helped with this.

First of all, several people reviewed the code: Matt Powell, Priya Dhawan, and Curtis Man. Each offered comments that made it better. Curtis, in a fun and lengthy conversation, also inspired the more efficient CalcNext algorithm we used. Thanks, all!

Henry Borys edits Dr. GUI, which is not an easy task, but Dr. GUI is far better as a result. Thanks, Henry!

Lastly, Dr. GUI needs to thank his manager, Pedro Silva, who, even though the article was running late and on deadline, told the good doctor about how cool resizing is in the .NET Framework. It was so cool that Dr. GUI couldn't resist—and it only took about an hour, including writing about it. Cool! Thanks!

Give It a Shot!

If you've got .NET, the way to learn it is to try it out—and if you don't have it, please consider getting it. If you spend an hour or so a week with Dr. GUI .NET, you'll be an expert in the .NET Framework in no time at all!

Be the First on Your Block—and Invite Some Friends!

It's always good to be the first to learn a new technology, but even more fun to do it with some friends! For more fun, organize a group of friends to learn .NET together!

Some Things to Try...

First, try out the code shown here. Some of it is excerpted from larger programs; you'll have to build up the program around those snippets, which is good practice. (Or use the code the good doctor provides, if you must.) Try playing with the code some. Here are some ideas:

  • Play with different patterns, and enjoy.
  • Play with different rules, and see how it changes things.
  • Change the program so that it will change the size of the array instead of the size of the cells when the image is resized. Add a checkbox to the UI so the user can decide whether or not to change the size of the array (and thereby create a new array) when the window's resized.
  • Write a benchmarking harness to create a large Life board. Put some cells in it (hint: seed the random-number generator with a consistent seed, and the pattern will be the same every time!) and time running a few hundred generations.
  • Using the benchmarking harness, try an array of arrays rather than a two-dimensional array and see which is faster.
  • Using the benchmarking harness, try writing the calculation code in C#, C# with pointers (unsafe!), managed C++, and C++ native code (access through COM interop). Which is fastest? Easiest to write? Hardest to write?
  • Change the user interface in interesting ways. Try a variety of Windows Forms controls and see what they do.
  • See if you can solve the timer/drawing problem.

What We've Done; What's Next

This time, we showed Conway's Game of Life as a Windows Forms application. Next time, we'll do it as an ASP.NET application.