hwnd interop (part 4)
A walkthrough of Win32 inside Avalon (HwndHost)
To reuse Win32 content inside Avalon applications, one uses HwndHost, which is a control that makes hwnds look like Avalon content. Like HwndSource, HwndHost is straightforward to use – subclass HwndHost and implement BuildWindowCore and DestroyWindowCore methods, then instantiate your HwndHost subclass and put it inside your Avalon application.
If your Win32 logic is already packaged up nicely as a control, then your BuildWindowCore implementation is little more than a call to CreateWindow. E.g., to create a Win32 LISTBOX control in C++:
virtual HandleRef BuildWindowCore(HandleRef hwndParent) override {
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));
}
virtual void DestroyWindowCore(HandleRef hwnd) override {
// HwndHost will dispose the hwnd for us
}
But suppose the Win32 code is not quite so self-contained? Let’s take a Win32 dialog box, and embed its contents into a larger Avalon application. Again, we’ll do this in Visual studio and C++, although it’s also possible to do this in the different language or at the command line.
We’ll start with this simple dialog, which is compiled into a C++ DLL project:
<picture>
Here’s what we need to do to get that into our larger Avalon application:
• Compile the dll as managed (/clr)
• Turn the dialog into a control
• Define subclass of HwndHost with BuildWindowCore and DestroyWindowCore methods
• Implement KeyboardInputSite property to work around Visual C++ beta 2 bug
• Override TranslateAccelerator method to handle dialog keys
• Override TabInto method to support tabbing
• Override OnMnemonic method to support mnemonics
• Instantiate our HwndHost subclass and put it under the right Avalon element
In the previous section, we already covered how to turn an unmanaged C++ program into a managed C++ program using /clr, so we’ll skip right to the second step.
Turn the dialog into a control
We can turn a dialog box into a child hwnd using the WS_CHILD and DS_CONTROL styles. Go into the resource file (.rc) where the dialog is defined, and find the beginning of the definition of the dialog:
IDD_DIALOG1 DIALOGEX 0, 0, 303, 121
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
Change the second line to:
STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL
That doesn’t fully packaged it into a fully self-contained control, we still need to call IsDialogMessage() so Win32 can process certain messages, but it does give us a straightforward way of putting those controls inside another hwnd.
Subclass HwndHost
We create our own subclass of HwndHost and override the BuildWindowCore and DestroyWindowCore methods:
public ref class MyHwndHost : public HwndHost, IKeyboardInputSink {
private:
HWND dialog;
protected:
virtual HandleRef BuildWindowCore(HandleRef hwndParent) override {
InitializeGlobals();
dialog = CreateDialog(hInstance,
MAKEINTRESOURCE(IDD_DIALOG1),
(HWND) hwndParent.Handle.ToPointer(),
(DLGPROC) About);
return HandleRef(this, IntPtr(dialog));
}
virtual void DestroyWindowCore(HandleRef hwnd) override {
// hwnd will be disposed for us
}
Here we use the usual CreateDialog to create our dialog box that’s really a control. Since this is one of the first methods that call in our DLL, we also do some standard Win32 initialization by calling a function we wrote called InitializeGlobals():
bool initialized = false;
void InitializeGlobals() {
if (initialized) return;
initialized = true;
// TODO: Place code here.
MSG msg;
HACCEL hAccelTable;
// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_TYPICALWIN32DIALOG, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
}
Work around Visual C++ beta 2 bug
Now that should be enough to get a fully compilable HwndHost subclass but due to a bug in the beta 2 release of Visual C++, we need to provide an implementation of KeyboardInputSite:
// BUG: Visual C++ shouldn't force us to override
// HwndHost's KeyboardInputSite.
IKeyboardInputSite^ site;
virtual property IKeyboardInputSite^ KeyboardInputSite {
IKeyboardInputSite^ get() {
return site;
}
void set(IKeyboardInputSite^ value) {
site = value;
}
};
Actually, there’s a couple bugs in C++ related to overriding methods. It’s possible to use the “override” keyword on a method that’s not there, and the compiler won’t complain. And if you don’t override a method that you need to override because its abstract (like DestroyWindowCore), the compiler won’t always tell you this. And if you’re new to managed C++, make sure to familiarize yourself with the override keyword, because not specifying that will get you a separate method with the same name rather than overriding the existing method. So you need to be a little careful, and if you find your override isn’t getting called you should carefully check your method signature. But the good news is once you type it right, it all works, had hopefully these problems will be fixed in the final release of Visual C++.
Override TranslateAccelerator method to handle dialog keys
If we ran this sample now, we would get a dialog control that displays, but it would ignore all of the keyboard processing that makes a dialog box a dialog box. Let’s address that. Our first step is overriding the TranslateAccelerator implementation (which comes from IkeyboardInputSink – an interface that HwndHost implements). This method gets called when the application receives WM_KEYDOWN and WM_SYSKEYDOWN.
#undef TranslateAccelerator
virtual bool TranslateAccelerator(Microsoft::Win32::MSG% msg,
ModifierKeys modifiers) override
{
MSG m = ConvertMessage(msg);
// Win32's IsDialogMessage() will handle most of our tabbing, but doesn't know
// what to do when it reaches the last tab stop
if (m.message == WM_KEYDOWN && m.wParam == VK_TAB) {
HWND firstTabStop = GetDlgItem(dialog, IDC_EDIT1);
HWND lastTabStop = GetDlgItem(dialog, IDCANCEL);
TraversalRequest^ request = nullptr;
if (GetKeyState(VK_SHIFT) && GetFocus() == firstTabStop) {
// this code should work, but shift-tab doesn't work in May CTP
request = gcnew TraversalRequest(TraversalMode::Last);
}
else if (GetFocus() == lastTabStop) {
request = gcnew TraversalRequest(TraversalMode::Next);
}
if (request != nullptr)
return KeyboardInputSite->OnNoMoreTabStops(request);
}
// Only call IsDialogMessage for keys it will do something with.
if (msg.message == WM_SYSKEYDOWN || msg.message == WM_KEYDOWN) {
switch (m.wParam) {
case VK_TAB:
case VK_LEFT:
case VK_UP:
case VK_RIGHT:
case VK_DOWN:
case VK_EXECUTE:
case VK_RETURN:
case VK_ESCAPE:
case VK_CANCEL:
IsDialogMessage(dialog, &m);
// IsDialogMessage should be called ProcessDialogMessage --
// it processes messages without ever really telling you
// if it handled a specific message or not
return true;
}
}
return false; // not a key we handled
}
This is a big one so let’s break it into pieces. First, we’re using C++ and C++ has macros, we need to be aware that there’s already a macro named TranslateAccelerator, which is defined in winuser.h:
#define TranslateAccelerator TranslateAcceleratorW
So we #undef TranslateAccelerator, so we define a TranslateAccelerator method and not a TranslateAcceleratorW method.
Similarly, there’s both the unmanaged winuser.h MSG and the managed Microsoft::Win32::MSG struct. You can disambiguate between the two using C++’s :: operator.
virtual bool TranslateAccelerator(Microsoft::Win32::MSG% msg,
ModifierKeys modifiers) override
{
::MSG m = ConvertMessage(msg);
Both MSGs have the same data, but sometimes it’s easier to work with the unmanaged definition, so in this sample we defined the obvious conversion routine:
MSG ConvertMessage(Microsoft::Win32::MSG% msg) {
MSG m;
m.hwnd = (HWND) msg.hwnd.ToPointer();
m.lParam = (LPARAM) msg.lParam.ToPointer();
m.message = msg.message;
m.wParam = (WPARAM) msg.wParam.ToPointer();
m.time = msg.time;
POINT pt;
pt.x = msg.pt_x;
pt.y = msg.pt_y;
m.pt = pt;
return m;
}
Back to TranslateAccelerator. Our basic game plan is to call Win32’s IsDialogMessage to do as much work as possible, but IsDialogMessage doesn’t know about anything outside the dialog so we’ll have to help it with that. So as we tab around the dialog, when we run past the last control in our dialog we need to set focus to the Avalon portion by calling IKeyboardInputSite::OnNoMoreStops.
// Win32's IsDialogMessage() will handle most of our tabbing, but doesn't know
// what to do when it reaches the last tab stop
if (m.message == WM_KEYDOWN && m.wParam == VK_TAB) {
HWND firstTabStop = GetDlgItem(dialog, IDC_EDIT1);
HWND lastTabStop = GetDlgItem(dialog, IDCANCEL);
TraversalRequest^ request = nullptr;
if (GetKeyState(VK_SHIFT) && GetFocus() == firstTabStop) {
// this code should work, but shift-tab doesn't work in May CTP
request = gcnew TraversalRequest(TraversalMode::Last);
}
else if (GetFocus() == lastTabStop) {
request = gcnew TraversalRequest(TraversalMode::Next);
}
if (request != nullptr)
return KeyboardInputSite->OnNoMoreTabStops(request);
}
Finally, we are ready to call IsDialogMessage. But one of the responsibilities of a TranslateAccelerator method is telling Avalon whether you handled the keystroke or not, so that if you didn’t the input event can tunnel and bubble through the rest of the application. Unfortunately, IsDialogMessage doesn’t tell us whether it handles a particular keystroke. Even worse, it will call DispatchMessage() on keystrokes it shouldn’t handle! So we reverse engineer IsDialogMessage, and only call it for the keys we know it will handle:
// Only call IsDialogMessage for keys it will do something with.
if (msg.message == WM_SYSKEYDOWN || msg.message == WM_KEYDOWN) {
switch (m.wParam) {
case VK_TAB:
case VK_LEFT:
case VK_UP:
case VK_RIGHT:
case VK_DOWN:
case VK_EXECUTE:
case VK_RETURN:
case VK_ESCAPE:
case VK_CANCEL:
IsDialogMessage(dialog, &m);
// IsDialogMessage should be called ProcessDialogMessage --
// it processes messages without ever really telling you
// if it handled a specific message or not
return true;
}
Override TabInto method to support tabbing
Now that we’ve implemented TranslateAccelerator, we can tab around inside the dialog box, and tab out of it into the greater Avalon application. But we can’t tab back into the dialog box. To solve that, we override TabInto:
virtual bool TabInto(TraversalRequest^ request) override {
if (request->Mode == TraversalMode::Last) {
HWND lastTabStop = GetDlgItem(dialog, IDCANCEL);
SetFocus(lastTabStop);
}
else {
HWND firstTabStop = GetDlgItem(dialog, IDC_EDIT1);
SetFocus(firstTabStop);
}
return true;
}
The TraversalRequest parameter tells us whether it’s a tab or shift tab.
Override OnMnemonic method to support mnemonics
Our keyboard handling is almost complete, but there’s one thing missing – mnemonics don’t work. If you press alt-F, focus doesn’t jump to the “First name:” edit box. And that’s where the OnMnemonic method comes in:
virtual bool OnMnemonic(Microsoft::Win32::MSG% msg, ModifierKeys modifiers) override {
MSG m = ConvertMessage(msg);
// If it's one of our mnemonics, set focus to the appropriate hwnd
if (msg.message == WM_SYSCHAR && GetKeyState(VK_MENU /*alt*/)) {
int dialogitem = 9999;
switch (m.wParam) {
case 's': dialogitem = IDOK; break;
case 'c': dialogitem = IDCANCEL; break;
case 'f': dialogitem = IDC_EDIT1; break;
case 'l': dialogitem = IDC_EDIT2; break;
case 'p': dialogitem = IDC_EDIT3; break;
case 'a': dialogitem = IDC_EDIT4; break;
case 'i': dialogitem = IDC_EDIT5; break;
case 't': dialogitem = IDC_EDIT6; break;
case 'z': dialogitem = IDC_EDIT7; break;
}
if (dialogitem != 9999) {
HWND hwnd = GetDlgItem(dialog, dialogitem);
SetFocus(hwnd);
return true;
}
}
return false; // key unhandled
};
(Why didn’t we call IsDialogMessage here? We have the same issue as before that we need to be able to tell Avalon whether we handled the keystroke or not, and IsDialogMessage won’t do that. But there’s a second problem, because IsDialogMessage refuses to process the mnemonic if the focused hwnd isn’t inside the dialog box.)
Instantiate our HwndHost subclass
Finally, it’s time to put our HwndHost into our larger Avalon application. If the main application is written in xaml, the easiest way to put in the right place is to leave an empty Border element where we want to put the HwndHost. Here we create a <Border> named “insertHwndHostHere”:
<StackPanel>
<Button Content="Avalon button"/>
<Border Name="insertHwndHostHere" Height="300" Width="500"/>
<Button Content="Avalon button"/>
</StackPanel>
Then all that’s left is a good place to instantiate the HwndHost, and connect it to the Border. In our example, we’ll put it inside the constructor for our Window subclass:
public Window1() {
InitializeComponent();
HwndHost host = new ManagedCpp.MyHwndHost();
insertHwndHostHere.Child = host;
}
In conclusion
You can build some wonderful programs using Avalon, and there’s no need to the throwaway you’re existing investment in Win32 technologies in order to leverage Avalon. As we’ve seen, we can put hwnds inside Avalon programs, and we can put Avalon code inside of hwnds, and even mixtures of those two, to create incredibly powerful programs that leverage your existing code. HwndSource, HwndHost, and IKeyboardInputSink make all of this possible.
© 2005 Microsoft Corporation. All rights reserved.
Comments
- Anonymous
July 17, 2005
Could you whip up an example that has IE window inside Avalon, in such manner that you could change the size of the IE window inside the avalon window from 1:1 to smaller "icon" which still shows any animations etc in the IE in the scaled down window. The IE window size would stay the same but the resolution would drop when scaling.
Is this kind of thing easy? - Anonymous
July 18, 2005
So that&nbsp;concludes my series about hwnd interop, which is really a draft of the hwnd interop white...