hwnd interop (part 3)

A walk through of Avalon inside Win32 (HwndSource)

To put Avalon inside Win32 applications, one uses HwndSource, which provides an hwnd that contains your Avalon content.  HwndSource is pretty straightforward to use – first you create the HwndSource, giving it similar parameters to CreateWindow.  Then you tell the HwndSource about the Avalon content you want inside it.  Finally, you get the hwnd out of the HwndSource.

Let’s demonstrate this by taking the Windows Date and Time Properties dialog (which you get to buy double clicking on the time in the lower right corner of the screen), and bring this dialog into the 21st century by replacing the ugly Win32-style clock with an Avalon clock. 

We can recreate this by creating a plain old C++ Win32 project in Visual Studio, and using the dialog editor to create:

<picture>

(You don’t need to use Visual Studio to use HwndSource, and you don’t need to uses C++ to write Win32 programs, but this is a fairly typical way to do it)

We need to do five things to put an Avalon clock into that dialog:

  1. Enable it to call managed code (/clr) by changing project settings in Visual Studio
  2. Create an Avalon Page in a separate DLL
  3. Put that Avalon Page inside an HwndSource
  4. Get an hwnd for that Page using the HwndSource->Handle property
  5. Use Win32 to decide where to place the hwnd within the larger Win32 application

/clr

The first step is to turn this unmanaged Win32 project into one that can call managed code.  We need to tell the compiler to use /clr, link to the necessary DLLs we want to use, and decorate our Main method for use with Avalon.

First, to enable the use of managed code inside our C++ project: Right-click on w32clock project and select "Properties".  On the "General" property page (the default), change Common Language Runtime support to "/clr".

Next, add references to DLLs necessary for Avalon: PresentationCore.dll, PresentationFramework.dll, System.dll, and WindowsBase.dll.  Right-click on w32clock project and select "References...", and inside that dialog:

  1. Right-click on w32clock project and select "References...". 
  2. Click Add New Reference, select PresentationCore.dll, click OK.
  3. Click Add New Reference, select PresentationFramework.dll, click OK.
  4. Click Add New Reference, select System.dll, click OK.
  5. Click Add New Reference, select WindowsBase.dll, click OK.
  6. Click OK to exit the w32clock Property Pages for adding references.

Finally, we add the STAThreadAttribute to our _tWinMain method for use with Avalon:

[System::STAThreadAttribute]
int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)

This attribute tells the CLR that when it initializes COM, it should use a single threaded apartment (STA), which is necessary for Avalon (and Windows Forms).

Create an Avalon Page

Next, we create a DLL that defines an Avalon Page.  It’s often easiest to create the Avalon Page as a stand-alone application, and write and debug the Avalon portion that way.  Once done, that project can be turned into a DLL by right-clicking the project, clicking on Properties, going to the Application, and changing Output type to Windows Class Library.

The Avalon dll project and then be combined with the Win32 project (one solution that contains two projects) – right-click on the solution, select Add\Existing Project.

To use that Avalon dll from the Win32 project, we need to add a reference:

  1. Right-click on w32clock project and select "References...". 
  2. Click Add New Reference.  Click the "Projects" tab.  Select AvClock, click OK.
  3. Click OK to exit the w32clock Property Pages for adding references.

HwndSource

Next, we use HwndSource to make the Avalon Page look like an hwnd.  We add this block of code to a C++ file:

namespace ManagedCode
{
    using namespace System;
    using namespace System::Windows;
    using namespace System::Windows::Interop;
    using namespace System::Windows::Media;

    HWND GetHwnd(HWND parent, int x, int y, int width, int height) {
        HwndSource^ source = gcnew HwndSource(
            0, // class style
            WS_VISIBLE | WS_CHILD, // style
            0, // exstyle
            x, y, width, height,
            "hi", // NAME
            IntPtr(parent)        // parent window
            );
       
        Application::RegisterComponent("AvClock");

        UIElement^ page = gcnew AvClock::Clock();
        source->RootVisual = page;
        return (HWND) source->Handle.ToPointer();
    }
}

Let’s discuss what each part does.  The first part is a bunch of using clauses so that we don’t need to fully qualify all of our names:

namespace ManagedCode
{
    using namespace System;
    using namespace System::Windows;
    using namespace System::Windows::Interop;
    using namespace System::Windows::Media;

Then we define a function that creates the Avalon Page, puts an HwndSource around it, and returns the hwnd:

    HWND GetHwnd(HWND parent, int x, int y, int width, int height) {

First it creates an HwndSource, whose parameters are similar to CreateWindow:

        HwndSource^ source = gcnew HwndSource(
            0, // class style
            WS_VISIBLE | WS_CHILD, // style
            0, // exstyle
            x, y, width, height,
            "hi", // NAME
            IntPtr(parent)        // parent window
            );

Next, we register our Avalon dll with the Avalon resource loader, so it knows where to find the resources and BAML files used by our Avalon clock:

        Application::RegisterComponent("AvClock");

Then we create the Avalon Page by calling its constructor:

        UIElement^ page = gcnew AvClock::Clock();

We then connect the page to the HwndSource:

        source->RootVisual = page;

And in the final line, return the hwnd for the HwndSource:

        return (HWND) source->Handle.ToPointer();

Positioning the hwnd

So now we have an hwnd which contains the Avalon clock, we need to put that hwnd inside the Win32 dialog.  If we knew just where to put the hwnd, we would just pass that size and location to the GetHwnd function we defined earlier.  But we used a resource file to define our dialog, so we aren’t exactly sure where any of the hwnds are positioned.  So we use the Visual Studio dialog editor to put a Win32 STATIC control where we want the clock to go (“Insert clock here”), and use that to position the Avalon clock.

Where we handle WM_INITDIALOG, we use GetDlgItem to retrieve the hwnd for our placeholder STATIC:

HWND placeholder = GetDlgItem(hDlg, IDC_CLOCK);

We then calculate the size and position of that placeholder STATIC, so that we can put our Avalon clock in that place:

RECT rectangle;
GetWindowRect(placeholder, &rectangle);
int width = rectangle.right - rectangle.left;
int height = rectangle.bottom - rectangle.top;
POINT point;
point.x = rectangle.left;
point.y = rectangle.top;
result = MapWindowPoints(NULL, hDlg, &point, 1);

Then we hide the placeholder STATIC:

ShowWindow(placeholder, SW_HIDE);

And create the Avalon clock hwnd in that location:

HWND clock = ManagedCode::GetHwnd(hDlg, point.x, point.y, width, height);