WPF Interoperability

Mixing up Windows Presentation Foundation with Win32, Windows Forms, and ActiveX


February 06, 2007
URL:http://www.drdobbs.com/windows/wpf-interoperability/197003872

Adam is a software design engineer on Microsoft's .NET Common Language Runtime QA team. Adam is also the author of Windows Presentation Foundation Unleashed (SAMS, 2007) on which this article is based.


Windows Presentation Foundation, especially in its first release, lacks some features that previous technologies already have. When creating a WPF-based user interface, you might want to exploit such features. For example, WPF currently doesn't include some of the standard controls that Windows Forms already has: DateTimePicker, MonthCalendar, NumericUpDown, MaskedTextBox, NotifyIcon, DataGridView, and more. Windows Forms also has support for multiple document interface (MDI) window management, wrappers over additional Win32 dialogs and APIs, and various handy APIs like Screen.AllScreens (which returns an array of screens with information about their bounds). Win32 has controls like an IP Address text box (SysIPAddress32) that have no equivalent in either Windows Forms or WPF. Windows Vista introduces Win32-based "glass" effects, task dialogs, and a new wizard framework that don't have first-class exposure to WPF. And tons of ActiveX controls exist for the purpose of embedding rich functionality into your own software.

Perhaps you've already put a lot of effort into developing your own pre-WPF user interfaces or controls. If so, you might want to leverage some of your work that's already in place. Maybe you have developed an application in a non-WPF technology with an extremely complicated main surface (like a CAD program) and just want to "WPFize" the outer edges of the applications with rich menus, toolbars, and so on. Maybe you've created a web application with tons of HTML content that you want to enhance but not replace.

Given that HTML can be hosted inside a WPF Frame and WPF content can be hosted inside HTML (as a XAML Browser Application or a loose XAMLpage), you can leverage existing HTML content alongside new WPF content. Fortunately, WPF's support for interoperability goes much deeper than that. It's fairly easy for WPF applications and controls to leverage all kinds of non-WPF content or APIs, such as all the examples in the previous two paragraphs. Some of these scenarios are possible thanks to the features I describe here, some are possible thanks to the .NET Framework's interoperability between managed and unmanaged code, and (in the case of calling miscellaneous Windows Forms APIs from WPF) some are possible simply because the other technology defines managed APIs that just happen to live in non-WPF assemblies.

Figure 1 summarizes the different UI technologies and the paths you can take to mix and match them. Win32 is a general bucket that includes any technology that runs on Windows: DirectX, MFC, WTL, OpenGL, and so on. Notice that there's a direct path between each set of technologies except for WPF and ActiveX. In that case, you must use another technology as an intermediate layer. The line between Win32 and Windows Forms is enabled by standard .NET Framework interoperability technologies for mixing managed and unmanaged code (and the fact that Windows Forms is based on Win32), and the line between Win32 and ActiveX is somewhat artificial because there are no real barriers separating Win32 and ActiveX.

[Click image to view at full size]
Figure 1: Relationship between various Windows UI technologies.

In this article, I focus on embedding controls of one type inside applications of another type. I first examine both directions of WPF/Win32 interoperability, then both directions of WPF/Windows Forms interoperability. Although the focus is on embedding controls, I look at another important scenario that isn't as straightforward as you might imagine--launching heterogeneous dialogs.

Embedding Win32 Controls in WPF Applications

In Win32, all controls are considered to be "windows," and Win32 APIs interact with them via window handles known as "HWNDs." All Windows-based UI technologies--DirectX, MFC, and the like--ultimately use HWNDs to some degree, so the ability to work with HWNDs gives the ability to work with all of these technologies.

Although WPF's subsystems (layout, animation, and so on) don't know how to interact directly with HWNDs, WPF defines a FrameworkElement that can host an arbitrary HWND. This FrameworkElement is System.Windows.Interop.HwndHost, and makes HWND-based controls look and act almost exactly like WPF controls.

To demonstrate HwndHost in a WPF application, look at what's involved in embedding a custom Win32 control to add webcam functionality to WPF. WPF's video support doesn't include anything for interacting with local video capture devices such as a simple webcam. Microsoft's DirectShow technology has support for this, so Win32 interoperability lets you leverage that same webcam support in WPF applications.

A Win32 Webcam Control

Listing One contains the unmanaged C++ definition for a custom Win32 Webcam control that wraps a few DirectShow COM objects.

#if !defined(WEBCAM_H)
#define WEBCAM_H

#include 

class Webcam 
{ 
public:
 static HRESULT Initialize(int width, int height);
 static HRESULT AttachToWindow(HWND hwnd);
 static HRESULT Start();
 static HRESULT Pause();
 static HRESULT Stop();
 static HRESULT Repaint();
 static HRESULT Terminate();
 static int GetWidth();
 static int GetHeight();
} ;
#endif // !defined(WEBCAM_H)
Listing One

The Webcam class is designed to work with a computer's default video capture device, so it contains a set of simple static methods for controlling this device. It is initialized with a width and height (which can be later retrieved via GetWidth and GetHeight methods). Then, after telling Webcam (via AttachToWindow) what HWND to render itself on, the behavior can be controlled with simple Start/Pause/Stop methods.

