在 Linux 上使用 C# 运行 NoesisGUI(一个 WPF 竞争框架)的入门指南





5.00/5 (6投票s)
本文将介绍如何在 Linux 上使用 MonoDevelop 和 C# 运行功能强大、特性丰富的 NoesisGUI(可能是 WPF 的最佳竞争者)的 -IntegrationSample-。
下载 MonoDevelop on Linux x86_64 的示例项目(源代码和预编译二进制文件)
引言
我对 C# 的富 GUI 框架非常感兴趣(请参阅我关于 Xlib、Athena、Motif、FWF 或 Roma Widget Set 的文章)。然而,自从 WPF 问世以来,很明显传统的控件集已经过时了。
要实现与 WPF 相当的功能,最强大且跨平台的 are Pango+Cairo,甚至更强大的 OpenGL。但是,基于 OpenGL 创建一个功能丰富的 GUI 框架是一项非常艰巨的任务(请参阅我关于 入门 和 OpenGL/OpenTK 的文章)。
最近,我偶然发现了 NoesisGUI,它也提供免费的 INDIE 版本(您只需保证年收入低于 100,000 欧元)。我立刻被它迷住了。
背景
NoesisGUI 被宣传为“基于 XAML 的跨平台游戏 UI 中间件”,但它也可以拥有严肃桌面应用程序的外观和感觉。不幸的是,下载的 NoesisGUI-ManagedSDK-2.0.2f1.zip 中的任何示例,或者 NoesisGUI 主页 上的示例,都未针对 Linux 进行准备。
本文提供了一个可在 Linux 上直接运行的 C# 项目,该项目使用 MonoDevelop 5.10 在 openSuse Leap 42.1 64位上创建。但我相信,让这个项目在稍旧或稍新的 Linux 版本以及不同发行版上运行也会很容易。
该项目提供了针对 Linux 及其依赖项的 NoesisGUI IntegrationSample 重构版本:
- 当前版本的 FreeGLUT 和 GLU
- 当前版本的 OpenGL (Mesa)
- MonoDevelop
- NoesisGUI 的托管包装器(C#)和 Linux x86_64 原生库
当使用 NoesisStyle.xaml 时,NoesisGUI IntegrationSample 的外观如下:
或者,安装包中包含 SimpleStyle.xaml 样式,外观如下:
以及 WindowsStyle.xaml 样式,外观如下:
提示:不要忘记将样式文件包含在编译目标中。
实现基于 GLUT。虽然 GLUT 库已被废弃,但 FreeGLUT 实现仍在维护中。
本入门教程将不讨论 GLUT 的缺点(例如,无法控制主循环,无法访问显示指针等)或“最新”OpenGL 包装器(如 GLFW)的优点。它将仅展示如何避免陷阱并使 NoesisGUI IntegrationSample 在 Linux 上运行。
Using the Code
下载的 NoesisGUI-ManagedSDK-2.0.2f1.zip 包含,除其他外:
- 各种平台的库二进制文件(见图)
- 托管包装器 DLL,以及
- NoesisGUI IntegrationSample 的 Windows C# 版本,位于 Src/Samples/GL
Bin/Linux_x86_64/libNoesis.so、Bin/NoesisManaged.dll 和 Src/Samples/GL/* 文件 是本文项目的基本组成部分。
注意:几乎每天都会提供新/更新的 NoesisGUI-ManagedSDK
可供下载,其中包含重大更改。
本文基于 NoesisGUI-ManagedSDK-2.0.2f1.zip。
C# 项目
本文提供了两个项目:NoesisGUI-GLUTWrapperLib
和 NoesisGUI-IntegrationSample
。
NoesisGUI-GLUTWrapperLib
项目将 C++ 粘合代码编译为动态库 libGLUTErapper.so,用于 GLUT 调用。
NoesisGUI-IntegrationSample
项目将 NoesisGUI IntegrationSample 编译到 MONO/.NET 4.5 框架。这些文件包括:
- libNoesis.so,包含 NoesisGUI 框架
- libGLUTWrapper.so,包含来自
NoesisGUI-GLUTWrapperLib
项目的 GLUT 调用的粘合代码 - libGLUTWrapper.cs,包含 libGLUTWrapper.so 调用的托管包装器,或者基于 OpenGL.cs 的完全托管的 GLUT 调用粘合代码
- GLUTWrapper.cs,包含方便的应用程序创建的协议和方法,基于 libGLUTWrapper.cs
- OpenGL.cs,包含 GLUT、GLU、GL、libX11 和 llibc API 协议,用于从托管代码调用这些库,以及
- Program.cs,包含应用程序代码,基于 GLUTWrapper.cs 和 OpenGL.cs
可以轻松地在 libGLUTWrapper.cs 的行为之间切换,在 libGLUTWrapper.so 调用和基于 OpenGL.cs 的完全托管的 GLUT 调用粘合代码之间切换。在文件顶部,您会找到这三行:
// 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
消息处理问题
Noesis 提供的 NoesisGUI IntegrationSample 的 Windows C# 版本在 Src/Samples/GL/GLUTWrapper/GLUTWrapper.cpp 中包含 C++ 粘合代码用于 GLUT 调用。在我的环境中,这段 C++ 粘合代码负责:
combobox
的下拉列表仅通过强制重绘(例如,窗口大小更改)来显示,以及- 应用程序在两到三次此类
combobox
操作后挂起。
这就是为什么我将 C++ 代码转移到类 libGLUTWrapper
中的完全托管的替代 C# 代码中,并且 NoesisGUI IntegrationSample 可以正常运行。
唯一无法转移的是:调用 C 运行时函数 atexit()
。
但是,通过使用 FreeGLUT 而不是 GLUT,调用以下代码会更好:
GLUT.SetOption(FreeGLUT.GLUT_ACTION_ON_WINDOW_CLOSE, FreeGLUT.GLUT_ACTION_CONTINUE_EXECUTION);
并在退出后进行清理
GLUT.MainLoop();
而不是调用 atexit()
。
IntegrationSample
在退出 GLUT.MainLoop()
后不需要清理,但如果您的应用程序需要,可以使用 FreeGLUT 相对于 GLUT 提供的额外可能性。
以下是 libGLUTWrapper
类中受影响的方法(请参阅注释块):
/// <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();
}
输入法问题
我是德国人。德语有 umlaut 字符。
- 我无法在 NoesisGUI IntegrationSample 原始示例代码的文本框中输入任何 umlaut 字符。
- 注册到 FreeGLUT 的按键按下/弹起回调没有接收到 umlaut 字符。
这是因为 NoesisGUI 和 FreeGLUT 目前不能按预期/要求处理 Unicode 字符(有关详细信息,请参阅 使用 X11 Display 获取 UTF-8 输入)。
对于 FreeGLUT,代码是开源的——我们可以看一看:
...
/* 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
...
由于没有 XOpenIM()
、XGetIMValues()
、XCreateIC()
、XSetICFocus()
和 Xutf8LookupString()
调用,而我的环境的区域设置为 de_DE.UTF-8
,因此使用 XLookupString()
调用时,UTF-8 键码被**错误处理**了。
为了避免此陷阱,我在应用程序的 MainClass
的 Run()
方法中检查并调整了区域设置,以适应 NoesisGUI 和 FreeGLUT 的按键处理,因为 NoesisGUI 和 FreeGLUT 的按键处理无法根据区域设置进行调整。
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);
...
本质上,我将区域设置从 de_DE.UTF-8
改为 de_DE
,这将支持的按键码范围从 UTF-8 切换到 ISO-8859-1。我相信,这种方法也可以应用于其他语言。一个示例:
- 使用
de_DE.UTF-8
时,umlaut 'ö' 被处理成**错误的键码** 195 - 结果是字母 'Ã'。 - 使用
de_DE
时,umlaut 'ö' 被处理成**正确的键码** 246 - 结果是字母 'ö'。
通过这些更改,NoesisGUI IntegrationSample 使用 NoesisStyle.xaml 样式可以正常工作。
SimpleStyle.xaml 和 WindowsStyle.xaml 样式使用备用字体(目前我还没有弄清楚它是在哪里配置的),该字体不提供 umlaut 的字形(而 NoesisStyle.xaml 样式使用 Roboto
字体)。
我解决这个问题所做的就是,在 *.xml 样式文件的最后一个 ColorBrush
定义之后,添加一个非常小的 ContentControl
样式,例如:
<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>
因此,调整后的 WindowsStyle.xaml 样式结果如下:
提示:不要忘记将字体文件包含在编译目标中。
关注点
看来我终于找到了一个现代的(基于 XAML 且支持 3D/加速图形)跨平台 GUI 框架。我会关注它,很可能会深入研究 NoesisGUI 的可能性。
历史
- 2018 年 3 月 15 日:初始版本
- 2018 年 3 月 18 日:为替代样式添加了截图,并添加了关于样式使用的字体的信息。