Creating a GUI (Part 1)

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


  CUnitFramework.zip (101.1 KiB, 19,879 hits)

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

This is the first tutorial of a 4-part series designed to create_ a GUI system. A GUI system, or graphical user interface, is one of the most user-friendly methods of letting users interact with your programs. During this tutorial series we will learn how to make a GUI system as shown below:

GUI

The GUI system will support buttons, sliders, checkboxes, radio buttons, drop down menus, list boxes, text labels, and edit boxes. All of these controls have the option of being placed in moveable windows as shown in the above screenshot.

Every graphical user interface ever made that uses a mouse needs to know at least 2 things: where the mouse is located and whether it is clicked. Once we know the position and state of the mouse, it is only a matter of determining whether the mouse is clicked over a control and responding to the click. Luckily the C-Unit Framework already supplies all this information in the OnMouse method.

All the controls that we will implement will be formed with a sort of jigsaw puzzle of Quads. If you don’t remember the Quad class from earlier tutorials, here it is again:

namespace CUnit
{
public class Quad : ICloneable
{
protected TransformedColoredTextured[] m_vertices;

public Quad() Toggle
{
// Empty
}


/// Creates a new Quad
/// Top left vertex.
/// Top right vertex.
/// Bottom left vertex.
/// Bottom right vertex.
public Quad( TransformedColoredTextured topLeft, TransformedColoredTextured topRight, Toggle
TransformedColoredTextured bottomLeft, TransformedColoredTextured bottomRight )
{
m_vertices = new TransformedColoredTextured[6];
m_vertices[0] = topLeft;
m_vertices[1] = bottomRight;
m_vertices[2] = bottomLeft;
m_vertices[3] = topLeft;
m_vertices[4] = topRight;
m_vertices[5] = bottomRight;
}


/// Gets and sets the vertices.
public TransformedColoredTextured[] Vertices Toggle
{
get { return m_vertices; }
set { value.CopyTo( m_vertices, 0 ); }
}


/// Gets the top left vertex.
public TransformedColoredTextured TopLeft Toggle
{
get { return m_vertices[0]; }
}


/// Gets the top right vertex.
public TransformedColoredTextured TopRight Toggle
{
get { return m_vertices[4]; }
}


/// Gets the bottom left vertex.
public TransformedColoredTextured BottomLeft Toggle
{
get { return m_vertices[2]; }
}


/// Gets the bottom right vertex.
public TransformedColoredTextured BottomRight Toggle
{
get { return m_vertices[5]; }
}


/// Gets and sets the X coordinate.
public float X Toggle
{
get { return m_vertices[0].X; }
set
{
float width = Width;
m_vertices[0].X = value;
m_vertices[1].X = value + width;
m_vertices[2].X = value;
m_vertices[3].X = value;
m_vertices[4].X = value + width;
m_vertices[5].X = value + width;
}
}


/// Gets and sets the Y coordinate.
public float Y Toggle
{
get { return m_vertices[0].Y; }
set
{
float height = Height;
m_vertices[0].Y = value;
m_vertices[1].Y = value + height;
m_vertices[2].Y = value + height;
m_vertices[3].Y = value;
m_vertices[4].Y = value;
m_vertices[5].Y = value + height;
}
}


/// Gets and sets the width.
public float Width Toggle
{
get { return m_vertices[4].X – m_vertices[0].X; }
set
{
m_vertices[1].X = m_vertices[0].X + value;
m_vertices[4].X = m_vertices[0].X + value;
m_vertices[5].X = m_vertices[0].X + value;
}
}


/// Gets and sets the height.
public float Height Toggle
{
get { return m_vertices[2].Y – m_vertices[0].Y; }
set
{
m_vertices[1].Y = m_vertices[0].Y + value;
m_vertices[2].Y = m_vertices[0].Y + value;
m_vertices[5].Y = m_vertices[0].Y + value;
}
}


/// Gets the X coordinate of the right.
public float Right Toggle
{
get { return X + Width; }
}


/// Gets the Y coordinate of the bottom.
public float Bottom Toggle
{
get { return Y + Height; }
}


/// Gets and sets the Quad’s color.
public int Color Toggle
{
get { return m_vertices[0].ColorValue; }
set
{
for ( int i = 0; i < 6; i++ )
{
m_vertices[i].ColorValue = value;
}
}
}


/// Writes the Quad to a string
/// String
public override string ToString() Toggle
{
string result = “X = “ + X.ToString();
result += “nY = “ + Y.ToString();
result += “nWidth = “ + Width.ToString();
result += “nHeight = “ + Height.ToString();
return result;
}


/// Clones the Quad.
/// Cloned Quad
public object Clone() Toggle
{
return new Quad( m_vertices[0], m_vertices[4], m_vertices[2], m_vertices[5] );
}

}
}

A Quad is simply a rectangle of polygons in screen coordinates. As I stated before, each control will be formed out of a jigsaw puzzle of these Quads. This allows us to scale the controls without any loss of image quality. For example, some of the controls are composed of 9 Quads to form a scaleable rectangle: the 4 corners, the 4 sides, and the middle. The moveable Panel in the screenshot above is an example of a 9 piece control. In this case, the top and bottom sides scale horizontally, the left and right sides scale vertically, the middle would be scaled to cover the size of the control, and the corners don’t scale at all. Below is the texture that holds all the pieces for the screenshot you see above.

Texture

Keeping this scaling in mind, we should create_ the images so that the portions that are scaled look the same when they’re both scaled big and when they’re scaled small. Of course, if you’re never going to change the size of your controls, you can make them look however you want.

Since all the controls are made out of this jigsaw puzzle of Quads, it should be fairly easy to create_ different GUI stylesheets. We just need to create_ a texture image and define the locations and dimensions of all the different pieces. A great way to do this is with XML. For each new GUI style, we’ll create_ a texture and then write up an XML file that contains the location, width, and height of each piece. Here is the XML file for the displayed style.

In the XML file, each control is made of several Quads. Each Quad has a name, rectangle and color. To store this information, we’ll create_ a couple of simple classes:

/// A ControlNode of the XML file
public class ControlNode
{
public string Name;
public List Images = new List();
}

/// An ImageNode of the XML file
public class ImageNode
{
public string Name;
public Color Color;
public RectangleF Rectangle;
}

These classes simply store the information found in the XML file.

Since all the controls share the same basic functionality, we’ll create_ an abstract Control class:

namespace CUnit.Gui
{
/// Base Control class.
public abstract class Control
{
public enum ControlState { Normal, Over, Down, Disabled };
public enum TextAlign { Left, Right, Top, Bottom, Center };

protected int m_id;
protected List m_quads;
protected List m_fontQuads;
protected Rectangle m_hotspot;
protected ControlState m_state;
protected TextAlign m_textAlign;
protected RectangleF m_textRect;
protected SizeF m_size;
protected bool m_disabled;
protected bool m_hasFocus;
protected bool m_mouseDown;
protected object m_data;
protected int m_zDepth;
protected string m_text = string.Empty;
protected bool m_hasTouchDownPoint;
protected Point m_touchDownPoint;
protected PointF m_position;
protected BitmapFont m_bFont;
protected float m_fontSize;
protected ColorValue m_textColor;
protected float m_fontPadding;

private int m_startVertex;
private int m_fontStartVertex;
private int m_panelID;

// Events
public event GuiManager.ControlDelegate OnControl;

/// Default constructor
public Control() Toggle
{
m_panelID = 0;
m_fontPadding = 5f;
m_state = ControlState.Normal;
m_textRect = new RectangleF();
m_quads = new List();
m_fontQuads = new List();
m_disabled = false;
m_hasFocus = false;
m_hasTouchDownPoint = false;
m_mouseDown = false;
m_data = null;
}


/// Mouse handler
/// Mouse position
/// Mouse buttons
/// Mouse wheel delta
/// true if the Control processed the mouse, false otherwise
public bool MouseHandler( Point cursor, bool[] buttons, float zDelta ) Toggle
{
// Grab the initial mouse down point
if ( !m_hasTouchDownPoint && buttons[0] )
{
m_touchDownPoint = cursor;
m_hasTouchDownPoint = true;
}
else if ( m_hasTouchDownPoint && !buttons[0] )
{
m_hasTouchDownPoint = false;
}

if ( ( !( this is EditBox ) && ( Contains( cursor ) || m_hasFocus ) ) || ( ( this is EditBox ) && Contains( cursor ) ) )
{
if ( zDelta != 0f )
{
OnZDelta( zDelta );
}
// In order to send message, mouse must have been pressed and
// released over the hotspot.
if ( !buttons[0] )
{
if ( m_mouseDown )
{
m_mouseDown = false;
if ( this is EditBox )
{
// EditBox has toggled focus
m_hasFocus = !m_hasFocus;
}
else
{
m_hasFocus = false;
}
if ( Contains( cursor ) )
{
OnMouseRelease( cursor );
if ( OnControl != null && !(this is Panel) && ( !(this is ListBox) || ( ( this is ListBox ) && ( this as ListBox ).HasNewData ) ) )
{
OnControl( m_id, Data );
}
}
}
bool result = true;
// ListBoxes need to continuously check mouse since it has sub parts that change on mouse over
// Mouseover on Panels should be able to reset over state on back controls.
if ( State == ControlState.Over && !( this is ListBox ) && !( this is Panel ) )
{
// State is already over
result = false;
}


State = ControlState.Over;
OnMouseOver( cursor, buttons );
return result;
}
else if ( buttons[0] && m_hasTouchDownPoint &&
Contains( m_touchDownPoint ) || m_hasFocus )
{
State = ControlState.Down;
OnMouseDown( cursor, buttons );

// Slider sends data while mouse is down
if ( ( this is Slider ) && OnControl != null )
{
OnControl( m_id, Data );
}

m_mouseDown = true;
if ( !( this is EditBox ) )
{
m_hasFocus = true;
}
return true;
}
}
else if ( !Contains( cursor ) && State != ControlState.Normal )
{
if ( !buttons[0] && !( this is EditBox ) )
{
m_hasFocus = false;
}
if ( !m_hasFocus )
{
State = ControlState.Normal;
return true;
}
}
return false;
}


/// Keyboard handler.
/// List of pressed keys
/// Pressed character
/// Pressed key from Form used for repeatable keys
/// true if a Control processed the keyboard, false otherwise
public virtual bool KeyboardHandler( List pressedKeys, char pressedChar, int pressedKey ) Toggle
{
if ( OnKeyDown( pressedKeys, pressedChar, pressedKey ) )
{
if ( OnControl != null )
{
OnControl( m_id, Data );
return true;
}
}
return false;
}


/// Key down handler.
/// List of pressed keys
/// Pressed character
/// Pressed key from Form used for repeatable keys
/// true if a Control processed the keyboard, false otherwise
public virtual bool OnKeyDown( List pressedKeys, char pressedChar, int pressedKey ) Toggle
{
return false;
}


/// Checks is the mouse is over the Control’s hotspot
/// Mouse position
/// true if the cursor is over the Control’s hotspot, false otherwise.
public virtual bool Contains( Point cursor ) Toggle
{
return m_hotspot.Contains( cursor );
}


/// Mouse Over event
/// Mouse position
/// Mouse buttons
protected virtual void OnMouseOver( Point cursor, bool[] buttons ) Toggle
{
// Empty
}


/// Mouse Down event
/// Mouse position
/// Mouse buttons
protected virtual void OnMouseDown( Point cursor, bool[] buttons ) Toggle
{
// Empty
}


/// Mouse Release event
/// Mouse position
protected virtual void OnMouseRelease( Point cursor ) Toggle
{
// Empty
}


/// Mouse wheel event
/// Mouse wheel delta
protected virtual void OnZDelta( float zDelta ) Toggle
{
// Empty
}


/// Build the text
protected virtual void BuildText() Toggle
{
// Empty
}


/// Gets the Control’s ID.
public virtual int ID Toggle
{
get { return m_id; }
}


/// Gets the Control’s Quads.
public virtual List Quads Toggle
{
get { return m_quads; }
}


/// Gets the Control’s Quads.
public virtual List FontQuads Toggle
{
get { return m_fontQuads; }
}


/// Gets the Control’s Data.
public virtual object Data Toggle
{
get { return m_data; }
}


/// Gets and sets the Control’s state.
public virtual ControlState State Toggle
{
get { return m_state; }
set { m_state = value; }
}


/// Gets and sets whether the Control is disabled.
public virtual bool Disabled Toggle
{
get { return ( m_state == ControlState.Disabled ); }
set
{
if ( value )
{
State = ControlState.Disabled;
}
else
{
State = ControlState.Normal;
}
}
}


/// Gets and sets the Control’s Z-Depth
public virtual int ZDepth Toggle
{
get { return m_zDepth; }
set { m_zDepth = value; }
}


/// Gets and sets the Control’s starting vertex in the GuiManager VertexBuffer
public virtual int StartVertex Toggle
{
get { return m_startVertex; }
set { m_startVertex = value; }
}


/// Gets and sets the Control’s starting vertex in the GuiManager font VertexBuffer
public virtual int FontStartVertex Toggle
{
get { return m_fontStartVertex; }
set { m_fontStartVertex = value; }
}


/// Gets the Control’s text.
public virtual string Text Toggle
{
get { return m_text; }
set { m_text = value; BuildText(); }
}


/// Gets and sets the Control’s position.
public virtual PointF Position Toggle
{
get { return m_position; }
set { m_position = value; }
}


/// Gets and sets the Control’s text padding from the Control.
public virtual float FontPadding Toggle
{
get { return m_fontPadding; }
set { m_fontPadding = value; }
}


/// Gets and sets the Control’s associated Panel ID.
public virtual int PanelID Toggle
{
get { return m_panelID; }
set { m_panelID = value; }
}


/// Gets and sets whether the Control has focus.
public virtual bool HasFocus Toggle
{
get { return m_hasFocus; }
set { m_hasFocus = value; }
}

}
}