Listing Two is the implementation of the Webcam class. The implementation begins with a simple Win32 window procedure, which makes sure to repaint the video whenever a WM_ERASEBKGND message is received. Inside Initialize, a Win32 window class called WebcamClass is defined and registered, and a bunch of DirectShow-specific COM objects are created and initialized. (The Terminate method releases all of these COM objects.) AttachToWindow not only tells DirectShow which window to render on, but it sets the size of the video to match the dimensions passed to Initialize. The other methods are simple wrappers for the underlying DirectShow methods.

LRESULT WINAPI WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{ 
 switch (msg)
 { 
  case WM_ERASEBKGND:
   DefWindowProc(hwnd, msg, wParam, lParam);
   Webcam::Repaint();
   break;
  default:
   return DefWindowProc(hwnd, msg, wParam, lParam);
 } 
 return 0;
} 
HRESULT Webcam::Initialize(int width, int height)
{ 
 _width = width;
 _height = height;

 // Create and register the Window Class
 WNDCLASS wc;
 wc.style     = CS_VREDRAW | CS_HREDRAW;
 wc.lpfnWndProc  = WndProc;
 wc.cbClsExtra  = 0;
 wc.cbWndExtra  = 0;
 wc.hInstance   = GetModuleHandle(NULL);
 wc.hIcon     = LoadIcon(NULL, IDI_APPLICATION);
 wc.hCursor    = LoadCursor(NULL, IDC_ARROW);
 wc.hbrBackground = (HBRUSH)(COLOR_SCROLLBAR+1);
 wc.lpszMenuName = 0;
 wc.lpszClassName = L"WebcamClass";
 RegisterClass(&wc);

 HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
  IID_IGraphBuilder, (void **)&_graphBuilder);
 ...Create and interact with several COM objects...
 return hr;
} 
HRESULT Webcam::AttachToWindow(HWND hwnd)
{ 
 if (!_initialized || !_windowlessControl)
  return E_FAIL;
 _hwnd = hwnd;
 // Position and size the video
 RECT rcDest;
 rcDest.left = 0;
 rcDest.right = _width;
 rcDest.top = 0;
 rcDest.bottom = _height;
 _windowlessControl->SetVideoClippingWindow(hwnd);
 return _windowlessControl->SetVideoPosition(NULL, &rcDest);
} 
HRESULT Webcam::Start()
{ 
 if (!_initialized || !_graphBuilder || !_mediaControl)
  return E_FAIL;
 _graphBuilder->Render(_pin);
 return _mediaControl->Run();
} 
HRESULT Webcam::Pause()
{ 
 if (!_initialized || !_mediaControl)
  return E_FAIL;
 return _mediaControl->Pause();
} 
HRESULT Webcam::Stop()
{ 
 if (!_initialized || !_mediaControl)
  return E_FAIL;
 return _mediaControl->Stop();
} 
HRESULT Webcam::Repaint()
{ 
 if (!_initialized || !_windowlessControl)
  return E_FAIL;
 return _windowlessControl->RepaintVideo(_hwnd, GetDC(_hwnd));
} 
HRESULT Webcam::Terminate()
{ 
 HRESULT hr = Webcam::Stop();
 ...Release several COM objects...
 return hr;
} 
int Webcam::GetWidth()
{ 
 return _width;
} 
int Webcam::GetHeight()
{ 
 return _height;
}
Listing Two

Using the Webcam Control In WPF

The first step in using the Webcam control in a WPF application is to create a project that is able to "see" this unmanaged control from the WPF-specific managed code that must be written. Many options exist for integrating managed code into an unmanaged codebase. If you're comfortable with C++, using C++/CLI to seamlessly mix managed and unmanaged code is usually the best approach. This is especially true for the Webcam class because it doesn't expose any functionality outside of the DLL in which it is compiled.

Listing Three defines a WPF Window--all in C++/CLI--and uses the HwndHost type to integrate the Win32 Webcam control. Because it is using and defining managed data types, it must be compiled with the /clr compiler option.

#include "stdafx.h"
#include "Webcam.h"

#using 
#using 
#using 
#using 

using namespace System;
using namespace System::Windows;
using namespace System::Windows::Controls;
using namespace System::Windows::Interop;
using namespace System::Runtime::InteropServices;

ref class MyHwndHost : HwndHost
{ 
protected: 
 virtual HandleRef BuildWindowCore(HandleRef hwndParent) override
 { 
  HWND hwnd = CreateWindow(L"WebcamClass", // Registered class
   NULL,                 // Title
   WS_CHILD,               // Style
   CW_USEDEFAULT, 0,           // Position
   Webcam::GetWidth(),          // Width
   Webcam::GetHeight(),          // Height
   (HWND)hwndParent.Handle.ToInt32(),   // Parent
   NULL,                 // Menu
   GetModuleHandle(NULL),         // hInstance
   NULL);                 // Optional parameter
   if (hwnd == NULL)
   throw gcnew ApplicationException("CreateWindow failed!");
  Webcam::AttachToWindow(hwnd);
  return HandleRef(this, IntPtr(hwnd));
 } 
 virtual void DestroyWindowCore(HandleRef hwnd) override
 { 
  // Just a formality:
  ::DestroyWindow((HWND)hwnd.Handle.ToInt32());
 } 
} ;
ref class Window1 : Window
{ 
public:
 Window1()
 { 
  DockPanel^ panel = gcnew DockPanel();
  MyHwndHost^ host = gcnew MyHwndHost();
  Label^ label = gcnew Label();
  label->FontSize = 20;
  label->Content = "The Win32 control is docked to the left.";
  panel->Children->Add(host);
  panel->Children->Add(label);
  this->Content = panel;

  if (FAILED(Webcam::Initialize(640, 480)))
  { 
   ::MessageBox(NULL, L"Failed to communicate with a video capture device.",
    L"Error", 0);
  } 
  Webcam::Start();
 } 
 ~Window1()
 { 
  Webcam::Terminate();
 } 
} ;
Listing Three

