Wednesday, May 18, 2011

Using .NET and WPF in Win32 legacy applications, Part 1: The basics

Introduction

There are applications that are just impossible to port to any new technology, because of sheer size. Unfortunately, most of these applications are very important to their creators, such as commercial applications that have been developed for decades. Those applications often matured over the time and are satisfying a broad range of users. Rewriting these in a new programming language or a new framework, while being economical, would be impossible.

That's why interoperation and downwards-compatibility is something every new platform should provide. .NET, with it's new Windows Presentation Foundation (WPF) is one of these platforms. You can use .NET in the (from Microsoft's point of view) "legacy" C++ programming language through C++/CLR. And you can use WPF controls and windows in the now deprecated WinForms classes using the "interop" class System.Windows.Interop.HwndSource. Fortunately, since WinForms is built on raw Win32 API under the hood, this HwndSource class is suitable to be used to integrate WPF with raw Win32 applications as well. And it's not that hard at all.

For the impatient among us, you can directly skip to the download and look at the source code. It's only a few dozen lines of code.

Getting started

To get started, we will create a new, empty C++/CLR project. We need C++/CLR, since, obviously, we need to access .NET from either C or C++ source code. I will assume that the "legacy" application you want to use WPF in is a C++ project, since C projects can easily be ported or integrated with C++ code.

Step 1: Creating a new C++/CLR project

In Microsoft Visual C++ 2010 Express, you do that by choosing File -> New -> Project from the menu. Then you select Visual C++ > CLR > CLR Empty Project as the template. I picked win32wpfinterop as the project name, but you can use any name you want. When you hit OK, you will have a new, empty project.

Step 2: Building the Win32 window skeleton

Since C++/CLR is a superset of the "normal" C++ that you write in Visual C++, you can use the Win32 API just as you please. The window example skeleton does not differ from normal C++ at all. Add a new C++ File to the Source Files folder (filter) and add your Win32 window code. Here's how my code looks:

#include <Windows.h>

// Constants
namespace {
   TCHAR * windowClassName = TEXT("win32host");
   TCHAR * windowTitle     = TEXT("Win32 Host (Win32 WPF Interop)");
   int          windowWidth     = 200;
   int          windowHeight    = 100;
}

// Window message procedure
LRESULT CALLBACK WindowProc(
  HWND hwnd,
  UINT uMsg,
  WPARAM wParam,
  LPARAM lParam)
{
   switch (uMsg) {
   case WM_DESTROY:
      ::PostQuitMessage (0);
      return 0;
      break;
   default:
      return ::DefWindowProc (hwnd, uMsg, wParam, lParam);
   }
}

// Main program entry  point
[System::STAThread] // This is IMPORTANT, but it's for in C++/CLR  only
int CALLBACK WinMain(
   HINSTANCE hInstance,
   HINSTANCE hPrevInstance,
   LPSTR lpCmdLine,
   int nCmdShow)
{
   // Register our Window class
   ::WNDCLASS wndclass;
   wndclass.style = CS_VREDRAW | CS_HREDRAW;
   wndclass.lpfnWndProc = &WindowProc;
   wndclass.cbClsExtra = 0;
   wndclass.cbWndExtra = 0;
   wndclass.hInstance = hInstance;
   wndclass.hIcon = NULL;
   wndclass.hCursor = NULL;
   wndclass.hbrBackground = reinterpret_cast <HBRUSHgt; (COLOR_BTNFACE + 1);
   wndclass.lpszMenuName = NULL;
   wndclass.lpszClassName = windowClassName;
   ::RegisterClass(&wndclass);

   // Create our main, raw win32 API window
   // We create the window invisible (meaning that we do not provide WS_VISIBLE as the window style parameter), because making it visible and then
   // adding a HwndSource will make it flicker.
   HWND mainWindow = ::CreateWindow(
      windowClassName,
      windowTitle,
      0,
      CW_USEDEFAULT,
      CW_USEDEFAULT,
      windowWidth,
      windowHeight,
      NULL,
      NULL,
      hInstance,
      0);

   // Now that setting up the HwndSource is finished, we can finally make our window visible
   ::ShowWindow (mainWindow, SW_SHOW);

   // Start message processing
   ::MSG message;
   while (::GetMessageA(&message, 0, 0, 0)) {
      switch (message.message) {
      case WM_QUIT:
         break;
      default:
         ::TranslateMessage(& message);
         ::DispatchMessage(& message);
         break;
      }
   }
   return 0;
}

Download here

Yeah I know, it's a bit length ;-) but that's just how Win32 code is. The good news is that adding WPF components is much less code than this skeleton. Here is how your window should look like:

Empty Win32 Window

You need to add the User32.lib to your references, since the functions we used are defined in the Windows' User32 library. You need to right-click on your project in the Solution Explorer and choose Properties. Then navigate to Configuration Properties > Linker > Input and add User32.lib as an Additional Dependency.

Step 3: Create a HwndSource

Before we will be going to use WPF classes, we need to add the required WPF Assemblies to our project references. Right-click your project in the Solution Explorer again, but this time choose References. Now press Add New Reference and get a cup of coffee, while Visual Studio loads every registered .NET assembly. (This has become much faster in Visual Studio 2010, but it is still unbearably slow.) We will need the following assemblies to our project: System (for String), WindowBase and PresentationCore (for HwndSource) and PresentationFramework (for Label).

As the simplest example, we will be adding Label (type System.Windows.Controls.Label) to the window in this tutorial. But we can not just add a Label object to the Window -- the Label class has no members that would take a HWND as the parent class or anything like that. Instead, we need to create an intermediate object that builds the very bridge between Win32's HWND and WPF's System.Windows.UIElement. That intermediate object is of the aforementioned type System.Windows.Interop.HwndSource. Creating a HwndSource-object is very similar to creating a new win32 control - The HwndSource constructor basically takes the same parameters as WinAPI's RegisterClass () and CreateWindow() do. We create a HwndSource using this call:

System::Windows::Interop::HwndSource ^ hwndSource = gcnew System::Windows::Interop::HwndSource (
      CS_VREDRAW | CS_HREDRAW, // window class styles
      WS_CHILD,                // window flags
      0,                       // extended windows styles
      0,                       // x position (will be overridden later)
      0,                       // y position (will be overridden later)
      "WPF Interop",           // window title (not visible)
      static_cast <System::IntPtr> (mainWindow)); // parent window

HwndSource is, at the same time, a pure HWND-style window and a WPF UIElement. This means it can be hosted by another HWND (because we passed the WS_CHILD flag), but can host a WPF UIElement itself. And since being able to host one element in WPF means that you can host any number of elements (e.g. by using a Grid), you have the ultimate freedom to embed complex controls written in .NET and WPF in your Win32 application.

Step 4: Finally, add the Label

It's a bit tricky to get hold of the HWND window handle of HwndSource, because of .NET and C++ type differences. The HWND is available as a System.IntPtr. You need to call ToInt32 and reinterpret_cast it to a HWND like so:

HWND hwndSourceHandle = reinterpret_cast <HWND> (hwndSource->Handle.ToInt32 ());

Now we can call any Win32 API functions on that HWND, such as SetWindowPos:

::SetWindowPos (
      hwndSourceHandle,
      NULL, // ignored
      10,   // x position
      10,   // y position
      180,  // width
      80,   // height
      SWP_NOZORDER | SWP_SHOWWINDOW);

The HwndSource now exists, but that alone is not much of a gain. It's not even visible. You need to create some visible WPF control and add that to the HwndSource. Adding to the HwndSource means setting the control as the HwndSource's RootVisual. Here's how you can do this:

System::Windows::Controls::Label ^ label = gcnew System::Windows::Controls::Label ();
label->Content = gcnew System::String ("WPF Label -- it works!");
label->Background = System::Windows::Media::Brushes::White;

hwndSource->RootVisual = label;

When you insert this code between the call to ::CreateWindow() and ::ShowWindow(), you will experience your first moment of success and a window like this should be visible:

Win32 Window with WPF Label

Controls that don't required keyboard focus work very good. Controls with keyboard focus work good, too, as long as there are no other controls in the host window that may steal the focus from WPF. The next part will be about making keyboard handling and focusing work correctly, so stay tuned.
(Don't expect it before June or July 2011, though)

There's a complete example project for Visual C++ 2010 (build with VC++ 2010 Express) with commented source code for download.

Download

And besides: You can build a window's complete content in a C# class library e.g. as a UserControl, add that class library's assembly as a Reference in your C++/CLR's project and go ahead and use it in this interop framework we just built. This basically means that you can embed C# windows and controls in your legacy C++ application.

No comments:

Post a Comment