Creating a GUI (Part 4)

  CU_MDX_GUI.zip (63.8 KiB, 7,603 hits)


  CUnitFramework.zip (101.1 KiB, 20,443 hits)

Extract the C-Unit Framework
inside the project (not solution) directory.

Our GUI system is now complete, all that is left to do is to learn how to use it. The GUI was designed to be used as easily as possible. In this tutorial, we will create_ 3 different Panels and load them with the Controls that we have implemented.

public enum ControlID { Panel1 = 1, Panel2, Panel3, Fullscreen, Options, Button1, Button2, Check1, Check2, RadioA1, RadioA2, RadioA3, RadioB1, RadioB2, RadioB3, Slider, SingleList, MultiList, Combo, EditBox, Label };

Since each Control needs a unique ID, it’s a good idea to simply create_ an enum. With an enum, we can easily refer to a Control that we have created. It also allows us to respond to GUI events with a simple switch statement using the ID’s as the case statements. One thing to note is that the ID of a Panel should never be 0 since 0 flags a Control as having no Panel.

///
/// This event will be fired immediately after the Direct3D device has been
/// created, which will happen during application initialization. This is the best
/// location to create_ Pool.Managed resources since these resources need to be
/// reloaded whenever the device is destroyed. Resources created
/// here should be released in the OnDetroyDevice callback.
///

/// The Direct3D device
public override void OnCreateDevice( Device device )
{
if ( m_gui != null )
{
m_gui.Clear();

}

m_gui = new Gui.GuiManager( “CUnit.xml”, new Gui.GuiManager.ControlDelegate( OnControl ) );
m_gui.CreateButton( (int)ControlID.Fullscreen, 0, new PointF( (float)BackBufferWidth – 120f, 10f ), new SizeF( 110f, 30f ), “Toggle Fullscreen”, 15f, new ColorValue( 0, 0, 0 ) );
m_gui.CreateButton( (int)ControlID.Options, 0, new PointF( (float)BackBufferWidth – 120f, 50f ), new SizeF( 110f, 30f ), “Options”, 15f, new ColorValue( 0, 0, 0 ) );
m_gui.CreatePanel( (int)ControlID.Panel1, new PointF( 10f, 100f ), new SizeF( 200f, 250f ) );
m_gui.CreatePanel( (int)ControlID.Panel2, new PointF( 250f, 100f ), new SizeF( 200f, 270f ) );
m_gui.CreatePanel( (int)ControlID.Panel3, new PointF( 400f, 100f ), new SizeF( 200f, 300f ) );
m_gui.CreateButton( (int)ControlID.Button1, (int)ControlID.Panel1, new PointF( 10f, 10f ), new SizeF( 100f, 25f ), “Button 1, 16f, new ColorValue( 1f, 1f, 1f ) );
m_gui.CreateButton( (int)ControlID.Button2, (int)ControlID.Panel1, new PointF( 10f, 40f ), new SizeF( 100f, 25f ), “Button 2, 16f, new ColorValue( 1f, 1f, 1f ) );
m_gui.CreateLabel( (int)ControlID.Label, (int)ControlID.Panel1, new PointF( 120f, 13f ), new SizeF( 70f, 50f ), “This is a lovely text label.”, 16f, new ColorValue( 0f, 0f, 0f ), BitmapFont.Align.Left );
m_gui.CreateCheckBox( (int)ControlID.Check1, (int)ControlID.Panel1, new PointF( 10f, 70f ), new SizeF( 25f, 25f ), “Lock Panel 2, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Right, false );
m_gui.CreateCheckBox( (int)ControlID.Check2, (int)ControlID.Panel1, new PointF( 10f, 100f ), new SizeF( 25f, 25f ), “Disable Panel 3, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Right, false );
m_gui.CreateRadioButton( (int)ControlID.RadioA1, (int)ControlID.Panel1, 0, new PointF( 10f, 140f ), new SizeF( 25f, 25f ), “Radio A1”, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Right, false );
m_gui.CreateRadioButton( (int)ControlID.RadioA2, (int)ControlID.Panel1, 0, new PointF( 10f, 170f ), new SizeF( 25f, 25f ), “Radio A2”, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Right, true );
m_gui.CreateRadioButton( (int)ControlID.RadioA3, (int)ControlID.Panel1, 0, new PointF( 10f, 200f ), new SizeF( 25f, 25f ), “Radio A3”, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Right, false );
m_gui.CreateRadioButton( (int)ControlID.RadioB1, (int)ControlID.Panel2, 1, new PointF( 165f, 10f ), new SizeF( 25f, 25f ), “Radio B1”, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Left, false );
m_gui.CreateRadioButton( (int)ControlID.RadioB2, (int)ControlID.Panel2, 1, new PointF( 165f, 40f ), new SizeF( 25f, 25f ), “Radio B2”, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Left, false );
m_gui.CreateRadioButton( (int)ControlID.RadioB3, (int)ControlID.Panel2, 1, new PointF( 165f, 70f ), new SizeF( 25f, 25f ), “Radio B3”, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Left, true );
m_gui.CreateSlider( (int)ControlID.Slider, (int)ControlID.Panel2, new PointF( 10f, 110f ), 180f, 0f, 100f, 20f, “Slider”, 16f, new ColorValue( 0f, 0f, 0f ), CUnit.Gui.Control.TextAlign.Bottom );
m_gui.CreateListBox( (int)ControlID.SingleList, (int)ControlID.Panel2, true, new PointF( 10f, 150f ), new SizeF( 180f, 103f ), 16f, new ColorValue( 0f, 0f, 0f ) );
m_gui.CreateListBox( (int)ControlID.MultiList, (int)ControlID.Panel3, false, new PointF( 10f, 10f ), new SizeF( 180f, 103f ), 16f, new ColorValue( 0f, 0f, 0f ) );
m_gui.CreateComboBox( (int)ControlID.Combo, (int)ControlID.Panel3, new PointF( 10f, 120f ), new SizeF( 180f, 25f ), 130f, 16f, new ColorValue( 0f, 0f, 0f ) );
m_gui.CreateEditBox( (int)ControlID.EditBox, (int)ControlID.Panel3, new PointF( 10f, 152f ), new SizeF( 180f, 130f ), “Enter some text!”, 16f, 100, new ColorValue( 0f, 0f, 0f ) );

for ( int i = 1; i <= 10; i++ )
{

m_gui.AddListableItem( (int)ControlID.SingleList, “Single Item “ + i.ToString(), i );
m_gui.AddListableItem( (int)ControlID.MultiList, “Multi Item “ + i.ToString(), i );
m_gui.AddListableItem( (int)ControlID.Combo, “Combo Item “ + i.ToString(), i );

}
( m_gui[(int)ControlID.SingleList] as Gui.ListBox ).SelectItem( “Single Item 2 );
( m_gui[(int)ControlID.MultiList] as Gui.ListBox ).SelectItem( “Multi Item 1 );
( m_gui[(int)ControlID.MultiList] as Gui.ListBox ).SelectItem( “Multi Item 5 );
( m_gui[(int)ControlID.Combo] as Gui.ComboBox ).SelectItem( “Combo Item 2 );

m_gui.OnCreateDevice( device );

}