If the implementation of Webcam::AttachToWindow in Listing Two were changed to discover the size of the HWND, the video could stretch to fill that area.

The first thing to notice about Listing Three is that it defines a subclass of HwndHost called MyHwndHost. This is necessary because HwndHost is actually an abstract class! It contains two methods that need to be overridden:

For both methods, HWNDs are represented as HandleRef types. HandleRef is a lightweight wrapper (in the System.Runtime.InteropServices namespace) that ties the lifetime of the HWND to a managed object. You typically pass this as the managed object when constructing a HandleRef.

Listing Three calls the Win32 CreateWindow API inside BuildWindowCore to create an instance of the WebcamClass window that was registered in Listing Two, passing the input HWND as the parent. The HWND returned by CreateWindow is not only returned by BuildWindowCore (inside a HandleRef), but it is also passed to the Webcam::AttachToWindow method so the video is rendered appropriately. Inside DestroyWindowCore, the Win32 DestroyWindow API is called to signify the end of the HWND's lifespan.

(A typical implementation of an HwndHost subclass calls CreateWindow inside BuildWindowCore and DestroyWindow inside DestroyWindowCore. However, calling DestroyWindow isn't necessary because a child HWND is automatically destroyed by Win32 when the parent HWND is destroyed. So in Listing Three, the implementation of DestroyWindowCore could be left empty.)

Inside the Window's constructor, the MyHwndHost is instantiated and added to a DockPanel just like any other FrameworkElement. The Webcam is then initialized and the video stream is started. (For some applications, initialization of the Win32 content might need to wait until all the WPF content has been rendered. In such cases, you can perform this initialization from Window's ContentRendered event.)

Listing Four contains the final piece needed for the WPF webcam application, which is the main method that creates the Window and runs the Application. It is also compiled with the /clr option. Figure 2 is the running application. (With Visual C++'s /clr compiler option, you can compile entire projects or individual source files as managed code.

#include "Window1.h"

using namespace System;
using namespace System::Windows;
using namespace System::Windows::Media;

[STAThreadAttribute]
int main(array ^args)
{ 
 Application^ application = gcnew Application();
 Window^ window = gcnew Window1();
 window->Title = "Hosting Win32 DirectShow Content in WPF";
 window->Background = Brushes::Orange;
 application->Run(window);
 return 0;
} 
Listing Four

It's tempting to simply compile entire projects as managed code, but it's usually best if you decide on a file-by-file basis what should be compiled as managed and what should be compiled as unmanaged. Otherwise, you could create extra work for yourself without any real gain.)

[Click image to view at full size]
Figure 2: Live webcam feed is embedded in the WPF window.

The /clr option works well, but it often increases build time and can sometimes require code changes. For example, .C files must be compiled as C++ under /clr, but .C files often require some syntax changes to be compiled as such. Also, managed code can't run under the Windows loader lock, so compiling DllMain (or any code called by it) as managed results in a (fortunately quite descriptive) runtime error.

Notice the gray area underneath the video stream in Figure 2. The reason this appears is that the MyHwndHost element is docked to the left side of the DockPanel in Listing Three, but the Webcam control is initialized with a fixed size of 640x480. This change is shown in the following code, with the result shown in Figure 3.

HRESULT Webcam::AttachToWindow(HWND hwnd)
{ 
 if (!_initialized || !_windowlessControl)
  return E_FAIL;
 _hwnd = hwnd;
 // Position and size the video
 RECT rcDest;
 GetClientRect(hwnd,&rcDest);
 _windowlessControl->SetVideoClippingWindow(hwnd);
 return _windowlessControl->SetVideoPosition(NULL, &rcDest);
} 

[Click image to view at full size]
Figure 3: The Webcam control, altered to fill the entire rectangle given to it.

Although the best solution for a webcam application is probably to give the HwndHost-derived element a fixed (or at least unstretched) size, it's important to understand that WPF layout applies only to the HwndHost. Within its bounds, you need to play by Win32 rules to get the layout you desire.

Supporting Keyboard Navigation

In addition to the two abstract methods that must be implemented, HwndHost has a few virtual methods that can optionally be overridden if you want to handle seamless keyboard navigation between WPF elements and hosted Win32 content. This doesn't apply to the hosted Webcam control as is, as it never needs to gain keyboard focus. But for controls that accept input, there are some common features that you'd undoubtedly want to support:

Figure 4 illustrates contents of a hypothetical WPF Window with two WPF controls surrounding a Win32 control (hosted in HwndHost) with four child Win32 controls. The numbers represent the expected order of navigation. For the three WPF controls (1, 6, and the HwndHost containing 2-5), the ordering could come implicitly from the way in which they were added to their parent or it could come from an explicit TabIndex being set for each control. For the four Win32 controls (2-5), the order is defined by application-specific logic.

