Table of Contents
- Preface
- Introduction
- Background
- Item System
- The Main Rule
- How to Create a Special Item
- Containers With Special Layout That Ignore the Main Layout
- Floating Independent Items
- Draggable Items that Receive a EventMouseDrag Event
- Window Drag Item
- Items that are Images
- Items that are Text, or Contain Text, Work with Text, etc.
- Using OpenGL Technology in SpaceVIL
- Application Style Themes
- The States System of Elements and Ways of User Interaction with the Element
- Conclusion
- Screenshots
- History
SpaceVIL framework can be found at spvessel.com.
Examples of use can be found at GitHub.
A getting started guide can also be found at spvessel.com and GitHub.
I'll tell you about the SpaceVIL framework, its capabilities and a brief story of its creation.
SpaceVIL (Space of visual item layouts) is a cross-platform and multilingual framework based on OpenGL technology with the GLFW library for creating windows. Using the framework, you can create GUI applications for Linux, Mac OS X and Windows operating systems.
When I decided to create SpaceVIL, I already studied (or just familiarized myself with) a bunch of UI frameworks. All of them were very different from each other. Each time I started working with a new framework, I realized that I can’t use most of my knowledge about other UI frameworks, because each of them differs from others and each of them has its own rules and the main idea of using Qt Widgets, Qt Quick, HTML, CSS, WPF, Windows Forms, Borland VCL, JavaAWT, JavaSwing, JavaFX. Knowledge of one of them will not be useful to others. If you know how to do something in six different frameworks, you still need to read the manual in 80% of cases to find out how to do this ”something” in the seventh. Unfortunately, UI systems are not cross-intuitive.
This was one of the reasons for creating a universal multilingual UI framework. When I change the programming language, I want to immerse myself in its features, but not into the new (for me) UI system (creating UI for apps was important in my previous work), because the previous UI system depends on the language, platform or OS. There is nothing complicated in creating such a framework. GUI apps have been around for a long time and we could assume that cross-platform, easy-to-use, multilingual framework should exist. But it’s not.
Qt does something similar for C++, but to be honest, Qt is a very big framework based on C++ with its implementation of the standard type library and offers more than pure C++. After using Qt, it is very difficult to return to C++ again. Qt includes classes for working with images, graphics, fonts, etc., which is not included in standard C++. If you want to return to C++, you have to collect all the pieces from open source to have something like Qt. And it'll be good if all the pieces are licensed by LGPL.
Other reasons for writing my own UI system is that existing systems are overly complex, work in different ways, have many exceptions, are closed, don't give enough freedom to control the rendering process and all UI features that you might want.
The system should be easy to use and flexible in implementing the developer's plans. Nothing should prevent developers from realizing their creativity. Actually, it's not so hard to provide flexibility and functionality, because all possible ways of interacting with the user are strictly limited by operating systems and input methods.
In my work, I often used OpenGL rendering technology. Using this technology, I successfully solved all my problems. Then, when the tasks switched to user interaction, I started thinking that all this already looks like a GUI system. I'm not a pro in OpenGL, but my knowledge was enough to use them in my own UI building system.
As a fun project, I decided to make some prototypes. They were mostly based on various open source OpenGL wrappers with a built-in window creation and management system. At the same time, I was trying to improve my C# skills, so I used platform-dependent modules in prototypes. The platform was usually Windows. Each prototype was made to solve certain problems and to test platform capabilities. When the prototypes grew enough, I began to analyze the future project, establish strict rules and develop goals. The prototypes were in C#, so I decided to include the .NET Core platform to make the project cross-platform (besides, at that time, it was impossible to create a UI app for .NET Core). My teammate used Java, so we decided to add the JVM platform to the project. Together, this should give us the following perspectives:
- Support for the following programming languages:
- .NET platform: C#, Visual Basic, C++ CLI
- JVM platform: Java, Scala, Kotlin
- Support for the following OSes:
- Ability to port the system to mobile platforms
And that's quite good. Only one UI building system with the same use for 3 OS types and 6 programming languages. The technology stack wasn't random – the use of OOP and OpenGL should simplify porting the framework to another pair of language+platform (C++, Python, etc.).
To make the system flexible and capable of easy porting and development, its modules were strictly separated so that they could be easily replaced if necessary without harming the entire system. These separate modules became the core of the system. These modules are: recipes and rules module (algorithms, interfaces, abstract classes), common layout module, and three basic abstract classes of items. Visualization is divided into a rendering engine with service static classes (for partial rendering management, styling, etc.) and a window creation/management system. All systems interact with each other according to strict rules.
This is the essence of the SpaceVIL framework. That is all the system needs, and from this moment, it shows its true flexibility.
Items in the framework is the main data packet type. The type passes through all systems and is the base construction material of the framework. Actually, I can remove all the items that already exist in the framework (about 54 items) and leave only three basic ones. It wouldn't affect workability. It may be hard to understand, but all 54 items are nothing more than a demonstration of the system's capabilities. These are just an implementation of recipes and instructions. Any framework user can use the framework rules to create their own recipes (items). There are no restrictions, because the kitchen of incredible opportunities is right in front of you.
There are only three main items - IBaseItem
, Primitive
, Prototype
. The first one is the basic template for your own recipe, the second is the recipe implementation for simple non-interactive items, and the third is the IBaseItem
implementation for interactive items (which receive events, they are also containers for the first two types).
Let's take a look at the system's evolution. I'll show you how to create your own item and will use the button as one of the easiest.
To create our own item, we need to inherit from one of the three basic items. In the case of the button, this is Prototype
, because we need the ability to receive events and interact with the item. After that, we need to style the button shape directly (or we can use a style system). And that's it, the button is almost ready. The only thing it still needs is the text on it. Buttons usually have some text.
So we need text. The text is a non-interactive item, so for its implementation, we can choose Primitive
as the base class. According to the framework's rules, many items can have their own interface to let the framework know how to process such recipes (just like in the kitchen – the main course is the first, a drink with it but it's not necessary, the dessert at the end, etc.). In the case of text, we can use the ITextContainer
interface. With Primitive and ITextContainer
, we only need to implement a text type item with any convenient approach and any libraries that we want.
Suppose the text item is ready. Now we need to place it into the button. That's easy. Since the button inherits Prototype
, it's a container for all IBaseItem
type items and it has a strong layout system inside. Therefore, we'll use one of the recipe rules – the InitElements()
implementation. Later, I’ll tell about all of the main rules. Inside the method, we'll call the AddItem(text)
method, where text is an instance of ITextContainer
, which we made earlier.
Then, we can add some useful functions to the button
class to change the text such as font, position, etc.
So we made the first item in the system that we can use.
The next item will be a button
with switching states (toggle button). Since it's almost done, we can just inherit the button
and add some logic. As one of the options, we can use a boolean variable to define the button
's on/off state. Also, we need to change a visual state according to the variable. We'll use two color variables – one for the ON state and the other for the OFF state. When does the state change? It changes when we click on the button. To do this, we need to add action to the EventMouseClick
event inside the overridden method InitElements()
. This event will switch the button colors or just mix them (do you still remember about service static
classes?). That's it, the toggle button is ready.
We already have two items. What else can we do? Using these items, we can make a CheckBox
.
Also easy to create. We inherit the Prototype
and will use the CheckBox
as a container for our toggle button and text. In this case, we need to receive the EventMouseClick
event in CheckBox
and redirect it to the same event in toggle button. Also, in the overridden method InitElements()
, we need to add two items – toggle button and text item and set their location inside the CheckBox
.
We already have three complex items. Use them, and especially the last one, we can make a RadioButton
item. Essentially, RadioButton
is similar to CheckBox
, but only one RadioButton
can be turned ON in the container. It is easily achieved by creating the UncheckOthers()
method, which will turn all other RadioButton
s off if one of them has been turned on. Here is an example algorithm: use the GetParent()
method to get the RudioButton
's container, get the list of container items – GetItems()
method – and then just turn off all RadioButton
s.
It's quite easy.
Using this approach, you can create your own library of items, because the main feature of the framework is not the elements, but the kitchen with the rules and recipes. Using the rules and recipes, you can create items of any complexity and for any purpose.
Now let's talk about the rules and recipes that give us so much flexibility and variability.
The main rule is the rule for adding items to containers. Almost any framework item can be added to another item (if it is Prototype
subclass), but it is necessary to follow a certain order.
Every item can be in one of two states: created or created and initialized by the system. This means that each item goes through two states: creation and initialization. It is important to note that the basic functionality of an item becomes available only after the initialization state.
Creation: The item constructor is called with the initial visualization parameters. In the constructor, constructors of internal items can be called. After creation, the item is not yet completely built (not initialized), so methods such as AddItem()
/RemoveItem()
are not available. Items cannot be added to an uninitialized container.
Initialization: This is a process when the item is initialized by the framework and then it is added to the items global storage. An item is initialized when it is added into another initialized item. The first initialized item is the window itself of the program. Let's see the code to understand this better.
Valid example (the code below is part of the window class, the InitComponents()
method):
ButtonCore btn = new ButtonCore();
ImageItem img = new ImageItem(<any image>);
AddItem(btn);
btn.AddItem(img);
Invalid example (the code below is part of the window class, the InitComponents()
method):
ButtonCore btn = new ButtonCore();
ImageItem img = new ImageItem(<any image>);
btn.AddItem(img);
AddItem(btn);
This rule is very strict, and sometimes it can be difficult or inconvenient to follow. There are two ways to “trick” the rule (in fact, the system always follows the rule).
First way: to wrap items in a higher level item. Using the previous example, we can use ButtonCore
and ImageItem
to create a higher level item – ImagedButton
.
Let's look at the implementation:
public class ImagedButton : ButtonCore
{
private ImageItem _img = null;
public ImagedButton(String text, Bitmap picture)
{
SetText(text);
_img = new ImageItem(picture);
}
public override void InitElements()
{
base.InitElements();
AddItem(_img);
}
}
The main code will change as follows:
ImagedButton btn = new ImagedButton("", <some image>);
AddItem(btn);
We followed the rule, but now we have an easier way to add the image to the button.
Second way: to override the AddItem()
method to delay internal initialization:
public class MyButton : ButtonCore
{
private List<IBaseItem> _list = new List<IBaseItem>();
public MyButton(String text) : base(text) { }
public override void AddItem(IBaseItem item)
{
if(item == null) return;
if(_init)
base.AddItem(item);
else
_list.Add(item);
}
private bool _init = false;
public override void InitElements()
{
base.InitElements();
foreach(var item in _list)
base.AddItem(item);
_list = null;
_init = true;
}
}
Now the following code (earlier it was invalid and threw an exception) will work:
ButtonCore btn = new MyButton("My Button");
ImageItem img = new ImageItem(<any image>);
btn.AddItem(img);
AddItem(btn);
Thus, we “trick” the main rule, in fact, we just followed it differently. SpaceVIL has such elements. For example, a ComboBox
item – its constructor can get any number of MenuItem
items, and all of them will be initialized after the ComboBox
is initialized.
Now let's move on to the rules for special items.
To create an item with special behavior, you must choose and follow the next rules.
Interfaces
IHorizontalLayout
- for realization of our own horizontal layout with basic vertical layout
Examples:
HorizontalStack
HorizontalScrollBar
CheckBox
RadioButton
IVerticalLayout
- for realization of our own vertical layout with basic horizontal layout
Examples:
VerticalStack
ListBox
TreeView
VerticalScrollBar
IFreeLayout
- for realization of our own vertical and horizontal layout. User must set all layout rules
Examples:
Grid
WrapGrid
FreeArea
RadialMenu
Usage Rules
- Implement one of the interfaces. The
UpdateLayout()
method declares item layout rules (algorithm). - According to purpose, override some of the following methods:
SetX
/SetY
, SetWidth
/SetHeight
, AddItem
/RemoveItem
(obviously, these methods should update items layout). Override this method as follows:
public override void SetWidth(int value)
{
base.SetWidth(value);
UpdateLayout();
}
The rules are simple, but the results can be impressive. For example, the Grid
layout is not like WrapGrid
, and FreeArea
and RadialMenu
are completely different. Items in FreeArea
are independent, they can overlap or be hidden outside the container. RadialMenu
arranges items in a circle with the ability to scroll.
Interfaces
Usage Rules
- Implement the interface.
- Inside the class constructor or inside
InitElements()
method, add the floating item to the global floating item storage (items are independent and don't have container items to which they can be added):
ItemsLayoutBox.AddItem(handler, this, LayoutType.Floating);
There are even fewer rules, but with their help, you can do interesting things. For example, ComboBox
, ContextMenu
, SideArea
and all types of dialog windows.
Generally, any new item does not belong to only one type. Types are mixed. For example, ContextMenu
and RadialMenu
are a mixture of a container and a floating item. Thus, one can create elements of any complexity and for any purpose.
Interfaces
Usage Rules
- Implement the interface.
- The interface is a marker. The system will send the
EventMouseDrag
event to classes marked with this interface.
Like the two previous ones, this type is very useful and helps to create many items, such as Slider
, ScrollBar
and any other item that needs to be held and dragged. Here are some good use cases: SideArea
(you can expand the visible area), RadialMenu
(items scroll when the mouse button is held down and the mouse moves), FreeArea
(you can shift the visible area).
Interfaces
Usage Rules
- Implement the interface.
- Like the previous one, this interface is a marker. The system processes classes marked with this interface in a special way. If you hold down the mouse button on such an item, the position of the window will correspond to the movements of the mouse.
Core implementations: TitleBar
and WindowAnchor
Interfaces
Usage Rules
- Just implement interface.
The main advantage of using this interface (over the standard implementation presented in the framework - ImageItem
) is improving processing algorithms, parallelization, support for rare formats, etc.
Interfaces
ITextContainer
ITextShortcuts
Usage Rules
- Implement one or all the interfaces
ITextContainer
is used to render text to texture. ITextShortcuts
is an additional marker for special processing by the system. This interface includes methods for implementing standard text shortcuts: copy
, paste
, cut
, select all
, undo
, redo
.
Interfaces
Usage Rules
- Just implement interface.
There are three useful methods: Initialize()
, Draw()
and Free()
. Initialize()
is to prepare OpenGL resources (if necessary) such as FBO, VBO, shaders, etc. Draw()
is used to render the scene and Free()
is used to free resources when the item is deleted.
These rules are designed to create unique items of any complexity. By combining the rules, you can create items that realize any of your ideas, starting with a text editor and ending with a graphic editor.
There are other rules designed not to create, but to configure or manage items or system. For example, window management, styling items, creation/edition/addition style themes for an application, creating and managing special effects, managing visual state of the item, rules for creating vector shapes, many service classes for implementing developer ideas, rules for event processing system, rules for caching and rendering optimization, items focus control rules, rules for two-layer rendering, etc. You don't need a deep knowledge of the system to use most of these features, because they are intuitive and work as you expect.
What has been described looks too much, but keep in mind that you will not need to learn most of the features of the system. I tried to make a “quick start” framework. You don't need deep knowledge to start working with the system. Just look at the contents of the framework, its methods and items, and you'll understand how it works. To make the development of new items interesting, the framework contains more than 54 different items that can be improved, inherited, edited and just know that such items can be created using SpaceVIL. The main thing you have to remember: the system requires a choice of recipes and following the rules, and the graphics engine will draw all your items in accordance with the general rules without any pitfalls.
Now let's see how the styling of items works. The styling module consists of three parts – theme, style, state. Using all of them together, you can effectively control the visual interactivity of items.
The style theme is a set of styles for each item used in an application. The system will “automatically” use the style to newly created item if it is present in the current theme.
But to make it clearer, we first consider the main stages of preparing the framework for work. Here are four common steps to do this:
- Initializing framework components via
Common.CommonService.InitSpaceVILComponents()
at the program entry point (Main
method). At this stage, the OS is checked, the availability of libraries and OS dependencies are also checked. The basic state of the system is initialized, including the base theme for all items in the framework. - The window class is created and initialized (
InitWindow()
method) with ActiveWindow
as the basis. This is usually a step to customize the window and place items. - A window instance is created at the program entry point (
Main
method) - Call the
Show()
method of the window, either directly, or using a window manager (WindowManager
), or using the global window storage (WindowsBox
).
All developer actions to configure SpaceVIL must be performed between the first and the second stages. For example, replacing main SpaceVIL style theme of styles with developer style theme, changing or replacing the basic styles in the current theme, changing the default SpaceVIL global settings, etc.
Now consider the case when a developer uses only elements that are built into the framework or their combinations (usually, if a wrapper-element is created, a separate style is not created for it). Let’s say that developer is not satisfied with the basic stylization of the button element (in the basic style, it has blue color, sharp corners and no border) and the developer would like to change the style a little, for example, the color of the button, sizes and add rounded edges. Since the changes are minor, the developer can use the style change method in the current theme to achieve this.
DefaultsService.GetDefaultStyle(typeof(SpaceVIL.ButtonCore)).Background = Color.Gray;
DefaultsService.GetDefaultStyle(typeof(SpaceVIL.ButtonCore)).SetSize(100, 35);
DefaultsService.GetDefaultStyle
(typeof(SpaceVIL.ButtonCore)).BorderRadius = new CornerRadius(8);
But what if there are too many changes? Or do you even have to change the internal styles? Then it’s better to create your own style and replace in the base theme as follows:
- Create a method that will return your new style:
public static Style GetButtonStyle()
{
Style style = Style.GetButtonCoreStyle();
style.Background = Color.FromArgb(255, 13, 176, 255);
style.Foreground = Color.Black;
style.BorderRadius = new CornerRadius(6);
style.Font = DefaultsService.GetDefaultFont(FontStyle.Regular, 18);
style.SetSizePolicy(SizePolicy.Expand, SizePolicy.Expand);
style.SetAlignment(ItemAlignment.HCenter,ItemAlignment.VCenter);
style.SetTextAlignment(ItemAlignment.HCenter,ItemAlignment.VCenter);
style.ItemStates.Add
(ItemStateType.Hovered, new ItemState(Color.FromArgb(60, 255, 255, 255)));
return style;
}
- Replace the button style in the default style theme with our own style:
DefaultsService.GetDefaultTheme().ReplaceDefaultItemStyle(
typeof(SpaceVIL.ButtonCore), GetButtonStyle());
Done, style replaced. All newly created buttons will get a new look (if, for some reason, the buttons were created before replacing the style, they will remain with the old style).
Let's take a close look at the line "Style style = Style.GetButtonCoreStyle();
". Why haven't I used "new Style();
"? In fact, everything is simple, the Style
class is very voluminous and you need to remember it well, since some properties of the class are strictly required and if I created style from scratch, then only for a completely new element and since I only need to change the appearance of the button (in fact, modify the existing basic style), then this is the cheapest option. It is simpler, less time is spent and the possibility of making mistakes in the style is excluded (basic styles are always correctly filled).
Of course, you can create, modify, replace and apply your own style themes in your application. For example, if you want different themes for different OSes or at the request of the user.
Registering a style for your own element in the current theme is also simple. All you need to do is create a style, add this style to the base theme, apply style (I recommend apply the style at the end of the constructor) and (if you create a complex element) override SetStyle()
method.
The user usually does not have many ways to interact with interactive elements, usually there are only six:
ItemStateType.Base
(Basic static idle state) ItemStateType.Hovered
(Hover state) ItemStateType.Pressed
(Pressed state) ItemStateType.Toggled
(Toggled state (on/off)) ItemStateType.Focused
(Focused state when an element receives events from the keyboard) ItemStateType.Disabled
(Disabled state, when an element ignores all events)
The states system applies only to interactive elements and can be completely ignored by the developer. The developer has the right to implement his or her own state system.
Using such a system is very simple, for each interactive element, the following basic methods are available:
- adding a new
state
via AddItemState(ItemStateType.Hovered, state)
where state
is an instance of the ItemState
class (greatly truncated compared to the Style
class) state
removal via RemoveItemState(ItemStateType.Hovered
)
As mentioned earlier, the framework has fairly simple rules and that is why the adding of a markup system through files such as XML and JSON (planned in the future) will be a simple and tedious task.
SpaceVIL is a powerful, flexible and easy to use UI framework that can cover up to 80-90% of all types of desktop programs. Developed by just two programmers.
We will be glad if you try our framework in action and tell us your opinion about it.
CLICK here to see other screenshots.
- 30th January, 2020: Initial version