Entrada de teclado y navegación TAB entre controles WPF en una aplicación Win32

 C Programming >> Programación C >  >> Tags >> WPF
Entrada de teclado y navegación TAB entre controles WPF en una aplicación Win32

Es posible alojar controles WPF en una aplicación Win32 y viceversa, pero debido a las diferencias entre estas tecnologías, pueden surgir varios problemas. Uno de ellos es el manejo de la entrada del teclado. Sin profundizar demasiado en las diferencias entre WPF y Win32, mostraré cómo proporcionar entrada de teclado para controles WPF alojados en aplicaciones Win32. Para leer sobre las diferencias y la interoperabilidad entre los dos, sugiero WPF y Win32 Interoperation.

Alojamiento de un control WPF en Win32

Para alojar un control WPF en una aplicación Win32, debe seguir varios pasos.

  • Cree un nuevo HwndSource, configurando la ventana principal como su principal. Este es un objeto clave que permite mostrar contenido WPF en una ventana de Win32.
  • Crear una instancia del control o ventana WPF
  • Asigne la referencia a esta instancia del control WPF o propiedad RootVisual de la ventana del objeto HwndSource.

Para simplificar este proceso, tengo esta pequeña clase auxiliar:

#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;
   }
};

Con esta clase puedo crear controles WPF como este:

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";

Habilitar entrada de teclado

Si bien puede usar el mouse con estos controles WPF agregados de esta manera, el teclado no está habilitado. Para proporcionar entrada de teclado para los controles de WPF, necesitamos enlazar HwndSource y agregar un controlador que reciba todos los mensajes de la ventana. Debemos manejar el mensaje WM_GETDLGCODE para que el sistema sepa qué tipo de mensajes queremos manejar por nuestra cuenta (en el control WPF).

Así es como agregamos el gancho:

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

Y así es como se ve el procedimiento de enlace (definido como un miembro estático de mi 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;
}

Al devolver todos estos códigos de diálogo, el sistema sabrá que la ventana desea procesar las teclas de flecha, las teclas de tabulación, todas las teclas y recibir el mensaje WM_CHAR.

Habilitación de la navegación por TAB

Aunque los controles de WPF ahora tienen entrada de teclado, resulta que la navegación con TAB (hacia adelante) o TAB+MAYÚS (hacia atrás) no funciona.

Aquí hay un ejemplo donde tengo una aplicación MFC con cuatro controles WPF, dos botones y dos cuadros de texto. Un botón y un cuadro de texto, así como los botones Aceptar y CANCELAR tienen tabulaciones.

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)";

El cuadro de diálogo de muestra tiene este aspecto:

Presionar la tecla TAB debería permitir navegar desde el botón 1 para editar 1, luego el botón Aceptar , botón CANCELAR y luego volver al botón 1. El botón 2 y editar 2, al no tener definido el estilo de tabulación, no deben incluirse en la navegación.

Sin embargo, como ya se mencionó, esto no funciona. Después de leer sobre una solución para este problema, parecía que la clave estaba en la interfaz IKeyboardInputSink, que tanto HwndSource como HwndHost implementan. Esta interfaz proporciona un receptor de teclado para componentes que administran tabulaciones, aceleradores y mnemónicos a través de los límites de interoperabilidad y entre HWND. Aparentemente, la solución fue:

  • derive la clase HwndSource
  • anula el método TabInto (en realidad, dado que este es un método sellado, tendrías que definir una nueva anulación para él) e implementar allí la lógica de tabulación
  • utilice este HwndSource derivado para presentar contenido WPF en una ventana de Win32

Aunque probé varias cosas, no logré que funcionara. Sin embargo, dado que ya tenía un enlace para todos los mensajes de la ventana y pedí explícitamente recibir WM_CHAR, fue posible usar esto para manejar TAB y TAB+SHIFT. Así que aquí hay una adición al ChildHwndSourceHook anterior:

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);
   }
}

Entonces, si obtenemos un WM_CHAR y el wParam es VK_TAB, entonces consultamos al padre para la siguiente tabulación (para navegación hacia adelante si no se presionó SHIFT, o navegación hacia atrás si también se presionó SHIFT). Si existe tal tabulación, establecemos el foco en esa ventana.

El método FindNextTabStop (agregado como miembro de la clase CWpfControlWrapper) tiene este aspecto:

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;
}

Hace lo siguiente:

  • obtiene la ventana siguiente/anterior en el orden z (que define el orden de parada de tabulación)
  • cuando llega al final/superior del orden z, comienza de nuevo, lo que permite recorrer las ventanas secundarias del padre
  • si el siguiente hijo en orden z es el control actual, entonces terminó de recorrer los hijos del padre y se detiene
  • si el hijo actual en el orden z tiene establecido el estilo WS_TABSTOP, entonces esta es la ventana que estamos buscando

Con esto definido, es posible usar la tecla TAB para navegar entre los controles WPF en una ventana de Win32.

Esta es la aplicación de demostración de MFC que puede probar:Mfc-Wpf Tabbing (1820 descargas).

Proyecto de código