Videogamers0 / MGUI

UI framework for MonoGame game engine.
MIT License
66 stars 7 forks source link
c-sharp csharp data-binding databinding dotnet6 framework gamedev gui monogame monogame-framework monogame-gui open-source user-interface xaml xaml-layout xaml-ui xna xna-framework

MGUI

MGUI is a UI framework for MonoGame (Src) that features a powerful layout engine and data binding engine similar to WPF, and a robust set of controls to build your UI with.

All control names are prefixed with 'MG' and many controls have similar names and properties to what you might find in WPF. Currently supported controls:

Wiki is under construction. More documentation coming soon... maybe...

Examples

A simple registration window created with MGUI:

Register.png

XAML Markup: ```xaml ```

Register.png

XAML Markup ```xaml

FF7Inventory.gif

XAML Markup ```xaml ```

MGUI can also parse and render your XAML markup at runtime using the MGXAMLDesigner control: XAML Designer

Getting Started:

  1. Clone this repo
  2. Use Visual Studio 2022 or later (since this project targets .NET 6.0, and makes use of some new-ish C# language features such as record structs (which may be unavailable in c# language version 8.0 or earlier))
  3. In your MonoGame project:
    • In the Solution Explorer:
      • Right-click your Solution, Add -> Existing Project. Browse for MGUI.Shared.csproj, and MGUI.Core.csproj.
      • Right-click your Project, Add -> Project Reference. Add references to MGUI.Shared and MGUI.Core.
      • You may need to:
      • Right-click your game's Content folder, Add -> Existing Item. Browse for MGUI\MGUI.Shared\Content\MGUI.Shared.Content.mgcb and MGUI\MGUI.Core\Content\MGUI.Core.Content.mgcb and add them both as links (in the file browser dialog, click the dropdown arrow next to the Add button and choose Add as link). This step will ensure that MGUI's content .xnb files are copied to your project's bin\Content folder. This step might not be necessary.
    • In your Game class:
      • In the Initialize method:
      • Instantiate MGUI.Shared.Rendering.MainRenderer
      • Instantiate MGUI.Core.UI.MGDesktop
      • Anywhere in your code, instantiate 1 or more MGWindow and add them to your MGDesktop instance via MGDesktop.Windows
      • In the Update method: Call MGDesktop.Update()
      • In the Draw method: Call MGDesktop.Draw()
Example code for your Game class: ```c# public class Game1 : Game, IObservableUpdate { private GraphicsDeviceManager _graphics; private SpriteBatch _spriteBatch; private MainRenderer MGUIRenderer { get; set; } private MGDesktop Desktop { get; set; } public event EventHandler PreviewUpdate; public event EventHandler EndUpdate; public Game1() { _graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; IsMouseVisible = true; } protected override void Initialize() { this.MGUIRenderer = new(new GameRenderHost(this)); this.Desktop = new(MGUIRenderer); MGWindow Window1 = new(Desktop, 50, 50, 500, 200); Window1.TitleText = "Sample Window with a single [b]Button[/b]: [color=yellow]Click it![/color]"; Window1.BackgroundBrush.NormalValue = new MGSolidFillBrush(Color.Orange); Window1.Padding = new(15); MGButton Button1 = new(Window1, button => { button.SetContent("I've been clicked!"); }); Button1.SetContent("Click me!"); Window1.SetContent(Button1); this.Desktop.Windows.Add(Window1); base.Initialize(); } protected override void LoadContent() { _spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void Update(GameTime gameTime) { PreviewUpdate?.Invoke(this, gameTime.TotalGameTime); // Call Desktop.Update() at start of your update loop, so that the UI has the 1st-chance to receive and handle user-inputs Desktop.Update(); // TODO: Add your game update logic here base.Update(gameTime); EndUpdate?.Invoke(this, EventArgs.Empty); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // TODO: Add your drawing code here // Call Desktop.Draw() at the end of your draw loop, so that the UI is rendered overtop of everything else Desktop.Draw(); base.Draw(gameTime); } } ``` ![window1.png](assets/samples/window1.png)

Multi-Platform

