Hidden Image Game

Description

In this article, we'll create a Hidden Image game. We'll demostrate few features available in Windows Forms in Microsoft .NET 2.0 for creating a consistent and user-friendly interface.

Recently I was challenged to quickly write an application for playing the Hidden Image game, popularized here in the Czech Republic by a certain TV show. Luckily I had my notebook with Visual Basic Express at hand. This example shows how easy it is to write software in Visual Basic Express, and how to almost effortlessly create a well-behaved Windows application. It also demonstrates how to use certain new features available in Windows Forms in Microsoft .NET 2.0 for creating a consistent and user-friendly interface using the following techniques:

  • GUI design basics with Windows Forms Designer.
  • Storing configuration settings for an application.
  • Creating a borderless, full-screen window and dynamically generating controls in it.

Rules of the Game

Hidden Image is not a computer game in the pure sense — it's more like a puzzle. Here's where a computer comes in handy, because it's simpler to show an image on the wall using a video projector than with cut paper cards (especially for us computer geeks, of course).)

To play the Hidden Image game, you need two opposite sides (players or teams) and one game master. The game master chooses an image. When displayed, the image is covered by numbered squares. Each player takes a turn removing one square, which reveals a portion of the image, and then attempts to guess what the image is. If the guess is correct, the player wins the game. If it's an incorrect guess, the other side takes a turn.

For this application I wanted to allow the game master to select the picture, set up certain parameters (like the number and color of the squares), and then display the covered picture. When clicking on any square, the square would disappear and reveal a part of the picture underneath. Pressing the ENTER key would reveal the entire picture, and pressing the ESC key would return you to the setup screen.

The Best User Experience

Certain applications are very visually appealing. They have fantastic colors, fantastic forms shapes, and rich graphics. They are beautiful. Unfortunately, in most cases they're not very useful. Unless your application is a role-playing adventure game, it is not a good idea to let your user wonder if the gimmick on the screen is actually a button or only a figment of the designer's imagination.

One key reason for the success of the Windows operating system is that the elements are generally the same in all the applications. You'll quickly realize that this is a menu, this is a button, and that this little square is actually a check box. Your applications should likewise follow common rules rather than trying to reinvent the wheel.

An application should also behave well on differing system configurations and not just on the machine that created it. Certain users — for example, me — have the taskbar on top of the screen instead of at the bottom. Some users like very high resolutions (for example, 1600 x 1200), while others prefer lower (and easier to read) resolutions (for example, 1024 x 768). Therefore it is a good idea to plan on making your Windows Forms resizable for the user's viewing comfort. And when the user resizes or moves Window Forms, the application should remember these changes and place the form in the same position the next time the user runs the application. It is also good to keep in mind that the computer may have different user accounts, and that user changes and preferences need to be stored per user.

This is the art of designing a decent user interface that creates the best user experience. Users, even inexperienced ones, should be able to use your application without unnecessary confusion.

Sound tricky? Not really. Microsoft .NET 2.0 has lots of features that allow you to painlessly address all these issues. You just need to know about them.

Creating the User Interface

Let's start Visual Basic Express and create a new Windows Application project (or simply download and open the finished project attached to this article). After creating the application, you'll see an empty form ready for you to place controls on it.

Below is what the finished application setup form should look like:

Generic Episode Image

Placing Controls on the Form

Begin with placing the group boxes, buttons, and text boxes. You'll notice that if you place a control near to another control or form border, little blue lines appear from the control sides. These lines help you to place the controls the same distance from one another and from the form boundaries. This new feature in Visual Studio 2005 makes aligning controls very easy.

When designing forms, you can also display the Layout toolbar. Right-click any toolbar and, in the resulting pop-up menu, click "Layout." A new toolbar will appear, with buttons to align the controls in relation to one another, make similar controls the same size, center them, and so on. Just play with the buttons and discover what they do.

At this time, there are a few regular controls on the form:

  • Two GroupBox controls, to group settings for a background picture and for overlay buttons.
  • A Text box control for setting a path to the image file.
  • A Button control to browse for the file.

