Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating OpenGL Windows in WPF

0.00/5 (No votes)
10 Mar 2009 1  
A guide to creating OpenGL applications with Windows Presentation Foundation

Preface

This article shows how to display OpenGL content hosted in a Windows Presentation Foundation (WPF) based application. While working on a new project, I came across this issue and I want to share what I found. I am not an expert on WPF or OpenGL, though.

The controls in this example are implemented using managed C++, because it makes using native OpenGL and Win32 libraries easier. This is a nice example of the usefulness of managed C++, I think. Note that you can create a wrapper for an existing native C++ control the same way. Tha Tao-Framework based version makes life even easier, not requiring any managed C++.

Contents

Scope & Prerequisites

The sample application will show a WPF program in C#, which displays an OpenGL window (or control). This is similar to the situation encountered in CAD / CAM applications. Also, the window should close when pressing the ESC key. In WinForms and Win32, this problem is fairly easy to solve. There are excellent articles and samples out there (Thanks to Jeff Molofee aka NeHe). However, due to the fact that the internal structure of WPF is very different from that of the Win32 API or WinForms, we have to change a few things.

Fortunately, there is a very nifty framework out there called "Tao" which can be found at The Tao Framework. The Tao Framework renders this article quite superfluous. What remains is a very simple sample application which can be downloaded at the top of the article along with parts of the source of Tao. Note that Tao is distributed under a different license (MIT License). See the file "Copying" for details.

Now, if you really want to know how to do it manually, here we go: In this article, I assume that you have a basic idea of how to create an OpenGL-window using Win32 API. You haven't heard of PIXELFORMATDESCRIPTOR before? In this case, you might want to read NeHe's first tutorial. (See the Resources Section).

Also, some very basic WPF know-how will be useful (e.g. how to reference custom controls in another assembly from XAML). I suggest you read Sacha Barber's excellent introduction to WPF here on The Code Project. (Huge thanks to Sacha for his articles!)

Keywords

Most notably, we will use WindowsFormsHost and HwndHost in this article.

The Issue

In order to create an OpenGL-window, we need to have a dedicated HWND. In Win32, we can simply create one of our own. WindowsForms controls, on the other hand, each have their own HWND anyway, so we can simply use that. In WPF, however, there is only one HWND for the application (With some exceptions: menus, for example, have their own window). As we do not want to interfere with the rendering of WPF's controls, acquiring the application's HWND is not a good idea (if possible at all). So how do we get a window for OpenGL to render to?

The Solution

Microsoft provides us with two simple classes made for WPF / Win32 interoperation. As the name suggests, these can be found in the namespace namespace System.Windows.Interop. These are the earlier mentioned WindowsFormsHost and HwndHost classes. We will host a WindowsForms UserControl in the WindowsFormsHost, and a custom Win32 API window using the HwndHost. Note that WindowsFormsHost is actually derived from HwndHost. Let's look at the simpler case of using a WindowsForms UserControl in a WindowsFormsHost first:

Using WindowsFormsHost

The WindowsFormsHost is a control we can simply embed in the WPF Applications' XAML file like this...

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

</int:WindowsFormsHost>

(Note that the namespaces used in here must be declared first. See the sample code or refer to Sacha's article for more information on that.)

... where the OpenGLUserControl itself is defined as (Managed C++):

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

or, in C#, as...

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

... respectively. This is not OpenGL-specific yet and can be used to host any Windows Forms control conveniently!

Implementing the Windows Forms Control

For our OpenGL-enabled Forms control, we'll need the following declaration and member variables:

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

If you haven't used managed C++ before, just ignore the '^'-symbol.

For initialization, we register a delegate in the constructor:

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

In C#, this would look a little simpler:

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

The initialization handler is as follows:

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

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

We need to resize the OpenGL-viewport if the window size changes, so we need to register another delegate:

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

(I won't write down the C# version every time in order not to bloat the article).

This method does little more than setting the OpenGL viewport and updating the projection matrix. As a matter of fact, I have chosen to use an orthogonal projection for reasons that I will explain shortly.

For perspective projections, the projection matrix must be recalculated when the window size changes, for example, using gluPerspective() or glFrustum(), that's why I left the code in this method.

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
}

Also, we have to override the OnPaintBackground() method to avoid flicker:

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

The actual OpenGL drawing can then be performed in the OnPaint() method:

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

That's it, basically! We now have a Windows Forms control which will display an OpenGL-window. The sample code also renders one of the impressive triangles!

Hosting Win32 API Windows

Now we can go one inheritance level higher and mess around with HwndHost so we can use any Win32 control (or window). First, we can no longer insert the control in XAML. Instead, we create a placeholder in XAML, in this case, just a Border-control:

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

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

... and programmatically attach a child to it upon load:

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;
}

