Keyboard input and TAB navigation between WPF controls in a Win32 application

It is possible to host WPF controls in a Win32 application, and the other way around, but because of the differences betweeb these technologies there are various issues that can appear. One of these is handling of keyboard input. Without diving too much into differences between WPF and Win32, I will show how to provide keyboard input for WPF controls hosted in a Win32 applications. For reading about the differences and the interoping between the two I suggest WPF and Win32 Interoperation.

Hosting a WPF Control in Win32

To host a WPF control in a Win32 application you need to follow several steps.

  • Create a new HwndSource, setting the parent window as it’s parent. This is a key object, that enables displaying of WPF content in a Win32 window.
  • Instantiate the WPF control or window
  • Assign the reference to this instance of the WPF control or window RootVisual property of the HwndSource object.

To simplify this process, I have this small helper class:

#pragma once

#include <vcclr.h>

class CWpfControlWrapper
{
   HWND m_hwndWPF;
   gcroot<System::Windows::Interop::HwndSource^> m_source;
   gcroot<System::Windows::Controls::Control^> m_control;

public:
   CWpfControlWrapper(void):m_hwndWPF(NULL) {}
   ~CWpfControlWrapper(void) {}

   template <typename T>
   T^ Control()
   {
      System::Windows::Controls::Control^ obj = m_control;
      return dynamic_cast<T^>(obj);
   }

   BOOL CreateControl(System::Type^ type, 
                      HWND parent, 
                      DWORD style, 
                      int x, int y, 
                      int width, int height)
   {
      System::Windows::Interop::HwndSourceParameters^ sourceParams = 
         gcnew System::Windows::Interop::HwndSourceParameters("WpfControlWrapper");

      sourceParams->PositionX = x;
      sourceParams->PositionY = y;
      sourceParams->Height = height;
      sourceParams->Width = width;
      sourceParams->ParentWindow = System::IntPtr(parent);
      sourceParams->WindowStyle = style;
      m_source = gcnew System::Windows::Interop::HwndSource(*sourceParams);

      m_control = (System::Windows::Controls::Control^)System::Activator::CreateInstance(type);
      m_source->RootVisual = m_control;

      m_hwndWPF = (HWND)m_source->Handle.ToPointer();      

      return m_hwndWPF == NULL ? FALSE : TRUE;
   }
};

With this class I can create WPF controls like this:

CWpfControlWrapper btn1;
btn1.CreateControl(System::Windows::Controls::Button::typeid, 
                   m_hWnd, 
                   WS_CHILD|WS_VISIBLE|WS_TABSTOP, 
                   10, 10, 210, 24);
btn1.Control<System::Windows::Controls::Button>()->Content = "Button 1";

Enabling Keyboard Input

While you can use the mouse with these WPF controls added like this, the keyboard in not enabled. To provide keyboard input for the WPF controls, we need to hook the HwndSource, adding a handler that receives all window messages. We must handle the WM_GETDLGCODE message to let the system know what kind of messages we want to handle on our own (in the WPF control).

This is how we add the hook:

m_source->AddHook(gcnew System::Windows::Interop::HwndSourceHook(
                  &CWpfControlWrapper::ChildHwndSourceHook));

And this is how the hook procedure looks (defined as a static member of my CWpfControlWrapper):

static System::IntPtr ChildHwndSourceHook(
  System::IntPtr hwnd, 
  int msg, 
  System::IntPtr wParam, 
  System::IntPtr lParam, 
  bool% handled)
{
  if (msg == WM_GETDLGCODE)
  {
     handled = true;
     return System::IntPtr(DLGC_WANTCHARS | DLGC_WANTTAB | DLGC_WANTARROWS | DLGC_WANTALLKEYS);
  }

  return System::IntPtr::Zero;
}

By returning all these dialog codes will let the system know that the window wants to process arrow keys, tab keys, all keys and receive the WM_CHAR message.

Enabling TAB Navigation

Even though the WPF controls now have keyboard input, it turns our that navigating with TAB (forward) or TAB+SHIFT (backwards) does not work.

Here is an example where I have an MFC application with four WPF controls, two buttons and two text boxes. One button and one text box, as well as the OK and CANCEL buttons have tab stops.

CWpfControlWrapper btn1;
btn1.CreateControl(System::Windows::Controls::Button::typeid, 
                  m_hWnd, 
                  WS_CHILD|WS_VISIBLE|WS_TABSTOP, 
                  10, 10, 210, 24);