For entering the number of rows and columns, you could use text boxes as well #151;. but it's not the best possible solution. You need to be sure that the entered value is a whole number within a certain range, for example between 2 and 20. So it's a good idea to use the NumericUpDown control instead of a text box. In the control's Properties window you can specify minimum and maximum values.

For setting the background and the foreground colors for the displayed number on the overlay squares, you can use two standard Label controls with the same border and size properties as those for the text box above them. What matters is their BackgroundColor property. Also, near each of them is a button that allows a user to choose the color.

You can use a similar approach to make it possible for the user to select the font used for the numbers on the overlay squares. Font specification is complicated. For example, you'll need to store font name, size, and style (bold, italic). We'll sort this out later as well, but for now, just place the controls and set their visual values.

Let's go ahead and run the application. It should display a nice window. You can click the buttons, write text in text boxes, and so on. Nothing will actually happen, but you can play.

Resizing the Form

Now try to resize window. The window resizes successfully, but the controls do not: If you make the form larger, the controls will remain in the upper-left corner; if you make the form smaller, they will be hidden. You want the controls to resize nicely with the form.

Close the application and display the properties for any control on the form. Notice the Anchor property. When you click it, you can select any combination of Top, Bottom, Left, and Right values.

Generic Episode Image

These values specify how the control should behave when its container control — in this case the Form — resizes. By default, Anchor is set to Top and Left. This means that the control will retain its distance from the top and left borders of the Window Form. For example, it would be better if the "Start" and "Close" buttons remained at the left and bottom borders of the form. Select more than two anchors (for example, Top, Left and Right), and the magic continues: When you resize the form, the control will stick to each selected Anchor and will resize as well. Play with the Anchor property for all the controls. Some controls (such as buttons) move to adjust to the change in size of the container control, while others (such as group boxes) will actually resize proportionate to the change in size of the container.

However, despite these built-in resizing features in Windows Forms and controls, it is usually a good idea to place some limits on the user's freedom to resize the form. If the form size becomes too small, some controls will disappear. To prevent this from happening, a Windows Form has MinimumSize and MaximumSize properties. In the form designer window, try resizing the form to the smallest possible size where all the controls are still fully displayed, readable, and usable. Then in the Properties window, copy the value in the form's Size property into the MinimumSize field. Now, when you run the application and resize the window, the controls resize nicely without allowing the window to become too small to be useful.

Using Common Dialogs

Now it's time to write some code and make the application actually do something. First, let's make some of the buttons go live. The "Browse" and "Change" buttons should use Windows common dialog boxes for file browsing and the selection of colors and fonts. There are two reasons for this. First, it's what users expect. Virtually all professionally written Windows programs use this approach, and anything else would seem strange. Second, it's actually the simplest way. Every other solution is harder to implement. It's one of those times when being lazy actually helps.

Let's start with file selection. Grab the OpenFileDialog control (in the Dialogs section of Toolbox) and place it somewhere on the form. It will immediately disappear from wherever you placed it and display in the grey area at the bottom part of the edit pane. This happens because the dialog control does not have any visual implementation on the form itself. It's not like a button or a text box, which are directly visible to users.

In the Properties window for this control, there are settings for a lot of things, like the title of the dialog, conditions the file must meet (for example, CheckFileExists), and so on. For our purposes, there is an interesting property called Filter. Values in this property specify what displays in the Files of type box.

Generic Episode Image

For the Hidden Image application, the values would be Images|*.jpg;*.bmp;*.gif;*.png;*.tif;*.tiff|All files|*.*. By default, we would allow users to choose from certain types of image files. As a last resort, a user can view all the files, regardless of extension. The file type extensions are separated by a pipe character.

Now, let's write a few lines of code that will display the Open File common dialog (pointing to the currently selected file), so that if the user clicks "Open," the name of the currently selected file appears in the text box. Double-click the "Browse" button to open the code editing pane and write the following code as handler of the Click event:

C#