The Control class determines what action to take based on user input. Probably the most important method is MouseHandler. MouseHandler takes in user input and determines what state the Control should be in based on the user input. For example, when the mouse is over the Control’s hotspot, the Control will enter the over state. The Control will return a different set of Quads depending on what state it is in. In order for the Control to send its data through the OnControl event, the mouse must be pressed and released over the Control’s hotspot. Most of the methods are empty, such as OnMouseDown, OnMouseOver, and OnMouseRelease, because they will be overridden in the inherited controls.

Each Control has its own hotspot(s). These hotspots are Rectangles that we can use to check when the user wants to interact with the control. By using the Rectangle.Contains method, we can determine if the mouse is over the Control’s hotspot(s) and take the appropriate action.

Since our GUI is displayed with Quads, and it will be updated frequently, we are going to render it with a dynamic VertexBuffer. If you recall from the dynamic buffer tutorial, dynamic vertex buffers are THE way to render geometry that is updated frequently. Also, since the text rendered in the GUI is created using my BitmapFont class, which prints text on Quads, we’ll be able to put the text in the same VertexBuffer.

The class that manages the VertexBuffer and all of the controls is GuiManager:

public class GuiManager
{
private List m_quads;
private List m_fontQuads;
private List m_controlNodes;
private List m_controls;
private VertexBuffer m_vb = null;
private Texture m_texture = null;
private ImageInformation m_imageInfo;
private string m_textureFileName;
private string m_fntFile;
private string m_fontImage;
private string m_searchName;
private bool m_inCallback;
private bool m_dirtyBuffer;
private bool m_panelDragging;
private int m_openComboBox;
private int m_activeEditBox;
private BitmapFont m_bFont = null;
private ControlDelegate m_onControl;
public delegate void ControlDelegate( int controlID, object data );
private const int MaxVertices = 4096;

public GuiManager( string xmlStyleSheet, ControlDelegate onControl ) Toggle
{
m_inCallback = false;
m_panelDragging = false;
m_dirtyBuffer = true;
m_quads = new List();
m_fontQuads = new List();
m_controlNodes = new List();
m_controls = new List();
m_onControl = onControl;
m_openComboBox = –1;
m_activeEditBox = –1;
ParseXML( xmlStyleSheet );
m_bFont = new BitmapFont( m_fntFile, m_fontImage );
}


/// Parses the XML file
/// XML file name
private void ParseXML( string xmlFile ) Toggle
{
XmlTextReader reader = new XmlTextReader( Utility.GetMediaFile( xmlFile ) );
reader.WhitespaceHandling = WhitespaceHandling.None;
while ( reader.Read() )
{
if ( reader.NodeType == XmlNodeType.Element )
{
if ( reader.Name == “Gui” )
{
// Read in the image file name
for ( int i = 0; i < reader.AttributeCount; i++ )
{
reader.MoveToAttribute( i );
if ( reader.Name == “ImageFile” )
{
m_textureFileName = Utility.GetMediaFile( reader.Value );
m_imageInfo = Texture.GetImageInformationFromFile( m_textureFileName );
}
else if ( reader.Name == “FntFile” )
{
m_fntFile = reader.Value;
}
else if ( reader.Name == “FontImage” )
{
m_fontImage = reader.Value;
}
}
}
else if ( reader.Name == “Control” )
{
ControlNode controlNode = new ControlNode();
for ( int i = 0; i < reader.AttributeCount; i++ )
{
reader.MoveToAttribute( i );
if ( reader.Name == “Name” )
{
controlNode.Name = reader.Value;
}
}
// Read the Image elements of this Control
while ( reader.NodeType != XmlNodeType.EndElement )
{
reader.Read();
if ( ( reader.NodeType == XmlNodeType.Element ) && ( reader.Name == “Image” ) )
{
ImageNode imageNode = new ImageNode();
for ( int i = 0; i < reader.AttributeCount; i++ )
{
reader.MoveToAttribute( i );
if ( reader.Name == “Name” )
{
imageNode.Name = reader.Value;
}
else if ( reader.Name == “X” )
{
imageNode.Rectangle.X = reader.ReadContentAsFloat();
}
else if ( reader.Name == “Y” )
{
imageNode.Rectangle.Y = reader.ReadContentAsFloat();
}
else if ( reader.Name == “Width” )
{
imageNode.Rectangle.Width = reader.ReadContentAsFloat();
}
else if ( reader.Name == “Height” )
{
imageNode.Rectangle.Height = reader.ReadContentAsFloat();
}
else if ( reader.Name == “Color” )
{
imageNode.Color = StringToColor( reader.Value );
}
}
controlNode.Images.Add( imageNode );
}
}
m_controlNodes.Add( controlNode );
}
}
}
}


/// Converts a hex string into a Color
/// Hex stream of form 0x00000000 or 00000000
/// New Color
private Color StringToColor( string hexString ) Toggle
{
if ( hexString.IndexOf( “0x” ) >= 0 )
{
hexString = hexString.Remove( 0, 2 );
}
System.Globalization.NumberStyles style =
System.Globalization.NumberStyles.AllowHexSpecifier;
int alpha = int.Parse( hexString.Substring( 0, 2 ), style );
int red = int.Parse( hexString.Substring( 2, 2 ), style );
int green = int.Parse( hexString.Substring( 4, 2 ), style );
int blue = int.Parse( hexString.Substring( 6, 2 ), style );
return Color.FromArgb( alpha, red, green, blue );
}


/// Call after the device is created.
/// D3D Device
public void OnCreateDevice( Device device ) Toggle
{
m_texture = new Texture( device, m_textureFileName );
if ( m_bFont != null )
{
m_bFont.OnCreateDevice( device );
}
}


/// Call when the device is destroyed.
public void OnDestroyDevice() Toggle
{
if ( m_texture != null )
{
m_texture.Dispose();
m_texture = null;
}
if ( m_bFont != null )
{
m_bFont.OnDestroyDevice();
}
}


/// Call when the device is lost.
public void OnLostDevice() Toggle
{
if ( m_vb != null )
{
m_vb.Dispose();
m_vb = null;
}
if ( m_bFont != null )
{
m_bFont.OnLostDevice();
}
}


/// Call after the device is reset.
/// D3D Device
public void OnResetDevice( Device device ) Toggle
{
m_vb = new VertexBuffer( device, MaxVertices * TransformedColoredTextured.StrideSize,
Usage.Dynamic | Usage.WriteOnly, TransformedColoredTextured.Format, Pool.Default, null );
BuildQuadList();
UpdateBuffer();
if ( m_bFont != null )
{
m_bFont.OnResetDevice( device );
}
}


/// Clears the Gui
public void Clear() Toggle
{
m_quads.Clear();
m_fontQuads.Clear();
m_controlNodes.Clear();
m_controls.Clear();
}


/// Creates a new Panel
/// Control id
/// Panel position
/// Panel size
public void CreatePanel( int id, PointF position, SizeF size ) Toggle
{
CheckUniqueID( id );
m_searchName = “Panel”;
ControlNode node = m_controlNodes.Find( HasSearchName );
if ( node == null )
{
throw new Exception( “Unable to find control node for type Panel.rn” );
}
Panel p = new Panel( id, new RectangleF( position, size ), node, m_imageInfo );
AddControl( 0, p );
}


/// Creates a new text Label
/// Control id
/// ID of Panel to associate Control with or 0 to make independant Control
/// Button position
/// Button size
/// Button text
/// Font size
/// Text color
/// Text alignment
public void CreateLabel( int id, int panelID, PointF position, SizeF size, string text, float fontSize, ColorValue textColor, BitmapFont.Align alignment ) Toggle
{
CheckUniqueID( id );
Label c = new Label( id, new RectangleF( position, size ), text, fontSize, textColor, m_bFont, alignment );
AddControl( panelID, c );
}


/// Creates a new Button
/// Control id
/// ID of Panel to associate Control with or 0 to make independant Control
/// Button position
/// Button size
/// Button text
/// Font size
/// Text color
public void CreateButton( int id, int panelID, PointF position, SizeF size, string text, float fontSize, ColorValue textColor ) Toggle
{
CheckUniqueID( id );
m_searchName = “Button”;
ControlNode node = m_controlNodes.Find( HasSearchName );
if ( node == null )
{
throw new Exception( “Unable to find control node for type Button.rn” );
}
Button c = new Button( id, new RectangleF( position, size ), text, fontSize, textColor, m_bFont, node, m_imageInfo );
AddControl( panelID, c );
}


/// Creates a new CheckBox
/// Control id
/// ID of Panel to associate Control with or 0 to make independant Control
/// Button position
/// Button size
/// Button text
/// Font size
/// Text color
/// Which side of the control to place the text on.
/// Whether the CheckBox is initially checked or not.
public void CreateCheckBox( int id, int panelID, PointF position, SizeF size, string text, float fontSize, ColorValue textColor, Control.TextAlign textAlignment, bool isChecked ) Toggle
{
CheckUniqueID( id );
m_searchName = “CheckBox”;
ControlNode node = m_controlNodes.Find( HasSearchName );
if ( node == null )
{
throw new Exception( “Unable to find control node for type CheckBox.rn” );
}
CheckBox c = new CheckBox( id, new RectangleF( position, size ), text, fontSize, textColor, textAlignment, isChecked, m_bFont, node, m_imageInfo );
AddControl( panelID, c );
}


/// Creates a new RadioButton
/// Control id
/// ID of Panel to associate Control with or 0 to make independant Control
/// Radio button groupID. Only one button per group may be selected at once.
/// Button position
/// Button size
/// Button text
/// Font size
/// Text color
/// Which side of the control to place the text on.
/// Whether the RadioButton is initially selected.
public void CreateRadioButton( int id, int panelID, int groupID, PointF position, SizeF size, string text, float fontSize, ColorValue textColor, Control.TextAlign textAlignment, bool isSelected ) Toggle
{
CheckUniqueID( id );
m_searchName = “RadioButton”;
ControlNode node = m_controlNodes.Find( HasSearchName );
if ( node == null )
{
throw new Exception( “Unable to find control node for type CheckBox.rn” );
}
RadioButton c = new RadioButton( id, groupID, new RectangleF( position, size ), text, fontSize, textColor, textAlignment, isSelected, m_bFont, node, m_imageInfo );

// If RadioButton is selected, deselect RadioButtons of same groupID
if ( isSelected )
{
c.NeedToDelectOthers = false;
for ( int i = 0; i < m_controls.Count; i++ )
{
if ( ( m_controls[i] is RadioButton ) &&
( ( m_controls[i] as RadioButton ).GroupID == c.GroupID ) )
{
( m_controls[i] as RadioButton ).Deselect();
}
}
}

AddControl( panelID, c );
}


/// Creates a new Slider
/// Control id
/// ID of Panel to associate Control with or 0 to make independant Control
/// Slider position
/// Slider width
/// Minimum slider value;
/// Maximum slider value;
/// Current slider value;
/// Slider text
/// Font size
/// Text color
/// Which side of the control to place the text on.
public void CreateSlider( int id, int panelID, PointF position, float width, float min, float max, float current, string text, float fontSize, ColorValue textColor, Control.TextAlign textAlignment ) Toggle
{
CheckUniqueID( id );
m_searchName = “Slider”;
ControlNode node = m_controlNodes.Find( HasSearchName );
if ( node == null )
{
throw new Exception( “Unable to find control node for type Slider.rn” );
}
Slider c = new Slider( id, position, width, min, max, current, text, fontSize, textColor, textAlignment, m_bFont, node, m_imageInfo );
AddControl( panelID, c );
}


/// Creates a new Button
/// Control id
/// ID of Panel to associate Control with or 0 to make independant Control
/// Whether single or multiple items can be selected
/// Button position
/// Button size
/// Button text
/// Font size
/// Text color
public void CreateListBox( int id, int panelID, bool singleItemSelect, PointF position, SizeF size, float fontSize, ColorValue textColor ) Toggle
{
CheckUniqueID( id );
m_searchName = “ListBox”;
ControlNode node = m_controlNodes.Find( HasSearchName );
if ( node == null )
{
throw new Exception( “Unable to find control node for type ListBox.rn” );
}
ListBox c = new ListBox( id, singleItemSelect, new RectangleF( position, size ), fontSize, textColor, m_bFont, node, m_imageInfo );
AddControl( panelID, c );
}


/// Creates a new Button
/// Control id
/// ID of Panel to associate Control with or 0 to make independant Control
/// true if only one item can be selected at a time, false to allow multiple selection
/// Button position
/// Button size
/// Button text
/// Font size
/// Text color
public void CreateComboBox( int id, int panelID, PointF position, SizeF size, float openHeight, float fontSize, ColorValue textColor ) Toggle
{
CheckUniqueID( id );
m_searchName = “ComboBox”;
ControlNode node = m_controlNodes.Find( HasSearchName );
if ( node == null )
{
throw new Exception( “Unable to find control node for type ComboBox.rn” );
}
ComboBox c = new ComboBox( id, new RectangleF( position, size ), openHeight, fontSize, textColor, m_bFont, node, m_imageInfo );
AddControl( panelID, c );
}


/// Creates a new EditBox
/// Control id
/// ID of Panel to associate Control with or 0 to make independant Control
/// Whether single or multiple items can be selected
/// Button position
/// Button size
/// Button text
/// Font size
/// Max number of characters allowed in the edit box.
/// Text color
public void CreateEditBox( int id, int panelID, PointF position, SizeF size, string text, float fontSize, int maxLength, ColorValue textColor ) Toggle
{
CheckUniqueID( id );
m_searchName = “EditBox”;
ControlNode node = m_controlNodes.Find( HasSearchName );
if ( node == null )
{
throw new Exception( “Unable to find control node for type EditBox.rn” );
}
EditBox c = new EditBox( id, new RectangleF( position, size ),text, fontSize, maxLength, textColor, m_bFont, node, m_imageInfo );
AddControl( panelID, c );
}


/// Adds an item to a ListBox or ComboBox
/// ID of Control to add Listable item.
/// Displayed text of item.
/// Data of item.
public void AddListableItem( int controlID, string text, object data ) Toggle
{
Control c = this[controlID];
if ( !( c is ListBox ) )
{
throw new Exception( “Error: Tried to add ListableItem to non-listable Control” );
}
( c as ListBox ).AddItem( text, data );
}


/// Adds a new Control to the list
/// PanelID to add Control to, or 0 for independant Control
/// New Control
private void AddControl( int panelID, Control c ) Toggle
{
if ( panelID == 0 )
{
// Control doesn’t have a Panel
// Find highest zDepth
if ( m_controls.Count > 0 )
{
c.ZDepth = m_controls[0].ZDepth + 1;
}
else
{
c.ZDepth = 1;
}
}
else
{
AttachControlToPanel( panelID, c );
}
c.OnControl += new ControlDelegate( m_onControl );
m_controls.Add( c );
SortControls();
BuildQuadList();
}


/// Attaches a Control to a Panel
/// Panel’s ID
/// Control
private void AttachControlToPanel( int panelID, Control c ) Toggle
{
// Attach Control to Panel
Control p = this[panelID];
if ( !( p is Panel ) )
{
throw new Exception( “Error: Tried to attach Control to non-panel Control.” );
}
c.PanelID = panelID;
c.ZDepth = p.ZDepth;
( p as Panel ).NumControls++;
c.Position += new SizeF( p.Position.X, p.Position.Y );
}


/// Deletes the Control with the specified ID.
/// Control ID
public void DeleteControl( int id ) Toggle
{
for ( int i = 0; i < m_controls.Count; i++ )
{
if ( m_controls[i].ID == id )
{
m_controls.RemoveAt( i );
BuildQuadList();
UpdateBuffer();
break;
}
}
}


/// Rebuilds the list of Quads.
private void BuildQuadList() Toggle
{
m_quads.Clear();
m_fontQuads.Clear();
for ( int i = 0; i < m_controls.Count; i++ )
{
// Set starting vertices to access when we render the Control
m_controls[i].StartVertex = m_quads.Count * 6;
m_quads.AddRange( m_controls[i].Quads );
m_controls[i].FontStartVertex = m_quads.Count * 6;
m_quads.AddRange( m_controls[i].FontQuads );
}
}


/// Writes all the vertices to the vertex buffers.
private void UpdateBuffer() Toggle
{
if ( m_inCallback )
{
return;
}
GraphicsBuffer data =
m_vb.Lock( 0, 6 * m_quads.Count, LockFlags.Discard );
for ( int i = 0; i < m_quads.Count; i++ )
{
data.Write( m_quads[i].Vertices );
}
m_vb.Unlock();
m_dirtyBuffer = false;
}


/// Renders the GUI
/// D3D Device
public void Render( Device device ) Toggle
{
if ( m_dirtyBuffer )
{
BuildQuadList();
UpdateBuffer();
}

// Set render states
device.SetRenderState( RenderStates.ZEnable, false );
device.SetRenderState( RenderStates.FillMode, (int)FillMode.Solid );
device.SetRenderState( RenderStates.ZBufferWriteEnable, false );
device.SetRenderState( RenderStates.FogEnable, false );
device.SetRenderState( RenderStates.AlphaTestEnable, false );
device.SetRenderState( RenderStates.AlphaBlendEnable, true );
device.SetRenderState( RenderStates.SourceBlend, (int)Blend.SourceAlpha );
device.SetRenderState( RenderStates.DestinationBlend, (int)Blend.InvSourceAlpha );

// Blend alphas
device.SetTextureState( 0, TextureStates.ColorArgument1, (int)TextureArgument.Texture );
device.SetTextureState( 0, TextureStates.AlphaArgument1, (int)TextureArgument.Texture );
device.SetTextureState( 0, TextureStates.AlphaArgument2, (int)TextureArgument.Diffuse );
device.SetTextureState( 0, TextureStates.AlphaOperation, (int)TextureOperation.Modulate );

// Set sampler states
device.SetSamplerState( 0, SamplerStates.MinFilter, (int)Filter.Linear );
device.SetSamplerState( 0, SamplerStates.MagFilter, (int)Filter.Linear );
device.SetSamplerState( 0, SamplerStates.MipFilter, (int)Filter.Linear );

// Render
device.VertexFormat = TransformedColoredTextured.Format;
device.SetTexture( 0, m_texture );
device.SetStreamSource( 0, m_vb, 0, TransformedColoredTextured.StrideSize );
foreach ( Control c in m_controls )
{
device.DrawPrimitives( PrimitiveType.TriangleList, c.StartVertex, 2 * c.Quads.Count );
if ( c.Text != string.Empty && c.Text != “” )
{
device.SetTexture( 0, m_bFont.Texture );
device.DrawPrimitives( PrimitiveType.TriangleList, c.FontStartVertex, 2 * c.FontQuads.Count );
device.SetTexture( 0, m_texture );
}

}
}


/// Keyboard handler.
/// List of pressed keys
/// Pressed character
/// Pressed key from Form used for repeatable keys
/// true if a Control processed the keyboard, false otherwise
public bool KeyBoardHandler( List pressedKeys, char pressedChar, int pressedKey ) Toggle
{
// Go front to back
for ( int i = m_controls.Count – 1; i >= 0; i– )
{
if ( m_controls[i].Disabled )
{
// Ignore disabled Controls
continue;
}
if ( m_controls[i].KeyboardHandler( pressedKeys, pressedChar, pressedKey ) )
{
BuildQuadList();
UpdateBuffer();
return true;
}
}
return false;
}


/// Mouse handler
/// Mouse position
/// Mouse buttons
/// Mouse wheel delta
/// true if the gui handled a mouse-click, false otherwise.
public bool MouseHandler( Point cursor, bool[] buttons, float zDelta ) Toggle
{
bool result = false;
m_inCallback = true;
// Go front to back
for ( int i = m_controls.Count – 1; i >= 0; i– )
{
if ( m_controls[i].Disabled )
{
// Ignore disabled Controls
continue;
}
if ( m_panelDragging && !buttons[0] )
{
m_panelDragging = false;
}
int numControls = m_controls.Count;
if ( ( !m_panelDragging || ( m_controls[i] is Panel ) ) && m_controls[i].MouseHandler( cursor, buttons, zDelta ) )
{
if ( numControls != m_controls.Count )
{
// If we’re here, we used a control to delete another control, so
// the index will be missing. Return to prevent IndexOutOfBounds
return true;
}
if ( ( m_controls[i] is Panel ) && ( m_controls[i].State == Control.ControlState.Down ) )
{
m_panelDragging = true;
}

if ( ( m_controls[i] is ComboBox ) && ( m_controls[i] as ComboBox ).IsOpen )
{
if ( ( m_openComboBox != –1 ) && m_openComboBox != m_controls[i].ID )
{
// Close any open ComboBox
for ( int j = 0; j < m_controls.Count; j++ )
{
if ( m_controls[j].ID == m_openComboBox )
{
( m_controls[j] as ComboBox ).IsOpen = false;
break;
}
}
}
m_openComboBox = m_controls[i].ID;
}

if ( ( m_controls[i] is EditBox ) && m_controls[i].HasFocus )
{
m_activeEditBox = i;
}

m_dirtyBuffer = true;

// For Controls in a Panel, mouse may have just been released from another
// Control’s focus, so we need to make sure we reset that Control’s state
if ( ( m_controls[i].State == Control.ControlState.Over ) && ( m_controls[i].PanelID != 0 )
&& !( m_controls[i] is Panel ) )
{
for ( int j = m_controls.Count – 1;  j >= 0 && m_controls[j].ZDepth == 1; j– )
{
if ( m_controls[j].State == Control.ControlState.Down )
{
m_controls[j].State = Control.ControlState.Normal;
break;
}
}
}

// If new RadioButton was selected, deselect RadioButtons of same groupID
if ( ( m_controls[i] is RadioButton ) && ( ( m_controls[i] as RadioButton ).NeedToDelectOthers ) )
{
( m_controls[i] as RadioButton ).NeedToDelectOthers = false;
for ( int j = 0; j < m_controls.Count; j++ )
{
if ( i == j )
{
continue;
}
if ( ( m_controls[j] is RadioButton ) &&
( ( m_controls[j] as RadioButton ).GroupID == ( m_controls[i] as RadioButton ).GroupID ) )
{
( m_controls[j] as RadioButton ).Deselect();
}
}
}

// We may have moved from a back Control to a
// front control so reset over states
for ( int j = 0; j < i; j++ )
{
if ( m_controls[j].State == Control.ControlState.Over )
{
m_controls[j].State = Control.ControlState.Normal;
break;
}
}

if ( m_controls[i].State == Control.ControlState.Down )
{
result = true;

// Mouse is down over another control so close any open ComboBox
if ( ( m_openComboBox != –1 ) && ( m_openComboBox != m_controls[i].ID ) )
{
// Close the open ComboBox
for ( int j = 0; j < m_controls.Count; j++ )
{
if ( m_controls[j].ID == m_openComboBox )
{
( m_controls[j] as ComboBox ).IsOpen = false;
m_openComboBox = –1;
break;
}
}
}

if ( ( m_activeEditBox != –1 ) && ( m_activeEditBox != i ) )
{
// Release EditBox focus
( m_controls[m_activeEditBox] as EditBox ).ReleaseFocus();
m_activeEditBox = –1;
}

Control c = m_controls[i];
// Adjust Z Depths
if ( m_controls[i].ZDepth != 1 )
{
if ( ( m_controls[i] is Panel ) && ( ( m_controls[i] as Panel ).NumControls > 0 ) )
{
// Control is Panel with Controls inside.
// Move Panel and its Controls to the front
for ( int j = 0; j < m_controls.Count; j++ )
{
if ( m_controls[j].PanelID == m_controls[i].ID )
{
m_controls[j].ZDepth = 1;
}
else if ( m_controls[j].ZDepth < m_controls[i].ZDepth )
{
m_controls[j].ZDepth++;
}
}
m_controls[i].ZDepth = 1;
}
else if ( m_controls[i].PanelID != 0 )
{
// Control is inside a Panel
// Move Panel and its Controls to the front
for ( int j = 0; j < m_controls.Count; j++ )
{
if ( ( ( m_controls[j].PanelID == m_controls[i].PanelID ) ||
( m_controls[j].ID == m_controls[i].PanelID ) ) && ( i != j ) )
{
m_controls[j].ZDepth = 1;
}
else if ( m_controls[j].ZDepth < m_controls[i].ZDepth )
{
m_controls[j].ZDepth++;
}
}
m_controls[i].ZDepth = 1;
}
else
{
// Control is either an independent Control or a Panel
for ( int j = 0; j < m_controls.Count; j++ )
{
if ( m_controls[j].ZDepth < m_controls[i].ZDepth )
{
m_controls[j].ZDepth++;
}
}
m_controls[i].ZDepth = 1;
}

// Resort the Controls
SortControls();
}

if ( ( c is Panel ) && ( c as Panel ).NumControls > 0 && !( c as Panel ).Locked )
{
// Panel is being dragged, move its Controls with it
for ( int j = m_controls.Count – 1; j >= 0; j– )
{
PointF position = m_controls[j].Position;
position.X += ( c as Panel ).XOffset;
position.Y += ( c as Panel ).YOffset;
m_controls[j].Position = position;
if ( m_controls[j].ID == c.ID )
{
// Reached the Panel
break;
}
}
}
}
break;
}
else if ( buttons[0] )
{
// Clicked off a control so close any open ComboBox
if ( m_openComboBox != –1 )
{
ComboBox openComboBox = this[m_openComboBox] as ComboBox;
// Close the open ComboBox
if ( !openComboBox.Contains( cursor ) )
{
openComboBox.IsOpen = false;
m_openComboBox = –1;
}
}

if ( ( m_activeEditBox != –1 ) && ( m_activeEditBox != i ) )
{
// Release EditBox focus
( m_controls[m_activeEditBox] as EditBox ).ReleaseFocus();
m_activeEditBox = –1;
}
}
}
m_inCallback = false;
return result;
}


/// Sorts the controls by Z Depth
private void SortControls() Toggle
{
ControlSorter sorter = new ControlSorter();
m_controls.Sort( sorter );
}


/// Sets a position for a Control
/// Control ID
/// New position
public void SetPosition( int id, PointF position ) Toggle
{
Control c = this[id];
if ( c == null )
{
return;
}
if ( c is Panel && ( c as Panel ).NumControls > 0 )
{
float xOffset = c.Position.X – position.X;
float yOffset = c.Position.Y – position.Y;

// Move Panel
c.Position = position;

// Move Controls of Panel
for ( int i = 0; i < m_controls.Count; i++ )
{
if ( m_controls[i].PanelID == c.ID )
{
PointF newPosition = m_controls[i].Position;
newPosition.X -= xOffset;
newPosition.Y -= yOffset;
m_controls[i].Position = newPosition;
}
}
}
else
{
c.Position = position;
}
UpdateBuffer();
}


/// Disables or enables a Control.
/// Control ID
/// true or false
/// If the Control is a Panel, all the Controls in the Panel will also be
/// disabled of enabled.
public void DisableControl( int id, bool disabled ) Toggle
{
Control c = this[id];
if ( c == null )
{
return;
}
if ( c.PanelID > 0 )
{
Control panel = this[c.PanelID];
if ( panel.Disabled && !disabled )
{
// Control must have same state as its containing Panel
return;
}
}
c.Disabled = disabled;
if ( ( c is Panel ) && ( ( c as Panel ).NumControls > 0 ) )
{
// Set all the Controls in the Panel
for ( int j = 0; j < m_controls.Count; j++ )
{
if ( m_controls[j].PanelID == id )
{
m_controls[j].Disabled = disabled;
}
}
}
BuildQuadList();
UpdateBuffer();
}


/// Makes sure an id is not currently in use
/// ID to check
private void CheckUniqueID( int id ) Toggle
{
foreach ( Control c in m_controls )
{
if ( c.ID == id )
{
throw new Exception( “Control ID: “ + id + ” is already in use.” );
}
}
}


/// Gets the Control with the corresponding ID. Returns null if Control wasn’t found.
public Control this[int i] Toggle
{
get
{
for ( int index = 0; index < m_controls.Count; index++ )
{
if ( i == m_controls[index].ID )
{
return m_controls[index];
}
}
return null;
}
}


/// Gets and sets whether the GUI needs to update_ its buffer.
public bool DirtyBuffer Toggle
{
get { return m_dirtyBuffer; }
set { m_dirtyBuffer = value; }
}


/// Search predicate used to find nodes in m_controlNodes
/// Current node.
/// true if the node’s name matches the desired node name, false otherwise.
private bool HasSearchName( ControlNode node ) Toggle
{
return ( node.Name == m_searchName );
}

}

