Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / OpenGL

Getting startet with OpenGL/OpenTK in MONO/.NET for serious applications

5.00/5 (7 votes)
26 Mar 2019CPOL19 min read 45.2K   1.6K  
Check OpenGL as a basis for appealing applications, that are not necessarily games.

Image 1 Download OIpenGL-Test1.zip Oct. 2015 (MonoDevelop C# solution, including source and debug binary)

Image 2 Download OIpenGL-Test2.zip Nov. 2018 (MonoDevelop C# solution, including source and debug binary)

Image 3 Download OpenGL-Test3.zip Mar. 2019 (MonoDevelop C# solution, including source and debug binary)

Introduction

My basic interest is to evaluate the opportunities to program an appealing serious/business/GUI-centered application for Unix/Linux using Mono, that is based on an up-to-date 2D/3D rendering engine (OpenGL) - remotely comparable to a WPF application for Windows using .NET, that is based on DirectX.

Background

There are a lot of good internet articles how to start programming with OpenGL for any possible OS and programming language. But after many hours of reading indroductions, tutorials and API documentation, i still do not feel sure about

  • the best toolkit (up-to-date, actively maintained, feature-rich, well documented) for C# OpenGL programming
    • OpenTK  - the most thin layer around OpenGL and most detailed documentation,
    • Pencil.Gaming (read this as well) - thin layer around OpenGL, GLEW and other stuff,
    • SFML.Net - a C# wrapper for SFML feature-rich platform abstraction for OpenGL and other stuff,
    • SDL2# - a C# wrapper for SDL very feature-rich platform abstraction for OpenGL and other stuff,
    • exotic/underground alternatives and
  • the best programming aproach (might depend on the toolkit) to implement a "no-game" application.

Annotation: Don't hesitate to comment my list of toolkits - i might have overlooked a cool one.

Nevertheless i want to share the knowledge i gained so far. I think i'll update this article in the future to share new findings. According to the list of toolkits above, i decided to use OpenTK, because

  • i have to learn the basics of OpenGL before i can judge the quality of toolkits that wrap/abstract OpenGL and OpenTK is the the most thin layer around OpenGL and gives native OpenGL feeling
  • the OpenTK home page is the most detailed one and seems to be the most active one

even if i already know before i start that

  • OpenTK is known to have trouble with windowing (the OpenGL rendering engine must always reside inside/be attached to an application window or control), especially on OS X, and
  • the programming effort might be the highest.

I recommend to read Bartlomiej Filipek's article Learning Modern OpenGL at code project to clear the very basic questions.

Using the code

Prerequisits

To (start at the very beginning and to) be sure about the prerequisits

  • i installed a fresh openSuse Linux 13.2 Tumbleweed x86_64 DE
  • within a vmWare Player 7.1.2 build-2780323 running on Windows 8.1
  • selecting only the basic X11 installation with XFCE desktop (no GNOME and no KDE to prevent side effects).

Since the XFCE desktop is GTK based, the GTK2 libraries are installed as well.

Without any explicit selection, a basic Mesa installation has already been performed by the initial setup.

Image 4

Subsequently i installed MonoDevelop. This package includs the dependecies to the Mono runtime as well.

Image 5

There seem to be two package dependency errors and MonoDevelop didn't start until MonoDoc-Core and mono-locale-extras are installed.

Image 6

The last prerequisit is to download the OpenTK and provide it for the project. My choice was to use the /opentk-1.1/stable-5/opentk-2014-07-23.zip, that contains the required assemblies within /Binaries/OpenTK/Release. The *.dll.config files already contain the library references for Linux and OS X. The /README.md is helpful.

Project references

Now a new .NET/Empty project solution can be created targeting Any CPU (i named my project OglAppealingApplTest) and a /References folder can be prepared with the OpenTK assemblies.

Image 7


Update with article version 2.0

Optimized text rendering

I have changed the text output from Mono's System.Drawing.Graphics.DrawString() implementation to the OpenFW library, that has been developed for OpenGL text rendering with FreeType and has been introduced by this article: Abstract of the text rendering with OpenGL/OpenTK in MONO/.NET

Update with article version 3.0

Project references updates

I have updated the OpenTK reference to a current NuGet package 3.0.1 , that can be optained from the "Official NuGet Gallery".

Thus the number of references is reduced to the OpenFW library only. All recent optimizations of the OpenFW library are driven by this article's enhancements from version 2.0 to 3.0. Currently the most sophisticated OpenFW library is delivered with this article's solution, but please stay up to date and take a look at the article to be informed about updates: Abstract of the text rendering with OpenGL/OpenTK in MONO/.NET

Image 8

Go on with the original article

Project

My OpenGL-Test1 solution / OglAppealingApplTest project has been created with MonoDevelop 5.0.1 on Mono 3.8.0 and looks like:

Image 9

References (Verweise):
- OpenTK.dll contains all the needed stuff,
   OpenTK.GLControl.dll shall be avoided for my objectives
   OpenTK.Compatibility.dll is not required (no Tao)
- Since OpenTK utilizes Mono's System.Forms implementation,
  the System.Drawing reference is required.

OpenTK folder
- contains extensions directly related to OpenGL
   (colors, brushes, converters, ...)

System folder
- contains the GUI controls, used for the test application
   The GUI controls are oriented to the WPF controls, that belong
   completely to the System.Windows namespace.


Update with article version 2.0

Software updates

I've updated MonoDevelop 5.0.1 to MonoDevelop 5.10 to overcome the frequent debugger crashes. And I changed the target platform from x86 to AnyCPU.

Project updates

This article's versions 2.0 and 2.1 are based on my OpenGL-Test2 solution. This solution added a reference to the OpenFW library to replace the original approach for text output with Mono's System.Drawing.Graphics.DrawString() implementation.

Update with article version 3.0

Project updates

This article's version 3.0 is based on my OpenGL-Test3 solution. Furthermore I've outsourced the widget framework to the OpenGL-Test3 solution's XrwOtk project and the X11 native call prototypes to the X11Wrapper project. For this version the references and packages have changed and look now like:

Image 10

References (Verweise):
- OpenFW.dll contains the text rendering stuff,
- System.dll contains .NET core classes
- System.Drawing.dll contains additional .NET classes (e.g. Color)
- X11Wrapper_V1.1_Preview is a library project reference to a
   library containing X11 native call prototypes - this project's source
   code is completely included in the download
- XrwOtk is a library project reference to a library containing the
   outsourced widget framework (that was previously located in the
   System folder) - this project's source code is completely included
   in the download.

Packages (Pakete)
- OpenTK NuGet package replaces the previously used OpenTK
   libraries.

Xrw in XrwOtk is a the abbreviation for X11 Roma Widget Set and chosen in reminiscence of my Xrw project and article Programming the Roma Widget Set (C# X11) - a zero dependency GUI application framework - Introduction. XrwOtk benefits from a lot of the knowledge gained there and uses large parts of the code.
Otk in XrwOtk points to the implementation of the project with OpenTK.

Image 11

Folders:
- Images contains generic images, that are available via resources,
- Microsoft.Replica contains .NET classes from namespaces MS
   and Microsoft, that are not part of the .NET standard
- OpenTK.Extensions contains .NET classes to extend OpenTK
- Properties contains the generic resources (image, text)
- System.Extensions contains .NET classes to extend the namespace
   System
- System.Replica contains .NET classes from namespace System,
   that are not part of the .NET standard
- XrwOtk contains .NET classes, that provide a simple application
   framework

The X11Wrapper library project is an excerpt of the X11 Roma Widget Set, introduced by the article Programming the Roma Widget Set (C# X11) - a zero dependency GUI application framework - Introduction. It represents a preview of the upcoming version 1.1 of the X11 Roma Widget Set and contains X11 native call prototypes.

Image 12

References (Verweise):
- System.dll contains .NET core classes,
- System.Core.dll contains .NET more core classes,
- System.Drawing.dll contains additional .NET classes (e.g. Color),
- System.Xml contains .NET classes for XML

Folders:
- XColor.Contracts contains prototypes and classes, that are
   useful when working with X11 colors,
- XGC.Contracts contains prototypes and classes for working with
   the X11 graphics context,
- XMisc.Contracts contains prototypes and classes, that do not
   fit one of the other folders,
- XWindow.Contracts contains prototypes and classes, that are
   useful when working with X11 native windows,
- XWM.Contracts contains prototypes and classes, that are
   useful when working with X11 window manager

Files:
. . .
- SimpleLogs contains .NET classes for logging - very similar to
   Loyc's message sinks written by Qwertie,
- X11Clipboard contains .NET classes regarding some aspects of
   the X11 ICCM for drag-and-drop described in article
   Add Clipboard Text Consumer Capability to Your C# OpenGL
   App on X11
and article Add Clipboard Text Provider Capability
   to Your C# OpenGL App on X11

. . .

Go on with the original article

The sample application

The compiled application shows a triangle filled with a linear gradient in the background and three absolute positioned buttons. Button 3 closes the window.

Image 13

The Buttons are children of a Canvas. The API  of all controls is oriented to the WPF controls.

The control creation looks like this:

C#
public MyWindow ()
    : base ()
{
    this.Resize += HandleResize;

    Canvas layoutManager = new Canvas();
    this.Content = layoutManager;

    Button button1 = new Button();
    button1.X = 50;
    button1.Y = 50;
    button1.Width = 100;
    button1.Height = 50;
    layoutManager.AddChild(button1);
    button1.Click += delegate(object sender, EventArgs e)
    {
        SimpleLog.LogLine (TraceEventType.Information, "Button 1 clicked.");
    };
    TextBlock text1 = new TextBlock ();
    text1.Text = "Button 1";
    text1.Foreground = new OpenTK.Media.SolidColorBrush (Color4.Red);
    button1.Content = text1;
    
    Button button2 = new Button();
    button2.X = 50;
    button2.Y = 150;
    button2.Width = 100;
    button2.Height = 50;
    layoutManager.AddChild(button2);
    button2.Click += delegate(object sender, EventArgs e)
    {
        SimpleLog.LogLine (TraceEventType.Information, "Button 2 clicked.");
    };
    TextBlock text2 = new TextBlock ();
    text2.Text = "Button 2";
    text2.Foreground = new OpenTK.Media.SolidColorBrush (Color4.Green);
    button2.Content = text2;

    Button button3 = new Button();
    button3.X = 50;
    button3.Y = 250;
    button3.Width = 100;
    button3.Height = 50;
    layoutManager.AddChild(button3);
    button3.Click += delegate(object sender, EventArgs e)
    {
        SimpleLog.LogLine (TraceEventType.Information, "Button 3 clicked.");
        this.Close ();
    };
    TextBlock text3 = new TextBlock ();
    text3.Text = "Button 3";
    text3.Foreground = new OpenTK.Media.SolidColorBrush (Color4.Blue);
    button3.Content = text3;
}

The application runs with the Window.Show() method call, that handles the events.


Update with article version 2.1

Hit test visibility

The mouse event handler have been extended to take care whether a UIElement has IsHitTestVisible property set to true (can be hit by the mouse pointer) or false and has set Focusable property set to true (can receive input focus) or false.

Therefore, it became necessary to set the IsHitTestVisible property of all TextBoxes within a Button controls to false.

...

text1.IsHitTestVisible = false;

...

...

text2.IsHitTestVisible = false;

...

...

text3.IsHitTestVisible = false;

...
Go on with the original article

Since there is no Invalidate() (Windows.Forms), ExposeEvent (X11) or WM_PAINT message (Win32), just a _invalidated flag does this job.


Update with article version 2.0

Prerequisits for multiple windows

First I've outsourced some of the original Window.Show() method code to base.ProcessMessage(). This separates the (infinite) loop while (_glWindow.Exists) from the real message processing and enables me to call base.ProcessMessage() method fom several origins.

C#
/// <summary>Shows this application window instance.</summary>
public virtual void Show ()
{
    UpdateLayout ();

    _glWindow.Visible = true;

    while (_glWindow.Exists)
    {
        bool hasRedrawn = base.ProcessMessage();
        if(!hasRedrawn)
            Thread.Sleep(SLEEP_ON_NO_DISPLAY_VALIDATION);
    }
    GuiThreadDispatcher currentDispatcher = GuiThreadDispatcher.CurrentDispatcher;
    currentDispatcher.Dispatch ();

    _glWindow.Visible = false;
}

Second I've added _context.MakeCurrent(_glWindow.WindowInfo) twice - once before and once after the _glWindow.ProcessEvents() call to ensure that event processing targest the proper window and to ensure that subsequent rendering targets the proper window. This enables me to call base.ProcessMessage() method fom different GL windows utilizing different GL contexts.

C#
/// <summary>Processes one GL window message.</summary>
/// <returns>Returns <c>true</c>, if display has been validated, or <c>false</c> otherwise.</returns>
/// <remarks>While a typical X11 message loop (utilizing XNextEvent()) blocks until
/// the next event arrives (and saves CPU power), a typical GL message loop
/// (utilizing ProcessEvents()) doesn't block (and wastes CPU power).</remarks>
public bool ProcessMessage()
{
    if (!_glWindow.Exists)
        return false;

    // Can be called from child windows (with own GL contect) as well.
    // Thus we have to ensure the right GL context.
    if (!_context.IsCurrent)
    {
        SimpleLog.LogLine (TraceEventType.Information, CLASS_NAME +
                  "::ProcessMessage() Reactivating GL context={0} ...", _context.GetHashCode());
        _context.MakeCurrent(_glWindow.WindowInfo);
    }

    _glWindow.ProcessEvents ();
    // Calls implementation.ProcessEvents ():
    //       Just delegates complete processing to implementation.
        // Calls LinuxNativeWindow.ProcessEvents ():
        //       Just calls ProcessKeyboard() and ProcessMouse().
            // Calls NativeWindowBase.ProcessEvents ():
            //       Just clears keyboard, to prevent confusion on missing KeyUp.
    if (_invalidated)
    {
        // During event processing, new GL windows (with own GL contect) can be created.
        // Thus we have to ensure the right GL context.
        if (!_context.IsCurrent)
        {
            SimpleLog.LogLine (TraceEventType.Information, CLASS_NAME +
                      "::ProcessMessage() Reactivating GL context={0} ...",
            _context.GetHashCode();
            _context.MakeCurrent(_glWindow.WindowInfo);
        }

        // ===================================================
        // Prepare the OpenGL environment and draw background.
        // ===================================================
        GL.ClearColor(Color.LightSalmon);
        GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

        GL.MatrixMode(MatrixMode.Projection);
        GL.LoadIdentity();
        GL.MatrixMode(MatrixMode.Modelview);
        GL.LoadIdentity();

        GL.Ortho(0, _glWindow.Width, _glWindow.Height, 0, -1, 1);
        GL.Viewport(0, 0, _glWindow.Width, _glWindow.Height);

        // ===================================================
        // OpenGL must render buffers at once.
        // ===================================================
        UpdateRender();

        // ===================================================
        // Finalize drawing.
        // ===================================================
        GL.Flush();
        _context.SwapBuffers();

        _invalidated = false;

        return true;
    }
    else
        return false;
}

Both changes are prerequisites to open sub windows - e.g. dialog boxes.

New functionality

I've added a dialog box sample utilizing a MessageBox. The subsequent code is an excerpt from the control creation:

...
button1.Click += delegate(object sender, EventArgs e)
{
    SimpleLog.LogLine (TraceEventType.Information, "Button 1 clicked.");

    Dialog subWindow = new MessageBox (this, 800, 150, "OpenTK Message Box",
                                       "Hello!\nThis is a dialog box.");
    subWindow.ShowModal();
};
...

The result looks like:

Image 14

Update with article version 3.0

I have introduced the DockPanel and Grid classes, that can be used do create a dynamic layout, that scales according to the window size. Consequently i have replace the old Canvas based layout with a DockPanel and Grid based layout.

A very simple and early preview of a menu - the topAreaPanel - has been added at the top of the window, to demonstrate the DockPanel's DockStyle.Top alignment.

C#
DockPanel.SetDock(topAreaPanel, DockStyle.Top);

A Grid - the centerAreaGrid - is used to layout the buttons and demonstrates the DockPanel's DockStyle.Fill alignment.

C#
DockPanel.SetDock(centerAreaGrid, DockStyle.Fill);

New functionality

Several changes have been made regarding the sample application's behaviour:

  • The Close (Beenden) button at the top of the window closes the application instead of Button 3.
  • The Button 3 toggles it's text between "Button, that\ncan toggle" and "Button, that\nhas toggled".
  • The Button 1 opens a MessageBox sample.
  • The Button 2 opens an InputBox sample
  • All text output is centered and more realistic now, because of the improvements of the OpenFW library.

Image 15

Due to the replacement of the Canvas based absolute positioned layout with a DockPanel and Grid based relative positioned layout, the initialization code has changed dramatically. First I introduce the DockPanel as the root layout manager, prepare the very simple and early preview of a menu - the topAreaPanel - and create the Grid to layout the buttons - the centerAreaGrid.

C#
/// <summary>Initializes a new instance of the <see cref="OglAppealingApp.MyWindow"/>
/// class.</summary>
/// <remarks>The default window size is 600px X 450px.</remarks>
public MyWindow ()
    : base ()
{
    Background = new System.Windows.Media.SolidColorBrush(
                         new System.Windows.Media.Color(Color.LightSalmon));

    DockPanel layoutManager = new DockPanel();
    this.Content = layoutManager;

    StackPanel topAreaPanel = new StackPanel();
    layoutManager.AddChild(topAreaPanel);
    DockPanel.SetDock(topAreaPanel, DockStyle.Top);
    SetupTopAreaPanel(topAreaPanel);

    Grid   centerAreaGrid   = new Grid();
    layoutManager.AddChild(centerAreaGrid);
    DockPanel.SetDock(centerAreaGrid, DockStyle.Fill);
    SetupCenterAreaGrid(centerAreaGrid);
}

To keep the code readable, I've relocated the setup of the DockPanel and the Grid to separate methods:

C#
/// <summary>Setups the top area panel. This area represents a very early stage of a
/// menu.</summary>
/// <param name="topAreaPanel">The pop area panel to set up.</param>
private void SetupTopAreaPanel(StackPanel topAreaPanel)
{
    topAreaPanel.Orientation = Orientation.Horizontal;
    topAreaPanel.Background = System.Windows.SystemColors.MenuBarBrush;

    Button topAreaButton1 = new Button();
    topAreaButton1.Width  = 80;
    topAreaButton1.Height = 24;
    topAreaButton1.Margin = new Thickness(1, 1, 3, 1);
    topAreaButton1.BorderThickness = new Thickness(0);
    topAreaButton1.BorderBrush = null;
    topAreaPanel.AddChild(topAreaButton1);
    topAreaButton1.HorizontalAlignment = HorizontalAlignment.Left;
    topAreaButton1.Click += delegate(object sender, EventArgs e)
    {
        ConsoleMessageSink.WriteInfo (null, "Button 'CLOSE' clicked.");
        this.Close ();
    };
    topAreaButton1.Background  = System.Windows.SystemColors.MenuBarBrush;
    topAreaButton1.BorderBrush = null;
    topAreaButton1.BorderThickness = new Thickness(1);
    topAreaButton1.Style = XrwOtk.Styles.MenuBarButtonMouseOverStyle;

    TextBlock topAreaButton1Text = new TextBlock ();
    topAreaButton1Text.Text = XrwOtk.Properties.Resources.CLOSE();
    topAreaButton1Text.IsHitTestVisible = false;
    topAreaButton1Text.Background = null;
    topAreaButton1.Content = topAreaButton1Text;
}
C#
/// <summary>Setups the center area grid. This area represents contains some tests.</summary>
/// <param name="centerAreaGrid">The  center area grid to set up.</param>
private void SetupCenterAreaGrid(Grid centerAreaGrid)
{

    centerAreaGrid.AddColumn("1*");
    centerAreaGrid.AddColumn("100");
    centerAreaGrid.AddColumn("9*");
    centerAreaGrid.AddRow   ("1*");
    centerAreaGrid.AddRow   ("50");
    centerAreaGrid.AddRow   ("1*");
    centerAreaGrid.AddRow   ("50");
    centerAreaGrid.AddRow   ("1*");
    centerAreaGrid.AddRow   ("50");
    centerAreaGrid.AddRow   ("3*");

    Button button1 = new Button();
    Grid.SetColumn(button1, 1);
    Grid.SetRow(button1, 1);
    centerAreaGrid.AddChild(button1);
    button1.Click += delegate(object sender, EventArgs e)
    {
        ConsoleMessageSink.WriteInfo (null, "Button 1 clicked.");

        System.Windows.MessageBoxResult result =
            MessageBox.Show (this, "Hello!\nThis is a message box sample.",
                             "OpenTK Message Box", MessageBoxButton.YesNoCancel,
                             MessageBoxImage.Error);
        if (result == MessageBoxResult.OK)
        {;}
    };
    TextBlock text1 = new TextBlock ();
    text1.Text = "Button 1";
    text1.Foreground = new System.Windows.Media.SolidColorBrush (
        new System.Windows.Media.Color(Color.Red));
    text1.IsHitTestVisible = false;
    text1.Background = null;
    button1.Content = text1;
    
    Button button2 = new Button();
    Grid.SetColumn(button2, 1);
    Grid.SetRow(button2, 3);
    centerAreaGrid.AddChild(button2);
    button2.Click += delegate(object sender, EventArgs e)
    {
        ConsoleMessageSink.WriteInfo (null, "Button 2 clicked.");

        System.Windows.MessageBoxResult result =
            InputBox.Show (this, "Hello!\nThis is an input box sample.\n\n" +
                           "Do you like to type in some test input?",
                           "OpenTK Input Box", MessageBoxImage.Question);
        if (result == MessageBoxResult.OK)
        {;}
    };
    TextBlock text2 = new TextBlock ();
    text2.Text = "Button 2";
    text2.Foreground = new System.Windows.Media.SolidColorBrush (
        new System.Windows.Media.Color(Color.Green));
    text2.IsHitTestVisible = false;
    text2.Background = null;
    button2.Content = text2;

    Button button3 = new Button();
    Grid.SetColumn(button3, 1);
    Grid.SetRow(button3, 5);
    centerAreaGrid.AddChild(button3);
    button3.Click += delegate(object sender, EventArgs e)
    {
        ConsoleMessageSink.WriteInfo (null, "Button 3 clicked.");

        if ((button3.Content as TextBlock).Text == toggleText[0])
            (button3.Content as TextBlock).Text =toggleText[1];
        else
            (button3.Content as TextBlock).Text =toggleText[0];
    };
    TextBlock text3 = new TextBlock ();
    text3.Text = toggleText[0];
    text3.Foreground = new System.Windows.Media.SolidColorBrush (
        new System.Windows.Media.Color(Color.Blue));
    text3.IsHitTestVisible = false;
    text3.Background = null;
    button3.Content = text3;
}
Go on with the original article

Strange behaviours I - Painting

Display update

WPF, I've chosen as a prototype (becaus it is based on a graphics hardware accelleration DirectX just like my project is based in OpenGL), has no Invalidate() (Windows.Forms), ExposeEvent (X11) or WM_PAINT message (Win32) as well. Instead the only way to invoke a paint is to call InvalidateVisual() (forces a complete new layout pass which calls UpdateLayout() and calls UpdateRender() subsequently to redraw). See Invalidate own WPF control and Data binding performance issues for a discussion about drawbacks.

Up to the finding of a good solution the Window class of my implementation provides the Invalidate() method to set the private _invalidated flag.

Currently all OpenGL render instructions must draw at once and end up with SwapBuffers().


Update with article version 2.0

Multiple windows

I've implemented a very lightweight approach to support multiple windows: Since the creation of a sub window interupts the main window message loop, a sub window message loop cares for the main window messages as well. This approach was easy to realize after the separation of the (infinite) loop while (_glWindow.Exists) from the real message processing. A sub window message loop looks like:

C#
/// <summary>Shows this dialog window instance.</summary>
/// <remarks>This will interrupt the <see cref="ParentWindow"/>'s message loop and
/// handle it's messages here. </remarks>
public virtual void ShowModal ()
{
    _isModal = true;
    UpdateLayout ();

    _glWindow.Visible = true;

    while (_glWindow.Exists)
    {
        bool hasRedrawn = base.ProcessMessage();
        if (_parentWindow != null)
            hasRedrawn |= _parentWindow.ProcessMessage();

        if(!hasRedrawn)
            Thread.Sleep(SLEEP_ON_NO_DISPLAY_VALIDATION);
    }

    GuiThreadDispatcher currentDispatcher = GuiThreadDispatcher.CurrentDispatcher;
    currentDispatcher.Dispatch ();

    _glWindow.Visible = false;
}

Currently this solution realizes these features while a modal dialog window is shown:

window feature expectation result
main redraw after invalidation redrawing, e.g. after resize OK
main redraw after interaction redrawing, e.g. hover effect OK
main ignore user interaction ignore click event OK
main ignode window manager commands ignore close from window frame missing
sub process user interaction process click event OK

CPU usage

While a typical X11 message loop (utilizing XNextEvent()) blocks until the next event arrives (and saves CPU power), a typical GL message loop (utilizing ProcessEvents()) doesn't block (and wastes CPU power). To minimize this negative effect, I call Thread.Sleep(SLEEP_ON_NO_DISPLAY_VALIDATION) after every event loop, that doesn't validate the display.

Go on with the original article

Strange behaviours II - Text rendering

The easiest approach to render text seems to be the application of System.Drawing.Graphics.DrawString(). Mono has implemented this method as a base for it's System.Windows.Forms implementation on X11. This characterizes the major advantage - such a text rendering is Windows/X11 platform independend.

The technical iplementation draws the text on a bitmap, marks the bitmap background color to be the transparent color of the bitmap and renders the bitmap as a texture with blending into the scene. To prevent off-colors, the scene background color defines the bitmap background color. This approach produces aceptable or good results, depending on the font size, font face and background/foreground contrast.

Unfornately Mono's System.Drawing.Graphics.DrawString() implementation don't care for System.Drawing.Graphics.TextRenderingHint, it's text rendering always looks like System.Drawing.Text.TextRenderingHint.AntiAlias. This causes two problems:

A better approach should be the application of FreeType, but this is to be proven.


Update with article version 2.0

Optimized text rendering

To overcome the text rendering drawbacks a reference to the OpenFW library has been added. OpenFW has been developed for OpenGL text rendering with FreeType and has been introduced by this article: Abstract of the text rendering with OpenGL/OpenTK in MONO/.NET. Please check the article for source code and updates.

Update of the prerequisits

The next image shows the newly referenced assembly file.

Image 16.

Project updates

The project has grown slightly compared to version 1.

Image 17

References (Verweise) (updated):
- OpenFW OpenTK.dll contains all the needed stuff,
   OpenTK.GLControl.dll shall be avoided for my objectives
   OpenTK.Compatibility.dll is not required (no Tao)

Images folder (new):
- contains a lot of roalty-free images for various purposes and most of
  them in multiple sizes from, the MessageBoxImages among them.
  The images are taken from the Programming the Roma Widget Set
  (C# X11) - a zero dependency GUI application framework - Introduc-
  tion article.
  Please check this article for source code and updates.

OpenTK folder (unchanged)

Properties folder (new):
- contains generic image and text properties

System folder (updated)

Quality improvement sample

The next two images compare the initial text rendering utilizing System.Drawing.Graphics.DrawString() to the new one utilizing OpenFW.

Image 18

Image 19

This is the code (old one within if (legacyTextRenering); and new one within else)...

C#
if (legacyTextRenering)
{
    if (glTxColor == Color4.Transparent)
    {
        List<Visual> parentHierarchy = VisualTreeHelper.ParentVisualHierarchy (this);
        for (int index = 0; index < parentHierarchy.Count; index++)
        {
            if (parentHierarchy [index] as Control != null)
            {
                bgBrush = (parentHierarchy [index] as Control).Background;
                if (bgBrush as OpenTK.Media.SolidColorBrush != null)
                {
                    Color4 bgColor = (bgBrush as OpenTK.Media.SolidColorBrush).Color;
                    glTxColor = bgColor;
                    if (glTxColor != Color4.Transparent)
                        break;
                }
            }
        }
    }

    // Choose a neutrally texture background color as a falback for transparent color.
    if (glTxColor == Color4.Transparent)
        glTxColor = Color4.Gray;
    // Calculate the texture background.
    System.Drawing.Color msTxColor = System.Drawing.Color.FromArgb (glTxColor.ToArgb());
    
    // Calculate the texture foreground (text) color.
    OpenTK.Media.Brush glFgBrush = Foreground;
    System.Drawing.Brush msFgBrush = new System.Drawing.SolidBrush (System.Drawing.Color.Black);
    if (glFgBrush as OpenTK.Media.SolidColorBrush != null)
    {
        System.Drawing.Color glFgColor = System.Drawing.Color.FromArgb (
            (glFgBrush as OpenTK.Media.SolidColorBrush).Color.ToArgb ());
        msFgBrush = new System.Drawing.SolidBrush (glFgColor);
    }

    // Prepare texture map.
    TextRenderer textRenderer = new TextRenderer(
        (int)(Width - margin.Left - margin.Right + 0.49),
        (int)(Height - margin.Top - margin.Bottom + 0.49));
    textRenderer.Clear(msTxColor);

    // Draw text to the texture map.
    PointF position = PointF.Empty;
    textRenderer.DrawString(text, ThemeManager.CurrentTheme.DefaultFont, msFgBrush, position);

    GL.Enable(EnableCap.Texture2D);
    GL.Enable(EnableCap.Blend);
    GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);

    GL.BindTexture(TextureTarget.Texture2D, textRenderer.Texture(msTxColor));
    GL.Begin(PrimitiveType.Quads);
    GL.Color3 (glTxColor.R, glTxColor.G, glTxColor.B); // System.Drawing.Color.Yellow); // msColor

    //Address: Left-Top       Set coordinates: X, Y
    GL.TexCoord2(0.0f, 0.0f); GL.Vertex2((float)(X + margin.Left), (float)(Y + margin.Top));
    //Address: Right-Top      Set coordinates: X, Y
    GL.TexCoord2(1.0f, 0.0f); GL.Vertex2((float)(X + margin.Left) + textRenderer.Width,
                                         (float)(Y + margin.Top));
    //Address: Right-Bottom   Set coordinates: X, Y
    GL.TexCoord2(1.0f, 1.0f); GL.Vertex2((float)(X + margin.Left) + textRenderer.Width,
                                         (float)(Y + margin.Top) + textRenderer.Height);
    //Address: Left-Bottom    Set coordinates: X, Y
    GL.TexCoord2(0.0f, 1.0f); GL.Vertex2((float)(X + margin.Left),
                                         (float)(Y + margin.Top) + textRenderer.Height);

    GL.End();
    GL.BindTexture(TextureTarget.Texture2D, 0);

    textRenderer.Dispose();
}
else
{
    System.Drawing.Color ftFgColor = System.Drawing.Color.FromArgb (
        (Foreground as OpenTK.Media.SolidColorBrush).Color.ToArgb ());
    FtText ftText = new FtText (Font, text,
        FtFontFace.LineSizeToCharacterSize(ThemeManager.CurrentTheme.DefaultFont.Height), false,
        GlUtil.GlyphVisualEffects.None, (int)(X + margin.Left + 0.49f),
        (int)(Y + margin.Top + 0.49f), ftFgColor, false, true);
    ftText.Draw ();
}
Update with article version 2.1

Fixes

There are some fixes and enhancements:

  • Now the FtText class supports the Measure() method, that enables text alignments like stretch, center and right. Button text can be centered now (see next image).
  • Furthermore the FtText class supports the Positions property now, that provides access to the glyph positions after a call to Render(). The returned System.Drawing.Point array contains one point more than FtText contains glyphs. The last/additional position is the position of the virtual next glyph.  
  • Layout takes care for Margin, HorizontalAlignment and VerticalAlignment properties now.
  • Hit test has been shifted from desired coordinates to render coordinates.
  • The layout manager of the MessageBox class has been upgraded from Canvas (fixed layout) to DockPanel class (floating layout). The buttons are clustered within a StackPanel class, that is aligned right.
  • The MessageBox constructor arguments MessageBoxButton and MessageBoxImage are evaluated now and the MessageBoxResult is set correctly.
  • The measure, arrange, render and shade steps are clearly separated now.

Image 20

The measure, arrange, render, shade roundtrip

To present an application's UI, four consecutive stages of UI calculation are implemented.

  1. Measure: Calculates the requested size of a UIElement. The requested size can be overruled/is limited to the availableSize by the parent UIElement, e.g. in case of insufficient space to meet the request. Measuring is done only if it has never been done before or measuring has been set to dirty (e. g. by a resize event). The measuring result is buffered to the field _desiredSize, accessible through the DesiredSize property. The subsequent stages arrange, render and shade become invalid automatically, if measuring calculates a result different from the buffered one.
  2. Arrange: Calculates the ideal size and position of every content/child UIElement (from the perspective of the current UIElement). The size and position are limited to the finalRect, provided by the parent UIElement. Arrangement is done only if it has never been done before or arrangement has been set to dirty (e. g. by a resize event). The arrangement result is buffered to the fields _size and _visualOffset (position relative to the parent), accessible through the RenderSize and VisualOffset properties. The subsequent stages render and shade become invalid automatically, if arrangement calculates a result different from the buffered one.
  3. Render: Calculates the sequence of drawing commands, required to display the current UIElement. Rendering is done only if it has never been done before or rendering has been set to dirty (e. g. by a mouse enter event). The rendering result is buffered to the field _drawingContent, accessible through the CommandSequence property. The GetContentBounds() method acquires the bounding box of the drawing commands. The stages measure, arrange and render are called independently from screen refresh.
  4. Shade: Plays the CommandSequence of a UIElement to display it during a screen refresh. A screen refresh can occure independently from any user interaction (mouse, keyboard, pen, ...) or data change (data receivement), e. g. by a window overlapping or window movement event.

Status change samples with the involvement of measure, arrange, render and shade

The next image shows the relationship between the four stages of UI calculation (click to enlarge).

Image 21

After program start, all four stages of UI calculation are invalid. The _invalidated field of the application window is initialized with true.

[A] All UIElements are created with measure and arrange invalid, because NeverMeasured and NeverArranged are initialized with true.

[B] The application window's message loop detects the window field _invalidated set to true and calls UpdateShade() to display the UI. UpdateShade() detects the render invalidity and calls UpdateRender() to calculate the sequence of drawing commands, required to display the UI. UpdateRender() detects the arrange invalidity and calls Arrange() with the finalRect to calculate the render geometry. Arrange() detects the measure invalidity and calls Measure() with availableSize to calculate the desired size. Measure() calculates the _desiredSize based on availableSize and Arrange() calculates _size and _visualOffset based on the finalRect as well as on the DesiredSize from all of it's content/child UIElements.

[C] UpdateRender() calculates the sequence of drawing commands based on the RenderSize and VisualOffset from all of it's content/child UIElements. UpdateShade() can display the UI now by playing the sequence of drawing commands and swap the buffers. All four stages of UI calculation are valid now.

[D] The user interacts with the application. For example, he moves the mouse poiner - which calls HandleGlMouseMove(). HandleGlMouseMove() continuously calculates the UIElement, the mouse pointer is over.

[E] If HandleGlMouseMove() detects a mouse movement into a sensitive UIElement, it calls OnMouseEnter(). Assumed the entered UIElement shall highlight - the mouse entering causes the UIElement to change it's style. This will lead to a DependencyProperty value change.

[F] The DependencyProperty setter calls TestSetValueAffects() to determien whether the  DependencyProperty value change affects measure, arrange or render.

[G] In case of the DependencyProperty metadata are flaged FrameworkPropertyMetadataOptions.AffectsMeasure or FrameworkPropertyMetadataOptions.AffectsArrange or FrameworkPropertyMetadataOptions.AffectsRender, InvalidateMeasure() or InvalidateArrange() or InvalidateRender() is called for the UIElement and Invalidatate() is called for the root window to set the field _invalidated to true. See [B] for next steps.

[H] The user interacts with the window manager. For example, he resizes the application window - which calls HandleGlWindowResize().

[I] HandleGlWindowResize() calls InvalidateVisual() for the UIElement, which calls InvalidateArrange() and InvalidateRender() for the UIElement and calls Invalidatate() for the root window to set the field _invalidated to true. See [B] for next steps.

Update with article version 3.0

New functionality

The standard font, accessible via System.Windows.SystemFonts.MessageFontFamily, has been optimized from DejaVu Sans-Book to Open Sans-Light and the button layout looks even more serious:

Image 22

The sample application introduces an InputBox sample, that can be opened via Button 2.

Image 23

The InputBox is designed as an application modal dialog (just like the MessageBox). The input control is a TextBox. Currently my TextBox implementation supports:

  • [left]/[right] key to move the caret one character to the left/right
  • [home]/[end] key to move the caret in front of the first/behind the last character
  • [shift]+[left]/[right]/[home]/[end] key to create/update a selection
  • [ctrl]+[c] to copy the selection to clipboard
  • [ctrl]+[x] to cut the selection to clipboard
  • [ctrl]+[v] to paste the clipboard content
  • pointer click to position the caret
  • pointer move with pressed button to create/update a selection

The keyboard focus of the InputBox is set to the TextBox initially.

Go on with the original article

Points of Interest

Are appealing serious/business/GUI-centered application for Unix/Linux based on OpenGL realistic? - Yes.

Is it possible to provide a WPF-like API? - Yes.

Are there pitfalls? - Yes, render instructions must draw at once, CPU usage must be reduced and text rendering is worthy of improvement.

History

Initial version from 21. October 2015.
Second version from 20. November 2018.
Second version's corrections from 27. November 2018.
Third version from 26. March 2019.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)