Create your own elements
Introduction
This article will guide you through the process of creating custom visual elements using the library. This will enable you to create passive elements as well as interactive elements that can be used in your applications.
Prerequisites
- .NET framework 6 or later
- ConsoleAppVisuals library: 3.0.0 or later
- Having looked at the project from the Introduction section
Setup workspace
We will take the example project of the Introduction section.
As a reminder, here is the file structure of the project:
Example_project <-- root
└───MyApp
├───bin
├───MyApp.csproj
└───Program.cs
Passive elements
Passive elements are visual elements that do not have any interactive behavior. They are used to display information to the user. They can be updated and change display properties.
Setup of a passive element
Start by creating a new file in your project and name it PassiveExample.cs
. Then, add the following code to the file (see real example in the example project):
using ConsoleAppVisuals;
namespace MyApp
{
public class PassiveExample : PassiveElement
{
#region Fields
// Add your custom fields here.
#endregion
#region Properties
// Add overridden properties here.
// You may also add your custom properties here.
#endregion
#region Constructor
/// <summary>
/// The natural constructor of the PassiveExample element.
/// </summary>
public PassiveExample(){}
#endregion
#region Methods
// Add your custom methods here.
#endregion
#region Rendering
/// <summary>
/// Renders the PassiveExample element.
/// </summary>
protected override void RenderElementActions()
{
// This method is mandatory to render correctly your element. If not, an error will be thrown.
// Add what the display code here.
}
#endregion
}
}
Customize your new passive element
Now let's look at the Element
class. This class is the base class for all visual elements. It contains all the properties and methods that are necessary for the rendering of the elements. You can override some of these properties and methods to customize the behavior of your element (the PassiveElement
class inherits from all the Element
class attributes, so you can take the Element
class as a model to create PassiveElements
).
The method that you can override are highlighted in yellow here:
/*
Copyright (c) 2024 Yann M. Vidamment (MorganKryze)
Licensed under GNU GPL v2.0. See full license at: https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/LICENSE.md
*/
namespace ConsoleAppVisuals.Models;
/// <summary>
/// The <see cref="Element"/> class is an abstract class that represents an element that can be rendered on the console.
/// </summary>
/// <remarks>
/// For more information, consider visiting the documentation available <a href="https://morgankryze.github.io/ConsoleAppVisuals/">here</a>.
/// </remarks>
public abstract class Element
{
#region Constants
/// <summary>
/// The default visibility of the elements when they are added to the window.
/// </summary>
/// <remarks>
/// This value should not be changed.
/// Each time the user adds an element to the window, it will try to toggle the visibility of the element.
/// </remarks>
private const bool DEFAULT_VISIBILITY = false;
private const int DEFAULT_HEIGHT = 0;
private const int DEFAULT_WIDTH = 0;
private const int DEFAULT_MAX_NUMBER_OF_THIS_ELEMENT = int.MaxValue;
#endregion
#region Sealed Properties
/// <summary>
/// Gets the id number of the element.
/// </summary>
/// <remarks>This property is sealed. The ID of an element is automatically generated and managed by the <see cref="Window"/> class.</remarks>
public int Id { get; set; }
/// <summary>
/// Gets the visibility of the element.
/// </summary>
/// <remarks>This property is sealed. The visibility of an element is managed by the <see cref="ToggleVisibility"/> method.</remarks>
public bool Visibility { get; private set; } = DEFAULT_VISIBILITY;
#endregion
#region Properties
/// <summary>
/// Gets the type of the element.
/// </summary>
[Visual]
public virtual ElementType Type { get; }
/// <summary>
/// Gets the height of the element, the vertical number of lines taken in the console.
/// </summary>
/// <remarks>This property is marked as virtual. It is recommended to override this property in derived classes to make it more specific.</remarks>
public virtual int Height { get; } = DEFAULT_HEIGHT;
/// <summary>
/// Gets the width of the element, the horizontal number of lines taken in the console.
/// </summary>
/// <remarks>This property is marked as virtual. It is recommended to override this property in derived classes to make it more specific.</remarks>
public virtual int Width { get; } = DEFAULT_WIDTH;
/// <summary>
/// Gets the placement of the element int the console. See the <see cref="Placement"/> enum to know the possible values.
/// </summary>
/// <remarks>This property is marked as virtual. It is recommended to override this property in derived classes to make it more specific.</remarks>
public virtual Placement Placement { get; set; }
/// <summary>
///Gets the text alignment of the text of the element. See the <see cref="TextAlignment"/> enum to know the possible values.
/// </summary>
/// <remarks>This property is marked as virtual. It is recommended to override this property in derived classes to make it more specific.</remarks>
public virtual TextAlignment TextAlignment { get; set; }
/// <summary>
/// Gets the maximum number of this element that can be drawn on the console.
/// </summary>
/// <remarks>This property is marked as virtual. It is recommended to override this property in derived classes to make it more specific.</remarks>
public virtual int MaxNumberOfThisElement { get; } = DEFAULT_MAX_NUMBER_OF_THIS_ELEMENT;
/// <summary>
/// Gets a line to place the element in the console.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the placement of the element is invalid.</exception>
/// <remarks>ATTENTION: This property is not marked as virtual. Override this property only to give it a constant value.</remarks>
public virtual int Line
{
get
{
var elements = Window.Range(0, Id);
return Placement switch
{
Placement.TopCenterFullWidth
=> elements
.Where(e => e.Placement == Placement.TopCenterFullWidth && e.Visibility)
.Sum(e => e.Height)
+ elements
.Where(e => e.Placement == Placement.TopCenter && e.Visibility)
.Sum(e => e.Height)
+ elements
.Where(e => e.Placement == Placement.TopLeft && e.Visibility)
.Sum(e => e.Height)
+ elements
.Where(e => e.Placement == Placement.TopRight && e.Visibility)
.Sum(e => e.Height),
Placement.TopCenter
=> elements
.Where(e => e.Placement == Placement.TopCenterFullWidth && e.Visibility)
.Sum(e => e.Height)
+ elements
.Where(e => e.Placement == Placement.TopCenter && e.Visibility)
.Sum(e => e.Height),
Placement.TopLeft
=> elements
.Where(e => e.Placement == Placement.TopCenterFullWidth && e.Visibility)
.Sum(e => e.Height)
+ elements
.Where(e => e.Placement == Placement.TopLeft && e.Visibility)
.Sum(e => e.Height),
Placement.TopRight
=> elements
.Where(e => e.Placement == Placement.TopCenterFullWidth && e.Visibility)
.Sum(e => e.Height)
+ elements
.Where(e => e.Placement == Placement.TopRight && e.Visibility)
.Sum(e => e.Height),
Placement.BottomCenterFullWidth
=> (Console.WindowHeight == 0 ? 0 : Console.WindowHeight - 1)
- (Height - 1)
- elements
.Where(e =>
e.Placement == Placement.BottomCenterFullWidth && e.Visibility
)
.Sum(e => e.Height),
_ => throw new ArgumentOutOfRangeException(nameof(Placement), "Invalid placement.")
};
}
}
#endregion
#region Methods
/// <summary>
/// Toggles the visibility of the element. If the maximum number of this element is reached, an exception is thrown.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when the maximum number of this element is reached.</exception>
/// <remarks>This method is effectively sealed. The only way to change the visibility of an element is to use this method.</remarks>
public void ToggleVisibility()
{
if (Visibility)
{
Visibility = false;
}
else if (Window.IsElementActivatable(Id))
{
Visibility = true;
}
else
{
throw new InvalidOperationException(
$"Operation not allowed, too many elements of {GetType()} already toggled from the maximum of {MaxNumberOfThisElement}. Consider turning off one element of this type."
);
}
}
#endregion
#region Rendering
/// <summary>
/// Renders the element on the console.
/// </summary>
/// <remarks>
/// For more information, consider visiting the documentation available <a href="https://morgankryze.github.io/ConsoleAppVisuals/">here</a>.
/// </remarks>
[Visual]
public void RenderElement()
{
if (Visibility)
{
RenderOptionsBeforeHand();
RenderElementActions();
RenderOptionsAfterHand();
}
}
/// <summary>
/// Defines the actions to perform when the element is called to be rendered on the console.
/// </summary>
/// <remarks>This method is marked as virtual. It is recommended to override this method in derived classes to make it more specific.</remarks>
[Visual]
protected virtual void RenderElementActions()
{
throw new NotImplementedException("Consider overriding this method in the derived class.");
}
/// <summary>
/// Defines actions to perform before rendering the element on the console.
/// </summary>
[Visual]
protected virtual void RenderOptionsBeforeHand() { }
/// <summary>
/// Defines actions to perform after rendering the element on the console.
/// </summary>
[Visual]
protected virtual void RenderOptionsAfterHand() { }
/// <summary>
/// Renders the space taken by the element on the console.
/// </summary>
/// <param name="ignoreVisibility">Whether to ignore the visibility of the element or not.</param>
/// <remarks>
/// For more information, consider visiting the documentation available <a href="https://morgankryze.github.io/ConsoleAppVisuals/">here</a>.
/// </remarks>
[Visual]
public void RenderElementSpace(bool ignoreVisibility = false)
{
if (Visibility || ignoreVisibility)
{
Core.SaveColorPanel();
Core.SetForegroundColor(Core.GetRandomColor());
Core.WriteMultiplePositionedLines(
false,
TextAlignment.Center,
Placement,
true,
Line,
GetRenderSpace()
);
Core.LoadSavedColorPanel();
}
}
/// <summary>
/// Gets the space taken by the element on the console.
/// </summary>
/// <returns>The space taken by the element.</returns>
/// <remarks>This method is marked as virtual. It is recommended to override this method in derived classes to make it more specific.</remarks>
[Visual]
protected virtual string[] GetRenderSpace()
{
var space = new string[Height];
for (int i = 0; i < space.Length; i++)
{
space[i] = new string(' ', Width);
}
return space;
}
/// <summary>
/// Clears the space taken by the element on the console.
/// </summary>
/// <remarks>
/// For more information, consider visiting the documentation available <a href="https://morgankryze.github.io/ConsoleAppVisuals/">here</a>.
/// </remarks>
[Visual]
public void Clear()
{
Core.ClearMultiplePositionedLines(Placement, Line, GetRenderSpace());
}
#endregion
}
Tip
Depending on the element you want to create, you may not need to override all of these methods. You can override only the ones that are necessary for your element. However I highly recommend to override these:
MaxNumberOfThisElement
: Define the maximum number of this element that can be displayed on the screen simultaneously.RenderElementActions()
: Describe how the element should be displayed.Height
andWidth
: Depending on the element, you may want to override these properties to define the size of your element.
Once your customization is done, you may use your element in your application just like a default element.
Interactive elements
Interactive elements are visual elements that have interactive behavior. They can be used to create buttons, prompts, menus, and other interactive elements. They can be updated and change display properties. But they also always give a response that the user can catch. The type of the response depends on the element.
Setup of an interactive element
Similar to the passive elements, you can create interactive elements but this time they inherit from the InteractiveElement
class. This class contains all the properties and methods that are necessary for the rendering of the elements. You can override some of these properties and methods to customize the behavior of your element.
Start by creating a new file in your project and name it InteractiveExample.cs
. Then, create your new element following this template (see real example in the example project):
using ConsoleAppVisuals;
namespace MyApp
{
public class InteractiveExample : InteractiveElement<T>
{
#region Fields
// Add your custom fields here.
#endregion
#region Properties
// Add overridden properties here.
// You may also add your custom properties here.
#endregion
#region Constructor
/// <summary>
/// The natural constructor of the InteractiveExample element.
/// </summary>
public InteractiveExample(){}
#endregion
#region Methods
// Add your custom methods here.
#endregion
#region Rendering
/// <summary>
/// Renders the InteractiveExample element.
/// </summary>
protected override void RenderElementActions()
{
// This method is mandatory to render correctly your element. If not, an error will be thrown.
// Add what the display code here.
}
#endregion
}
}
Customize your new interactive element
Now let's look at the InteractiveElement
class. This class inherits from the Element
class and contains all the properties and methods that are necessary for the rendering of the elements. You can override some of these properties and methods to customize the behavior of your element.
Important
To define a new interactive element, you must define the type of the response that the element will give. This type can be pretty much everything, but a classic type like int
, string
, ... is to prefer. In the example above, the type T
is used. You can replace it with the type you want to use.
The method and properties that you can override are the same as the PassiveElement
class at some exceptions:
MaxNumberOfThisElement
: is set to one.RenderOptionsBeforeHand
&RenderOptionsBeforeHand
: cannot be modified.
Two new methods are available and cannot be modified:
SendResponse()
: This method is called when the user interacts with the element. It is used to send a response to the window (highly recommended to see the example project ot understand its implementation).GetResponse()
: This method is called when the user has interacted with the element. It is used to get the response from the user (you also haveGetResponseHistory()
to get the history of the responses).
To understand how is defined the interaction response, I highlighted the two attributes that are used to define the response:
/*
Copyright (c) 2024 Yann M. Vidamment (MorganKryze)
Licensed under GNU GPL v2.0. See full license at: https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/LICENSE.md
*/
namespace ConsoleAppVisuals.Models;
/// <summary>
/// The <c>InteractionEventArgs</c> class is a generic class that represents the event arguments for the interactive elements.
/// </summary>
/// <remarks>
/// For more information, consider visiting the documentation available <a href="https://morgankryze.github.io/ConsoleAppVisuals/">here</a>.
/// </remarks>
public class InteractionEventArgs<T> : EventArgs
{
#region Fields
/// <summary>
/// Gets the status after exiting the interactive element. See the <see cref="Status"/> enumeration to know the possible values.
/// </summary>
/// <value>Status.Escaped : pressed escape, Status.Deleted : pressed backspace, Status.Selected : pressed enter</value>
public Status Status { get; set; }
/// <summary>
/// Gets the <typeparamref name="T"/> value of the response after exiting the interactive element.
/// </summary>
public T Value { get; set; }
#endregion
#region Constructor
/// <summary>
/// The <c>InteractionEventArgs</c> class is a generic class that represents the event arguments for the interactive elements.
/// </summary>
/// <param name="status">The status of the exit from the menu.</param>
/// <param name="value">The value of the response after exiting the interactive element.</param>
/// <remarks>
/// For more information, consider visiting the documentation available <a href="https://morgankryze.github.io/ConsoleAppVisuals/">here</a>.
/// </remarks>
public InteractionEventArgs(Status status, T value)
{
Status = status;
Value = value;
}
#endregion
}
Where Status
depends on the values of the Status
enum and Value
depends on the T
type of the InteractiveElement
you created.
Once your customization is done, you may use your element in your application just like a default element.
Visualize all elements available
Now that you know how to create your own elements, you can check if they are available in the library. To do so, you can use built-in elements to display all the elements available in the library.
Window.Open();
ElementsList passiveList = new ElementsList(ElementType.Passive);
Window.AddElement(passiveList);
Window.Render(passiveList);
Window.Freeze();
Window.DeactivateElement(passiveList);
Window.RemoveElement(passiveList);
Window.Close();
Or target only interactive elements:
Window.Open();
ElementsList interactiveList = new ElementsList(ElementType.Interactive);
Window.AddElement(interactiveList);
Window.Render(interactiveList);
Window.Freeze();
Window.DeactivateElement(interactiveList);
Window.RemoveElement(interactiveList);
Window.Close();
Note
You may repeat the same process for the ElementType.Default
and ElementType.Animated
to see all the elements available in the library.
Note also that creating an AnimatedElement
is just like creating a InteractiveElement
but without sending a response. You may add a way for the user to press a key to skip the animation or to stop it. see the loading bars
Have a question, give a feedback or found a bug? Feel free to open an issue or start a discussion on the GitHub repository.