GuiManager is the mothership of the GUI system. Starting at the top, we have the initialization methods. When GuiManager is instanciated, it parses through the XML file and saves all the data in a List of ControlNodes. Since the GuiManager uses resources like a Texture and VertexBuffer, it has the normal resource On*Device methods that we’ve seen in all the other tutorials.

Next up are all the Control creation methods. When a Control is created, GuiManager finds the ControlNode for the desired Control, creates the Control, and adds the Control to its List of Controls. Remember, a ControlNode holds all the Quad definitions read from the XML file. Controls must also have a unique ID in order to respond to it, so this is enforced through the CheckUniqueID method.

A new Control is added to the List in the AddControl method. If the Control is supposed to be attached to a Panel, the AddControl method calls AttachControlToPanel. Otherwise, AddControl simply adds the Control to the List.

The BuildQuadList and UpdateBuffer methods are used to fill up the dynamic VertexBuffer. The BuildQuadList method iterates through the List of Controls and builds a List of the Quads of all the Controls. Notice in this method that we also assign a start vertex to each Control. This start vertex is used as an offset when rendering the Controls. Since both the image Quads and the text Quads are all stored in the same VertexBuffer, we need to be able to switch Textures from the gui texture to the font texture in order to maintain the proper display. If we just rendered all the text last, all the text would be displayed on top of the graphics, even if the text belonged to a Control that was obscured by another Control. The UpdateBuffer method simply updates the VertexBuffer with the vertices found in the List of Quads generated by BuildQuadList. Since the List of Controls is sorted by z-depth, the List of Quads and thus the VertexBuffer will also be sorted by z-depth.

