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:
- Enable it to call managed code (/clr) by changing project settings in Visual Studio
- Create an Avalon Page in a separate DLL
- Put that Avalon Page inside an HwndSource
- Get an hwnd for that Page using the HwndSource->Handle property
- 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:
- Right-click on w32clock project and select "References...".
- Click Add New Reference, select PresentationCore.dll, click OK.
- Click Add New Reference, select PresentationFramework.dll, click OK.
- Click Add New Reference, select System.dll, click OK.
- Click Add New Reference, select WindowsBase.dll, click OK.
- 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:
- Right-click on w32clock project and select "References...".
- Click Add New Reference. Click the "Projects" tab. Select AvClock, click OK.
- 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);
Comments
- Anonymous
July 17, 2005
How about a source code (for not C/C++ experts)? - Anonymous
July 18, 2005
So that&nbsp;concludes my series about hwnd interop, which is really a draft of the hwnd interop white... - Anonymous
September 22, 2005
Nick, thanks for all those insights and keep up the good work.
I have tried to host an Avalon Page within a Win32 app, the calls you mention succeed but all I get is a totally black window. Any clues ? Cheers, Derek. - Anonymous
March 21, 2006
Ok. Let's say I put the cool WPF stuff inside My Win32 app. That WPF stuff contains a WPF button.
Can the Win32 app be notified when the button is pressed?
Also, can the Win32 app change the caption of the button?
These are important questions. If the answer is NO, then the HwndSource feature is almost useless - it inserts XAML content for decorative purposes only. - Anonymous
March 21, 2006
Please give us an example which uses Windows API in more concrete terms, for example:
- we use LoadLibrary to load this DLL
- we declare the following data structure ...
- then, we use GetProcAddress to get the address of this marvelous hwndSource function (or whatever function we use)
This way I can very easily translate the example to Delphi or Python.
The way you talk about it, there is a misterious /clr switch for the compiler. Delphi 6 which I use doesn't have such a switch.
To truly enable interoperability between Win32 and WPF you must offer Win32 API instructions.. not "use the magic /clr switch, if your compiler doesn't have it, bad luck". - Anonymous
March 24, 2006
Yes, your C++ code can listen to WPF events like button clicks. It can also be done without managed C++ (/clr), basically looks like the C# examples for HwndSource/HwndHost in the SDK.