65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2015年10月21日

CPOL

19分钟阅读

viewsIcon

46355

downloadIcon

1619

考察 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 上最薄的层和最详细的文档,
    • Pencil.Gaming (也请阅读 这篇) - OpenGL、GLEW 和其他内容的薄层,
    • SFML.Net - SFML 的 C# 包装器,为 OpenGL 和其他内容提供功能丰富的平台抽象,
    • SDL2# - SDL 的 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-Coremono-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.1Mono 3.8.0 上创建的,看起来像

引用 (Verweise)
- OpenTK.dll 包含所有所需内容,
   OpenTK.GLControl.dll 应避免用于我的目标
   OpenTK.Compatibility.dll 不需要(没有 Tao)
- 由于 OpenTK 利用了 Mono 的 System.Forms 实现,
  需要 System.Drawing 引用。

OpenTK 文件夹
- 包含与 OpenGL 直接相关的扩展
   (颜色、画笔、转换器等)

System 文件夹
- 包含用于测试应用程序的 GUI 控件
   GUI 控件面向 WPF 控件,它们属于
   完全属于 System.Windows 命名空间。


文章版本 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)
- OpenFW.dll 包含文本渲染内容,
- System.dll 包含 .NET 核心类
- System.Drawing.dll 包含额外的 .NET 类(例如 Color
- X11Wrapper_V1.1_Preview 是对一个库项目的引用,该库
   包含 X11 本机调用原型 - 此项目的源代码
   完全包含在下载中
- XrwOtk 是对一个库项目的引用,该库包含
   外包的小部件框架(以前位于
   System 文件夹中) - 此项目的源代码完全包含
   在下载中。

包 (Pakete)
- OpenTK NuGet 包取代了以前使用的 OpenTK
   库。

XrwOtk 中的 Xrw 是 X11 Roma Widget Set 的缩写,选择它是为了纪念我的 Xrw 项目和文章 编程 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 介绍。XrwOtk 受益于许多从那里获得的知识,并使用了大部分代码。
XrwOtk 中的 Otk 指的是使用 OpenTK 实现的项目。

文件夹
- Images 包含通过资源可用的通用图像,
- Microsoft.Replica 包含来自命名空间 MS
   和 Microsoft 的 .NET 类,这些类不属于 .NET 标准
- OpenTK.Extensions 包含用于扩展 OpenTK 的 .NET 类
- Properties 包含通用资源(图像、文本)
- System.Extensions 包含用于扩展命名空间的 .NET 类
   系统
- System.Replica 包含来自命名空间 System 的 .NET 类,
   这些类不属于 .NET 标准
- XrwOtk 包含 .NET 类,提供了一个简单的应用程序
   框架

X11Wrapper 库项目是 X11 Roma Widget Set 的摘录,由文章 编程 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 介绍 介绍。它代表了即将推出的 X11 Roma Widget Set 1.1 版本的预览,并包含 X11 本机调用原型。

引用 (Verweise)
- System.dll 包含 .NET 核心类,
- System.Core.dll 包含 .NET 更多核心类,
- System.Drawing.dll 包含额外的 .NET 类(例如 Color),
- System.Xml 包含用于 XML 的 .NET 类

文件夹
- XColor.Contracts 包含在处理 X11 颜色时有用的原型和类,
   在处理 X11 颜色时有用,
- XGC.Contracts 包含用于处理 X11 图形上下文的原型和类,
   X11 图形上下文,
- XMisc.Contracts 包含不适合其他文件夹的原型和类,
   其他文件夹,
- XWindow.Contracts 包含在处理 X11 本机窗口时有用的原型和类,
   在处理 X11 本机窗口时有用,
- XWM.Contracts 包含在处理 X11 窗口管理器时有用的原型和类
   在处理 X11 窗口管理器时有用

文件
. . .
- SimpleLogs 包含用于日志记录的 .NET 类 - 与 Loyc 的消息接收器 非常相似,由 Qwertie 编写,
   Loyc 的消息接收器Qwertie 编写,
- X11Clipboard 包含关于 X11 ICCCM 拖放 的某些方面的 .NET 类,在文章中描述
   X11 ICCCM 拖放,在文章中描述
   在 X11 上为您的 C# OpenGL 应用程序添加剪贴板文本消费者功能 和文章 在 X11 上为您的 C# OpenGL 应用程序添加剪贴板文本提供者功能
. . .

继续阅读原文

示例应用程序

编译后的应用程序显示一个背景填充线性渐变的三角形和三个绝对定位的按钮。按钮 3 关闭窗口。

ButtonCanvas 的子项。所有控件的 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 更新

命中测试可见性

鼠标事件处理程序已扩展,以考虑 UIElementIsHitTestVisible 属性是否设置为 true(可以被鼠标指针点击)或 false,并且 Focusable 属性是否设置为 true(可以接收输入焦点)或 false

因此,有必要将 Button 控件中所有 TextBoxIsHitTestVisible 属性设置为 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 更新

我引入了 DockPanelGrid 类,它们可用于创建根据窗口大小进行缩放的动态布局。因此,我将旧的基于 Canvas 的布局替换为基于 DockPanelGrid 的布局。

在窗口顶部添加了一个非常简单和早期的菜单预览 - topAreaPanel - 以演示 DockPanelDockStyle.Top 对齐方式。

DockPanel.SetDock(topAreaPanel, DockStyle.Top);

一个 Grid - centerAreaGrid - 用于布局按钮并演示 DockPanelDockStyle.Fill 对齐方式。

DockPanel.SetDock(centerAreaGrid, DockStyle.Fill);

新功能

示例应用程序的行为发生了一些变化

  • 窗口顶部的关闭Beenden)按钮关闭应用程序而不是按钮 3
  • 按钮 3 在“可以切换的按钮”和“已切换的按钮”之间切换其文本。
  • 按钮 1 打开一个 MessageBox 示例。
  • 按钮 2 打开一个 InputBox 示例
  • 由于 OpenFW 库的改进,现在所有文本输出都居中并且更真实。

