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

First Steps to Make NoesisGUI, a WPF Competable Framework, Running on Linux with C#

5.00/5 (6 votes)
18 Mar 2018CPOL6 min read 24.7K   252  
How to run the -IntegrationSample- of the incredible feature-rich NoesisGUI, that might be the best WPF competitor, on Linux using MonoDevelop and C#.

Image 1 Download sample project for MonoDevelop on Linux x86_64 (source code and precompiled binaries)

Introduction

I am very interested in rich GUI frameworks for C# (see my articles regarding Xlib, Athena, Motif, FWF or the Roma Widget Set). However, since WPF has entered the stage, it is clear that classic widget sets are outdated.

The most powerful and cross-platform technologies, to achieve something comparable to WPF, are Pango+Cairo and even more OpenGL. But it is a very hard job to create a feature-rich GUI framework based on OpenGL (see my articles regarding getting started and OpenGL/OpenTK).

Recently, I accidentally stumbled upon NoesisGUI, which is also available in a free-of-charge INDIE edition (all you must guarantee is a gross annual revenue < 100k€). And I was immediately fascinated.

Background

The Noeses GIU is advertised as "Multiplatform Game UI Middleware Based On XAML" - but it can also have the look and feel of a serious desktop application. Unfortunately, none of the samples included in the download NoesisGUI-ManagedSDK-2.0.2f1.zip or the Noeses GIU home page is prepared to run on Linux.

This article provides a ready-to-run C# project, created with MonoDevelop 5.10 on openSuse Leap 42.1 64bit. But I'm sure - it will be easy to make this project run on a little older or newer Linux version as well as on different distributions.