private void ButtonBrowse_Click(System.Object sender, System.EventArgs e)
{
    //  Display file browser dialog
    this.OpenFileDialogImage.FileName = this.TextBoxImageFilePath.Text;
    if (this.OpenFileDialogImage.ShowDialog(this) != 
        System.Windows.Forms.DialogResult.OK)
    {
        return;
    }

    this.TextBoxImageFilePath.Text = this.OpenFileDialogImage.FileName;
}


Visual Basic
Private Sub ButtonBrowse_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)_
                                Handles ButtonBrowse.Click
    ' Display file browser dialog
    Me.OpenFileDialogImage.FileName = Me.Text boxImageFilePath.Text
    If Me.OpenFileDialogImage.ShowDialog(Me) <> Windows.Forms.DialogResult.OK Then Return
    Me.Text boxImageFilePath.Text = Me.OpenFileDialogImage.FileName
End Sub

The approach for the Color common dialog box and color selection is very similar. The code for the Font common dialog box will be a little different so we'll come back to it later.

AutoComplete Magic

One of the nice features in Windows is auto-completion of text box entries. That's when a user starts to type an entry (for example, a path to a file) and the text box offers possible entries based on previously entered values. To enable this feature in the Hidden Image application, you need to enable a couple of properties for the file name text box on the setup form. Set the AutoCompleteMode property to SuggestAppend and the AutoCompleteSource property to FileSystem.

Don't Forget the Keyboard

Don't forget that people can control your application using the keyboard as well as the mouse. If an application is well designed, advanced users usually find using the keyboard quicker and easier.

Three things help to make using the keyboard easier and more productive:

  • Default buttons.
  • Tab order.
  • Keyboard accelerators.

Let's look at default buttons first. In the case of most dialogs, there are buttons like "OK" and "Cancel" that function like the ENTER and ESC keys. You can enable this function using the dialog form's AcceptButton and CancelButton properties. In this case, set them to the "Start" and "Close" buttons. When you press ENTER or ESC, the application will behave as if the corresponding button has been clicked. The functionality of the "Start" button can be added later, but the "Close" button is easy to enable right away — just close the form:

C#

private void ButtonClose_Click(System.Object sender, System.EventArgs e)
{
      //  Close window and quit application
      this.Close();
}



Visual Basic
Private Sub ButtonClose_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)_
                                Handles ButtonClose.Click
    ' Close window and quit application
    Me.Close()
End Sub

Next is the tab order. In a standard application, pressing the TAB key would move the focus to the next control in sequence. The order in which the controls are selected is specified by their TabIndex property. For the best user experience, this should follow the order in which the fields are usually filled in, which is usually the same as the order in which the controls line up on the form.

By default, the tab order is assigned as the controls are placed on the form. The first control is 0, the next control is 1, and so on. In most cases, that's not what we want. Of course you can set the TabIndex values by hand, but Visual Basic offers a clever alternative. On the menu, click "View" and then "Tab Order." You'll notice there are now small squares with numbers in the corners of all the controls:

Click here for larger image

(Click image to zoom)

Click the controls in the same order you want the tab order to follow. After you're finished, click "View" and then "Tab Order" again to hide the numbers.

You'll need to assign TabIndex values even to controls that are not directly selectable, such as Labels. Why? Because of the keyboard accelerators.

You may notice that some letters on the running form are underlined (you may need to press and hold the ALT key to see this, depending on your system configuration). If you press ALT and the underlined letter, you'll shift the focus to the appropriate control. Or, if the control itself (for example, a label) cannot be selected, the focus moves to the next control in the tab order. So, if you press ALT+I, the text box for entering the file name is selected.

You can choose which letter is underlined by placing an ampersand before it. If you name the label "&Image File," the letter "I" will be underlined and used as a keyboard accelerator. This applies to the first letter of most labels. If the letter has already been assigned to another label, the next letter is underlined. So we have "F&ont" here, because "F" is already used in "&Foreground color."

Managing the Application Settings

