Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

Creating OpenGL Windows in WPF

Rate me:
Please Sign up or sign in to vote.
4.86/5 (23 votes)
10 Mar 2009CPOL9 min read 160.8K   7.8K   62   14
A guide to creating OpenGL applications with Windows Presentation Foundation
Image 1

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...

XML
<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++):

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

or, in C#, as...

C#
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:

C++
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:

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

In C#, this would look a little simpler:

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

The initialization handler is as follows:

C#
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:

C++
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.

C++
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:

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

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

C++
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:

XML
<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:

C#
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.

C++
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:

C++
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:

C++
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:

C++
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:

Image 2

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

C++
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer emphess
Germany Germany
Chris lives in Munich, Germany and tries to study Physics.

Alongside, he works as a self-employed software consultant on projects from various fields.

Unfortunately, he loved software ever since he started with C++, and his addiction has become a lot worse since the introduction of .NET 2.0.

In his spare time, he tries to go sailing, drink some nice single malt whisky and, in very rare cases, he also sleeps.

You can reach him at ChrisNOSPAM at emphess.net or via his blog emphess.net.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Madhan Mohan Reddy P6-Jun-12 1:22
professionalMadhan Mohan Reddy P6-Jun-12 1:22 
Generalerror setPixelFormat on Win7 64 bit Pin
Alessio M26-Aug-10 4:37
Alessio M26-Aug-10 4:37 
GeneralRe: error setPixelFormat on Win7 64 bit Pin
Christoph Menge26-Aug-10 5:03
Christoph Menge26-Aug-10 5:03 
Generalmfc app into wpf Pin
hoschie869-Dec-09 21:01
hoschie869-Dec-09 21:01 
GeneralOnRender is not always called at the end of a screen resizing Pin
xf1003625-Oct-09 13:12
xf1003625-Oct-09 13:12 
GeneralRe: OnRender is not always called at the end of a screen resizing Pin
Christoph Menge27-Oct-09 13:34
Christoph Menge27-Oct-09 13:34 
GeneralOpenSG and WPF Pin
njss27-Mar-09 6:57
njss27-Mar-09 6:57 
GeneralDoes not run on Vista x64! Pin
Member 289334415-Oct-08 14:27
Member 289334415-Oct-08 14:27 
GeneralRe: Does not run on Vista x64! PinPopular
AndyDent18-Dec-08 21:22
professionalAndyDent18-Dec-08 21:22 
GeneralA bit misguided Pin
Rich Visotcky13-Aug-08 5:29
Rich Visotcky13-Aug-08 5:29 
GeneralRe: A bit misguided PinPopular
Christoph Menge13-Aug-08 5:38
Christoph Menge13-Aug-08 5:38 
GeneralRe: A bit misguided Pin
mounik5-Apr-09 17:39
mounik5-Apr-09 17:39 
GeneralRe: A bit misguided Pin
Member 17148103-Jun-10 9:23
Member 17148103-Jun-10 9:23 
GeneralThank you Pin
John Schroedl26-Feb-08 3:18
professionalJohn Schroedl26-Feb-08 3:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.