The project provides the NoesisGUI IntegrationSample reworked for Linux and its dependencies are:

  • current version of FreeGLUT and GLU
  • current version of OpenGL (Mesa)
  • MonoDevelop
  • the managed wrapper (C#) and the Linux x86_64 native library for NoesisGUI

This is what the NoesisGUI IntegrationSample looks like, if NoesisStyle.xaml is used:

Image 2

Alternatively the installation packages includes the SimpleStyle.xaml stule, that looks like:

Image 3

And the WindowsStyle.xaml stule, that looks like:

Image 4

Hint: Do not forget to include the style file to the compilation target.

The implementation is based on GLUT. While the GLUT library is abandoned, the FreeGLUT implementation is still maintained.

This first step tutorial will not discuss the drawbacks of GLUT (no main loop control, no access to the display pointer, ...) or the advantages of a 'state of the art' OpenGL wrapper like GLFW. It will just show how to avoid the pitfalls and make the NoesisGUI IntegrationSample run on Linux.

Using the Code

The downloaded NoesisGUI-ManagedSDK-2.0.2f1.zip includes, among other things:

  • the library binaries for various platforms (see image)
  • the managed wrapper DLL and
  • the Windows C# version of the NoesisGUI IntegrationSample at Src/Samples/GL

Image 5

The Bin/Linux_x86_64/libNoesis.so, Bin/NoesisManaged.dll and the Src/Samples/GL/* files are the base of this article's project.

Note: Almost daily, a new/updated NoesisGUI-ManagedSDK is available for download with significant changes.

This article is based on the NoesisGUI-ManagedSDK-2.0.2f1.zip.

The C# Projects

This article provides two projects, NoesisGUI-GLUTWrapperLib and NoesisGUI-IntegrationSample.

The NoesisGUI-GLUTWrapperLib project compiles the C++ glue code for GLUT calls to a dynamic library libGLUTErapper.so.

Image 6

The NoesisGUI-IntegrationSample project compiles the NoesisGUI IntegrationSample against the framework MONO/.NET 4.5. The files are:

  • libNoesis.so, contains the NoesisGUI framework
  • libGLUTWrapper.so, contains the glue code for GLUT calls from the NoesisGUI-GLUTWrapperLib project
  • libGLUTWrapper.cs, contains the managed wrapper for libGLUTWrapper.so calls and alternatively completely managed glue code for GLUT calls, based on OpenGL.cs
  • GLUTWrapper.cs, contains prototypes and methods for convenient application creation based on libGLUTWrapper.cs
  • OpenGL.cs, contains the GLUT, GLU, GL, libX11 and llibc API prototypes, used by calls into these libraries from managed code, and
  • Program.cs, contains the application code, based on GLUTWrapper.cs and OpenGL.cs

It is easy to switch the libGLUTWrapper.cs behaviour between managed wrapper for libGLUTWrapper.so calls and completely managed glue code for GLUT calls, based on OpenGL.cs. At the top of the file, you'll find these three lines:

C#
// If USE_FULLY_MANAGED is unset, libGLUTWrapper.cs works as an managed wrapper for libGLUTWrapper.so.
// If USE_FULLY_MANAGED is set, libGLUTWrapper.cs works completely managed based on OpenGL.cs.
#define USE_FULLY_MANAGED

Message Processing Problems

The Windows C# version of the NoesisGUI IntegrationSample, provided by Noesis, contains C++ glue code for GLUT calls at Src/Samples/GL/GLUTWrapper/GLUTWrapper.cpp. On my environment, this C++ glue code is responsible for:

  • the dropdown of a combobox is displayed only by a forced redraw (e.g., a change of the window size) and
  • the application hangs after two or three such combobox actions

That's why I transferred the C++ code into the completely managed alternative C# code within the class libGLUTWrapper, and the NoesisGUI IntegrationSample works fine with that.

There is only one thing that could not be transferred: The call of the C runtime function atexit().

But by using FreeGLUT instead of GLUT, it's much better to call:

C#
GLUT.SetOption(FreeGLUT.GLUT_ACTION_ON_WINDOW_CLOSE, FreeGLUT.GLUT_ACTION_CONTINUE_EXECUTION);

and clean up after exiting:

C#
GLUT.MainLoop();

instead of calling atexit().

The IntegrationSample doesn't need a clean up after exiting the GLUT.MainLoop(), but if your application will need it, use the additional possibilities, FreeGLUT has compared to GLUT.

Here is the affected method from libGLUTWrapper class (see comment block):

C#
/// <summary>
/// Register all callbacks and run the GL message loop.
/// </summary>
internal static void GLUT_Run()
{
    GLUT.DisplayFunc(GLUT_Display);
    GLUT.ReshapeFunc(GLUT_Resize);
    GLUT.MouseFunc(GLUT_MouseButton);
    GLUT.MotionFunc(GLUT_MouseMove);
    GLUT.PassiveMotionFunc(GLUT_MouseMove);
    GLUT.KeyboardFunc(GLUT_KeyboardDown);
    GLUT.KeyboardUpFunc(GLUT_KeyboardUp);
    GLUT.SpecialFunc(GLUT_KeyboardSpecialDown);
    GLUT.SpecialUpFunc(GLUT_KeyboardSpecialUp);

    // ==== Register an exit proc only from native code (never from managed code)! ====
    // The purpose of an exit proc is to clean up/free resources directly before the
    // library is unloaded. To avoid concurrency issues or unexpected behavior, it must
    // be guaranteed that NO other threads may still access the resources to free by
    // now. And this can not be guaranteed for managed code.
    // atexit(new ExitProcDelegate(GLUT_Close));

    GLUT.MainLoop();
}

Input Method Problems

I'm German. The German language has umlauts.

  • I cannot enter any umlauts into the text box with the NoesisGUI IntegrationSample original example code.
  • The key down/up callbacks, registered to FreeGLUT, receive no umlauts.

This is because the NoesisGUI and FreeGLUT currently don't process unicode characters as expected/required (see Get UTF-8 input with X11 Display for details).

For FreeGLUT, the code is open source - and we can take a look at it:

C++
...

/* Check for the ASCII/KeySym codes associated with the event: */
len = XLookupString(&event.xkey, asciiCode, sizeof(asciiCode),
                    &keySym, &composeStatus);

/* GLUT API tells us to have two separate callbacks... */
if (len > 0)
{
    /* ...one for the ASCII translatable keypresses... */
    if (keyboard_cb)
    {
        fgSetWindow( window );
        fgState.Modifiers = fgPlatformGetModifiers( event.xkey.state );
        keyboard_cb(asciiCode[ 0 ],
                    event.xkey.x, event.xkey.y);
        fgState.Modifiers = INVALID_MODIFIERS;
    }
}
else

...

Since there are no XOpenIM(), XGetIMValues(), XCreateIC(), XSetICFocus() and Xutf8LookupString() calls, while the locale setting on my environment is de_DE.UTF-8, the UTF-8 key code is processed wrong with the XLookupString() call.

To avoid this pitfall, I check and adjust the locale within the Run() method of the application's MainClass according to the NoesisGUI and FreeGLUT key processing, because the NoesisGUI and FreeGLUT key processing can't be adjusted to the locale.

C#
private void Run(string[] args)
{
    // Check the locale of the runtime library and - if necessary - adjust it.
    sbyte[] requestedLocale  = C.StringToSByteArray("");
    IntPtr  currentLocale    = C.setlocale((int)C.LocaleCategory.LC_ALL, requestedLocale);
    // Report unset locale.
    if (currentLocale == IntPtr.Zero)
    {
        // Report unsupported C-language translation.
        Console.WriteLine("The c runtime library localization is currently not supported. " +
            "This might result in unexpected key code to character translation.");
    }
    else
    {
        String localeString = currentLocale != IntPtr.Zero ?
           System.Runtime.InteropServices.Marshal.PtrToStringAuto (currentLocale) : "";
        // Report not configured C-language translation.
        if (string.IsNullOrEmpty(localeString))
            Console.WriteLine("The c runtime library locale is currently not configured. This " +
                "might result in unexpected key code to character translation.");
        // Report minimal environment for C-language translation.
        else if (localeString == "C" || localeString == "POSIX")
            Console.WriteLine("The c runtime library locale is currently configured as '" +
                currentLocale +
                "'. This might result in unexpected key code to character translation.");

        if (localeString == "de_DE.UTF-8")
        {
            requestedLocale = C.StringToSByteArray("de_DE");
            currentLocale   = C.setlocale((int)C.LocaleCategory.LC_ALL, requestedLocale);
            localeString    = currentLocale != IntPtr.Zero ?
                System.Runtime.InteropServices.Marshal.PtrToStringAuto (currentLocale) : "";
        }
    }

    if (!X11.XSupportsLocale())
    {
        Console.WriteLine("The X11 runtime library localization is currently not supported. " +
            "This might result in unexpected key code to character translation.");
    }
    else
    {
        sbyte[] requestedModifier  = new sbyte[] {(sbyte)'\0'};
        IntPtr currentModifier = X11.XSetLocaleModifiers(requestedModifier);

        if (currentModifier == IntPtr.Zero)
        {
            // Report unset X11-language translation.
            Console.WriteLine("The X11 runtime library locale modifier for the XIM is currently" +
                " not set. This might result in unexpected key code to character translation.");
        }
        else
        {
            String modifierString = (currentModifier != IntPtr.Zero ?
                System.Runtime.InteropServices.Marshal.PtrToStringAuto(currentModifier) : "");
            // Report unset X11-language translation.
            if (string.IsNullOrEmpty(modifierString))
                Console.WriteLine("The X11 runtime library locale modifier for the XIM is currently" +
                    " not set. This might result in unexpected key code to character translation.");
            // Report non-standard environment for X11-language translation.
            else if (modifierString != "@im=local")
                Console.WriteLine("The X11 runtime library locale modifier for the XIM is currently" +
                    " not set. This might result in unexpected key code to character translation.");
        }
    }

    // ==========================================================================
    // Internationalization issues must be completed before any GLUT, GLU or GL
    // Initialization starts.
    // ==========================================================================

    GLUT.Init(args.Length, args);

    ...

Essentially, I set the locale from de_DE.UTF-8 to de_DE, which switches the supported key code range from UTF-8 to ISO-8859-1. I'm sure, this approach can be adopted for other languages as well. A sample:

  • With de_DE.UTF-8 the umlaut 'ö' is processed with the wrong key code 195 - this results in the letter 'Ã'.
  • With de_DE the umlaut 'ö' is processed with the correct key code 246 - this results in the letter 'ö'.

With these changes, the NoesisGUI IntegrationSample works fine using the NoesisStyle.xaml style.

The SimpleStyle.xaml and WindowsStyle.xaml styles use the fallback font (currently I din't figure out where it is configured), that doesn't provide glyphs for umlauts (while the NoesisStyle.xaml style uses the Roboto font).

Image 7

All I've done to solve this, is adding a very small ContentControl style to the *.xml style file after the last ColorBrush definition, e. g.:

XML
<SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF"/>

<Style x:Key="RootContainerStyle" TargetType="{x:Type ContentControl}">
    <Setter Property="FontFamily" Value="Fonts/#Roboto"/>
    <Setter Property="FontSize" Value="12"/>
    <Setter Property="FontStyle" Value="Normal"/>
    <Setter Property="FontWeight" Value="Normal"/>
</Style>

Consequently the adjusted WindowsStyle.xaml style results in:

Image 8

Hint: Do not forget to include the font file to the compilation target.

Points of Interest

It looks like I finally found a modern (XAML based and 3D/accelerated graphics supporting) cross-platform GUI framework. I'll keep an eye on this and most likely, I'll dive deeper into the possibilities of NoesisGUI.

History

  • 15.03.2018: Initial version
  • 18.03.2018: Added screenshots for alternatively styles and added information about fonts used by styles.

License

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