Tales Framework for XNA: Controls
Filed under: Programming , Tales Framework , Xbox 360 , XNA
I’m finally putting together a post on the Tales Framework, the XNA library I am developing. While I haven’t had much time to work on the framework, I did complete the framework code that was holding back the preview. I’ll post the tech preview once I clean-up the sample. Until then I’ll post some details.
I thought I would start on the UI side, in particular, the idea of controls. I’m sure many first time XNA developers have experience with .NET Windows Forms, Windows Presentation Foundation (WPF) or ASP.NET development. All three use the idea of controls as the base item that represents what you see and what you interact with.
XNA doesn’t have such a concept. While XNA supports sophisticated 3D rendering and low-level 2D sprite handling, the idea of a control framework is quite useful for everything from building menu screens, to HUDs (Heads-Up-Displays), to managing UI state.
So in my attempt to dig deep into XNA I created a control structure that feels similar to Windows Forms and WPF while attempting to recognize the useful and practical differences that XNA brings.
The base class for UI controls is GameControl, which inherits from DrawableGameComponent. I have kept intact the use of Draw and Update but added a fair number of abilities:
- Controls are hierarchical meaning they have a parent and can have children. This eases things like animation; moving a control automatically moves the children.
- There are properties, logic and classes related to layout. This includes specifying how a control docks within the parent’s client space, how a control fills the parent’s client space, padding and layout handlers for managing how children are placed within the control’s client space.
- There are
InputBindingswhich map input events such as button and key presses to callback methods. - There are events and properties for handling focus. Ultimately, focus is managed through a
FocusManager, which ties into how input is handled.
In addition to this base class, a variety of subclasses exist as starting points to ease the creation of menus and HUDs. The subclasses include:
Panel– Commonly used base class for controls that have children.GameControlcan have children too, but Panel has logic for handling things like analyzing children locations to get the preferred size of the panel.Screen– Commonly used base class for display screens, such as menus or the main game screen.MenuScreen– Commonly used base class for menu screens. This class primarily adds logic on how to cycle between menu options.MenuItem– Common class used as a menu. This class adds logic and events for selection handling.Label– Simple control that represents text.ImageBox– Simple control that represents an image.NumberSpinnerControl– A control that manages the logic for creating a control that can increment/decrement numbers.OptionSpinnerControl– A control that manages the logic for spinning through a set of non-numeric options.
Generally speaking the controls do not specify how they look but instead contain logic and members that manage how they are used. To manage what they look like you need to subclass. For example, the following code shows a sample number spinner. In this case the control indicates how it wants to look both when the control is in focus and not in focus:
/// <summary>
/// Sample number spinner that has a label containing text
/// that indicates what the number represents, and another
/// label that represents the current number.
/// </summary>
public class SampleNumberSpinnerControl : NumberSpinnerControl {
private Label _valueLabel;
private string _nameText;
private Texture2D _background;
private Color _focusBackgroundColour = new Color( 0xFF, 0xFF, 0xFF, 0x33 );
/// <summary>
/// Constructor taking the parameters needed for the numeric spinner.
/// </summary>
/// <param name="theText">User text describing the control.</param>
/// <param name="theMinimumValue">The minimum numeric value.</param>
/// <param name="theMaximumValue">The maximum numeric value.</param>
/// <param name="theParent">The parent control.</param>
public SampleNumberSpinnerControl(
string theText,
int theMinimumValue,
int theMaximumValue,
int theCurrentValue,
DrawableGameComponent theParent )
: base(
theMinimumValue,
theMaximumValue,
theCurrentValue,
theParent ) {
_nameText = theText;
}
/// <summary>
/// Called to load any needed content, this implementation
/// creates a background texture used when the control is in focus.
/// </summary>
protected override void LoadContent( ) {
base.LoadContent( );
_background = new Texture2D(
this.GraphicsDevice,
1, 1, 1,
TextureUsage.None,
SurfaceFormat.Color );
_background.SetData<Color>( new Color[ ] { Color.White } );
}
/// <summary>
/// Called to unload loaded content, this implementation
/// frees the background texture.
/// </summary>
protected override void UnloadContent( ) {
base.UnloadContent( );
_background.Dispose( );
}
/// <summary>
/// Called to setup the control. This implementation
/// indicates how we want the control to look..
/// </summary>
public override void Initialize( ) {
base.Initialize( );
// use horizontal flow layout where children align in the middle vertically
HorizontalFlowLayout layout = new HorizontalFlowLayout(
VerticalAlignment.Middle );
// 6 pixels of padding on left and right, 2 on top and bottom.
this.Padding = new Padding( 6, 2 );
// indicate the control fills the parent's horizontal space
this.Fill = new Vector2( 1.0f, 0 );
this.LayoutHandler = layout;
}
/// <summary>
/// Called to create children controls.
/// </summary>
protected override void InitializeControls( ) {
base.InitializeControls( );
SpriteBatch batch = new SpriteBatch( this.Parent.Game.GraphicsDevice );
Label label = new Label( this );
// first setup the user description label
label.Fill = new Vector2( 0.4f, 0f );
label.Font = SystemDefaults.VisualDefaults.MenuFont;
label.Text = _nameText;
label.SpriteBatch = batch;
this.Controls.Add( label );
// create a panel that holds arrows/label showing the numeric value
Panel valuePanel = new Panel( this );
valuePanel.Fill = new Vector2( 0.6f, 0f );
this.Controls.Add( valuePanel );
// first, left aligned, label ... a simple arrow to
// indicate you can press left to lower the value
label = new Label( valuePanel );
label.Padding = new Padding( 5, 0, 5, 0 );
label.HorizontalDock = HorizontalAlignment.Left;
label.Font = SystemDefaults.VisualDefaults.MenuFont;
label.Text = "<";
label.SpriteBatch = batch;
valuePanel.Controls.Add( label );
// now setup the center aligned label that holds the numeric text
_valueLabel = new Label( valuePanel );
_valueLabel.HorizontalDock = HorizontalAlignment.Center;
_valueLabel.Font = SystemDefaults.VisualDefaults.MenuFont;
_valueLabel.SpriteBatch = batch;
_valueLabel.Color = SystemDefaults.VisualDefaults.NormalTextColor;
_valueLabel.Text = GetValueString( );
valuePanel.Controls.Add( _valueLabel );
// now setup the final, right aligned, label ... a simple arrow
// to indicate you can press right to increase the value
label = new Label( valuePanel );
label.Padding = new Padding( 5, 0, 5, 0 );
label.HorizontalDock = HorizontalAlignment.Right;
label.Font = SystemDefaults.VisualDefaults.MenuFont;
label.Text = ">";
label.SpriteBatch = batch;
valuePanel.Controls.Add( label );
}
/// <summary>
/// This overridden Draw paints a background if the control
/// is in focus.
/// </summary>
public override void Draw( GameTime gameTime ) {
if( this.Focused ) {
_valueLabel.SpriteBatch.Begin( SpriteBlendMode.AlphaBlend );
_valueLabel.SpriteBatch.Draw(
_background,
new Rectangle(
( int )this.PhysicalPosition.X,
( int )this.PhysicalPosition.Y,
( int )this.Size.X,
( int )this.Size.Y ),
this._focusBackgroundColour );
_valueLabel.SpriteBatch.End( );
}
base.Draw( gameTime );
}
/// <summary>
/// OnFocusChanged is called when the focus changes. This
/// implementation changes the label that shows the current
/// numeric value to use a different colour when it is in
/// focus than when it is not in focus.
/// </summary>
protected override void OnFocusedChanged( ) {
base.OnFocusedChanged( );
if( this.Focused ) {
_valueLabel.Color = SystemDefaults.VisualDefaults.FocusTextColor;
} else {
_valueLabel.Color = SystemDefaults.VisualDefaults.NormalTextColor;
}
}
/// <summary>
/// This method is called when the numeric value has
/// changed. This implementation sets the value
/// label to the value the user has changed it to.
/// </summary>
protected override void OnValueChanged( ) {
_valueLabel.Text = GetValueString( );
base.OnValueChanged( );
}
/// <summary>
/// Simple method used to get the string value
/// of the current chosen number.
/// </summary>
protected string GetValueString( ) {
return this.Value.ToString( );
}
}
In the screenshot below you can see the sample numeric spinner in action. In fact you can see three. Two of spinners are not in focus, but the middle one is. You can see the description text aligned to left, beside it is a panel that contains the arrows (aligned to the left and right of the panel) and the numeric value (centered in the panel). The spinner that has focus has a lighter grey background and the value text is dark blue.
Hopefully this has given you a good first look at the control structure for the Tales Framework. In future posts I’ll look at how input and focus work, as well as how network session handling works.
Side Notes:
- Some of the items outlined here are going to change prior to my 1.0 release. I’ll be outlining the items I plan on changing when I release the preview.
- Until the release of XNA Game Studio 3.0, the Tales Framework is built for XNA GS 2.0.
- If you use Windows Live Writer to write your blog entries and want something to do syntax highlighting like you see above, please contact me. Last year I wrote a configurable Live Writer plug-in that can handle most languages.
Comments