[Click image to view at full size]
Figure 4: A scenario in which keyboard navigation is important with hosted Win32 content.

Tabbing Into the Hosted Win32 Content

"Tabbing into" the Win32 content means two things:

Both of these actions can be supported fairly easily by overriding HwndHost's TabInto method, which is called when HwndHost receives focus via Tab or Shift+Tab. In C++/CLI, a typical implementation would look likes this:

virtual bool TabInto(TraversalRequest^ request) override
{ 
 if (request->FocusNavigationDirection == 
              FocusNavigationDirection::Next)
  SetFocus(hwndForFirstWin32Control);
 else
  SetFocus(hwndForLastWin32Control);
 return true;
} 

TabInto's parameter reveals whether the user has just pressed Tab (giving FocusNavigationDirection.Next) or Shift+Tab (giving FocusNavigationDirection.Previous). Therefore, this code uses this information to decide whether to give focus to its first child or last child. It does this using the Win32 SetFocus API. After setting focus to the correct element, it returns true to indicate that it successfully handled the request.

Tabbing Out of the Hosted Win32 Content

Supporting tabbing into a Win32 control is not enough, of course. If you don't also support tabbing out of the control, keyboard navigation can get "stuck" inside of the Win32 control. For Figure 4, tabbing out of the control means being able to navigate from 5 to 6 with Tab, or from 2 to 1 with Shift+Tab.

Supporting this direction is a little more complicated than the other direction. That's because after focus enters Win32 content, WPF no longer has the same kind of control over what's going on. The application still receives Windows messages that are ultimately passed along to HwndHost, but WPF's keyboard navigation functionality can't "see" what's going on with focus.

Therefore, there is no TabOutOf method to override. Instead, there is a TranslateAccelerator method, which gets called whenever the application receives WM_KEYDOWN or WM_SYSKEYDOWN message from Windows (much like the Win32 API with the same name). Listing Five is a typical C++/CLI implementation of TranslateAccelerator for the purpose of supporting tabbing out of Win32 content (and tabbing within it).

TranslateAccelerator is passed a reference to a "raw" Windows message (represented as a managed System.Windows.Interop.MSG structure) and a ModifierKeys enumeration that reveals if the user is pressing Shift, Alt, Control, and/or the Windows key. (This information can also be retrieved using the Win32 GetKeyState API.)

In Listing Five, the code only takes action if the message is WM_KEYDOWN and if Tab is being pressed (which includes Shift+Tab). After determining whether the user pressed Tab or Shift+Tab using GetKeyState, the code must determine if it is time to tab out of the control or within the control. Tabbing out should occur if focus is already on the first child control and the user pressed Shift+Tab, or if focus is already on the last child control and the user pressed Tab. So in these cases, the implementation calls OnNoMoreTabStops on HwndHost's KeyboardInputSite property. This is the way to tell WPF that focus should return under its control. OnNoMoreTabStops needs to be passed a FocusNavigationDirection value so it knows which WPF element should get focus (1 or 6 in Figure 4). The implementation of TranslateAccelerator must return true if it handles the keyboard event. Otherwise, the event bubbles or tunnels to other elements. One point that Listing Five glosses over is that setting the values of hwndOfPreviousControl and hwndOfNextControl appropriately involves a small amount of application-specific code to determine what the previous/next Win32 control is based on the HWND that currently has focus.

virtual bool TranslateAccelerator(MSG% msg, ModifierKeys modifiers) override
{ 
 if (msg.message == WM_KEYDOWN && msg.wParam == IntPtr(VK_TAB))  { 
  // Handle Shift+Tab
  if (GetKeyState(VK_SHIFT))   { 
   if (GetFocus() == hwndOfFirstControl)
   { 
    // We're at the beginning, so send focus to the previous WPF element
    return this->KeyboardInputSite->OnNoMoreTabStops(
     gcnew TraversalRequest(FocusNavigationDirection::Previous));
   } 
   else 
    return (SetFocus(hwndOfPreviousControl) != NULL);
  } 
  // Handle Shift without Tab
  else   { 
   if (GetFocus() == hwndOfLastControl)
   { 
    // We're at the end, so send focus to the next WPF element
    return this->KeyboardInputSite->OnNoMoreTabStops(
     gcnew TraversalRequest(FocusNavigationDirection::Next));
   } 
   else 
    return (SetFocus(hwndOfNextControl) != NULL);
  } 
 } 
} 
Listing Five

With such an implementation of TranslateAccelerator and TabInto (from the previous section), a user of the application represented by Figure 4 would now be able to navigate all the way from 1 to 6 and back from 6 to 1 using Tab and Shift+Tab, respectively.

Supporting Access Keys

The final piece of keyboard navigation to support is jumping to a control via an access key (sometimes called a "mnemonic"). For example, the text boxes in Figure 4 would likely have corresponding labels with an access key (indicated by an underlined letter). When hosted in a WPF application, we still want focus to jump to the corresponding controls when the user presses Alt and the access key.

To support this, you can override HwndHost's OnMnemonic method. Like TranslateAccelerator, it is given a raw Windows message and a ModifierKeys enumeration. So you could implement it as follows, if you want to support two access keys--a and b:

