Tutorial 2


This next tutorial will cover how to create a class that inherits from Dialog, and retrieve some useful information from it. This method of using windows was developed while putting together my tile map editor application. It is the best way of using the GUI that I have discovered so far, and is pretty closely based on other window systems I've used in the past. I'm sure some other techniques will expose themselves in the future.

Setting Up The Project

I have already covered setting up a new project, and setting up the GUIManager in Tutorial 1, so you can go back and look it up there if necessary. Simply follow Tutorial 1 up until the Adding Controls heading, and you'll be ready to go.

Creating The Dialog

We will create a Dialog box, where the user can enter their name into a textbox. It will have OK and Cancel buttons, so that the dialog can return a result.

The first thing to do is to create a new class called UserDetailsDialog, which is inherited from Dialog. Dialog is inherited from Window, and the only addition is that it returns a result, which can be queried after the Dialog is closed.

using System;
using Microsoft.Xna.Framework;
using WindowSystem;

namespace WindowSystemTestbed
{
    public class UserDetailsDialog : Dialog
    {
    }
}

Next we can add a few fields to our new class.

private const int LargeSeparation = 10;
private const int SmallSeparation = 5;
private TextButton OKButton;
private TextButton cancelButton;
private TextBox nameTextBox;

The LargeSeparation and SmallSeparation constants are something I add to all my dialogs. They allow me to control the spacing between the window edge and between dialog controls. I use LargeSeparation for the window edge, and between unrelated controls. SmallSeparation is used for the space between related controls, like between a textbox and it's label.

The rest of the fields are controls that we will need to keep track of after they are set up. We will need to get the text from nameTextBox for example. Other controls like labels can just be added without keeping a reference if they don't need to change, or we don't need to query them after events are triggered.

Next we should add a property, so that the user's name can be queried from outside the class.

public string Name
{
    get { return this.nameTextBox.Text; }
}

The constructor sets the control properties and adds them as child controls, as well as settings it's own properties.

public UserDetailsDialog(Game game, GUIManager guiManager)
    : base(game, guiManager)
{
    // Name label
    Label nameLabel = new Label(game, guiManager);
    Add(nameLabel);
    nameLabel.Text = "Name:";
    nameLabel.X = LargeSeperation;
    nameLabel.Y = LargeSeperation;
    nameLabel.Width = 75;
    nameLabel.Height = nameLabel.TextHeight;

    // Name textbox
    this.nameTextBox = new TextBox(game, guiManager);
    Add(this.nameTextBox);
    this.nameTextBox.Initialize();
    this.nameTextBox.X = nameLabel.X;
    this.nameTextBox.Y = nameLabel.Y + nameLabel.Height + SmallSeperation;

    // Set the window width to the default textbox width
    ClientWidth = nameTextBox.Width + (2 * LargeSeperation);

    // Cancel button
    this.cancelButton = new TextButton(game, guiManager);
    Add(this.cancelButton);
    this.cancelButton.Text = "Cancel";
    this.cancelButton.X = ClientWidth - this.cancelButton.Width - LargeSeperation;
    this.cancelButton.Y = this.nameTextBox.Y + this.nameTextBox.Height + LargeSeperation;
    this.cancelButton.Click += new ClickHandler(OnButtonClicked);

    // OK button
    this.OKButton = new TextButton(game, guiManager);
    Add(this.OKButton);
    this.OKButton.Text = "OK";
    this.OKButton.X = this.cancelButton.X - SmallSeperation - this.OKButton.Width;
    this.OKButton.Y = this.cancelButton.Y;
    this.OKButton.Click += new ClickHandler(OnButtonClicked);

    // Set the window height to the amount needed to show all controls
    ClientHeight = this.OKButton.Y + this.OKButton.Height + LargeSeperation;

    // Set the window title
    TitleText = "User Details";

    // This dialog does not need to be resized by the user
    Resizable = false;
    CenterWindow();
}

Note that I add child controls before setting their properties. This is because some controls don't become fully set up until they are initialised, which happens automatically when they are added to the GUI or another control. You could also just call Initialize() manually, but who needs the extra typing?

