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?

15 comments:

Jason said...

Hey Aaron. I appreciate the work you've put into this and I know how it feels not to get any sort of feedback. I looked for your email address, but didn't see it anywhere (not that I blame you).

Anyway, I've just started looking into your GUI because I didn't want to re-invent the wheel with a game I'm designing. I have some questions, but keep in mind I haven't had a lot of time to look over the code so this might already be implemented.

- Using your GUI_Testbed, I get this warning spammed about 10 times when a window is created using your default code: "A first chance exception of type 'System.OverflowException' occurred in mscorlib.dll"

- You have a Resizable bool, but no Moveable? I'd like to have a static window placed on the screen in some cases that isn't moveable or resizeable.

- In that same vein, is there a way to remove the title bar? So a window is just a rectanglar area without a title and close button?

- How about an option to make the background of the window transparent, but keep the controls and labels normal?

- Finally, an option to have the window stick to the edges of the screen instead of drawing off-screen?

I know I could edit it myself, but I'd probably just end up making ugly hacks everywhere because I don't have the same level of knowledge that you would. :)

A tutorial on skinning the GUI would be really helpful. Something using textures for components, backgrounds, and title bars?

Thanks for the great software.

Aaron said...

Hey Jason, thanks for the comment. My email address is aaronkm [at] gmail [dot][com]. I'm trying to reduce spam, not working so far!

I have no idea what those exceptions are, but it sounds like an infinite loop or something. If you can provide me any more information I'd be grateful.

I never thought to make a window unmovable, but it wouldn't be hard to do. Simply add a property to the Window class, IsMovable. When it's set to false, remove the movableArea and fullWindowMovableArea controls from the GUI, using Remove(). Similarly, Add() them when set to true. The GUI checks that a control is only added or removed once.

Originally I had a removable title bar, but it complicated matters. I figured that for game message boxes, I could create a separate class. It wouldn't be movable, resizeable or have a title bar. The only reason I haven't created it yet, is because I haven't made a game that requires it yet. A bit of study of the system will show that it wouldn't be hard to implement. It's definitely going to be included in the future though.

Originally that was the way transparency worked. Simply change the Window.Transparency property to set the box control transparency instead of the actual window.

I hadn't thought of the sticky thing before, I'm sure it's possible. I'll put it on my to-do list!

The nest tutorial should be next week. They always end up taking longer than I plan!

Good luck!

Anonymous said...

hey arron, I also want to say that this is super work, and I apreciate the fact that you released it, source included.

please keep up the good work, and I look forward to seeing any improvments you make to this in the future.

This is by far the best ui i have seen for xna thus far...

Jason said...

Lots of Jasons here!

Aaron, I'm a self-taught programmer and have never used the debugger to any extent, so I couldn't easily determine the source of the exception. The program doesn't exit, it just prints the exceptions. I'll look into it more.

I've edited some functionality using your wonderfully documented code and you're right, it was pretty easy to implement most of it. Instead of butchering your work though, I think I'll just extend it through inheritance. Looking forward to the next tutorial!

Bob said...

Aaron. Keep up the good work. I haven't had time to check it out much yet, but GUI is definitely one of the missing links for XNA development right now.

It looks like you've put quite a bit of thought into this, and I look forward to playing with it.

Aaron said...

Sorry I haven't replied for a while, I've just started a new placement and haven't had an Internet connection for a few days.

Thanks for all the great comments, it really is motivating to work on a project that people actually use.

Jason, the first one, why don't you try sending me the actual exception message, and the piece of code that's throwing it. I am quite keen to reproduce this error, because it's the only real bug I know about at the moment.

By the way, I hope to release the source code of my tile map editor which uses this GUI, sometime next week. That should give you guys some help by picking through it's code. After that I may start extending the GUI. If people could compile lists of features or changes they wish to see, that would be quite helpful.

SofaKng said...

Hey Aaron! This looks EXCELLENT!

Is it compatible with the latest version of XNA? (aka the XNA Refresh)

I'd definitely like to use this in my project.

Oh, also, does your GUI library have support for menubars?

Aaron said...

sofakng:

Yes, I finally got round to testing it with XNA Refresh, and it all works fine out of the box.

Menu bars are fully implemented, and the first tutorial actually shows you how to use them. The only thing missing at the moment are menu item dividers, and possibly keyboard shortcuts. The next release may deal with controlling the GUI using the keyboard and joypad.

Anthony H said...
This comment has been removed by the author.
Anthony H said...

This has been in use by our group at www.legendro.net

I've been rebuilding a 3d client for our game server which is currently only a hack/emulator for the game Ragnarok Online. We are using your Windows system to allow us to make our own game. We save locations of the different windows in an XML file, and it reloads them.

For user authentication, I added a dirty little hack but it is somewhat efficient. But the design is pretty much a password field, where you specify the textbox constructor with a replacement string that will be displayed/printed by the GUIManager.

Example:
passTextBox = new TextBox(game, guiManager, "*");

Source: TextBox.cs

We will be sticking with this UI system till we finish our game sometime in August.

Also, if you need testing, or a place to get some results, don't hesitate to ask some from us.

contact me at anthony [dot- hernandez [at- clownphobia [dot- com

Aaron said...

Anthony - It's cool that you've found my library useful. I've had some others asking for password textboxes too. Guess it'll be an addition for the next version.

I'd appreciate any bugs or feature requests. I'm probably going to move the project onto SourceForge at some point, as soon as I can think of a proper name for it! Then it'll be easier to log them.

I'm not actually working on the system at the moment, although I will pick it up again at some point in the future. I'm trying to develop a program I created for my final university project, a graphical game script editor.

If anybody is interested in helping with development of the GUI, then I'd like to hear from you.

Johnathon said...

Hi Aaron, I found your blog from an rss feed over at Mykres Space and all I can say is wow. I've been struggling incorporating a GUI system into my teams project, and this sounds like just the thing to solve our problems. It looks great and I can't wait to see your next tutorial!

Sparky said...

Hey Aaron

Great GUI library!!

Like some of the other guys here, I have a question or 2. Forgive my potential ignorance though!

In your GUI testbed application, you create a new "Window" every time the Enter key is pressed and add some controls to it.

I added a button and then added a Click event to that button. If there are several of these "Windows" on the screen at the same time, how can you reference the correct "Window" that the clicked button is on within the event-handler function?

Does that make any sense?

Regards

Mark

Aaron said...

Sparky:

First of all... thanks!

To be honest, the testbed isn't the way real applications should be developed. The reason a new window comes up each time, is so that I get a bunch of controls on the screen, and check they're being freed properly after the windows are closed.

The first tutorial should give you a good overview of how to use the system, and keeping hold of references in particular.

If that's not really what you were asking, then just leave another message. Good luck!

Unknown said...

Hi Aaron!

This is a fantastic tool for my project! But i need a little bit help.

I can create a dialog box, but the Combobox doesnt work for me.I cant select item from the list.

Thanks for help!

Sanci