All the Controls have an assigned z-depth. This z-depth is used to determine the display order of the Controls. For example, if you click on a Control, it will be brought to the front of the screen all other Controls will be shifted back one space. The Controls with the lower z-depth (front Controls) should be rendered after Controls with higher z-depths (back Controls) in order for them to appear on top. To implement this z-sorting, the List of Controls must be sorted by z-depth. To do this, the SortControls method sorts the List with the use of the List search predicate shown below:

/// Sorts Controls by Z Depth
public class ControlSorter : IComparer
{
/// IComparer implementation
/// Control 1
/// Control 2
public int Compare( Control x, Control y )
{
if ( x.ZDepth < y.ZDepth )
{
return 1;
}
if ( x.ZDepth == y.ZDepth )
{
// Panel goes underneath its Controls
if ( ( x is Panel ) && !( y is Panel ) )
{
return1;
}
else if ( ( y is Panel ) && !( x is Panel ) )
{
return 1;
}

// For Controls on same Panel, sort bottom to top
if ( x.Position.Y < y.Position.Y )
{
return 1;
}
if ( x.Position.Y > y.Position.Y )
{
return1;
}
return 0;
}
return1;
}
}

The ControlSorter class sorts Controls by z-depth. When Controls are attached to a Panel, the Panel should be rendered first, followed by its Controls. When Controls are on the same Panel, they have the same z-depth, so they are sorted bottom to top. We want the higher Control rendered last because Controls like the ComboBox have a drop-down component and we want that component to be displayed on top.

