Search code examples
c#checkboxmonogame

Monogame - Create CheckBox object


I've been working on a GUI for my game. So far I've used code to create objects that look like "forms" (not to be confused with winforms, I'm using monogame). Also I created buttons. But now I'm having difficulty creating a checkbox. So here is my CheckBoxGUI class:

// take a look into the recent edits for this post if you need to see my old code.

My TextOutliner class used to draw text with borders/outlines:

// take a look into the recent edits for this post if you need to see my old code.

And finally, here's how I use the CheckBoxGUI class in my TitleScreen:

// take a look into the recent edits for this post if you need to see my old code.

Here is my problem (at least that's where I think it is, in the method that handles the checkbox logic):

// take a look into the recent edits for this post if you need to see my old code.

And finally, Draw():

// take a look into the recent edits for this post if you need to see my old code.

So the method CheckBoxInputTS allows me to check/uncheck a CheckBoxGUI item with mouse clicks, but I can't actually attach any functionality to this. For instance I can do the following inside that same method:

// take a look into the recent edits for this post if you need to see my old code.

Perhaps I'm missing something simple, or I don't understand how to actually make further improvements into the code I already have to make it support different functionalities for the checkBoxes... However, when I test this, if I open the window and check the box the window closses and won't open again. The IsOpen member of the WindowGUI class controls whether the window is visible or not through an if-statement in the class' Draw() method.

The same thing happens if I try to use other examples for the checkboxes... Once checkBox.IsChecked has a user-given value I can't change it.

If someone can find what I'm doing wrong and help me clean-up this code, it will be much appreciated. Thanks in advance!

Edit: Here's what I have so far, based on the proposed answer:

public class CheckBoxGUI : GUIElement
{
    Texture2D checkBoxTexEmpty, checkBoxTexSelected;
    public Rectangle CheckBoxTxtRect { get; set; }
    public Rectangle CheckBoxMiddleRect { get; set; }

    public bool IsChecked { get; set; }
    public event Action<CheckBoxGUI> CheckedChanged;

    public CheckBoxGUI(Rectangle rectangle, string text, bool isDisabled, ContentManager content)
    : base(rectangle, text, isDisabled, content)
    {
        CheckBoxTxtRect = new Rectangle((Bounds.X + 19), (Bounds.Y - 3), ((int)FontName.MeasureString(Text).X), ((int)FontName.MeasureString(Text).Y));
        CheckBoxMiddleRect = new Rectangle((Bounds.X + 16), Bounds.Y, 4, 16);
        if (checkBoxTexEmpty == null) checkBoxTexEmpty = content.Load<Texture2D>(Game1.CheckBoxPath + @"0") as Texture2D;
        if (checkBoxTexSelected == null) checkBoxTexSelected = content.Load<Texture2D>(Game1.CheckBoxPath + @"1") as Texture2D;
    }

    public void OnCheckedChanged()
    {
        var h = CheckedChanged;
        if (h != null)
            h(this);
    }

    public override void UnloadContent()
    {
        base.UnloadContent();
        if (checkBoxTexEmpty != null) checkBoxTexEmpty = null;
        if (checkBoxTexSelected != null) checkBoxTexSelected = null;
    }

    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);
        if (!Game1.IsSoundDisabled)
        {
            if ((IsHovered) && (!IsDisabled))
            {
                if (InputManager.IsLeftClicked())
                {
                    ClickSFX.Play();
                    IsHovered = false;
                }
            }
        }
        if (IsClicked) OnCheckedChanged();
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        if ((FontName != null) && ((Text != string.Empty) && (Text != null)))
        {
            if (IsChecked) spriteBatch.Draw(checkBoxTexSelected, Bounds, Color.White);
            else if (IsDisabled) spriteBatch.Draw(checkBoxTexEmpty, Bounds, Color.Gray);
            else spriteBatch.Draw(checkBoxTexEmpty, Bounds, Color.Gray);
            TextOutliner.DrawBorderedText(spriteBatch, FontName, Text, CheckBoxTxtRect.X, CheckBoxTxtRect.Y, ForeColor);
        }
    }
}

And then the GUIElement class:

public abstract class GUIElement
{
    protected SpriteFont FontName { get; set; }
    protected string Text { get; set; }
    protected SoundEffect ClickSFX { get; set; }
    public Rectangle Bounds { get; set; }
    public Color ForeColor { get; set; }
    public Color BackColor { get; set; }
    public bool IsDisabled { get; set; }
    public bool IsHovered { get; set; }
    public bool IsClicked { get; set; }