Also notice that control positions are set in relation to each other. I find this keeps the layout quite dynamic, especially for resizeable windows. I will consider providing layout managers in a future release, perhaps similar to those found in the Java Swing library.

The TextBox and TextButton controls don't have their Width or Height properties set. That's because some controls have useful default sizes, which allows some flexibility when modifying the default GUI settings.

Next we will add an event handler to the class, which will be called whenever a button is clicked. The buttons had this event handler added for their Click events in the constructor.

protected void OnButtonClicked(UIComponent sender)
{
    if (sender == this.OKButton)
         SetDialogResult(DialogResult.OK);

    CloseWindow();}

This method simply checks which button was clicked, and sets the result accordingly before closing the dialog. We don't bother checking for the Cancel button because cancel is the default result. This is because that is also the result when the user clicks on the window close button. If necessary the close button can be removed by setting the HasCloseButton property to false.

Using The Dialog

Back to the main game class which I have called Tutorial2, and we're going to attempt to use our new dialog.

The application will show an information message box, explaining how to use our simple program, then show our User Details dialog box when the user presses enter. If the user clicks on the OK button, a message box will display the name entered into the dialog textbox.

Firstly, add a dialog reference to our Game class.

private UserDetailsDialog dialog;

Next add an event handler that will be called when the user presses a key. In the constructor add the following code.

// Add a key down event handler
this.input.KeyDown += new KeyDownHandler(OnKeyDown);

Then create the event handler method.

private void OnKeyDown(KeyEventArgs args)
{
    if (args.Key == Keys.Enter)
    {
        // Only create a new dialog if one isn't already shown
        if (this.gui.GetModal() == null)
        {
            // Create and show dialog
            this.dialog = new UserDetailsDialog(this, this.gui);
            this.dialog.Close += new CloseHandler(OnDialogClosed);
            this.dialog.Show(true);
         }
    }
}

Basically this function just checks that no other modal window is already opened, if not then it pops up a new User Details dialog. Modal just means any window that has exclusive focus, such as a message box, or our dialog. To show a window as modal, pass true to the Window.Show() method, otherwise pass false.

When your program calls a modal dialog in Windows, it takes full control of you're processing until the dialog is closed. Unfortunately, our system is a little different. We have to add an event handler to the window's Close event, that checks which window was closed, and acts accordingly.

The following code is the method called when our dialog is closed.

private void OnDialogClosed(UIComponent sender)
{
    if (sender == this.dialog)
    {
        if (this.dialog.DialogResult == DialogResult.OK)
        {
            MessageBox message = new MessageBox(
                this,
                this.gui,
                "Name: " + this.dialog.Name,
                "User Name",
                MessageBoxButtons.OK,
                MessageBoxType.Info
                );
                message.Show(true);
        }

        // Remove event handler so garbage collector will pick it up
        this.dialog.Close -= OnDialogClosed;
        this.dialog = null;
    }
}

A message box is shown repeating the name entered by the user, only if the result was OK. Then the event handler is removed from the dialog and the dialog is set to null, to prevent it from lingering about in memory. This is very important to remember, especially in a real application. I've caught out by event handlers and the garbage collector on few occasions.

The final things to add is a message box explaining how to use the application. This should be placed in the Game class Initialize() method, to ensure it's shown at the start of the program.

// Show a message box informing the user how to use the program
MessageBox info = new MessageBox(
    this,
    this.gui,
    "Press enter to bring up the dialog.",
    "Info",
    MessageBoxButtons.OK,
    MessageBoxType.Info
    );
info.Show(true);

Conclusion

I hope this tutorial was helpful, it was definitely more difficult to write than the last one. I would really appreciate any feedback. I think in the next tutorial I will cover skinning and modifying the GUI.

All tutorial source code is included in the WindowSystemTestbed project.

Last edited Sep 8, 2008 at 9:07 PM by cdmac, version 3

Comments

JoeBanana Nov 9, 2007 at 6:56 AM 
I can't wait to see it...