Back in GuiManager, when the GUI is rendered, we must first set a few render states in order for the GUI to be displayed correctly. We need disable ZEnable and ZBufferWriteEnable since our Quads are made of transformed vertices. We also enable alpha blending in case the GUI has any transparency. We also enable some sampler state filters which increase the quality of the displayed images. Notice when the VertexBuffer is being rendered, we switch Textures if the Control has any text. This is where Control.StartVertex comes into play, which is assigned in BuildQuadList.

Another important method of GuiManager is the MouseHandler method. This method performs a lot of calculations to keep the GUI running smoothly. The MouseHandler method iterates through the List of Controls from front to back and processes any Control actions. The reason why we go front to back is that if a front Control is processed, we can break out of the loop to prevent us from processing any back Controls. The MouseHandler method contains a lot of logic that would take a long time to describe so I’ll just summarize some of the key points that we have to keep track of:

  • When a Panel is being dragged, we also have to move all of its attached Controls. We also don’t want to process any other Control while a Panel is being dragged.
  • Whenever we open a ComboBox and then click anywhere else, we should close the ComboBox.
  • When a mouse is pressed over one Control and then released over another, we have to reset the down state of the first control to the normal state.
  • When a RadioButton is selected, we have to deselect the other RadioButtons of the same group.
  • When a Control is in the over state and is partially obscured by another Control (for example, a Panel is moved over half a Button), the back Control’s over state should be returned to normal when the mouse moves over the front Control.
  • When a text EditBox has focus and we click anywhere else, release the focus from the EditBox.
  • When we click on a Control that is not in front, we have to recalculate all the z-depths. We move the Control to the front. If we click on a Panel or a Control in a Panel, we have to move the Panel and all of its Controls to the front.

All of the other methods are pretty much self-explanatory or explained in the comments so I won’t go over them here. I’d say this tutorial is long enough, so in the next tutorial we’ll start implementing some of the Controls.