virtual bool OnMnemonic(MSG% msg, ModifierKeys modifiers) override
{ 
 // Ensure that we got the expected message
 if (msg.message == WM_SYSCHAR && (modifiers | ModifierKeys.Alt))  { 
  // Convert the IntPtr to a char
  char key = (char)msg.wParam.ToPointer();
  // Only handle the 'a' and 'b' characters
  if (key == 'a')
   return (SetFocus(someHwnd) != NULL);
  else if (key == 'b')
   return (SetFocus(someOtherHwnd) != NULL);
 } 
 return false;
} 

Embedding WPF Controls in Win32 Applications

Lots of compelling WPF features can be integrated into a Win32 application: 3D, rich document support, animation, easy restyling, and so on. Even if you don't need this extra "flashiness," you can still take advantage of important features, such as flexible layout and resolution independence.

Since WPF's HWND interoperability is bidirectional, WPF controls can be embedded in Win32 applications much like how Win32 controls are embedded in WPF applications. In this section, I take a built-in WPF control--DocumentViewer, the viewer for XPS documents--and embed it in a simple Win32 window using a class called HwndSource.

HwndSource does the opposite of HwndHost--it exposes any WPF Visual as an HWND. Listing Six demonstrates the use of HwndSource with the relevant C++ source file from a Win32 project included with this book's source code. It is compiled with /clr, so it is managed code that uses both managed and unmanaged data types.

In this project, a simple dialog is defined via a Win32 resource script (not shown here). The application's entry point (_tWinMain) simply shows this dialog via the Win32 DialogBox function, specifying DialogFunction as the window procedure that receives the Win32 messages.

Inside DialogFunction, only two messages are processed--WM_INITDIALOG, which creates and embeds the WPF control on initialization, and WM_CLOSE, which terminates the dialog appropriately. Inside the processing of WM_INITDIALOG, an HwndSourceParameters structure is created, and some of its fields are initialized to give the HwndSource an initial size, position, and style. Most important, it is given a parent HWND (which, in this case, is the dialog itself). For Win32 programmers, this type of initialization should look familiar. It's mostly the same kind of information that you would pass to the Win32 CreateWindow function.

After HwndSourceParameters is populated, the code only needs to do two simple steps to put the WPF content in place. It instantiates an HwndSource object with the HwndSourceParameters data, then it sets HwndSource's RootVisual property (of type System.Windows.Media.Visual) to an appropriate instance. Here, a DocumentViewer is instantiated. Figure 5 is the result.

[Click image to view at full size]
Figure 5: The WPF DocumentViewer control hosted in a simple Win32 dialog.

Although this example uses a built-in WPF control, you can follow the same approach with your own arbitrarily complex WPF content. Just take the top-level element (like a Grid or Page) and use HwndSource to expose it to the rest of Win32 as one big HWND.

Getting the Right Layout

Because you're in the world of Win32 when doing this type of integration, there's no special layout support for the top-level WPF control. In Listing Six, the DocumentViewer is given an initial placement of (10,10) and a size of (500,350).

#include "stdafx.h"
#include "HostingWPF.h"
#include "commctrl.h"

#using 
#using 
#using 

LRESULT CALLBACK DialogFunction(HWND hDlg, UINT message, WPARAM wParam,
 LPARAM lParam)
{ 
 switch (message)
 { 
  case WM_INITDIALOG:
  { 
   // Describe the HwndSource
   System::Windows::Interop::HwndSourceParameters p;
   p.WindowStyle = WS_VISIBLE | WS_CHILD;
   p.PositionX = 10;
   p.PositionY = 10;
   p.Width = 500;
   p.Height = 350;
   p.ParentWindow = System::IntPtr(hDlg);

   System::Windows::Interop::HwndSource^ source =
    gcnew System::Windows::Interop::HwndSource(p);    

   // Attach a new DocumentViewer to the HwndSource
   source->RootVisual = gcnew System::Windows::Controls::DocumentViewer();

   return TRUE;
  } 
  case WM_CLOSE:
   EndDialog(hDlg, LOWORD(wParam));
   return TRUE;
 } 
 return FALSE;
} 
[System::STAThread]
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 LPTSTR lpCmdLine, int nCmdShow)
{ 
 DialogBox(hInstance, (LPCTSTR)IDD_MYDIALOG, NULL, (DLGPROC)DialogFunction);
 return 0;
} 
Listing Six

But that placement and size is never going to change without some explicit code to change them. For example, Listing Seven makes the DocumentViewer occupy the entire space of the window, even as the window is resized.

#include "stdafx.h"
#include "HostingWPF.h"
#include "commctrl.h"

#using 
#using 
#using 

ref class Globals
{ 
public:
 static System::Windows::Interop::HwndSource^ source;
} ;
LRESULT CALLBACK DialogFunction(HWND hDlg, UINT message, WPARAM wParam,  LPARAM lParam)
{ 
 switch (message)
 { 
  case WM_INITDIALOG:
  { 
   System::Windows::Interop::HwndSourceParameters p;
   p.WindowStyle = WS_VISIBLE | WS_CHILD;
   // Initial size and position don't matter due to WM_SIZE handling:
   p.PositionX = 0; p.PositionY = 0;
   p.Width = 100; p.Height = 100;
   p.ParentWindow = System::IntPtr(hDlg);

   Globals::source = gcnew System::Windows::Interop::HwndSource(p);    
   Globals::source->RootVisual =     gcnew System::Windows::Controls::DocumentViewer();
   return TRUE;
  } 
  case WM_SIZE:
   RECT r;
   GetClientRect(hDlg, &r);
   SetWindowPos((HWND)Globals::source->Handle.ToPointer(), NULL,
    r.left, r.top, r.right - r.left, r.bottom - r.top, 0);
   return TRUE;
  case WM_CLOSE:
   EndDialog(hDlg, LOWORD(wParam));
   return TRUE;
 } 
 return FALSE;
} 
[System::STAThreadAttribute]
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 LPTSTR lpCmdLine, int nCmdShow)
{ 
 DialogBox(hInstance, (LPCTSTR)IDD_MYDIALOG, NULL, (DLGPROC)DialogFunction);
 return 0;
} 
Listing Seven

