How to Create an Image Button in Nuclex

The Nuclex framework's GuiManager is an extremely well written, easy to use tool for creating GUIs in an XNA game. It comes with built in functionality to support buttons, sliders, progress bars and text inputs, and is capable of being extended to support custom controls. One custom control that I had a need for was an Image Button. An ImageButtonControl would be similar to a regular button, but using an icon instead of text. This would be used to create hotbars like the ones seen in an MMO or RTS.

Luckily, creating an ImageButtonControl to use with Nuclex is easy and straightforward. However, this article will assume you are already familiar enough with the GuiManager tools to create simple dialogs with ButtonControl objects. The basic outline is as follows:

  1. Create an ImageButtonControl class that extends PressableControl. PressableControl will contain support for the various event handlers that a button will need.
  2. Create a FlatImageButtonControlRenderer class that dictates how to draw the ImageButtonControl on screen. It will be very similar to the FlatButtonControlRenderer, but contain some additional logic for drawing the image on screen.
  3. Bind the FlatImageButtonControlRenderer to the GuiManager so it knows how to draw the ImageButtonControl.
  4. Create a spritesheet to contain all of the images that will need to be drawn within the GUI.
  5. Modify the skin XML file to contain frames for the new ImageButtonControl.
  6. Add an ImageButtonControl to a WindowControl, just as you would with a ButtonControl. 

ImageButtonControl class

The ImageButtonControl class is very straightforward. It simply extends PressableControl and adds a single field for specifying the ImageFrame that will be drawn. This ImageFrame will be defined in step 5 - editing the skin XML file.

using Microsoft.Xna.Framework.Graphics;
using Nuclex.UserInterface.Controls;
using Nuclex.UserInterface.Controls.Desktop;

public class ImageButtonControl : PressableControl
{
    public string ImageFrame;
}

FlatImageButtonControlRenderer class

The FlatImageButtonControlRenderer class is a little more complex, but still relatively easy. If you look at the FlatButtonControlRenderer, you will see that this class is nearly identical. Afterall, they serve a very similar purpose - draw a button on screen. The two key differences between the ButtonControl and ImageButtonControl renderers are:

  1. The ImageButtonControl renderer contains a second DrawElement command to draw the image. This comes after the button itself is drawn, so that the image is visible on top of the button.
  2. The ImageButtonControl renderer class' states are used for both the imagebutton frame and the image frame. This allows the image to change depending on whether or not it is normal, highlighted, depressed or disabled.

using Microsoft.Xna.Framework.Graphics;
using Nuclex.UserInterface;
using Nuclex.UserInterface.Visuals.Flat;
using Nuclex.UserInterface.Controls.Desktop;

public class FlatImageButtonControlRenderer : IFlatControlRenderer<ImageButtonControl>
{
    public void Render(ImageButtonControl control, IFlatGuiGraphics graphics)
    {
        RectangleF controlBounds = control.GetAbsoluteBounds();

        // Determine the style to use for the button
        int stateIndex = 0;
        if (control.Enabled)
        {
            if (control.Depressed)
            {
                stateIndex = 3;
            }
            else if (control.MouseHovering || control.HasFocus)
            {
                stateIndex = 2;
            }
            else
            {
                stateIndex = 1;
            }
        }

        // Draw the button's frame
        graphics.DrawElement("imagebutton" + states[stateIndex], controlBounds);

        // Draw the image itself
        graphics.DrawElement(control.ImageFrame + states[stateIndex], controlBounds);
    }

    /// <summary>Names of the states the button control can be in</summary>
    /// <remarks>
    ///   Storing this as full strings instead of building them dynamically prevents
    ///   any garbage from forming during rendering.
    /// </remarks>
    private static readonly string[] states = new string[] {
        ".disabled",
        ".normal",
        ".highlighted",
        ".depressed"
    };
}

Binding the Renderer

In order for the GuiManager to properly render the new ImageButtonControl using the FlatImageButtonControlRenderer, you will need to add the FlatImageButtonControlRenderer to the list of known renderers. When creating your GuiManager, you will need to modify it to include a line to do this:

Gui = new GuiManager(Game.Graphics, Game.Input)
{
    Screen = new Screen(game.GraphicsDevice.Viewport.Width, game.GraphicsDevice.Viewport.Height)
};

Gui.Screen.Desktop.Bounds = new UniRectangle(
    25, 25,
    new UniScalar(1.0f, -25.0F), new UniScalar(1.0f, -25.0F)
);

Gui.Initialize();
Gui.Visualizer = FlatGuiVisualizer.FromFile(game.Services, "Content/menu_gui.xml");

// add the FlatImageButtonControlRenderer to the list of renderers
((FlatGuiVisualizer)Gui.Visualizer).RendererRepository.AddAssembly(typeof(FlatImageButtonControlRenderer).Assembly);

Gui.UpdateOrder = 1000;

Components = new GameComponentCollection { Gui };

Spritesheet

Next, create the spritesheet that contains all of the images to be rendered, similar to the one shown here. This spritesheet is used for my space shooter Fusion, which I am currently porting from Javascript to C#.

 

Skin XML