    public GUIElement(Rectangle newBounds, string newText, bool isDisabled, ContentManager content)
    {
        Bounds = newBounds;
        Text = newText;
        IsDisabled = isDisabled;
        FontName = Game1.GameFontSmall;
        ForeColor = Color.White;
        BackColor = Color.White;
        ClickSFX = content.Load<SoundEffect>(Game1.BGSoundPath + @"1") as SoundEffect;
    }

    public virtual void UnloadContent()
    {
        if (Bounds != Rectangle.Empty) Bounds = Rectangle.Empty;
        if (FontName != null) FontName = null;
        if (Text != string.Empty) Text = string.Empty;
        if (ClickSFX != null) ClickSFX = null;
    }

    public virtual void Update(GameTime gameTime)
    {
        if (!IsDisabled)
        {
            if (Bounds.Contains(InputManager.MouseRect))
            {
                if (InputManager.IsLeftClicked()) IsClicked = true;
                ForeColor = Color.Yellow;
                IsHovered = true;
            }
            else if (!Bounds.Contains(InputManager.MouseRect))
            {
                IsHovered = false;
                ForeColor = Color.White;
            }
        }
        else ForeColor = Color.Gray;
    }
}

And this is my usage:

// Fields
readonly List<GUIElement> elements = new List<GUIElement>();
CheckBoxGUI chk;
bool check = true;
string text;

// LoadContent()
chk = new CheckBoxGUI(new Rectangle(800, 200, 16, 16), "Hide FPS", false, content);
chk.CheckedChanged += cb => check = cb.IsChecked;
elements.Add(chk);

// UnloadContent()
foreach (GUIElement element in elements) element.UnloadContent();

// Update()
for (int i = 0; i < elements.Count; i++) elements[i].Update(gameTime);
foreach (CheckBoxGUI chk in elements) chk.Update(gameTime);

// Draw()
if (chk.IsChecked) check = true;
else check = false;
if (check) text = "True";
else text = "False";
spriteBatch.DrawString(Game1.GameFontLarge, text, new Vector2(800, 400), Color.White);
if (optionsWindow.IsOpen) chk.Draw(spriteBatch);