Since the dawn of personal computing, there has been a need to store configuration settings. You can store the settings in many ways: as text files, in the registry, and so on. Microsoft .NET uses XML files for this. And, best of all, you do not need to worry about reading and converting values; it will be done for you automatically.

Some settings should be stored for all users of the application. This is called "having Application scope." These settings are usually configured during installation or initial setup by an administrator and should not be modified by regular users. They're stored in an application configuration file that is present in the same directory as the application's EXE file; it has the same name with a ".config" extension added. In this case, there are no such settings.

All our settings are "per user." This means that you need to store them someplace the user can read and write (remember, your user must not have administrator privileges). The ideal location for this is the user profile, usually in C:\Documents and Settings\<User Name>\Local Settings\Application Data\<Vendor Name>\<Product Name>. Actually, .NET is kind enough to take care of creating and managing the file. You just need to give it some information about your application.

Go to the Solution Explorer window and double-click "My Project." A new window will appear. On the Application tab, click the "Assembly Information" button and fill in the information about yourself and the application:

Generic Episode Image

The values you enter here will be used for specifying the location of the configuration files (among other things).

While you're in "My Project", go to the "Settings" tab. Here you can define the configuration properties of your application. Choose a settings name (for example ImageFilePath), type (String), scope (User), and default value (C:\WINDOWS\Web\Wallpaper\Bliss.bmp). Note that you can use pretty complex types for your settings, such as color (System.Drawing.Color) or font specification (System.Drawing.Font).

Click here for larger image

(Click image to zoom)

Visual Basic will automatically create a proxy class that takes care of reading and writing those values and you'll be able to use them directly with the correct type, for example as My.Settings.OverlayFont.

As you see, this is another example of laziness being a good thing (I just love being a programmer!). Not only are you able to read and write application settings without having to bother about configuration files, but the files are also placed correctly. Your application won't crash if a user does not have write privileges for the file (the user can always save to his profile). Tools like Windows XP File and Settings Transfer Wizard are an added bonus.

Using the Application Settings

Now it's time to use — read and write — the settings. Basically there are two possible ways to do this. One is to work with them programmatically, using the auto generated My.Settings class mentioned before. We can then write the code that will make the button that selects font family, size, etc. go "live."

As before, place the FontDialog control on the form and write the following code to the Click event handler of the appropriate button:

C#

private void ButtonFont_Click(System.Object sender, System.EventArgs e)
{
    //  Display font dialog
    this.FontDialogChange.Font = 
        HiddenImage.Properties.Settings.Default.OverlayFont;
    if (this.FontDialogChange.ShowDialog(this) != 
        System.Windows.Forms.DialogResult.OK)
    {
        return;
    }

    HiddenImage.Properties.Settings.Default.OverlayFont = 
        this.FontDialogChange.Font;
    this.LabelFont.Text =
        string.Format(
            "{0}, {1} pt",
            HiddenImage.Properties.Settings.Default.OverlayFont.Name,
            HiddenImage.Properties.Settings.Default.OverlayFont.SizeInPoints);
}


Visual Basic
Private Sub ButtonFont_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)_
                                    Handles ButtonFont.Click
    ' Display font dialog
    Me.FontDialogChange.Font = My.Settings.OverlayFont
    If Me.FontDialogChange.ShowDialog(Me) <> Windows.Forms.DialogResult.OK Then Return
    My.Settings.OverlayFont = Me.FontDialogChange.Font
    Me.LabelFont.Text = String.Format("{0}, {1} pt", My.Settings.OverlayFont.Name, _
        My.Settings.OverlayFont.SizeInPoints)
End Sub

The second way to use application settings is declaratively, with property bindings. We'll show an example of this with the image file name text box. Click the text box and display its properties. Find the (ApplicationSettings) item in the property box and click the small "+" before it. Then click the "" button in the (PropertyBinding) row. The following dialog box will appear:

Generic Episode Image

You can see that you can bind certain properties to application settings. Actually you can easily create new application settings with correct data types from here (using the "New…" link).

So, bind the Text property to the ImageFilePath application setting. In the same way bind all the other controls — for the number of rows and columns, and using BackgroundColor for the color specification labels.