Et Voila!

Implementing the 'Control'

Implementing the control itself also is somewhat different from the WindowsForms-case:

  • Using the HwndHost, we create the HWND ourselves. To do that, we also need to register our Window class.
  • For drawing purposes, we only overwrite the OnRender() method. There is no Paint/PaintBackground distinction.
  • We have to care about the system's DPI setting manually.
  • Our Window now has its own WindowProc!

In principle, you could take a complete Win32 application and put it into the HwndHost. The procedure of creating a window is the same as under Win32, but it has to be performed in the overwritten BuildWindowCore() method.

virtual HandleRef BuildWindowCore(HandleRef hwndParent) override 

Yet, meaningful interaction of WPF and Win32 is fraught with its own perils. More about that later.

Allowing Multiple Instances of the Window

A little note at the side: To allow that, we check whether the WNDCLASS has already been registered:

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

    // (register class) ...
}

Registering the Window Class

This works exactly as it does in Win32. This, however, seems a little strange: The class HwndHost supplies us with a managed method called WndProc(). MSDN suggests to overwrite this, but I didn't manage to initialize the window this way.

When registering the Window class, one can specify the WNDPROC to be used. Leaving it empty resulted in strange access violations during initialization, while the following simple implementation proved to work out fine, thus rendering the overrideable WndProc() method irrelevant:

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; 
    // ...
}

Keeping Focused

At this point, however, the window does not have focus. Unfortunately, that will prevent not only our WNDPROC to handle any key events, but it will also prevent HwndHost from forwarding the keyboard information to WPF. Thus, we have to acquire focus manually by a little more sophisticated version of 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 );
    }
}

Note that we have to check for LOWORD(_wParam) > 0, otherwise the message stands for losing focus rather than gaining it.

Using the simple message handler presented above, most of the commands will be forwarded to the parent. We can thus easily catch key events in the WPF-based Window class which owns the host.

However, this topic can be a lot more complicated, especially if we want to have two-way interaction between the Win32 control and WPF. This is outside this article's scope, however.

DPI-Awareness

Since there are now more and more devices with a screen resolution of above 96 DPI, it becomes more important for applications to be DPI-Aware. To tell the truth, I never bothered about the DPI until I set it to 120 myself.

This is why I chose to use an orthogonal projection here: It enables us to check (by visual means) whether we correctly mapped the screen, or not.

In our case here, the problem becomes highly annoying: If you don't take the system's DPI setting into consideration, you will have a large border where you simply cannot draw to - your GL window is too small:

In order to avoid that, we get the system's DPI setting on initialization and multiply the new window size with it upon resize:

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);

    // ...
}

Conclusion

Although the presented techniques are very similar at a first glance, they are targeted at different things: The HwndHost is a class the actual control is derived from. On the other hand, the WindowsFormsHost is a WPF-Control which we can place in an XAML file - the actual control in this case must be a UserControl.

While the WindowsFormsHost allows the use of an arbitrary WinForms user control with very little effort, usage of the HwndHost can be quite tricky, especially when it comes to input handling. This is largely because it completely breaks the controls scheme of the GUI and, in the case of input events, even overrides the main (WPF) application. On the other hand, being able to combine Win32 and WPF with a few tricks is still marvellous!

Open Questions, To-dos

One thing that bothers me is the exact behaviour of CS_OWNDC. I have read some articles on the net about it, but in the end I did not find an explanation that satisfied me. Removing it from the code doesn't seem to change anything, but I wonder what happens when we perform more complex rendering operations?

Another issue is performance. I did not talk about it in the article at all for a reason. My system is barely capable of displaying a transparency-enabled Vista desktop at full resolution... In my case, performance is a catastrophe! However, I believe that is largely due to a fill rate bottleneck of my old GeForce MX 5200. Also, we can't make a performance measurement using a timer that invalidates the control from time to time, of course!

Thanks, Sources, Postlude

Thank you for reading!

This is my first article. Phew... quite a bit of work!
Any feedback is highly appreciated!

Resources

History

  • 2009-03-05 v. 1.2: Added Tao Code
  • 2008-02-20 v. 1.1: Fixed some spelling mistakes
  • 2008-02-19 v. 1.0: Initial release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here