在 MONO/.NET 中使用 OpenGL/OpenTK 开发严肃应用程序的入门指南





5.00/5 (7投票s)
考察 OpenGL 作为吸引人的应用程序的基础,这些应用程序不一定是游戏。
下载 OIpenGL-Test1.zip 2015年10月 (MonoDevelop C# 解决方案,包括源代码和调试二进制文件)
下载 OIpenGL-Test2.zip 2018年11月 (MonoDevelop C# 解决方案,包括源代码和调试二进制文件)
下载 OpenGL-Test3.zip 2019年3月 (MonoDevelop C# 解决方案,包括源代码和调试二进制文件)
引言
我的基本兴趣是评估使用 Mono 在 Unix/Linux 上开发吸引人的严肃/商业/GUI 中心应用程序的可能性,该应用程序基于最新的 2D/3D 渲染引擎 (OpenGL)——与基于 DirectX 的 Windows WPF 应用程序有一定可比性。
背景
有很多关于如何在各种操作系统和编程语言中开始 OpenGL 编程的优秀网络文章。但在阅读了数小时的介绍、教程和 API 文档后,我仍然不确定
- C# OpenGL 编程的最佳工具包(最新、积极维护、功能丰富、文档完善)
- 实现“非游戏”应用程序的最佳编程方法(可能取决于工具包)。
注解: 请随意评论我的工具包列表——我可能忽略了一个很酷的。
尽管如此,我还是想分享我目前所获得的知识。我想我将来会更新这篇文章以分享新发现。根据上面列出的工具包,我决定使用 OpenTK,因为
- 我必须先学习 OpenGL 的基础知识,然后才能判断封装/抽象 OpenGL 的工具包的质量,而 OpenTK 是 OpenGL 上最薄的层,提供了原生的 OpenGL 感觉
- OpenTK 主页是最详细的,似乎也是最活跃的
即使我早在开始之前就知道
- OpenTK 在窗口化方面存在问题(OpenGL 渲染引擎必须始终位于应用程序窗口或控件内部/附加到应用程序窗口或控件),尤其是在 OS X 上,并且
- 编程工作量可能是最高的。
我建议阅读 Bartlomiej Filipek 在 CodeProject 上的文章 学习现代 OpenGL,以澄清最基本的问题。
使用代码
先决条件
为了(从头开始并)确保满足先决条件
- 我安装了一个全新的 openSuse Linux 13.2 Tumbleweed x86_64 DE
- 在运行 Windows 8.1 的 vmWare Player 7.1.2 build-2780323 中
- 只选择了带有 XFCE 桌面的基本 X11 安装(没有 GNOME 和 KDE 以防止副作用)。
由于 XFCE 桌面基于 GTK,因此也安装了 GTK2 库。
在没有任何明确选择的情况下,初始设置已经执行了基本的 Mesa 安装。
随后我安装了 MonoDevelop。此软件包还包括对 Mono 运行时的依赖项。
似乎有两个包依赖错误,MonoDevelop 直到安装了 MonoDoc-Core 和 mono-locale-extras 才启动。
最后一个先决条件是 下载 OpenTK 并为项目提供。我的选择是使用 /opentk-1.1/stable-5/opentk-2014-07-23.zip,它在 /Binaries/OpenTK/Release 中包含所需的程序集。*.dll.config 文件已经包含 Linux 和 OS X 的库引用。/README.md 很有帮助。
项目引用
现在可以创建一个新的 .NET/空项目 解决方案,目标是 Any CPU(我将我的项目命名为 OglAppealingApplTest),并使用 OpenTK 程序集准备一个 /References 文件夹。
文章版本 2.0 更新
优化的文本渲染
我已将文本输出从 Mono 的 System.Drawing.Graphics.DrawString()
实现更改为 OpenFW
库,该库是为使用 FreeType 进行 OpenGL 文本渲染而开发的,并已通过本文介绍:MONO/.NET 中 OpenGL/OpenTK 文本渲染摘要
文章版本 3.0 更新
项目引用更新
我已将 OpenTK 引用更新为当前的 NuGet 包 3.0.1,可从“Official NuGet Gallery”获取。
因此,引用数量减少到仅 OpenFW
库。OpenFW
库的所有最新优化都由本文从 2.0 版到 3.0 版的增强功能推动。目前,最复杂的 OpenFW
库随本文的解决方案一起提供,但请保持更新并查阅文章以获取最新信息:MONO/.NET 中 OpenGL/OpenTK 文本渲染摘要
继续阅读原文
项目
我的 OpenGL-Test1 解决方案 / OglAppealingApplTest 项目是使用 MonoDevelop 5.0.1 在 Mono 3.8.0 上创建的,看起来像
![]() | 引用 (Verweise) OpenTK 文件夹 System 文件夹 |
文章版本 2.0 更新
软件更新
我已将 MonoDevelop 5.0.1 更新到 MonoDevelop 5.10 以克服频繁的调试器崩溃。我还将目标平台从 x86 更改为 AnyCPU。
项目更新
本文的 2.0 和 2.1 版本基于我的 OpenGL-Test2 解决方案。此解决方案添加了对 OpenFW
库的引用,以替换使用 Mono 的 System.Drawing.Graphics.DrawString()
实现进行文本输出的原始方法。
文章版本 3.0 更新
项目更新
本文的 3.0 版本基于我的 OpenGL-Test3 解决方案。此外,我已将小部件框架外包给 OpenGL-Test3 解决方案的 XrwOtk 项目,并将 X11 本机调用原型外包给 X11Wrapper 项目。对于此版本,引用和包已更改,现在看起来像
![]() | 引用 (Verweise) 包 (Pakete) |
XrwOtk 中的 Xrw 是 X11 Roma Widget Set 的缩写,选择它是为了纪念我的 Xrw 项目和文章 编程 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 介绍。XrwOtk 受益于许多从那里获得的知识,并使用了大部分代码。
XrwOtk 中的 Otk 指的是使用 OpenTK 实现的项目。
![]() | 文件夹 |
X11Wrapper 库项目是 X11 Roma Widget Set 的摘录,由文章 编程 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 介绍 介绍。它代表了即将推出的 X11 Roma Widget Set 1.1 版本的预览,并包含 X11 本机调用原型。
![]() | 引用 (Verweise) 文件夹 文件 |
继续阅读原文
示例应用程序
编译后的应用程序显示一个背景填充线性渐变的三角形和三个绝对定位的按钮。按钮 3 关闭窗口。
Button
是 Canvas
的子项。所有控件的 API 都面向 WPF 控件。
控件创建如下所示
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;
}
应用程序通过处理事件的 Window.Show()
方法调用运行。
文章版本 2.1 更新
命中测试可见性
鼠标事件处理程序已扩展,以考虑 UIElement
的 IsHitTestVisible
属性是否设置为 true
(可以被鼠标指针点击)或 false
,并且 Focusable
属性是否设置为 true
(可以接收输入焦点)或 false
。
因此,有必要将 Button
控件中所有 TextBox
的 IsHitTestVisible
属性设置为 false
。
... text1.IsHitTestVisible = false; ... ... text2.IsHitTestVisible = false; ... ... text3.IsHitTestVisible = false; ...
继续阅读原文
由于没有 Invalidate()
(Windows.Forms)、ExposeEvent
(X11) 或 WM_PAINT
消息 (Win32),因此只需一个 _invalidated
标志即可完成此工作。
文章版本 2.0 更新
多窗口的先决条件
首先,我将原始 Window.Show()
方法的部分代码外包到 base.ProcessMessage()
。这使得(无限)循环 while (_glWindow.Exists)
与实际的消息处理分离,并使我能够从多个来源调用 base.ProcessMessage()
方法。
/// <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;
}
其次,我添加了两次 _context.MakeCurrent(_glWindow.WindowInfo)
——一次在 _glWindow.ProcessEvents()
调用之前,一次在其之后,以确保事件处理针对正确的窗口,并确保随后的渲染也针对正确的窗口。这使我能够从不同的 GL 窗口调用 base.ProcessMessage()
方法,并利用不同的 GL 上下文。
/// <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;
}
这两项更改都是打开子窗口(例如对话框)的先决条件。
新功能
我添加了一个使用 MessageBox
的对话框示例。以下代码是控件创建的摘录
... 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(); }; ...
结果看起来像
文章版本 3.0 更新
我引入了 DockPanel
和 Grid
类,它们可用于创建根据窗口大小进行缩放的动态布局。因此,我将旧的基于 Canvas
的布局替换为基于 DockPanel
和 Grid
的布局。
在窗口顶部添加了一个非常简单和早期的菜单预览 - topAreaPanel
- 以演示 DockPanel
的 DockStyle.Top
对齐方式。
DockPanel.SetDock(topAreaPanel, DockStyle.Top);
一个 Grid
- centerAreaGrid
- 用于布局按钮并演示 DockPanel
的 DockStyle.Fill
对齐方式。
DockPanel.SetDock(centerAreaGrid, DockStyle.Fill);
新功能
示例应用程序的行为发生了一些变化
- 窗口顶部的关闭(Beenden)按钮关闭应用程序而不是按钮 3。
- 按钮 3 在“可以切换的按钮”和“已切换的按钮”之间切换其文本。
- 按钮 1 打开一个
MessageBox
示例。 - 按钮 2 打开一个
InputBox
示例 - 由于
OpenFW
库的改进,现在所有文本输出都居中并且更真实。
由于将基于 Canvas
的绝对定位布局替换为基于 DockPanel
和 Grid
的相对定位布局,初始化代码发生了巨大变化。首先,我引入 DockPanel
作为根布局管理器,准备非常简单和早期的菜单预览 - topAreaPanel
- 并创建 Grid
来布局按钮 - centerAreaGrid
。
/// <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);
}
为了保持代码可读性,我将 DockPanel
和 Grid
的设置重新定位到单独的方法中
/// <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;
}
/// <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;
}
继续阅读原文
奇怪的行为 I - 绘画
显示更新
我选择 WPF 作为原型(因为它基于图形硬件加速的 DirectX,就像我的项目基于 OpenGL 一样),它也没有 Invalidate()
(Windows.Forms)、ExposeEvent
(X11) 或 WM_PAINT
消息 (Win32)。相反,调用绘制的唯一方法是调用 InvalidateVisual()
(强制进行完整的重新布局,该布局会调用 UpdateLayout()
,随后调用 UpdateRender()
进行重绘)。有关缺点的讨论,请参阅 Invalidate own WPF control 和 Data binding performance issues。
在找到一个好的解决方案之前,我的实现中的 Window
类提供了 Invalidate()
方法来设置私有 _invalidated
标志。
目前,所有 OpenGL 渲染指令必须一次性绘制并以 SwapBuffers()
结束。
文章版本 2.0 更新
多窗口
我实现了一种非常轻量级的方法来支持多个窗口:由于子窗口的创建会中断主窗口消息循环,因此子窗口消息循环也会处理主窗口消息。在将(无限)循环 while (_glWindow.Exists)
与实际的消息处理分离后,这种方法很容易实现。子窗口消息循环看起来像
/// <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;
}
目前,此解决方案在显示模态对话框时实现了以下功能
window | 功能 | 预期 | 结果 |
主 | 失效后重绘 | 重绘,例如调整大小后 | 好的 |
主 | 交互后重绘 | 重绘,例如悬停效果 | 好的 |
主 | 忽略用户交互 | 忽略点击事件 | 好的 |
主 | 忽略窗口管理器命令 | 忽略窗口框架的关闭 | 缺失 |
子 | 处理用户交互 | 处理点击事件 | 好的 |
CPU 使用率
典型的 X11 消息循环(使用 XNextEvent()
)会阻塞直到下一个事件到来(并节省 CPU 功率),而典型的 GL 消息循环(使用 ProcessEvents()
)不会阻塞(并浪费 CPU 功率)。为了最大程度地减少这种负面影响,我在每个不验证显示的事件循环之后调用 Thread.Sleep(SLEEP_ON_NO_DISPLAY_VALIDATION)
。
继续阅读原文
奇怪的行为 II - 文本渲染
渲染文本最简单的方法似乎是应用 System.Drawing.Graphics.DrawString()
。Mono 在 X11 上将其实现为 System.Windows.Forms
实现的基础。这说明了它的主要优点——这种文本渲染与 Windows/X11 平台无关。
技术实现是将文本绘制到位图上,将位图背景颜色标记为位图的透明颜色,然后将位图作为纹理渲染到场景中并进行混合。为了防止偏色,场景背景颜色定义了位图背景颜色。这种方法根据字体大小、字体面和背景/前景对比度产生可接受或良好的结果。
不幸的是,Mono 的 System.Drawing.Graphics.DrawString()
实现不关心 System.Drawing.Graphics.TextRenderingHint
,它的文本渲染总是看起来像 System.Drawing.Text.TextRenderingHint.AntiAlias
。这导致了两个问题
- 抗锯齿字体并非总是最佳解决方案。平滑外观的代价:抗锯齿字体伤害眼睛并损害视力 是一个有趣的来源。
- 抗锯齿字体渲染基于混合颜色(背景和字体)像素。假设背景是渐变,会出现不需要的偏色。
更好的方法应该是应用 FreeType,但这有待证明。
文章版本 2.0 更新
优化的文本渲染
为了克服文本渲染的缺点,添加了对 OpenFW
库的引用。OpenFW
是为使用 FreeType 进行 OpenGL 文本渲染而开发的,并已通过本文介绍:MONO/.NET 中 OpenGL/OpenTK 文本渲染摘要。请查看文章以获取源代码和更新。
先决条件的更新
下一张图片显示了新引用的程序集文件。
.
项目更新
与版本 1 相比,项目略有增长。
![]() | 引用 (Verweise) (已更新) Images 文件夹 (新增) OpenTK 文件夹 (未更改) Properties 文件夹 (新增) System 文件夹 (已更新) |
质量改进示例
接下来的两张图片比较了最初使用 System.Drawing.Graphics.DrawString()
的文本渲染和现在使用 OpenFW
的新渲染。
这是代码(旧代码在 if (legacyTextRenering)
中;新代码在 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;
}
}
}
}
// 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 ();
}
文章版本 2.1 更新
修复
有一些修复和增强功能
- 现在
FtText
类支持Measure()
方法,该方法支持拉伸、居中和右对齐等文本对齐方式。Button
文本现在可以居中(参见下图)。 - 此外,
FtText
类现在支持Positions
属性,该属性在调用Render()
后提供对字形位置的访问。返回的System.Drawing.Point
数组比FtText
包含的字形多一个点。最后一个/附加位置是虚拟下一个字形的位置。 - 布局现在考虑了
Margin
、HorizontalAlignment
和VerticalAlignment
属性。 - 命中测试已从所需坐标转移到渲染坐标。
MessageBox
类的布局管理器已从Canvas
(固定布局)升级到DockPanel
类(浮动布局)。按钮聚集在右对齐的StackPanel
类中。MessageBox
构造函数参数MessageBoxButton
和MessageBoxImage
现在已评估,并且MessageBoxResult
设置正确。- 测量、排列、渲染和着色步骤现在明确分离。
测量、排列、渲染、着色往返
为了呈现应用程序的用户界面,实现了四个连续的用户界面计算阶段。
- 测量:计算
UIElement
的请求大小。请求大小可以被父UIElement
覆盖/限制为availableSize
,例如在空间不足以满足请求的情况下。仅当从未进行过测量或测量已被设置为脏(例如,通过调整大小事件)时才进行测量。测量结果缓冲到字段_desiredSize
,可通过DesiredSize
属性访问。如果测量计算出的结果与缓冲结果不同,则后续阶段排列、渲染和着色会自动失效。 - 排列:计算每个内容/子
UIElement
的理想大小和位置(从当前UIElement
的角度来看)。大小和位置受父UIElement
提供的finalRect
限制。仅当从未进行过排列或排列已被设置为脏(例如,通过调整大小事件)时才进行排列。排列结果缓冲到字段_size
和_visualOffset
(相对于父级的偏移量),可通过RenderSize
和VisualOffset
属性访问。如果排列计算出的结果与缓冲结果不同,则后续阶段渲染和着色会自动失效。 - 渲染:计算显示当前
UIElement
所需的绘图命令序列。仅当从未进行过渲染或渲染已被设置为脏(例如,通过鼠标进入事件)时才进行渲染。渲染结果缓冲到字段_drawingContent
,可通过CommandSequence
属性访问。GetContentBounds()
方法获取绘图命令的边界框。测量、排列和渲染阶段独立于屏幕刷新调用。 - 着色:播放
UIElement
的CommandSequence
,以便在屏幕刷新期间显示它。屏幕刷新可以独立于任何用户交互(鼠标、键盘、笔等)或数据更改(数据接收)发生,例如,通过窗口重叠或窗口移动事件。
涉及测量、排列、渲染和着色的状态变化示例
下图显示了 UI 计算的四个阶段之间的关系(点击放大)。
程序启动后,UI 计算的所有四个阶段都无效。应用程序窗口的 _invalidated
字段被初始化为 true
。
[A]所有 UIElements
都创建时测量和排列无效,因为 NeverMeasured
和 NeverArranged
初始化为 true
。
[B]应用程序窗口的消息循环检测到窗口字段 _invalidated
设置为 true
,并调用 UpdateShade()
来显示 UI。UpdateShade()
检测到渲染无效并调用 UpdateRender()
来计算显示 UI 所需的绘图命令序列。UpdateRender()
检测到排列无效并使用 finalRect
调用 Arrange()
来计算渲染几何体。Arrange()
检测到测量无效并使用 availableSize
调用 Measure()
来计算所需大小。Measure()
根据 availableSize
计算 _desiredSize
,并且 Arrange()
根据 finalRect
以及所有内容/子 UIElement
的 DesiredSize
计算 _size
和 _visualOffset
。
[C] UpdateRender()
根据所有内容/子 UIElement
的 RenderSize
和 VisualOffset
计算绘图命令序列。UpdateShade()
现在可以通过播放绘图命令序列并交换缓冲区来显示 UI。现在 UI 计算的所有四个阶段都有效。
[D]用户与应用程序交互。例如,他移动鼠标指针 - 这会调用 HandleGlMouseMove()
。HandleGlMouseMove()
持续计算鼠标指针所在的 UIElement
。
[E]如果 HandleGlMouseMove()
检测到鼠标移动到敏感的 UIElement
中,它会调用 OnMouseEnter()
。假设进入的 UIElement
应该高亮显示——鼠标进入会导致 UIElement
改变其样式。这将导致 DependencyProperty
值更改。
[F]DependencyProperty
setter 调用 TestSetValueAffects()
来确定 DependencyProperty
值更改是否影响测量、排列或渲染。
[G]如果 DependencyProperty
元数据被标记为 FrameworkPropertyMetadataOptions.AffectsMeasure
或 FrameworkPropertyMetadataOptions.AffectsArrange
或 FrameworkPropertyMetadataOptions.AffectsRender
,则为 UIElement
调用 InvalidateMeasure()
或 InvalidateArrange()
或 InvalidateRender()
,并为根窗口调用 Invalidatate()
以将字段 _invalidated
设置为 true
。有关下一步,请参阅 [B]。
[H]用户与窗口管理器交互。例如,他调整应用程序窗口的大小 - 这会调用 HandleGlWindowResize()
。
[I] HandleGlWindowResize()
为 UIElement
调用 InvalidateVisual()
,这会为 UIElement
调用 InvalidateArrange()
和 InvalidateRender()
,并为根窗口调用 Invalidatate()
以将字段 _invalidated
设置为 true
。有关下一步,请参阅 [B]。
文章版本 3.0 更新
新功能
通过 System.Windows.SystemFonts.MessageFontFamily
访问的标准字体已从 DejaVu Sans-Book
优化为 Open Sans-Light
,按钮布局看起来更加严肃
示例应用程序引入了一个 InputBox
示例,可以通过按钮 2 打开。
InputBox
被设计为应用程序模态对话框(就像 MessageBox
一样)。输入控件是 TextBox
。目前我的 TextBox
实现支持
- [左]/[右] 键将插入符号向左/向右移动一个字符
- [Home]/[End] 键将插入符号移动到第一个/最后一个字符之前/之后
- [Shift]+[左]/[右]/[Home]/[End] 键创建/更新选择
- [Ctrl]+[C] 将选择内容复制到剪贴板
- [Ctrl]+[X] 将选择内容剪切到剪贴板
- [Ctrl]+[V] 粘贴剪贴板内容
- 点击指针定位插入符号
- 按下按钮移动指针以创建/更新选择
InputBox
的键盘焦点最初设置为 TextBox
。
继续阅读原文
关注点
基于 OpenGL 的 Unix/Linux 吸引人的严肃/商业/GUI 中心应用程序是否现实? - 是的。
是否可以提供类似 WPF 的 API? - 是的。
是否存在陷阱? - 是的,渲染指令必须一次性绘制,CPU 使用率必须降低,文本渲染值得改进。
历史
初始版本:2015年10月21日。
第二版:2018年11月20日。
第二版更正:2018年11月27日。
第三版:2019年3月26日。