Not everything can be bound in this way. For example, you would not use binding for font, because you wouldn't want to have the text of the label written in the selected font.

Remembering the Form Size and Position

As mentioned before, a well-behaved application should remember its window placement and size. We can use the application settings for this as well.

First, define FormOptionsSize configuration value (of type System.Drawing.Size) and FormOptionsLocation (of type System.Drawing.Point).

The following code in the form's FormClosed event handler will save the window position and size when you close the window:

C#

private void FormOptions_FormClosed(object sender, 
    System.Windows.Forms.FormClosedEventArgs e)
{
    //  Save form size and position, if not minimized/maximized
    if (!(this.WindowState == FormWindowState.Normal))
    {
        return;
    }

    HiddenImage.Properties.Settings.Default.FormOptionsLocation = this.Location;
    HiddenImage.Properties.Settings.Default.FormOptionsSize = this.Size;
}


Visual Basic
Private Sub FormOptions_FormClosed(ByVal sender As Object,_
 ByVal e As System.Windows.Forms.FormClosedEventArgs)_
 Handles Me.FormClosed
    ' Save form size and position, if not minimized/maximized
    If Not Me.WindowState = FormWindowState.Normal Then Return
    My.Settings.FormOptionsLocation = Me.Location
    My.Settings.FormOptionsSize = Me.Size
End Sub

Note that we've added a condition that ensures that nothing is saved when a window is in a minimized or maximized state. That's because the Location and Size properties have awkward values in this case. The code can store the WindowState as well, but it's not usually a good idea to start an application minimized. (The user might think it hasn't been started at all.)

Now you need to make sure that the saved values are used when the form is open, and you can do this in the Load event handler. The selected font is displayed in the label as well. There is no need to load values of the selected files and so on, because they would automatically be loaded by data binding.

C#

private void FormOptions_Load(System.Object sender, System.EventArgs e)
{
    //  Setup form size and position
    this.Location = HiddenImage.Properties.Settings.Default.FormOptionsLocation;
    this.ClientSize = HiddenImage.Properties.Settings.Default.FormOptionsSize;

    //  Show font name and size in text form
    this.LabelFont.Text = string.Format(
        "{0}, {1} pt",
        HiddenImage.Properties.Settings.Default.OverlayFont.Name,
        HiddenImage.Properties.Settings.Default.OverlayFont.SizeInPoints);
}


Visual Basic
Private Sub FormOptions_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles MyBase.Load
    ' Setup form size and position
    Me.Location = My.Settings.FormOptionsLocation
    Me.ClientSize = My.Settings.FormOptionsSize

    ' Show font name and size in text form
    Me.LabelFont.Text = String.Format("{0}, {1} pt", My.Settings.OverlayFont.Name, _
         My.Settings.OverlayFont.SizeInPoints)
End Sub

Playing the Game

We have successfully created our application, which nicely resizes and remembers its configuration. That's good, but it's not good enough. We need the application to actually do what it was created for. So let's add a new form and name it FormGame. There's no need to do anything else with it for now.

Double-click the "Start" button in the main window and write few lines of code to display the newly created FormGame:

C#

private void ButtonStart_Click(System.Object sender, System.EventArgs e)
{
    //  Show game form
    FormGame GameForm = new FormGame();
    GameForm.ShowDialog(this);
}


Visual Basic
Private Sub ButtonStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles ButtonStart.Click
    ' Show game form
    Dim GameForm As New FormGame
    GameForm.ShowDialog(Me)
End Sub

Showing the Picture

The actual game itself is created in the new form. Because we need to display the form in full-screen and without any borders, only the image is shown across entire screen. To achieve this, set the following properties of the FormGame form:

  • FormBorderStyle = None
  • ShowInTaskbar = False
  • WindowState = Maximized

To display the selected picture, place a PictureBox on the form and in the Properties window find the Dock property and set the value to Fill. This property has a similar effect to Anchor: It causes the selected control to stick to one side of the form or, in case of the Fill setting, to fill entire form, which is exactly what's needed. Another solution would be to make the picture box the same size as the form and then set the Anchor property to all four sides. Using the Dock property is easier.