由于将基于 Canvas 的绝对定位布局替换为基于 DockPanelGrid 的相对定位布局,初始化代码发生了巨大变化。首先,我引入 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);
}

为了保持代码可读性,我将 DockPanelGrid 的设置重新定位到单独的方法中

/// <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 controlData 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) (已更新)
- OpenFW OpenTK.dll 包含所有所需内容,
   OpenTK.GLControl.dll 应避免用于我的目标
   OpenTK.Compatibility.dll 不需要(没有 Tao)

Images 文件夹 (新增)
- 包含大量免费图像,用于各种目的,其中大部分具有多种尺寸,包括 MessageBoxImage
  它们中的许多具有多种尺寸,包括 MessageBoxImage
  图像取自 编程 Roma Widget Set
  (C# X11) - 一个零依赖的 GUI 应用程序框架 - 介绍
  介绍 文章。
  请查看此文章以获取源代码和更新。

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 包含的字形多一个点。最后一个/附加位置是虚拟下一个字形的位置。
  • 布局现在考虑了 MarginHorizontalAlignmentVerticalAlignment 属性。
  • 命中测试已从所需坐标转移到渲染坐标。
  • MessageBox 类的布局管理器已从 Canvas(固定布局)升级到 DockPanel 类(浮动布局)。按钮聚集在右对齐的 StackPanel 类中。
  • MessageBox 构造函数参数 MessageBoxButtonMessageBoxImage 现在已评估,并且 MessageBoxResult 设置正确。
  • 测量、排列、渲染和着色步骤现在明确分离。

测量、排列、渲染、着色往返

为了呈现应用程序的用户界面,实现了四个连续的用户界面计算阶段。

  1. 测量:计算 UIElement 的请求大小。请求大小可以被父 UIElement 覆盖/限制为 availableSize,例如在空间不足以满足请求的情况下。仅当从未进行过测量或测量已被设置为脏(例如,通过调整大小事件)时才进行测量。测量结果缓冲到字段 _desiredSize,可通过 DesiredSize 属性访问。如果测量计算出的结果与缓冲结果不同,则后续阶段排列渲染着色会自动失效。
  2. 排列:计算每个内容/子 UIElement 的理想大小和位置(从当前 UIElement 的角度来看)。大小和位置受父 UIElement 提供的 finalRect 限制。仅当从未进行过排列或排列已被设置为脏(例如,通过调整大小事件)时才进行排列。排列结果缓冲到字段 _size_visualOffset(相对于父级的偏移量),可通过 RenderSizeVisualOffset 属性访问。如果排列计算出的结果与缓冲结果不同,则后续阶段渲染着色会自动失效。
  3. 渲染:计算显示当前 UIElement 所需的绘图命令序列。仅当从未进行过渲染或渲染已被设置为脏(例如,通过鼠标进入事件)时才进行渲染。渲染结果缓冲到字段 _drawingContent,可通过 CommandSequence 属性访问。GetContentBounds() 方法获取绘图命令的边界框。测量排列渲染阶段独立于屏幕刷新调用。
  4. 着色:播放 UIElementCommandSequence,以便在屏幕刷新期间显示它。屏幕刷新可以独立于任何用户交互(鼠标、键盘、笔等)或数据更改(数据接收)发生,例如,通过窗口重叠或窗口移动事件。

涉及测量、排列、渲染和着色的状态变化示例

下图显示了 UI 计算的四个阶段之间的关系(点击放大)。

程序启动后,UI 计算的所有四个阶段都无效。应用程序窗口的 _invalidated 字段被初始化为 true

[A]所有 UIElements 都创建时测量和排列无效,因为 NeverMeasuredNeverArranged 初始化为 true

[B]应用程序窗口的消息循环检测到窗口字段 _invalidated 设置为 true,并调用 UpdateShade() 来显示 UI。UpdateShade() 检测到渲染无效并调用 UpdateRender() 来计算显示 UI 所需的绘图命令序列。UpdateRender() 检测到排列无效并使用 finalRect 调用 Arrange() 来计算渲染几何体。Arrange() 检测到测量无效并使用 availableSize 调用 Measure() 来计算所需大小。Measure() 根据 availableSize 计算 _desiredSize,并且 Arrange() 根据 finalRect 以及所有内容/子 UIElementDesiredSize 计算 _size_visualOffset

[C] UpdateRender() 根据所有内容/子 UIElementRenderSizeVisualOffset 计算绘图命令序列。UpdateShade() 现在可以通过播放绘图命令序列并交换缓冲区来显示 UI。现在 UI 计算的所有四个阶段都有效。

[D]用户与应用程序交互。例如,他移动鼠标指针 - 这会调用 HandleGlMouseMove()HandleGlMouseMove() 持续计算鼠标指针所在的 UIElement

[E]如果 HandleGlMouseMove() 检测到鼠标移动到敏感的 UIElement 中,它会调用 OnMouseEnter()。假设进入的 UIElement 应该高亮显示——鼠标进入会导致 UIElement 改变其样式。这将导致 DependencyProperty 值更改。

[F]DependencyProperty setter 调用 TestSetValueAffects() 来确定 DependencyProperty 值更改是否影响测量、排列或渲染。

[G]如果 DependencyProperty 元数据被标记为 FrameworkPropertyMetadataOptions.AffectsMeasureFrameworkPropertyMetadataOptions.AffectsArrangeFrameworkPropertyMetadataOptions.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日。

© . All rights reserved.