The result is shown in Figure 6.

[Click image to view at full size]
Figure 6: The WPF DocumentViewer control hosted and resized in a simple Win32 dialog.

The most important code is the handling of the WM_SIZE message. It uses the Win32 GetClientRect API to get the current window size, and then it applies it to the HwndSource using the Win32 SetWindowPos API. There are two interesting points about this new implementation:

Embedding Windows Forms Controls In WPF Applications

You've seen that WPF can host Win32 controls by wrapping any HWND inside an HwndHost. And Windows Forms controls can easily be exposed as Win32 controls. (Unlike WPF controls, they are all HWND-based, so System.Windows.Forms.Control directly defines a Handle property exposing the HWND.) Therefore, you could imagine using the same techniques previously discussed to host Windows Forms controls inside WPF.

However, there is the opportunity for much richer integration between Windows Forms and WPF, without delving into the underlying HWND-based plumbing. Sure, they have different rendering engines and different controls. But they both have rich .NET-based object models with similar properties and events, and both have services (such as layout and data binding) that go above and beyond their Win32 common denominator.

Indeed, WPF takes advantage of this opportunity and also has built-in functionality for direct interoperability with Windows Forms. This support is still built on top of the Win32 HWND interoperability described in the last two sections, but with many features to make the integration much simpler. The hard work is done for you, so you can communicate more directly between the technologies, usually without the need to write any unmanaged code.

As with Win32 interoperability, WPF defines a pair of classes to cover both directions of communication. The analog to HwndHost is called WindowsFormsHost, and appears in the System.Windows.Forms.Integration namespace (in the WindowsFormsIntegration.dll assembly).

Embedding a PropertyGrid with Procedural Code

Windows Forms has several interesting built-in controls that WPF lacks in its first version. One control--the powerful PropertyGrid--helps to highlight the deep integration between Windows Forms and WPF, so let's use that inside a WPF Window. (Of course, you can also create custom Windows Forms controls and embed them in WPF Windows as well.)

The first step is to add a reference to System.Windows.Forms.dll and WindowsFormsIntegration.dll to your WPF-based project. After you've done this, your Window's Loaded event is an appropriate place to create and attach a hosted Windows Forms control. For example, let's take a simple Window containing a Grid called grid:


  private void Window_Loaded(object sender, RoutedEventArgs e)
{ 
 // Create the host and the PropertyGrid control
 System.Windows.Forms.Integration.WindowsFormsHost host =
  new System.Windows.Forms.Integration.WindowsFormsHost();
 System.Windows.Forms.PropertyGrid propertyGrid =
  new System.Windows.Forms.PropertyGrid();

 // Add the PropertyGrid to the host, and the host to the WPF Grid
 host.Child = propertyGrid;
 grid.Children.Add(host);

 // Set a PropertyGrid-specific property
 propertyGrid.SelectedObject = this;
} 

The integration-specific code is as simple as instantiating WindowsFormsHost and setting its Child property to the desired object. WindowsFormsHost's Child property can be set to any object that derives from System.Windows.Forms.Control.

The last line, which sets PropertyGrid's SelectedObject property to the instance of the current WPF Window, enables a pretty amazing scenario. PropertyGrid displays the properties of any .NET object, and, in some cases, enables the editing of the object's values. It does this via .NET reflection. Because WPF objects are .NET objects, PropertyGrid provides a fairly rich way to edit the current Window's properties on-the-fly without writing any extra code. Figure 7 shows the previously defined Window in action. When running this application, you can see values change as you resize the Window, you can type in new property values to resize the Window, you can change its background color or border style, and so on. (The WindowsFormsHost class actually derives from HwndHost, so it supports the same HWND interoperability features described earlier, just in case you want to dig into lower-level mechanics, such as overriding its WndProc method.)

[Click image to view at full size]
Figure 7: The hosted Windows Forms PropertyGrid enables you to change properties of the WPF Window on the fly.

Notice that the enumeration values for properties such as HorizontalContent Alignment are automatically populated in a drop-down list, thanks to the standard treatment of .NET enums. But Figure 7 highlights some additional similarities between Windows Forms and WPF, other than being .NET-based. Notice that Window's properties are grouped into categories such as "Behavior," "Content," and "Layout." This comes from CategoryAttribute markings that are used both by Windows Forms and WPF. The type converters used by WPF are also compatible with Windows Forms, so you can type in "red" as a color, for example, and it gets automatically converted to the hexadecimal ARGB representation (#FFFF0000). Another neat thing about the PropertyGrid used in this manner is that you can see attached properties that could be applied to the object, with the syntax you would expect.

Embedding a PropertyGrid with XAML

