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

在 WPF 中创建 OpenGL 窗口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (22投票s)

2008年2月19日

CPOL

9分钟阅读

viewsIcon

165946

downloadIcon

7940

关于使用 Windows Presentation Foundation 创建 OpenGL 应用程序的指南。

前言

本文展示了如何在基于Windows Presentation Foundation (WPF) 的应用程序中托管OpenGL内容。在开发一个新项目时,我遇到了这个问题,并想分享我的发现。我不是WPF或OpenGL的专家。

本示例中的控件是通过托管C++实现的,因为它更容易使用本地OpenGL和Win32库。我认为这是托管C++实用性的一个很好的例子。请注意,您也可以通过相同的方式为现有的本地C++控件创建包装器。基于Tao Framework的版本使事情更加容易,不需要任何托管C++。

目录

范围与先决条件

示例应用程序将展示一个C#的WPF程序,该程序显示一个OpenGL窗口(或控件)。这与CAD/CAM应用程序中遇到的情况类似。此外,按ESC键时窗口应关闭。在WinForms和Win32中,这个问题相对容易解决。有很多优秀的文章和示例(感谢Jeff Molofee,即NeHe)。然而,由于WPF的内部结构与Win32 API或WinForms的内部结构截然不同,我们需要做一些改变。

幸运的是,有一个非常巧妙的框架叫做“Tao”,可以在 The Tao Framework 找到。Tao Framework使本文显得多余。剩下的只是一个非常简单的示例应用程序,可以下载到文章顶部,以及Tao的部分源代码。请注意,Tao是在不同的许可(MIT License)下分发的。有关详细信息,请参阅“Copying”文件。

现在,如果您真的想知道如何手动完成,那么我们开始吧:在本文中,我假设您对如何使用Win32 API创建OpenGL窗口有一个基本的了解。您之前没听说过 `PIXELFORMATDESCRIPTOR`?在这种情况下,您可能需要阅读NeHe的第一个教程。(请参阅 资源 部分)。

此外,一些非常基础的WPF知识也会很有用(例如,如何在XAML中从另一个程序集中引用自定义控件)。我建议您在此处阅读The Code Project上Sacha Barber撰写的优秀WPF入门文章。(非常感谢Sacha的文章!)

关键词

最值得注意的是,本文将使用 `WindowsFormsHost` 和 `HwndHost`。

问题

为了创建一个OpenGL窗口,我们需要有一个专用的 `HWND`。在Win32中,我们可以简单地创建自己的。另一方面,`WindowsForms` 控件本身都有自己的 `HWND`,所以我们可以直接使用它。然而,在WPF中,**应用程序**只有一个 `HWND`(有些例外:例如,菜单有自己的窗口)。由于我们不想干扰WPF控件的渲染,获取应用程序的 `HWND` 不是一个好主意(如果可能的话)。那么我们如何才能得到一个供OpenGL渲染的窗口呢?

解决方案

Microsoft为WPF/Win32互操作提供了两个简单的类。顾名思义,这些类位于 `namespace System.Windows.Interop`。这就是前面提到的 `WindowsFormsHost` 和 `HwndHost` 类。我们将使用 `WindowsFormsHost` 托管一个 `WindowsForms` `UserControl`,并使用 `HwndHost` 托管一个自定义的Win32 API窗口。请注意,`WindowsFormsHost` 实际上是从 `HwndHost` 派生的。让我们先看看使用 `WindowsFormsHost` 中的 `WindowsForms UserControl` 的简单情况。

使用WindowsFormsHost

我们可以像这样在WPF应用程序的XAML文件中嵌入 `WindowsFormsHost` 控件...

<int:WindowsFormsHost Name="windowsFormsHost1">
    <oglc:OpenGLUserControl  Name="openGLControl1"/>

</int:WindowsFormsHost>

(请注意,这里使用的 `namespace` 必须先声明。有关更多信息,请参阅示例代码或Sacha的文章。)

...其中 `OpenGLUserControl` 本身定义为(托管C++)

public ref class OpenGLUserControl : public UserControl
{
    // ...
};

或者,在C#中,分别定义为...

public class OpenGLUserControl : UserControl
{
    // ...
};

...。这还不特定于OpenGL,并且可以方便地用于托管任何Windows Forms控件!

实现Windows Forms控件

对于我们支持OpenGL的Forms控件,我们需要以下声明和成员变量

public ref class OpenGLUserControl : public UserControl
{
    private:
        HDC          m_hDC;
        HWND         m_hWnd;
        HGLRC        m_hRC;
        
        System::ComponentModel::Container^ components;
    //...
}

如果您以前没有使用过托管C++,请忽略“^”符号。

初始化时,我们在构造函数中注册一个委托

