|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
PrefaceThis 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. Contents
Scope & PrerequisitesThe 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. 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 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!) KeywordsMost notably, we will use The IssueIn order to create an OpenGL-window, we need to have a dedicated The SolutionMicrosoft provides us with two simple classes made for WPF / Win32 interoperation. As the name suggests, these can be found in the namespace Using WindowsFormsHostThe <int:WindowsFormsHost Name="windowsFormsHost1">
<oglc:OpenGLUserControl Name="openGLControl1"/>
</int:WindowsFormsHost>
(Note that the ... where the 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 ControlFor 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. 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 virtual void OnPaintBackground( PaintEventArgs^ e ) override
{
// not doing anything here avoids flicker
}
The actual OpenGL drawing can then be performed in the 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 WindowsNow we can go one inheritance level higher and mess around with <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
In principle, you could take a complete Win32 application and put it into the 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 WindowA little note at the side: To allow that, we check whether the 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 ClassThis works exactly as it does in Win32. This, however, seems a little strange: The class 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 FocusedAt this point, however, the window does not have focus. Unfortunately, that will prevent not only our 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 DPI-AwarenessSince 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.
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);
// ...
}
ConclusionAlthough the presented techniques are very similar at a first glance, they are targeted at different things: The While the Open Questions, To-dosOne thing that bothers me is the exact behaviour of Thanks, Sources, PostludeThank you for reading! This is my first article. Phew... quite a bit of work! Resources
History
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||