Monday, April 30, 2007

Tutorial 2

I haven't had much feedback regarding the first tutorial, so I'm assuming nobody has had any problems with it. I hope it was useful to some of you.

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 methods 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 the previous tutorial, 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 XNAWindowSystem;

namespace GUI_Testbed
{
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 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 that kind of 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, because I'm quite new to writing documents like this.

I'm sure some of you are wondering where to find the source code for my tutorials, well they're not available at the moment. I will add it to the GUI_Testbed project for the next release of the WindowSystem.

I'm not sure what to cover in the next one. I'll probably wait until I get some responses from this one before I decide. I think I should cover skinning and modifying the GUI at some point. Any ideas?

Thursday, April 26, 2007

Fixed Project Files

Now that I have an Internet connection again, I decided to fix the project files that you guys have had problems with. It seems Visual Studio uses relative paths for everything, except the XNA content assemblies.

Download Source Code

This release is exactly the same as the last one, so don't bother downloading if you've already fixed your project files.

Microsoft have recently released an update for XNA, which I will be checking out tonight. It has font support, although I gather it's fairly basic. I will try to integrate this into the GUI, although I suspect I will run into the same problems I had before with their font sample code. In case you haven't read my comments from a while ago, the problem is that anti-aliased fonts simply won't work with the GUI at the moment. The only way I can see it working is to implement multiple passes when rendering the controls. Personally I think that small font sizes should be used anyway, which look better without anti-aliasing.

Monday, April 23, 2007

Quick Update

I've been pleased to learn that some people have started to use my window system, at least to play around with. If anybody manages to finish a game or tool that uses the GUI, then I'd love to hear about it.

Obviously the system is in it's early stages, and is yet to be used in an actual game. The good news is that I now have an Internet connection on my development computer now, so I'll be able to resume work on it sometime over the next few days. Before I do that however, I'd like to write another couple tutorials to bring everybody up to speed on how to use the system. Also, I'd like to write an article on the design of the system, which may help people to understand it better, especially for those who wish to expand upon it.

I'm hoping to finish the first version of my XNA tile map editor I've been working on in parallel with the window system. The source code for that should shed some light on how best to use the system. The tutorials are actually based on what I learned from building that application.

Regarding the Internet connection, I spent several hours yesterday looking for that perfect spot in my basement room where I could get a signal from the wireless router with more than a one bar signal strength. Turns out it's in the closet, so the computer is currently half inside my room and the closet!

Wednesday, April 18, 2007

Tutorial 1

This first tutorial will cover setting up the GUIManager, adding new controls, and handling their events.

Setting Up The Project

The simplest way to begin is to use the GUI_Testbed project as a starting template. I simply created a new source file containing a class called Tutorial1, and set that as the startup Game object in Program.cs. You could also just use the existing GUI_Testbed.cs if you don't feel like starting from scratch.

Setting Up The GUIManager

The first thing to do is to add a couple using statements for the InputEventSystem and the XNAWindowSystem.


using System;
using Microsoft.Xna.Framework;
...
using InputEventSystem;
using XNAWindowSystem;


Now add some fields.


private GraphicsDeviceManager graphics;
private InputEvents input;
private GUIManager gui;


In the constructor we set up these objects, and add input and gui to the list of game components. It is important that the InputEvents object is created before the GUIManager object because it adds itself to the game services in the constructor, which the GUI will need access to. Not creating an InputEvents object at all will cause the program to crash.


public Tutorial1()
{
this.graphics = new GraphicsDeviceManager(this);

this.input = new InputEvents(this);
Components.Add(this.input);

this.gui = new GUIManager(this);
Components.Add(this.gui);

// GUI requires variable timing to function correctly
IsFixedTimeStep = false;
Window.Title = "XNA Window System Tutorial 1";
}


The GUIManager object should be initialised in the overriden Initialize() method, before any child controls are added. This is so that order of resource loading can be predicted.


protected override void Initialize()
{
// Has to be initialised before child controls can be added
this.gui.Initialize();

base.Initialize();
}


You will also want to override the Draw() method, to clear the screen each frame, although this probably doesn't affect the GUI as it handles drawing itself.

Adding Controls

I'm going to use a menu bar with various menus and menu items as an example. This will show how to create controls, how to add them to the GUI, and how to handle their events.

First we should add some fields, which are the menu bar and the menu items that the user will actually select. This means that we don't actually need to keep a reference to the 'File' menu for example, because it handles itself. We do need to keep references to child menu items that will actually be clicked, so that we can determine which one was selected. An example would be the 'New' or 'Save' menu items.


private MenuBar menuBar;
private MenuItem newMenuItem;
private MenuItem openMenuItem;
private MenuItem saveMenuItem;
private MenuItem saveAsMenuItem;
private MenuItem exitMenuItem;
private MenuItem undoMenuItem;
private MenuItem redoMenuItem;


Next we have to actually set up the GUI objects, and add them to the GUI. This should take place in Initialize(), after the GUIManager has been initialised.


this.menuBar = new MenuBar(this, gui);
MenuItem fileMenu = new MenuItem(this, gui);
fileMenu.Text = "File";
this.newMenuItem = new MenuItem(this, gui);
this.newMenuItem.Text = "New...";
fileMenu.Add(this.newMenuItem);
this.openMenuItem = new MenuItem(this, gui);
this.openMenuItem.Text = "Open...";
fileMenu.Add(this.openMenuItem);
this.saveMenuItem = new MenuItem(this, gui);
this.saveMenuItem.Text = "Save";
this.saveMenuItem.IsEnabled = false;
fileMenu.Add(this.saveMenuItem);
this.saveAsMenuItem = new MenuItem(this, gui);
this.saveAsMenuItem.Text = "Save As...";
this.saveAsMenuItem.IsEnabled = false;
fileMenu.Add(this.saveAsMenuItem);
this.exitMenuItem = new MenuItem(this, gui);
this.exitMenuItem.Text = "Exit";
fileMenu.Add(this.exitMenuItem);
menuBar.Add(fileMenu);
MenuItem editMenu = new MenuItem(this, gui);
editMenu.Text = "Edit";
this.undoMenuItem = new MenuItem(this, gui);
this.undoMenuItem.Text = "Undo";
this.undoMenuItem.IsEnabled = false;
editMenu.Add(this.undoMenuItem);
this.redoMenuItem = new MenuItem(this, gui);
this.redoMenuItem.Text = "Redo";
this.redoMenuItem.IsEnabled = false;
editMenu.Add(this.redoMenuItem);
this.menuBar.Add(editMenu);

// Add menubar to gui
this.gui.Add(this.menuBar);


You may notice that some of the menu items have a property called IsEnabled set to false. This basically makes the item grey and unclickable, just how windows does it. This property only applies to MenuItem objects currently, but at some point in the future, I will probably make it apply to all controls.

Now if you run the application, it should show a menu bar with all the new menus and menu items.

Control Events

Next we will add event handlers that will be called whenever a menu item is clicked. The reason for holding references to menu items is for comparison with the clicked control, determining which one was actually clicked.

So add the following code to the Initialize() method, probably after the menu bar is added to the GUI.


// Add event handlers
this.newMenuItem.Click += new ClickHandler(OnMenuItemClicked);
this.openMenuItem.Click += new ClickHandler(OnMenuItemClicked);
this.exitMenuItem.Click += new ClickHandler(OnMenuItemClicked);
this.undoMenuItem.Click += new ClickHandler(OnMenuItemClicked);
this.redoMenuItem.Click += new ClickHandler(OnMenuItemClicked);


Finally we need to add a new method that is called whenever a menu item is clicked. We simply check which item was clicked, and act accordingly.


private void OnMenuItemClicked(UIComponent sender)
{
if (sender == this.newMenuItem)
{
MessageBox messageBox = new MessageBox(
this,
gui,
"New clicked!",
"Tutorial 1",
MessageBoxButtons.OK,
MessageBoxType.Info
);
messageBox.Show(true);
}
else if (sender == this.openMenuItem)
{
MessageBox messageBox = new MessageBox(
this,
gui,
"Open clicked!",
"Tutorial 1",
MessageBoxButtons.OK,
MessageBoxType.Info
);
messageBox.Show(true);
}
else if (sender == this.undoMenuItem)
{
MessageBox messageBox = new MessageBox(
this,
gui,
"Undo clicked!",
"Tutorial 1",
MessageBoxButtons.OK,
MessageBoxType.Info
);
messageBox.Show(true);
}
else if (sender == this.redoMenuItem)
{
MessageBox messageBox = new MessageBox(
this,
gui,
"Redo clicked!",
"Tutorial 1",
MessageBoxButtons.OK,
MessageBoxType.Info
);
messageBox.Show(true);
}
else if (sender == this.exitMenuItem)
Exit();
}


This first tutorial was really simple, but should show you the basics of using the window system. It should be enough if all you want to do is add a couple buttons to your game. In the next tutorial I will show how to create a class that inherits from Window, to show how to begin to create a real application.