There's no reason that you have to instantiate a WindowsFormsHost instance in procedural code; you could alternatively define it right inside your XAML file. Furthermore, there's nothing to stop you from using Windows Forms controls inside of XAML, other than limitations of the expressiveness of XAML. (The controls must have a default constructor, useful instance properties to set, and so on.)

Not all Windows Forms controls work well within XAML, but PropertyGrid works reasonably well. For example, the previous XAML can be replaced with the following XAML:

<Window x:Class=.Window1"
xmlns=://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x=://schemas.microsoft.com/winfx/2006/xaml"
xmlns:swf="clr- namespace:System.Windows.Forms;assembly=System.Windows.Forms"
Title=a Windows Forms Property Grid in WPF"
Loaded=_Loaded">
<Grid>
  <WindowsFormsHost>
   <swf:PropertyGrid x:Name="propertyGrid"/>
  </WindowsFormsHost>
</Grid>
</Window>

The System.Windows.Forms.Integration .NET namespace is already included as part of WPF's standard XML namespace, so WindowsFormsHost can be used without any additional work. And with the System.Windows.Forms .NET namespace given the prefix of swf, the PropertyGrid object can be instantiated directly in the XAML file. The PropertyGrid can be added as a child element to WindowsFormsHost because its Child property is marked as a content property. PropertyGrid's properties can generally be set in XAML rather than C#. We still need some procedural code, however, to set the SelectedObject property:

private void Window_Loaded(object sender, RoutedEventArgs e)
{ 
 propertyGrid.SelectedObject = this;
} 

Embedding WPF Controls in Windows Forms Applications

WPF controls can be embedded inside a Windows Forms application thanks to a companion class of WindowsFormsHost called ElementHost. ElementHost is like HwndSource, but is customized for hosting WPF elements inside a Windows Forms Form rather than inside an arbitrary HWND. ElementHost is a Windows Forms control (deriving from System.Windows.Forms.Control) and internally knows how to display WPF content.

To demonstrate the use of ElementHost, I create a simple Windows Forms application that hosts a WPF Expander control. After creating a standard Windows Forms project in Visual Studio, the first step is to add ElementHost to the Toolbox using the Tools, Choose Toolbox Items menu item. This presents the dialog in Figure 8.

[Click image to view at full size]
Figure 8: Adding ElementHost to the Toolbox in a Windows Forms project.

With ElementHost in the Toolbox, you can drag it onto a Windows Form just like any other Windows Forms control. Doing this automatically adds references to the necessary WPF assemblies (PresentationFramework.dll, PresentationCore.dll, and so on). Listing Eight is the main source file to a Windows Forms project, whose Form contains an ElementHost called elementHost docked to the left and a Label on the right.

using System.Windows.Forms;
using System.Windows.Controls;

namespace WindowsFormsHostingWPF
{ 
 public partial class Form1 : Form
 { 
  public Form1()
  { 
   InitializeComponent();
   // Create a WPF Expander
   Expander expander = new Expander();
   expander.Header = "WPF Expander";
   expander.Content = "Content";
   // Add it to the ElementHost
   elementHost.Child = expander;
  } 
 } 
} 
Listing Eight

This code uses the System.Windows.Controls namespace for Expander, which it simply instantiates and initializes inside the Form's constructor. ElementHost, like WindowsFormsHost, has a simple Child property that can be set to any UIElement. This property must be set in source code rather than in the Windows Forms designer, so here it is set to the Expander instance. The result is shown in Figure 9. Notice that, by default, the Expander occupies all the space given to the ElementHost.

[Click image to view at full size]
Figure 9: A Windows Forms application containing a WPF Expander control.

Taking this example a step further, you can use a combination of ElementHost and WindowsFormsHost to have a Windows Forms control embedded in a WPF control embedded in a Windows Forms application! All we need to do is set the Content of the WPF Expander to a WindowsFormsHost, which can contain an arbitrary Windows Forms control. Listing Nine does just that, placing a Windows Forms MonthCalendar inside a WPF Expander, all on the same Windows Forms Form. Figure 10 shows the result.

using System.Windows.Forms;
using System.Windows.Controls;
using System.Windows.Forms.Integration;

namespace WindowsFormsHostingWPF
{ 
 public partial class Form1 : Form
 { 
  public Form1()
  { 
   InitializeComponent();
   // Create a WPF Expander
   Expander expander = new Expander();
   expander.Header = "WPF Expander";
   // Create a MonthCalendar and wrap it in a WindowsFormsHost
   WindowsFormsHost host = new WindowsFormsHost();
   host.Child = new MonthCalendar();
   // Place the WindowsFormsHost in the Expander
   expander.Content = host;
   // Add the Expander to the ElementHost
   elementHost.Child = expander;
  } 
 } 
} 
Listing Nine

[Click image to view at full size]
Figure 10: The Windows Forms MonthCalendar is inside the WPF Expander, which is on a Windows Forms Form.

Embedding ActiveX Controls in WPF Applications

There must be thousands of ActiveX controls in existence, and they can be easily embedded in WPF applications. But that's not because of any hard work done by the WPF team. Ever since version 1.0, Windows Forms has had a bunch of plumbing built-in for interoperability with ActiveX controls. Rather than duplicating all of that plumbing natively inside WPF, the team decided to simply depend on Windows Forms for this scenario. WPF gets the functionality "for free" just by working well with Windows Forms.