We need to show the picture resized to full screen, so set the SizeMode property to Zoom, which will ensure this behavior.

Then place the following code to the Load event handler of the form:

C#

private void FormGame_Load(System.Object sender, System.EventArgs e)
{
    //  Load image
    try
    {
        this.PictureBoxImage.Image = 
            Image.FromFile(HiddenImage.Properties.Settings.Default.ImageFilePath);
    }
    catch (Exception ex)
    {
        Interaction.MsgBox("Error loading image: " + ex.Message, 
            MsgBoxStyle.Exclamation, null);
        this.Close();
    }
}


Visual Basic
Private Sub FormGame_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)_
                                 Handles MyBase.Load
    ' Load image
    Try
        Me.PictureBoxImage.Image = Image.FromFile(My.Settings.ImageFilePath)
    Catch ex As Exception
        MsgBox("Error loading image: " & ex.Message, MsgBoxStyle.Exclamation)
        Me.Close()
    End Try
End Sub

As you see, you can directly use the value from My.Settings.ImageFilePath so that it automatically sets the file name selected by the user with the binding created earlier.

When doing anything with user-entered data, it's important to expect the worst possible error. The file specified may be corrupted, it may not actually be an image, or it may not even exist at all. The above code catches the possible exception thrown when a file cannot be read as image.

If you run the application now, it will display the selected an image in full-screen. You can close the message box window using ALT+F4.

Dynamically Generating Controls

To cover the image, use Label controls. These must generate programmatically at runtime because you won't know during the design stage how many of them there will be.

A control is, from the code point of view, a class just like any other. You can create its instance and set its properties. You can extend the Load event handler using the following code:

C#

private void FormGame_Load(System.Object sender, System.EventArgs e)
{
    //  Generate basic overlay label width/height
    Int32 H = this.PictureBoxImage.Height / 
        System.Convert.ToInt32(HiddenImage.Properties.Settings.Default.Rows);
    Int32 W = this.PictureBoxImage.Width / 
        System.Convert.ToInt32(HiddenImage.Properties.Settings.Default.Columns);

    //  Create overlay labels
    for (int Y = 1; Y <= System.Convert.ToInt32(
        HiddenImage.Properties.Settings.Default.Rows); Y++)
    {
        for (int X = 1; X <= System.Convert.ToInt32(
            HiddenImage.Properties.Settings.Default.Columns); X++)
        {
            //  Create and place overlay label
            System.Windows.Forms.Label L = new System.Windows.Forms.Label();
            this.PictureBoxImage.Controls.Add(L);
            L.Left = (X - 1) * W;
            L.Top = (Y - 1) * H;
            L.Width = System.Convert.ToInt32(Interaction.IIf(
                X == HiddenImage.Properties.Settings.Default.Columns, 
                this.PictureBoxImage.Width - L.Left, W));
            L.Height = System.Convert.ToInt32(Interaction.IIf(
                Y == HiddenImage.Properties.Settings.Default.Rows,
                this.PictureBoxImage.Height - L.Top, H));

            //  Setup overlay label display
            L.Text = System.Convert.ToString((Y - 1) * 
                HiddenImage.Properties.Settings.Default.Columns + X);
            L.BackColor = 
                HiddenImage.Properties.Settings.Default.OverlayBackgroundColor;
            L.ForeColor = 
                HiddenImage.Properties.Settings.Default.OverlayForegroundColor;
            L.Font = HiddenImage.Properties.Settings.Default.OverlayFont;
            L.TextAlign = ContentAlignment.MiddleCenter;
            L.BorderStyle = BorderStyle.FixedSingle;

            //  Attach event handlers
            L.Click += new System.EventHandler(ClickHandler);
            L.MouseEnter += new System.EventHandler(MouseEnterHandler);
            L.MouseLeave += new System.EventHandler(MouseLeaveHandler);
        }
    }

    //  Load image
    try
    {
        this.PictureBoxImage.Image = Image.FromFile(
            HiddenImage.Properties.Settings.Default.ImageFilePath);
    }
    catch (Exception ex)
    {
        Interaction.MsgBox("Error loading image: " + ex.Message, 
            MsgBoxStyle.Exclamation, null);
        this.Close();
    }
}


