Input da tastiera e navigazione TAB tra i controlli WPF in un'applicazione Win32

 C Programming >> Programmazione C >  >> Tags >> WPF
Input da tastiera e navigazione TAB tra i controlli WPF in un'applicazione Win32

È possibile ospitare i controlli WPF in un'applicazione Win32 e viceversa, ma a causa delle differenze tra queste tecnologie possono verificarsi vari problemi. Uno di questi è la gestione dell'input da tastiera. Senza immergermi troppo nelle differenze tra WPF e Win32, mostrerò come fornire l'input da tastiera per i controlli WPF ospitati in un'applicazione Win32. Per leggere le differenze e l'interoperabilità tra i due suggerisco l'interoperabilità WPF e Win32.

Ospitare un controllo WPF in Win32

Per ospitare un controllo WPF in un'applicazione Win32 è necessario seguire diversi passaggi.

  • Crea un nuovo HwndSource, impostando la finestra genitore come genitore. Questo è un oggetto chiave, che abilita la visualizzazione del contenuto WPF in una finestra Win32.
  • Crea un'istanza del controllo o della finestra WPF
  • Assegna il riferimento a questa istanza del controllo WPF o alla proprietà RootVisual della finestra dell'oggetto HwndSource.

Per semplificare questo processo, ho questa piccola classe di supporto:

#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 questa classe posso creare controlli WPF come questo:

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

Abilitazione dell'input da tastiera

Sebbene tu possa usare il mouse con questi controlli WPF aggiunti in questo modo, la tastiera non è abilitata. Per fornire l'input da tastiera per i controlli WPF, è necessario agganciare HwndSource, aggiungendo un gestore che riceve tutti i messaggi della finestra. Dobbiamo gestire il messaggio WM_GETDLGCODE per far sapere al sistema che tipo di messaggi vogliamo gestire da soli (nel controllo WPF).

Ecco come aggiungiamo il gancio:

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

Ed ecco come appare la procedura hook (definita come membro statico del mio 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;
}

Restituendo tutti questi codici di dialogo, il sistema saprà che la finestra desidera elaborare i tasti freccia, i tasti di tabulazione, tutti i tasti e ricevere il messaggio WM_CHAR.

Abilitazione della navigazione TAB

Anche se i controlli WPF ora hanno l'input da tastiera, la navigazione con TAB (avanti) o TAB+SHIFT (indietro) non funziona.

Ecco un esempio in cui ho un'applicazione MFC con quattro controlli WPF, due pulsanti e due caselle di testo. Un pulsante e una casella di testo, nonché i pulsanti OK e ANNULLA hanno punti di tabulazione.

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

La finestra di dialogo di esempio ha il seguente aspetto:

La pressione del tasto TAB dovrebbe consentire la navigazione dal pulsante 1 alla modifica 1, quindi il pulsante OK , pulsante ANNULLA e poi di nuovo al pulsante 1. Pulsante 2 e modifica 2, non avendo lo stile di tabulazione definito, non dovrebbero essere inclusi nella navigazione.

Come già accennato, questo non funziona, tuttavia. Dopo aver letto una soluzione per questo problema, sembrava che la chiave si trovasse nell'interfaccia IKeyboardInputSink, implementata sia da HwndSource che da HwndHost. Questa interfaccia fornisce una tastiera sink per i componenti che gestiscono tabulazioni, acceleratori e mnemonici oltre i limiti di interoperabilità e tra HWND. Apparentemente la soluzione era:

  • derivare la classe HwndSource
  • sostituire il metodo TabInto (in realtà, poiché si tratta di un metodo sigillato, dovresti definire un nuovo override per esso) e implementare lì la logica di tabulazione
  • usa questo HwndSource derivato per presentare il contenuto WPF in una finestra Win32

Anche se ho provato diverse cose non sono riuscito a farlo funzionare. Tuttavia, poiché avevo già un hook per tutti i messaggi di finestra e avevo chiesto esplicitamente di ricevere WM_CHAR, è stato possibile utilizzarlo per gestire TAB e TAB+SHIFT. Quindi ecco un'aggiunta a ChildHwndSourceHook sopra:

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

Quindi, se otteniamo un WM_CHAR e wParam è VK_TAB, allora interroghiamo il genitore per il prossimo punto di tabulazione (per la navigazione in avanti se SHIFT non è stato premuto, o per la navigazione all'indietro se è stato premuto anche SHIFT). Se è presente una tale tabulazione, impostiamo lo stato attivo su quella finestra.

Il metodo FindNextTabStop (aggiunto come membro della classe CWpfControlWrapper) è simile al seguente:

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

Esegue le seguenti operazioni:

  • ottiene la finestra successiva/precedente nell'ordine z (che definisce l'ordine di interruzione delle tabulazioni)
  • quando raggiunge la fine/inizio dell'ordine z, ricomincia da capo, il che consente di scorrere le finestre figlie del genitore
  • se il figlio successivo nell'ordine z è il controllo corrente, ha finito di scorrere i figli del genitore e si ferma
  • se il figlio corrente nell'ordine z ha lo stile WS_TABSTOP impostato, allora questa è la finestra che stiamo cercando

Con questo definito, è possibile utilizzare il tasto TAB per navigare tra i controlli WPF su una finestra di Win32.

Ecco l'applicazione demo MFC che puoi provare:Mfc-Wpf Tabbing (1820 download).

CodiceProgetto