btn1.Control<System::Windows::Controls::Button>()->Content = "Button 1 (tab stop)";

CWpfControlWrapper btn2;
btn2.CreateControl(System::Windows::Controls::Button::typeid, 
                  m_hWnd, 
                  WS_CHILD|WS_VISIBLE, 
                  10, 40, 210, 24);
btn2.Control<System::Windows::Controls::Button>()->Content = "Button 2 (no tab stop)";

CWpfControlWrapper edit1;
edit1.CreateControl(System::Windows::Controls::TextBox::typeid, 
                   m_hWnd, 
                   WS_CHILD|WS_VISIBLE|WS_TABSTOP, 
                   10, 70, 210, 24);
edit1.Control<System::Windows::Controls::TextBox>()->Text = "edit 1 (tab stop)";

CWpfControlWrapper edit2;
edit2.CreateControl(System::Windows::Controls::TextBox::typeid, 
                   m_hWnd, 
                   WS_CHILD|WS_VISIBLE, 
                   10, 100, 210, 24);
edit2.Control<System::Windows::Controls::TextBox>()->Text = "edit 2 (no tab stop)";

The sample dialog box looks like this:

Pressing the TAB key should allow navigating from button 1 to edit 1, then button OK, button CANCEL and then back to button 1. Button 2 and edit 2, not having the tab stop style defined, should not be included in the navigation.

As already mentioned, this does not work, however. After reading about a solution for this problem, it looked like the key lied in the IKeyboardInputSink interface, that both HwndSource and HwndHost implement. This interface provides a keyboard sink for components that manage tabbing, accelerators, and mnemonics across interop boundaries and between HWNDs. Apparently the solution was to:

  • derive the HwndSource class
  • override the TabInto method (actually, since this is a sealed method you’d have to define a new override for it) and implement there the tabbing logic
  • use this derived HwndSource to present WPF content in a Win32 window

Though I tried several things I didn’t manage to make it work. However, since I already had a hook for all window messages, and explicitly asked for receiving WM_CHAR, it was possible to use this to handle TAB and TAB+SHIFT. So here is an addition to the ChildHwndSourceHook above:

else if(msg == WM_CHAR)
{
   if(wParam.ToInt32() == VK_TAB)
   {
      handled = true;
      HWND nextTabStop = FindNextTabStop((HWND)hwnd.ToPointer(), 
                                         (GetKeyState(VK_SHIFT) & 0x8000) != 0x8000);
      if(nextTabStop)
         ::SetFocus(nextTabStop);
   }
}

So if we get a WM_CHAR and the wParam is VK_TAB, then we query the parent for the next tab stop (for forward navigation if SHIFT was not pressed, or backwards navigation if SHIFT was also pressed). If there is such a tab stop we set focus on that window.

The FindNextTabStop method (added as a member of the CWpfControlWrapper class) looks like this:

static HWND FindNextTabStop(HWND wnd, bool forward)
{
  HWND nextstop = NULL;
  HWND nextwnd = wnd;
  do
  {
     // get the next/previous window in the z-order
     nextwnd = ::GetWindow(nextwnd, forward ? GW_HWNDNEXT : GW_HWNDPREV);

     // if we are at the end of the z-order, start from the top/bottom
     if(nextwnd == NULL) 
        nextwnd = ::GetWindow(wnd, forward ? GW_HWNDFIRST : GW_HWNDLAST);

     // if we returned to the same control then we iterated the entire z-order
     if(nextwnd == wnd)
        break;

     // get the window style and check the WS_TABSTOP style
     DWORD style = ::GetWindowLongPtr(nextwnd, GWL_STYLE);
     if((style & WS_TABSTOP) == WS_TABSTOP)
        nextstop = nextwnd;
  }while(nextstop == NULL);
  
  return nextstop;
}

It does the following:

  • it gets the next/previous window in the z-order (which defines the tab stop order)
  • when it reaches the end/top of the z-order, it starts all over again, which enables looping through the child windows of the parent
  • if the next child the in z-order is the current control, then it finished looping through the children of the parent and it stops
  • if the current child in the z-order has the WS_TABSTOP style set, then this is the window we are looking for

With this defined, it is possible to use the TAB key to navigate between the WPF controls on a Win32 window.

Here is the MFC demo application that you can try: Mfc-Wpf Tabbing (3296 downloads ) .

3 Replies to “Keyboard input and TAB navigation between WPF controls in a Win32 application”

  1. Pingback: shoes
  2. Pingback: Aziz

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.