this->Load += gcnew System::EventHandler(this, 
&OpenGLUserControl::InitializeOpenGL);

在C#中,它看起来会简单一些

this.Load += new System.EventHandler(InitializeOpenGL);

初始化处理程序如下

virtual void 
InitializeOpenGL( Object^ sender, EventArgs^ e)
{
    // Get the HWND from the base object
    m_hWnd = (HWND) this->Handle.ToPointer();

    // ... ChoosePixelFormat, SetPixelFormat, 
    //wglCreateContext, etc.
}

我们需要在窗口大小改变时调整OpenGL视口,所以我们需要注册另一个委托

    this->SizeChanged += gcnew EventHandler(this, 
            &OpenGLUserControl::ResizeOpenGL);

(为了避免文章臃肿,我不会每次都写出C#版本。)

此方法所做的只是设置OpenGL视口和更新投影矩阵。事实上,我选择使用正交投影,原因稍后会解释。

对于透视投影,当窗口大小改变时,投影矩阵必须重新计算,例如使用 `gluPerspective()` 或 `glFrustum()`,这就是为什么我将代码保留在此方法中的原因。

void ResizeOpenGL(Object^ sender, EventArgs^ e)
{
    // ...
    glViewport( 0, 0, Width, Height );
    // ...
    glOrtho(-1.0, 1.0, -1.0, 1.0, 1.0, 100.0);
    // or gluPerspective(), glFrustum(), etc.
    // for perspective projections, we need the 
    // aspect ratio of the window
}

此外,为了避免闪烁,我们必须重写 `OnPaintBackground()` 方法

virtual void OnPaintBackground( PaintEventArgs^ e ) override
{ 
    // not doing anything here avoids flicker
}

实际的OpenGL绘图可以在 `OnPaint()` 方法中执行

virtual void OnPaint( System::Windows::Forms::PaintEventArgs^ e ) override
{
    // Do very fancy rendering
}

基本上就是这样!我们现在有了一个可以显示OpenGL窗口的Windows Forms控件。示例代码还渲染了一个令人印象深刻的三角形!

托管Win32 API窗口

现在我们可以再向上继承一个级别,并使用 `HwndHost` 来处理,这样我们就可以使用任何Win32控件(或窗口)。首先,我们不能再在XAML中插入控件了。相反,我们在XAML中创建一个占位符,这里只是一个 `Border` 控件

<Window x:Class="WPFOpenGLApp.OpenGLHWndWindow"
    Title="OpenGL Test Window" Height="300" Width="480"

    Loaded="Window_Loaded">
    <Grid>
        <Border Name="hwndPlaceholder" />
    </Grid>
</Window>

...并在加载时以编程方式将其附加到子元素上

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    // Create our OpenGL Hwnd 'control'...
    HwndHost host = new WPFOpenGLLib.OpenGLHwnd();

    // ... and attach it to the placeholder control:
    hwndPlaceholder.Child = host;
}

大功告成!

实现“控件”

实现控件本身与 `WindowsForms` 情况也略有不同

  • 使用 `HwndHost`,我们自己创建 `HWND`。为此,我们还需要注册我们的 `Window` 类。
  • 为了绘图,我们只重写 `OnRender()` 方法。没有 `Paint`/`PaintBackground` 的区别。
  • 我们必须手动处理系统的DPI设置。
  • 我们的 `Window` 现在有了自己的 `WindowProc`!

原则上,您可以采用一个完整的Win32应用程序并将其放入 `HwndHost` 中。创建窗口的过程与Win32下的相同,但必须在重写的 `BuildWindowCore()` 方法中执行。

virtual HandleRef BuildWindowCore(HandleRef hwndParent) override 

然而,WPF和Win32之间有意义的交互本身就充满危险。稍后将详细讨论。

允许窗口的多个实例

旁注:为了允许这样做,我们检查 `WNDCLASS` 是否已被注册

bool RegisterWindowClass()
{
    //
    // Create custom WNDCLASS
    //
    WNDCLASS wndClass;
    if(GetClassInfo(m_hInstance, 
                    m_sWindowName, &wndClass))
    {
        // Class is already registered!
        return true;
    }

    // (register class) ...
}

注册窗口类

这与Win32中的情况完全一样。然而,这似乎有点奇怪:`HwndHost` 类提供了名为 `WndProc()` 的托管方法。MSDN建议重写它,但我没有设法通过这种方式初始化窗口。

在注册 `Window` 类时,可以指定要使用的 `WNDPROC`。将其留空会导致初始化过程中出现奇怪的访问冲突,而以下简单的实现已被证明可以正常工作,从而使可重写的 `WndProc()` 方法变得无关紧要。

LRESULT WINAPI 
MyMsgProc(HWND _hWnd, UINT _msg, 
          WPARAM _wParam, LPARAM _lParam)
{
    return DefWindowProc( _hWnd, _msg, _wParam, _lParam );
}