This is all the GUI creation code. It appears a bit messy here but it’s all just a bunch of one-line Control creation method calls. To initialize the GUI, simply create_ a new GuiManager instance and pass in the XML file name and a delegate to send all the Control events to. All of the GuiManager.Create* methods are fairly similar. They take an ID, a Panel ID (which can be set to 0 to use the Control without a Panel), a location, a size, and some other parameters that are specific to each Control.

GuiManager has some other methods that we can call to initialize it such as GuiManager.AddListableItem. This method is used to add items to the Controls that hold lists of items such as the ListBox and the ComboBox. GuiManager also has an indexer that allows us to access individual Controls by ID, as you can see in the SelectItem calls.

/// Gui Control handler
/// Control ID
/// Control data
public override void OnControl( int controlID, object data )
{
switch ( controlID )
{
case (int)ControlID.Button1:
m_feedback = “You pressed Button 1.”;
BuildText();
break;

case (int)ControlID.Button2:

m_feedback = “You pressed Button 2.”;
BuildText();
break;

case (int)ControlID.Check1:

m_feedback = “CheckBox 1 is “ + ((bool)data ? “checked.” : “unchecked.” );
( m_gui[(int)ControlID.Panel2] as Gui.Panel ).Locked = (bool)data;
BuildText();
break;

case (int)ControlID.Check2:

m_feedback = “CheckBox 2 is “ + ( (bool)data ? “checked.” : “unchecked.” );
m_gui.DisableControl( (int)ControlID.Panel3, (bool)data );
BuildText();
break;

case (int)ControlID.RadioA1:

m_feedback = “RadioButton A1 is “ + ( (bool)data ? “checked.” : “unchecked.” );
BuildText();
break;

case (int)ControlID.RadioA2:

m_feedback = “RadioButton A2 is “ + ( (bool)data ? “checked.” : “unchecked.” );
BuildText();
break;

case (int)ControlID.RadioA3:

m_feedback = “RadioButton A3 is “ + ( (bool)data ? “checked.” : “unchecked.” );
BuildText();
break;

case (int)ControlID.RadioB1:

m_feedback = “RadioButton B1 is “ + ( (bool)data ? “checked.” : “unchecked.” );
BuildText();
break;

case (int)ControlID.RadioB2:

m_feedback = “RadioButton B2 is “ + ( (bool)data ? “checked.” : “unchecked.” );
BuildText();
break;

case (int)ControlID.RadioB3:

m_feedback = “RadioButton B3 is “ + ( (bool)data ? “checked.” : “unchecked.” );
BuildText();
break;

case (int)ControlID.Slider:

m_feedback = “Slider value is “ + ( (float)data ).ToString() + “.”;
BuildText();
break;

case (int)ControlID.SingleList:

m_feedback = “ListBox data is “ + ( ( ( (ArrayList)data ).Count > 0 ) ? ( (ArrayList)data )[0].ToString() : “nothing.” );
BuildText();
break;

case (int)ControlID.MultiList:

if ( ( (ArrayList)data ).Count == 0 )
{
m_feedback = “ListBox data is nothing.”;

}
else
{

m_feedback = “ListBox data is “;
foreach ( object o in ( data as ArrayList ) )
{
m_feedback += o.ToString() + “, “;

}

}
BuildText();
break;

case (int)ControlID.Combo:

m_feedback = “ComboBox data is “ + ( (ArrayList)data )[0].ToString();
BuildText();
break;

case (int)ControlID.EditBox:

m_feedback = “EditBox data is “ + (string)data;
BuildText();
break;

case (int)ControlID.Fullscreen:

m_framework.ToggleFullscreen();
break;

case (int)ControlID.Options:

m_framework.PushState( new DeviceOptionsDialog( m_framework ) );
break;

}

}