Using Windows Forms as an intermediate layer between ActiveX and WPF might sound suboptimal, but the development experience is just about as pleasant as can be expected. To demonstrate how to embed an ActiveX control in a WPF application, we'll use the Microsoft Terminal Services Control that ships with Windows. This control contains basically all the functionality of Remote Desktop, but controllable via a few simple APIs.

The first step for using an ActiveX control is to get a managed and Windows Forms-compatible definition of the relevant types. This can be done in two ways:

No matter which approach you use, two DLLs get generated. You should add references to these in your WPF-based project (along with System.Windows.Forms.dll and WindowsFormsIntegration.dll). One is an Interop Assembly that contains "raw" managed definitions of the unmanaged interfaces, classes, enums, and structures defined in the type library contained inside the ActiveX DLL. The other is an assembly that contains a Windows Forms control corresponding to each ActiveX class. The first DLL is named with the library name from the original type library, and the second DLL is named the same but with an Ax prefix.

The Microsoft Terminal Services Control, the original ActiveX DLL is called mstscax.dll and is found in Windows' system32 directory. (In the Choose Toolbox Items dialog, it shows up as "Microsoft Terminal Services Control.") Running the ActiveX Importer generates MSTSCLib.dll and AxMSTSCLib.dll.

With the four relevant two assemblies added to a project (MSTSCLib.dll, AxMSTSCLib.dll, System.Windows.Forms.dll, and WindowsFormsIntegration.dll), Listings Ten and Eleven contain the XAML and C# code to host the control and get the resulting application shown in Figure 11.

[Click image to view at full size]
Figure 11: Hosting the Terminal Services ActiveX Control in a WPF Window.

There's nothing special about the XAML in Listing Ten; it simply contains a DockPanel with a TextBox and Button for choosing a server and connecting to it.

<Window x:Class=.Window1"
xmlns=://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x=://schemas.microsoft.com/winfx/2006/xaml"
Title=the Terminal Services ActiveX Control">
<DockPanel Name=" Margin="">
  <StackPanel Margin=",0,0,10" DockPanel.Dock=" Orientation=">
   <TextBox x:Name=" Width="" Margin=",0,10,0"/>
   <Button x:Name=" Click=_Click">Connect</Button>
  </StackPanel>
</DockPanel>
</Window>
Listing Ten

In Listing Eleven, a WindowsFormsHost is added to the DockPanel, and the Windows Forms representation of the ActiveX control is added to the WindowsFormsHost. This control is called AxMsTscAxNotSafeForScripting. (In versions of Windows prior to Windows Vista, it has the somewhat simpler name of AxMsTscAx.) The interaction with the AxMsTscAxNotSafeForScripting control is quite simple. Its Server property can be set to a simple string, and you can connect to the server by calling Connect.

using System;
using System.Windows;
using System.Windows.Forms.Integration;

namespace HostingActiveX
{ 
 public partial class Window1 : Window
 { 
  AxMSTSCLib.AxMsTscAxNotSafeForScripting termServ;
  public Window1()
  { 
   InitializeComponent();

   // Create the host and the ActiveX control
   WindowsFormsHost host = new WindowsFormsHost();
   termServ = new AxMSTSCLib.AxMsTscAxNotSafeForScripting();
   // Add ActiveX control to host, and the host to the WPF panel
   host.Child = termServ;
   panel.Children.Add(host);
  } 
  void connectButton_Click(object sender, RoutedEventArgs e)
  { 
   termServ.Server = serverBox.Text;
   termServ.Connect();
  } 
 } 
} 

Listing Eleven

Of course, the instantiation of the WindowsFormsHost and the AxMsTscAxNotSafeForScripting control can be done directly in XAML, replacing the highlighted code in Listing Eleven. This is shown in Listing Twelve. You could go a step further and use data binding to replace the first line in connectButton_Click, but you would still need the event handler for calling the Connect method.

<Window x:Class=.Window1"
xmlns=://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x=://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ax="clr-namespace:AxMSTSCLib;assembly=AxMSTSCLib"
      Title=the Terminal Services ActiveX Control">
<DockPanel Name=" Margin="">
  <StackPanel Margin=",0,0,10" DockPanel.Dock=" Orientation=">
   <TextBox x:Name=" Margin=",0,10,0"/>
   <Button x:Name=" Click=_Click">Connect</Button>
  </StackPanel>
  <WindowsFormsHost>
   <ax:AxMsTscAxNotSafeForScripting x:Name="termServ"/>
  </WindowsFormsHost>
</DockPanel>
</Window>
Listing Twelve

Conclusion

The benefits of interoperability are broader than what I've discussed here. You could completely overhaul an application's user interface with WPF, but hook it up to back-end logic that was already in place with the old user interface--even if that logic was unmanaged code. This could be done with a number of techniques, such as using C++/CLI, PInvoke, or COM Interoperability.

Still, there are clear benefits for having an all-WPF user interface rather than a hybrid one. For example, in a pure WPF user interface, all the elements can be scaled, styled, and restyled in a similar fashion. They can be seamlessly overlaid on top of each other. In addition, you don't have to worry about mixing resolution-independent elements with resolution-dependent elements. A pure WPF user interface also opens the door to being able to run in a partial-trust environment (depending on how you separate your back-end logic).

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.