Visual Basic
Private Sub FormGame_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)_
                                 Handles MyBase.Load
    ' Generate basic overlay label width/height
    Dim H As Int32 = Me.PictureBoxImage.Height \ CInt(My.Settings.Rows)
    Dim W As Int32 = Me.PictureBoxImage.Width \ CInt(My.Settings.Columns)

    ' Create overlay labels
    For Y As Int32 = 1 To CInt(My.Settings.Rows)
        For X As Int32 = 1 To CInt(My.Settings.Columns)
            ' Create and place overlay label
            Dim L As New System.Windows.Forms.Label
            Me.PictureBoxImage.Controls.Add(L)
            L.Left = (X - 1) * W
            L.Top = (Y - 1) * H
            L.Width = CInt(IIf(X = My.Settings.Columns, Me.PictureBoxImage.Width - L.Left, W))
            L.Height = CInt(IIf(Y = My.Settings.Rows, Me.PictureBoxImage.Height - L.Top, H))

            ' Setup overlay label display
            L.Text = CStr((Y - 1) * My.Settings.Columns + X)
            L.BackColor = My.Settings.OverlayBackgroundColor
            L.ForeColor = My.Settings.OverlayForegroundColor
            L.Font = My.Settings.OverlayFont
            L.TextAlign = ContentAlignment.MiddleCenter
            L.BorderStyle = BorderStyle.FixedSingle
        Next
    Next

    ' Load image
    Try
        Me.PictureBoxImage.Image = Image.FromFile(My.Settings.ImageFilePath)
    Catch ex As Exception
        MsgBox("Error loading image: " & ex.Message, MsgBoxStyle.Exclamation)
        Me.Close()
    End Try
End Sub

The only things left to solve are the width and height of the last labels. A control's size property must be an integer, and the screen resolution may not be evenly divisible by the number of squares. Actually the size of the squares at the bottom and to the right of the screen are not the same as the others that fill the remaining space. If this weren't the case, we would be able to see a few pixels of the picture underneath on the right and bottom sides. If you run the aplication now, you'll see no image, only the numbered grid.

Make the Squares Live

A particular square should hide itself when clicked. To achieve this use the Click event handler with the following code:

C#

private void ClickHandler(object sender, EventArgs e)
{
    //  Make label transparent (leave only border)
    System.Windows.Forms.Label L = ((System.Windows.Forms.Label)(sender));
    L.BackColor = Color.Transparent;
    L.Text = "";
}


Visual Basic
Private Sub ClickHandler(ByVal sender As Object, ByVal e As EventArgs)
    ' Make label transparent (leave only border)
    Dim L As System.Windows.Forms.Label = DirectCast(sender, System.Windows.Forms.Label)
    L.BackColor = Color.Transparent
    L.Text = ""
End Sub

The squares should also indicate visually when the mouse pointer moves over them. A good visual cue is to swap the squares' foreground and background colors. There are two methods for doing this:

C#

private void MouseEnterHandler(object sender, EventArgs e)
{
    //  Invert color on hover, if label is not revealed
    System.Windows.Forms.Label L = ((System.Windows.Forms.Label)(sender));
    if (L.BackColor == Color.Transparent)
    {
        return;
    }

    L.BackColor = HiddenImage.Properties.Settings.Default.OverlayForegroundColor;
    L.ForeColor = HiddenImage.Properties.Settings.Default.OverlayBackgroundColor;
}

private void MouseLeaveHandler(object sender, EventArgs e)
{
    //  Return color on hover end, if label is not revealed
    System.Windows.Forms.Label L = ((System.Windows.Forms.Label)(sender));
    if (L.BackColor == Color.Transparent)
    {
        return;
    }

    L.BackColor = HiddenImage.Properties.Settings.Default.OverlayBackgroundColor;
    L.ForeColor = HiddenImage.Properties.Settings.Default.OverlayForegroundColor;
}