Now edit your skin XML file to contain additional frames for each image, along with each of the image states. Inside each frame, I have not only placed the image in the center, but I have also created padding by injecting a 1x1 clear png, and creating regions around the borders. Without this, the image will appear to cover the entire button, and you won't see the button borders.

<?xml version="1.0" encoding="utf-8" ?>
<skin
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation="skin.xsd"
  name="MenuSkin"
>

  <resources>
    <font name="menu_font" contentPath="menu_font" />
    <bitmap name="menu_skin_bitmap" contentPath="menu_skin" />
    <bitmap name="spritesheet" contentPath="spritesheet" />
    <bitmap name="clear" contentPath="clear" />
  </resources>

  <frames>
    <!-- Frame used for a pushbutton in its normal state -->
    

    <frame name="imagebutton.normal">
      <region source="menu_skin_bitmap" hplacement="left" vplacement="top" x="21" y="0" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="top" x="31" y="0" w="1" h="10" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="top" x="32" y="0" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="left" vplacement="stretch" x="21" y="10" w="10" h="1" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="stretch" x="31" y="10" w="1" h="1" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="stretch" x="32" y="10" w="10" h="1" />
      <region source="menu_skin_bitmap" hplacement="left" vplacement="bottom" x="21" y="11" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="bottom" x="31" y="11" w="1" h="10" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="bottom" x="32" y="11" w="10" h="10" />
      <text font="menu_font" hplacement="center" vplacement="center" color="#EFEFEF" />
    </frame>

    <frame name="imagebutton.highlighted">
      <region source="menu_skin_bitmap" hplacement="left" vplacement="top" x="42" y="0" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="top" x="52" y="0" w="1" h="10" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="top" x="53" y="0" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="left" vplacement="stretch" x="42" y="10" w="10" h="1" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="stretch" x="52" y="10" w="1" h="1" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="stretch" x="53" y="10" w="10" h="1" />
      <region source="menu_skin_bitmap" hplacement="left" vplacement="bottom" x="42" y="11" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="bottom" x="52" y="11" w="1" h="10" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="bottom" x="53" y="11" w="10" h="10" />
      <text font="menu_font" hplacement="center" vplacement="center" color="#FFF000" />
    </frame>

    <frame name="imagebutton.depressed">
      <region source="menu_skin_bitmap" hplacement="left" vplacement="top" x="42" y="0" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="top" x="52" y="0" w="1" h="10" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="top" x="53" y="0" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="left" vplacement="stretch" x="42" y="10" w="10" h="1" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="stretch" x="52" y="10" w="1" h="1" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="stretch" x="53" y="10" w="10" h="1" />
      <region source="menu_skin_bitmap" hplacement="left" vplacement="bottom" x="42" y="11" w="10" h="10" />
      <region source="menu_skin_bitmap" hplacement="stretch" vplacement="bottom" x="52" y="11" w="1" h="10" />
      <region source="menu_skin_bitmap" hplacement="right" vplacement="bottom" x="53" y="11" w="10" h="10" />
      <text font="menu_font" hplacement="center" vplacement="center" color="#17daf7" />
    </frame>

    <frame name="hotbar.cannon.normal">
      <region source="clear" hplacement="left" vplacement="stretch" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="right" vplacement="stretch" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="stretch" vplacement="top" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="stretch" vplacement="bottom" x="0" y="0" w="5" h="5"/>
      <region source="spritesheet" hplacement="stretch" vplacement="stretch" x="0" y="672" w="56" h="56"/>
    </frame>

    <frame name="hotbar.cannon.depressed">
      <region source="clear" hplacement="left" vplacement="stretch" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="right" vplacement="stretch" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="stretch" vplacement="top" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="stretch" vplacement="bottom" x="0" y="0" w="5" h="5"/>
      <region source="spritesheet" hplacement="stretch" vplacement="stretch" x="56" y="672" w="56" h="56"/>
    </frame>

    <frame name="hotbar.cannon.highlighted">
      <region source="clear" hplacement="left" vplacement="stretch" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="right" vplacement="stretch" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="stretch" vplacement="top" x="0" y="0" w="5" h="5"/>
      <region source="clear" hplacement="stretch" vplacement="bottom" x="0" y="0" w="5" h="5"/>
      <region source="spritesheet" hplacement="stretch" vplacement="stretch" x="112" y="672" w="56" h="56"/>
    </frame>
    
  </frames>

</skin>

Putting it Together.

Now that you have everything set in place, you can create an instance of the ImageButtonControl and add it to a WindowControl. The only new property that needs to be set is the name of the image frame to be rendered.

_cannonButton = new ImageButtonControl();
_cannonButton.Bounds = new UniRectangle(
    new UniScalar(0.0F, 15),
    new UniScalar(0.0F, 15),
    new UniScalar(0.0F, 50), 
    new UniScalar(0.0F, 50)
);
_cannonButton.ImageFrame = "hotbar.cannon";
Children.Add(_cannonButton);

And now you have image buttons that you can use in your game. Unfortunately, this technique is limited to static images that existed at compile time - so no user generated images or screenshots. But for the purpose of having a hotbar, this technique works very nicely, and doesn't require modifying Nuclex at all.