bool RegisterWindowClass()
{
    WNDCLASS wndClass;
    wndClass.lpfnWndProc = (WNDPROC)MyMsgProc; 
    // ...
}

保持焦点

然而,此时窗口没有焦点。不幸的是,这不仅会阻止我们的 `WNDPROC` 处理任何键盘事件,还会阻止 `HwndHost` 将键盘信息转发给WPF。因此,我们必须通过更复杂的 `MyMsgProc` 版本手动获取焦点。

LRESULT WINAPI 
MyMsgProc(HWND _hWnd, UINT _msg, 
          WPARAM _wParam, LPARAM _lParam)
{
    switch(_msg)
    {
        // Make sure the window gets focus when it has to!
    case WM_IME_SETCONTEXT:
        if(LOWORD(_wParam) > 0)
            SetFocus(_hWnd);
        return 0;

    default:
        return DefWindowProc( _hWnd, _msg, _wParam, _lParam );
    }
}

请注意,我们必须检查 `LOWORD(_wParam) > 0`,否则该消息代表失去焦点而不是获得焦点。

使用上面提供的简单消息处理程序,大多数命令将被转发给父级。因此,我们可以轻松地在拥有宿主控件的基于WPF的 `Window` 类中捕获键盘事件。

然而,这个话题可能要复杂得多,特别是如果我们想要Win32控件和WPF之间进行双向交互。但这超出了本文的范围。

DPI感知

由于现在有越来越多的屏幕分辨率高于96 DPI的设备,应用程序具有DPI感知能力变得越来越重要。说实话,直到我将DPI设置为120之前,我从未在意过DPI。

这就是为什么我选择使用正交投影:它使我们能够(通过视觉方式)检查我们是否正确映射了屏幕。

在我们这里,这个问题变得非常烦人:如果您不考虑系统的DPI设置,您将有一个很大的边框,您根本无法绘制——您的GL窗口太小了!

为了避免这种情况,我们在初始化时获取系统的DPI设置,并在调整大小时将其乘以新的窗口大小。

virtual HandleRef 
BuildWindowCore(HandleRef hwndParent) override 
{
    // ...
    m_hDC = GetDC(m_hWnd);

    // Technically, the DPI can be different for 
    // X and Y resolution. It is not particularly
    // a lot of work to support that feature, so we do it.
    m_dScaleX = GetDeviceCaps(m_hDC, LOGPIXELSX) / 96.0;
    m_dScaleY = GetDeviceCaps(m_hDC, LOGPIXELSY) / 96.0;
}

virtual void 
OnRenderSizeChanged(SizeChangedInfo^ sizeInfo) override
{
    // ...

    int iHeight = (int) 
         (sizeInfo->NewSize.Height * m_dScaleY);
    int iWidth = (int) 
         (sizeInfo->NewSize.Width * m_dScaleX);

    glViewport( 0, 0, iWidth, iHeight);

    // ...
}

结论

尽管所介绍的技术乍一看非常相似,但它们针对的是不同的事物:`HwndHost` 是一个类,**实际控件**从中派生。另一方面,`WindowsFormsHost` 是一个我们可以在XAML文件中放置的WPF控件——在这种情况下,实际控件必须是一个 `UserControl`。

虽然 `WindowsFormsHost` 可以毫不费力地使用任意的WinForms用户控件,但使用 `HwndHost` 可能非常棘手,尤其是在输入处理方面。这在很大程度上是因为它完全破坏了GUI的控件方案,并且在输入事件的情况下,甚至会覆盖主(WPF)应用程序。另一方面,能够通过一些技巧组合Win32和WPF仍然是惊人的!

未解决的问题,待办事项

有一件事让我感到困扰的是 `CS_OWNDC` 的确切行为。我在网上读了一些关于它的文章,但最终没有找到令我满意的解释。从代码中删除它似乎并没有改变任何东西,但我很想知道当我们执行更复杂的渲染操作时会发生什么。

另一个问题是性能。我没有在文章中讨论过,这是有原因的。我的系统几乎无法以全分辨率显示支持透明度的Vista桌面……在我的情况下,性能是一场灾难!然而,我认为这在很大程度上是由于我旧的GeForce MX 5200的填充率瓶颈。此外,我们自然无法使用定时器来使控件失效来测量性能!

致谢,资源,后记

感谢阅读!

这是我的第一篇文章。呼……工作量很大!
非常感谢任何反馈!

资源

历史

  • 2009-03-05 v. 1.2: 添加了Tao代码
  • 2008-02-20 v. 1.1: 修复了一些拼写错误
  • 2008-02-19 v. 1.0: 初始发布
© . All rights reserved.