Solution

  • As an alternative to what @craftworkgames suggested, you can also pass a delegate through the checkbox constructor, and invoke it directly. The difference between this and events is marginal; an event is basically a list of delegates (i.e. managed pointers to functions), and "firing an event" simply calls all handlers you attached (using +=) in succession. Using events will provide a more uniform way (at least more comfortable to .NET developers), but I will also mention some other aspects in my answer.

    This is the general idea:

    public class CheckBoxGUI : GUIElement
    {
        public bool IsChecked { get; set; }
    
        // instead of using the event, you can use a single delegate
        readonly Action<CheckBoxGUI> OnCheckedChanged;
    
        public CheckBoxGUI(Action<CheckBoxGUI> onCheckedChanged, Rectangle rectangle)
            : base(rectangle) // we need to pass the rectangle to the base class
        {
            OnCheckedChanged = onCheckedChanged;
        }
    
        public override bool Update(GameTime gameTime, InputState inputState)
        {
            if (WasClicked(inputState)) // imaginary method
            {
                // if we are here, it means we need to handle the inputState
                IsChecked = !IsChecked;
    
                // this method will be invoked 
                OnCheckedChanged(this);
    
                return true;
            } 
            else 
            {
                return false;
            }
        }
    
        ... drawing methods, load/unload content
    }
    

    When you instantiate the checkbox, you can simply pass an anonymous method which will get called on state change:

    var checkbox = new CheckBoxGUI(
        // when state is changed, `win.IsOpen` will be set to a new value
        cb => win.IsOpen = cb.IsChecked, 
        new Rectangle(0, 0, 100, 100)
    );
    

    (btw, the difference between this approach and events is rather negligible:)

    // if you had a 'CheckedChanged' event, instantiation would look something like this
    var checkbox = new CheckBoxGUI(...);
    checkbox.CheckedChanged += cb => win.IsOpen = cb.IsChecked;
    

    Create a base gui class with shared functionality

    I would also extract most functionality into a base class. This is the same reason Control class exists as the base class in WinForms, and you will be repeating this over and over again unless you use a base class.

    If nothing else, all elements have a bounding rectangle and the same way of checking for mouse clicks:

    public class GUIElement
    {
        public Rectangle Bounds { get; set; }
    
        public GUIElement(Rectangle rect)
        {
            Bounds = rect;
        }
    
        public virtual bool Update(GameTime game, InputState inputState)
        {
            // if another element already handled this click,
            // no need to bother
            if (inputState.Handled)
                return false;           
    
            // you should actually check if mouse was both clicked and released 
            // within these bounds, but this is just a demo:
            if (!inputState.Pressed)
                return false;
    
            // within bounds?
            if (!this.Bounds.Contains(inputState.Position))
                return false;
    
            // mark as handled: we don't want this event to 
            // propagate further
            inputState.Handled = true;
            return true;
        }
    
        public virtual void Draw(GameTime gameTime, SpriteBatch sb) { }
    }
    

    When you decide that you want your mouse clicks to be triggered on mouse-up events (like it's usually done), you only have a single place in your code where this change should happen.

    Btw. this class should also implement other properties shared between all your UI elements. You don't need to specify their values through the constructor, but you must specify defaults. This is similar to what WinForms does:

    public class GUIElement
    {
        public Rectangle Bounds { get; set; }
        public SpriteFont Font { get; set; }
        public Color ForeColor { get; set; }
        public Color BackColor { get; set; }
    
        // make sure you specify all defaults inside the constructor 
        // (except for Bounds, of course)
    }
    

    To abstract the mouse/touch/gamepad input, I have used a rather simplistic InputState above (you might want to extend it later):

    public class InputState
    {
        // true if this event has already been handled
        public bool Handled;
    
        // true if mouse is being held down 
        public bool Pressed;
    
        // mouse position
        public Point Position;
    }
    

    Inheriting from the base class

    With this in place, your CheckBoxGUI now inherits the goodies from GUIElement:

    // this is the class from above
    public class CheckBoxGUI : GUIElement
    {
        public bool IsChecked { get; set; }
    
        readonly Action<CheckBoxGUI> OnCheckedChanged;
    
        public CheckBoxGUI(Action<CheckBoxGUI> onCheckedChanged, Rectangle rectangle)
            : base(rectangle) // we need to pass the rectangle to the base class
        {
            OnCheckedChanged = onCheckedChanged;
        }
    
        // Note that this method returns bool, unlike 'void Update'.
        // Also, intersections should be handled here, not outside.
        public override bool Update(GameTime gameTime, InputState inputState)
        {
            var handled = base.Update(gameTime, inputState);
            if (!handled)
                return false;
    
            // if we are here, it means we need to handle the inputState
            IsChecked = !IsChecked;
            OnCheckedChanged(this);
            return true;
        }
    
        ... drawing methods, load/unload content
    }
    

    Your game/scene class

    At the beginning of your main Game (or Scene) update method, you only need to create the InputState instance and call HandleInput for all elements:

    public void Update(GameTime gameTime)
    {
        var mouse = Mouse.GetState();
        var inputState = new InputState()
        {
            Pressed = mouse.LeftButton == ButtonState.Pressed,
            Position = mouse.Position
        };
    
        foreach (var element in this.Elements)
        {
            element.Update(gameTime, inputState);
        }
    }
    

    Event based alternative

    Using the event-based approach, you would change the check box to:

    // this is the class from above
    public class CheckBoxGUI : GUIElement
    {
        public bool IsChecked { get; set; }
        public event Action<CheckBoxGUI> CheckedChanged;
    
        protected virtual void OnCheckedChanged()
        {
            var h = CheckedChanged;
            if (h != null)
                h(this);
        }
    
        public CheckBoxGUI(Rectangle rectangle)
            : base(rectangle) // we need to pass the rectangle to the base class
        { }
    
        // the rest of the class remains the same
    }
    

    And instantiate it:

    var cb = new CheckBoxGUI(new Rectangle(0, 0, 100, 100));
    cb.CheckedChanged += cb => win.IsOpen = cb.IsChecked;
    

    Update

    This is how your Update call stack should look like:

    • Game.Update

      • calls TitleScreen.Update
    • TitleScreen.Update

      • calls element.Update for gui elements (not just checkboxes)
    • GuiElement.Update

      • checks if this element was clicked (to be reused by derived classes)
    • CheckBoxGUI.Update

      • calls GuiElement.Update (base.Update) to check if just clicked
      • if clicked, changes its state, visual properties and fires the event

    If you are doing things right, you should have a list of gui elements in your screen class, not actual instances. Screen should not know or care which exact type of the element is being drawn:

    readonly List<GuiElement> elements = new LIst<GuiElement>();
    var chk = new CheckBoxGUI(new Rectangle(800, 200, 16, 16), "Window", false, content);
    chk.CheckedChanged += cb => win.IsOpen = cb.IsChecked;
    
    // from now on, your checkbox is just a "gui element" which knows how to
    // update itself and draw itself
    elements.Add(chk);
    
    // your screen update method
    foreach (var e in elements) 
        e.Update(gameTime);
    
    // your screen draw method
    foreach (var e in elements) 
        e.Draw(gameTime);