Visual Basic
Private Sub MouseEnterHandler(ByVal sender As Object, ByVal e As EventArgs)
    ' Invert color on hover, if label is not revealed
    Dim L As System.Windows.Forms.Label = DirectCast(sender, System.Windows.Forms.Label)
    If L.BackColor = Color.Transparent Then Return
    L.BackColor = My.Settings.OverlayForegroundColor
    L.ForeColor = My.Settings.OverlayBackgroundColor
End Sub

Private Sub MouseLeaveHandler(ByVal sender As Object, ByVal e As EventArgs)
    ' Return color on hover end, if label is not revealed
    Dim L As System.Windows.Forms.Label = DirectCast(sender, System.Windows.Forms.Label)
    If L.BackColor = Color.Transparent Then Return
    L.BackColor = My.Settings.OverlayBackgroundColor
    L.ForeColor = My.Settings.OverlayForegroundColor
End Sub

Next let's "wire" the methods specified above to run at the right moment. You can use the AddHandler command for this while generating the controls. Change the FormGame_Load method by entering the following code to the inner loop (right after line L.BorderStyle = BorderStyle.FixedSingle):

C#

//  Attach event handlers
L.Click += new System.EventHandler(ClickHandler);
L.MouseEnter += new System.EventHandler(MouseEnterHandler);
L.MouseLeave += new System.EventHandler(MouseLeaveHandler);


Visual Basic
' Attach event handlers
AddHandler L.Click, AddressOf ClickHandler
AddHandler L.MouseEnter, AddressOf MouseEnterHandler
AddHandler L.MouseLeave, AddressOf MouseLeaveHandler

These methods will handle the events of all the label controls. The source parameter indicates which particular control caused the event.

The application is almost complete. If we run it now, the squares will hide when they are clicked, and that's what we want.

Catching the Keys

As mentioned earlier, pressing the ENTER key should reveal the entire image, while pressing the ESC key should cancel the game. So, how to you achieve this? By using the form's KeyDown event. Attach the following code to that method:

C#

private void FormGame_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
{
    switch (e.KeyData)
    {
        case Keys.Escape:
        case Keys.Back:
            //  Close window
            this.Close();
            break;
        case Keys.Space:
        case Keys.Enter:
            //  Reveal image
            foreach (System.Windows.Forms.Control C in this.PictureBoxImage.Controls)
            {
                C.Hide();
            }
            break;
    }
}


Visual Basic
Private Sub FormGame_KeyDown(ByVal sender As Object,_
 ByVal e As System.Windows.Forms.KeyEventArgs)_
  Handles Me.KeyDown
    Select Case e.KeyData
        Case Keys.Escape
            ' Close window
            Me.Close()
        Case Keys.Enter
            ' Reveal image
            For Each C As Control In Me.PictureBoxImage.Controls
                C.Hide()
            Next
    End Select
End Sub


We did not hide the label controls completely in the per-square hiding code, but made their contents transparent to leave a border. In the new code revealing the entire image, we will hide them, to show the picture in all its beauty rather than with black grid bars over it.

Conclusion

I hope that by building this application along with me you have learned some useful things to help you in developing more complicated applications.

First, it is important to have a decent user interface — and it is easy and painless to achieve it with Microsoft .NET 2.0. Second, we reviewed how the application settings framework works and how you can easily manage configuration settings. Also you now know how to bind them to control properties and use them procedurally from the code. Third, we dynamically generated controls at run time, setting their properties and handling their events. And last but not at least, you have a fun game to play with friends. So, let's start!

The Discussion

  • User profile image
    AngryUser

    Nice Article so far, BUT: IMHO you don't save your settings. So the line

    HiddenImage.Properties.Settings.Default.Save()

    is missing in FormOptions_FormClosed.

Comments closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to send us feedback you can Contact Us.