Download OIpenGL-Test1.zip Oct. 2015 (MonoDevelop C# solution, including source and debug binary)
Download OIpenGL-Test2.zip Nov. 2018 (MonoDevelop C# solution, including source and debug binary)
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.
Subsequently i installed MonoDevelop. This package includs the dependecies to the Mono runtime as well.
There seem to be two package dependency errors and MonoDevelop didn't start until MonoDoc-Core and mono-locale-extras are installed.
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.
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
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:
| 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:
| 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.
| 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.
| 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.
The Button
s are children of a Canvas
. The API of all controls is oriented to the WPF controls.
The control creation looks like this:
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 TextBox
es 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.
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.
public bool ProcessMessage()
{
if (!_glWindow.Exists)
return false;
if (!_context.IsCurrent)
{
SimpleLog.LogLine (TraceEventType.Information, CLASS_NAME +
"::ProcessMessage() Reactivating GL context={0} ...", _context.GetHashCode());
_context.MakeCurrent(_glWindow.WindowInfo);
}
_glWindow.ProcessEvents ();
if (_invalidated)
{
if (!_context.IsCurrent)
{
SimpleLog.LogLine (TraceEventType.Information, CLASS_NAME +
"::ProcessMessage() Reactivating GL context={0} ...",
_context.GetHashCode();
_context.MakeCurrent(_glWindow.WindowInfo);
}
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);
UpdateRender();
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:
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.
DockPanel.SetDock(topAreaPanel, DockStyle.Top);
A Grid
- the centerAreaGrid
- is used to layout the buttons and demonstrates the DockPanel
's DockStyle.Fill
alignment.
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.
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
.
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:
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;
}
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:
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.
.
Project updates
The project has grown slightly compared to version 1.
| 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 MessageBoxImage s 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
.
This is the code (old one within if (legacyTextRenering)
; and new one within else
)...
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;
}
}
}
}
if (glTxColor == Color4.Transparent)
glTxColor = Color4.Gray;
System.Drawing.Color msTxColor = System.Drawing.Color.FromArgb (glTxColor.ToArgb());
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);
}
TextRenderer textRenderer = new TextRenderer(
(int)(Width - margin.Left - margin.Right + 0.49),
(int)(Height - margin.Top - margin.Bottom + 0.49));
textRenderer.Clear(msTxColor);
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);
GL.TexCoord2(0.0f, 0.0f); GL.Vertex2((float)(X + margin.Left), (float)(Y + margin.Top));
GL.TexCoord2(1.0f, 0.0f); GL.Vertex2((float)(X + margin.Left) + textRenderer.Width,
(float)(Y + margin.Top));
GL.TexCoord2(1.0f, 1.0f); GL.Vertex2((float)(X + margin.Left) + textRenderer.Width,
(float)(Y + margin.Top) + textRenderer.Height);
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.
The measure, arrange, render, shade roundtrip
To present an application's UI, four consecutive stages of UI calculation are implemented.
- 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. - 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. - 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. - 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).
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 UIElement
s.
[C] UpdateRender()
calculates the sequence of drawing commands based on the RenderSize
and VisualOffset
from all of it's content/child UIElement
s. 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:
The sample application introduces an InputBox
sample, that can be opened via Button 2.
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.