hwnd interop (part 1)

So I've been trying for a while to finish a white paper about Avalon's hwnd interop functionality.  I'm still not done, but figured I would post what I have so far so I can get people's feedback.  I'm pasting this from MS-Word, which generates some, err, creative HTML, so let me know if the formatting gets too bad and I'll try to do something about it.  Here's part 1:

Combining Avalon and Win32 technologies

With Avalon, you can do all sorts of things that aren’t possible or practical in Win32. But what if you have an existing Win32 code base, do you need to throw it all away in order to use Avalon? Happily, you don’t, because Avalon can interoperate with your existing hwnd-based Win32 code. You can put Avalon inside of hwnds, hwnds inside of Avalon, and even “nested interop” like Avalon inside hwnds inside more Avalon inside more hwnds. And because hwnds are used in a lot of different technologies – including MFC, ATL, DirectX, and ActiveX – Avalon can interop with all of them.

To put Avalon inside of a parent hwnd, one uses the HwndSource class to get an hwnd for the Avalon content. As with all .Net APIs, you can use your choice of languages; I’ll use C++ to demonstrate HwndSource:

       UIElement^ avalonPage = gcnew MyAvalonPage();
       HwndSource^ source = gcnew HwndSource(
            0, // class style
            WS_VISIBLE | WS_CHILD, // window style
    0, // exstyle
            x, y, width, height,
            "my avalon page", // NAME
            IntPtr(parent) // parent window
            );
       
        source->RootVisual = avalonPage;
        HWND avalonHwnd = (HWND) source->Handle.ToPointer();

To put hwnds inside of Avalon, one uses the HwndHost class. In C++, this looks like:

    public ref class MyHwndHost : HwndHost {
      protected:
        virtual HandleRef BuildWindowCore(HandleRef hwndParent) {
            HWND handle = CreateWindowEx(0, L"LISTBOX",
                L"this is a Win32 listbox",
                WS_CHILD | WS_VISIBLE | LBS_NOTIFY
                   | WS_VSCROLL | WS_BORDER,
                0, 0, // x, y
                30, 70, // height, width
            (HWND) hwndParent.Handle.ToPointer(), // parent hwnd
                0, // hmenu
                0, // hinstance
                0); // lparam
 
            return HandleRef(this, IntPtr(handle));
        }
 
        // dummy implementation of KeyboardInputSite property omitted
    };

Managed and unmanaged code

Avalon APIs are managed code, but most existing Win32 programs are written in unmanaged C++. You can’t call Avalon from a true unmanaged program, but by using the /clr flag to the Visual C++ compiler, you can create a mixed managed-unmanaged program, where you can seamlessly mix managed and unmanaged API calls. The above examples mix managed APIs like HwndHost and BuildWindowCore with unmanaged APIs like CreateWindowEx. Just add /clr to your existing unmanaged project, recompile, and Presto – instant managed code!

If you haven’t yet used Visual C++ 2005 (code-named “Whidbey”), you’ll notice some new keywords like “gcnew” and “nullptr”; these supersede the older double-underscore syntax (like “__gc”) and make writing managed code in C++ a truly natural, “it just works” experience. You can learn more about the C++ 2005 managed features on https://msdn2.microsoft.com/library/xey702bw(en-us,vs.80).aspx.

There is one hitch, though, to using Avalon and C++ together – you can’t compile xaml files in a C++ project. There’s a lot of different ways around this, my usual approach is to create a C# dll that contains my xaml pages, and have my C++ .exe include that dll. But there’s other ways as well:

  • C# .exe and C++ .dll
  • Use Parser::LoadXml instead of compiling your xaml
  • Don’t use xaml at all, write all your Avalon in code

Use whatever works best for you.

How Avalon uses Hwnds

As you’ve seen, you can create some really powerful applications by using “hwnd interop” to mix Avalon with other UI technologies. But to make the most of Avalon’s “hwnd interop”, you need to understand how Avalon uses hwnds. That’s because within a single HDC or HWND, you can't mix Avalon rendering with DirectX rendering or GDI/GDI+ rendering. So WM_PAINT either goes to GDI or to Avalon, but not to both. This has a number of implications, including the so-called "airspace" restrictions, which brings us to how Avalon uses hwnds under the hood.

All Avalon elements on the screen are ultimately backed by hwnds. When you create an Avalon Window, under the hood Avalon creates a top-level hwnd, and uses an HwndSource to put the Window's Avalon content inside the hwnd. The rest of your Avalon content in the application shares that one great big hwnd. (Except for menus, combo box drop downs, and other pop-ups -- those create their own top-level window, which is why an Avalon menu can go past the edge of the window containing it) And when you use HwndHost to put an hwnd inside Avalon, behind the scenes Avalon tells Win32 how to position the new child hwnd relative to Avalon Window's hwnd.

At the hwnd boundary, Avalon has to play by Win32's rules. Which only make sense, ultimately someone needs to be in charge of telling who gets to draw where on the screen. So while Avalon can do anything it wants inside that hwnd -- make buttons rotate, support transparency from one Avalon element to another, animate -- Avalon has to behave like an hwnd when it talks to other hwnds. And Win32 has certain expectations, particularly "perfect clipping" -- that any pixel of the hwnd that is visible on the screen can be rendered by painting that hwnd and not any hwnds underneath. So while Win32 supports some special cases of hwnd transparency, it doesn't support the general case, which requires rendering hwnds on the bottom before hwnds on the top.