The OnControl method is the method that is called whenever a GUI event occurs. To respond to the event, simply do a switch on the controlID and respond accordingly. Examples of how to respond to each type of Control is shown above. Some Controls have different types of data so we have to cast the data according to the type of Control that sent the data. For example, CheckBoxes and RadioButtons send data as a bool, Sliders send data as a float, ListBoxes and ComboBoxes send data as an ArrayList, and EditBoxes send data as a string.

/// Keyboard event handler
/// List of pressed keys
/// Unicode character read from Windows Form
/// Pressed key from Form used for repeatable keys
/// Time since last frame
public override void OnKeyboard( List pressedKeys, char pressedChar, int pressedKey, float elapsedTime )
{
if ( m_gui.KeyBoardHandler( pressedKeys, pressedChar, pressedKey ) )
{
return;

}

}

///

Mouse event handler
/// Mouse position in client coordinates
/// X-axis delta
/// Y-axis delta
/// Mouse wheel delta
/// Mouse buttons
public override void OnMouse( Point position, int xDelta, int yDelta, int zDelta, bool[] buttons, float elapsedTime )
{
if ( m_gui.MouseHandler( position, buttons, zDelta ) )
{
return;

}

}

To send input data to GuiManager just call the corresponding input method. These methods both return values that say whether a Control has processed input. If a Control has processed input, then we don’t want the application to further process the input so we return from the method. For example, say we had a mouselook feature where we have to press and drag the mouse to look around. If we are sliding a Slider, we don’t also want to look around. In this case, GuiManager.MouseHandler returns true and we can return to prevent further processing.

///
/// This event will be fired immediately after the Direct3D device has been
/// reset, which will happen after a lost device scenario, a window resize, and a
/// fullscreen toggle. This is the best location to create_ Pool.Default resources
/// since these resources need to be reloaded whenever the device is reset. Resources
/// created here should be released in the OnLostDevice callback.
///

/// The Direct3D device
public override void OnResetDevice( Device device )
{
if ( m_gui != null )
{
m_gui.OnResetDevice( device );

}

// Clip…

m_gui.SetPosition( (int)ControlID.Fullscreen, new PointF( (float)BackBufferWidth – 120f, 10f ) );
m_gui.SetPosition( (int)ControlID.Options, new PointF( (float)BackBufferWidth – 120f, 50f ) );

}

GuiManager has all the usual resource handling methods (On*Device) that need to be called in their corresponding methods in order to keep its resources valid.

One last thing to notice is that when the device is reset, I update_ the location of a couple of Controls. This keeps these Controls in the top right corner of the window when the window is resized and when we toggle to fullscreen mode.

Some final notes on the GUI. Whenever a Control is updated, the entire VertexBuffer is rebuilt. If GuiManager has lots of Controls, you may see a dip in the fps in cases where the VertexBuffer is continuously updated such as when sliding a Slider or dragging a Panel. These are usually short actions and are not in performance intensive instances so I didn’t bother spending the time to improve it. You could improve performance by only updating the affected Controls, but I’ll leave that as an exercise for the readers 🙂

Whew! Four tutorials and more than 5000 lines of code later and we have our GUI system! Wasn’t that fun? Now go off and create_ your GUI-filled programs. I need some sleep…