MGUI.Core targets net6.0-windows by default. If you wish to use MGUI on another OS, open MGUI\MGUI.Core\MGUI.Core.csproj and change <TargetFramework>net6.0-windows</TargetFramework> to <TargetFramework>net6.0</TargetFramework>. Some features have better implementations when targeting net6.0-windows, but everything probably™ works fine when targeting net6.0. In particular, targeting net6.0 breaks some intellisense related to MarkupExtensions (such as MGBinding) when using the Visual Studio Xaml designer (but it still works at runtime, don't ask me why).

Input Handling

MGUI uses its own framework to detect and respond to inputs.

If you'd like to utilize this framework in your own code, get the MouseTracker and/or KeyboardTracker instance from the InputTracker. Call MouseTracker.CreateHandler(...)/KeyboardTracker.CreateHandler(...), and subscribe to the events in the returned handler instance.

EX: Creates a MouseHandler instance in Game1.Initialize and subscribes to an event

public class Game1 : Game, IObservableUpdate, IMouseHandlerHost
{
    private MainRenderer MGUIRenderer { get; set; }
    private MGDesktop Desktop { get; set; }

    //  IObservableUpdate implementation
    public event EventHandler<TimeSpan> PreviewUpdate;
    public event EventHandler<EventArgs> EndUpdate;

    //  IMouseHandlerHost implementation
    bool IMouseViewport.IsInside(Vector2 Position) => Window.ClientBounds.Contains(Position);
    Vector2 IMouseViewport.GetOffset() => Vector2.Zero;

    private MouseHandler MH { get; set; }

    protected override void Initialize()
    {
        this.MGUIRenderer = new MainRenderer(new GameRenderHost<Game1>(this));
        this.Desktop = new MGDesktop(MGUIRenderer);

        //  Create a MouseHandler
        this.MH = MGUIRenderer.Input.Mouse.CreateHandler(this, null);

        //  Subscribe to 1 or more mouse-related events
        MH.MovedInside += (sender, e) =>
        {
            // TODO: Do something in response to the mouse moving
        };

        base.Initialize();
    }

    protected override void Update(GameTime gameTime)
    {
        PreviewUpdate?.Invoke(this, gameTime.TotalGameTime);
        Desktop.Update();
        MH.ManualUpdate();
        // TODO: Add your update logic here
        base.Update(gameTime);
        EndUpdate?.Invoke(this, EventArgs.Empty);
    }

    // constructor and other functions ommitted for brevity...
}
Full example code: Suppose you want to react to WASD keys to move your player, but you don't want to move the player if the WASD was handled by an `MGTextBox` on the UI: ```c# public class Game1 : Game, IObservableUpdate, IKeyboardHandlerHost { private GraphicsDeviceManager _graphics; private SpriteBatch _spriteBatch; private MainRenderer MGUIRenderer { get; set; } private MGDesktop Desktop { get; set; } public event EventHandler PreviewUpdate; public event EventHandler EndUpdate; public Game1() { _graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; IsMouseVisible = true; } private KeyboardHandler PlayerMovementHandler; protected override void Initialize() { _graphics.PreferredBackBufferWidth = 400; _graphics.PreferredBackBufferHeight = 300; _graphics.ApplyChanges(); this.MGUIRenderer = new(new GameRenderHost(this)); this.Desktop = new(MGUIRenderer); // Create a simple UI that may need to handle keyboard events MGWindow Window1 = new(Desktop, 20, 20, 200, 100); Window1.Padding = new(10); MGTextBox TextBox = new(Window1); Window1.SetContent(TextBox); Desktop.Windows.Add(Window1); // Create a KeyboardHandler instance that will respond to WASD key presses PlayerMovementHandler = MGUIRenderer.Input.Keyboard.CreateHandler(this, null); PlayerMovementHandler.Pressed += (sender, e) => { if (e.Key is Keys.W or Keys.A or Keys.S or Keys.D) { //TODO Move the player e.SetHandledBy(this, false); } }; base.Initialize(); } protected override void LoadContent() { _spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void Update(GameTime gameTime) { PreviewUpdate?.Invoke(this, gameTime.TotalGameTime); Desktop.Update(); // By updating this handler AFTER we've updated our UI, // the handler won't receive events that were already handled by our UI's TextBox PlayerMovementHandler.ManualUpdate(); // TODO: Add your update logic here base.Update(gameTime); EndUpdate?.Invoke(this, EventArgs.Empty); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // TODO: Add your drawing code here Desktop.Draw(); base.Draw(gameTime); } } ``` If you don't want to use MGUI's input framework, then you can just check if the `EventArgs` in `MouseTracker`/`KeyboardTracker` have already been handled before your code attempts to handle them: ```c# BaseKeyPressedEventArgs W_PressEvent = MGUIRenderer.Input.Keyboard.CurrentKeyPressedEvents[Keys.W]; if (Keyboard.GetState().IsKeyDown(Keys.W) && (W_PressEvent == null || !W_PressEvent.IsHandled)) { //TODO do something } ```

DataBinding

MGUI has its own DataBinding engine to allow you to bind UI data to data in your DataContext. Set MGWindow.WindowDataContext or MGElement.DataContextOverride. Then you can use the MGBinding markup extension in XAML to configure your bindings.

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        SizeToContent="WidthAndHeight" TitleText="DataBindings" Padding="10">
        <TextBlock Text="{MGBinding Path=SomeSampleString, Mode=OneWay}" />
</Window>

{MGBinding Path=SomeSampleString, Mode=OneWay} creates a DataBinding that will automatically set TextBlock.Text = TextBlock.DataContext.SomeSampleString;

MGUI's DataBinding engine supports several features such as:

DataBindings_1

Because MGUI uses its own DataBinding engine, DataBindings will work even on non-Windows platforms. (Though you will need to change MGUI.Core to target net6.0 instead of net6.0-windows, and some intellisense won't work in the Visual Studio Xaml designer.)