Come faccio a rendere mobile una finestra WPF trascinando la cornice della finestra estesa?

In applicazioni come Windows Explorer e Internet Explorer, è ansible afferrare le aree del fotogramma estese sotto la barra del titolo e trascinare le windows.

Per le applicazioni WinForms, i moduli e i controlli sono quanto più vicini alle API Win32 native che possono ottenere; si limiterebbe semplicemente a sovrascrivere il gestore WndProc() nella loro forma, elaborare il messaggio della finestra WM_NCHITTEST e WM_NCHITTEST il sistema a pensare che un clic sull’area del frame fosse davvero un clic sulla barra del titolo restituendo HTCAPTION . L’ho fatto nelle mie app di WinForms con un effetto delizioso.

In WPF, posso anche implementare un metodo WndProc() simile e collegarlo all’handle della mia finestra WPF estendendo il frame della finestra nell’area client, in questo modo:

 // In MainWindow // For use with window frame extensions private IntPtr hwnd; private HwndSource hsource; private void Window_SourceInitialized(object sender, EventArgs e) { try { if ((hwnd = new WindowInteropHelper(this).Handle) == IntPtr.Zero) { throw new InvalidOperationException("Could not get window handle for the main window."); } hsource = HwndSource.FromHwnd(hwnd); hsource.AddHook(WndProc); AdjustWindowFrame(); } catch (InvalidOperationException) { FallbackPaint(); } } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { switch (msg) { case DwmApiInterop.WM_NCHITTEST: handled = true; return new IntPtr(DwmApiInterop.HTCAPTION); default: return IntPtr.Zero; } } 

Il problema è che, dal momento che sto impostando ciecamente handled = true e restituendo HTCAPTION , facendo clic ovunque, l’icona della finestra oi pulsanti di controllo causano il trascinamento della finestra. Cioè, tutto ciò che è evidenziato in rosso sotto causa il trascinamento. Ciò include anche le maniglie di ridimensionamento ai lati della finestra (l’area non client). I miei controlli WPF, vale a dire le caselle di testo e il controllo struttura a tabs, smettono anche di ricevere clic come risultato:

Quello che voglio è solo per

  1. la barra del titolo e
  2. le regioni dell’area clienti …
  3. … che non sono occupati dai miei controlli

essere trascinabile. Cioè, voglio solo trascinare queste regioni rosse (area client + barra del titolo):

Come WndProc() mio metodo WndProc() e il resto della XAML / code-behind della mia finestra, per determinare quali aree devono restituire HTCAPTION e quali no? Sto pensando a qualcosa sulla falsariga di usare Point s per verificare la posizione del click rispetto alle posizioni dei miei controlli, ma non sono sicuro di come farlo in WPF land.

EDIT [4/24]: un modo semplice per farlo è avere un controllo invisibile, o anche la finestra stessa, rispondere a MouseLeftButtonDown invocando DragMove() sulla finestra (vedi la risposta di Ross ). Il problema è che per qualche ragione DragMove() non funziona se la finestra è ingrandita, quindi non funziona con Windows 7 Aero Snap. Dal momento che sto andando per l’integrazione di Windows 7, non è una soluzione accettabile nel mio caso.

Codice d’esempio

Grazie a un’e-mail che ho ricevuto questa mattina, mi è stato richiesto di creare un’app di esempio funzionante che dimostri questa funzionalità. L’ho fatto ora; lo puoi trovare su GitHub (o su CodePlex ora archiviato ). Basta clonare il repository o scaricare ed estrarre un archivio, quindi aprirlo in Visual Studio, ed eseguirlo ed eseguirlo.

L’applicazione completa nella sua interezza è dotata di licenza MIT, ma probabilmente la smonterai e inserirai alcuni bit del suo codice piuttosto che usare il codice app in modo completo, non che la licenza ti impedisca di farlo. Inoltre, mentre so che il design della finestra principale dell’applicazione non è affatto simile a quello dei wireframe sopra, l’idea è la stessa proposta nella domanda.

Spero che questo aiuti qualcuno!

Soluzione passo-passo

Finalmente l’ho risolto. Grazie a Jeffrey L Whitledge per avermi indicato nella giusta direzione! La sua risposta è stata accettata perché se non fosse per questo non sarei riuscito a trovare una soluzione. EDIT [9/8]: questa risposta è ora accettata in quanto è più completa; Invece, per il suo aiuto, darò a Jeffrey una bella generosità.

Per i posteri, ecco come l’ho fatto (citando la risposta di Jeffrey se pertinente mentre vado):

Ottieni la posizione del clic del mouse (da wParam, lParam forse?), E usalo per creare un Point (possibilmente con una sorta di trasformazione delle coordinate?).

Questa informazione può essere ottenuta dal lParam del messaggio WM_NCHITTEST . La coordinata x del cursore è la sua parola di ordine basso e la coordinata y del cursore è la sua parola di ordine superiore, come descritto da MSDN .

Poiché le coordinate sono relative all’intero schermo, devo richiamare Visual.PointFromScreen() sulla mia finestra per convertire le coordinate in modo che siano relative allo spazio della finestra.

Quindi chiama il metodo statico VisualTreeHelper.HitTest(Visual,Point) passando this e il Point appena creato. Il valore restituito indicherà il controllo con l’ordine Z più alto.

Ho dovuto passare al controllo Grid primo livello invece di this come visivo per testare il punto. Allo stesso modo dovevo controllare se il risultato era nullo invece di controllare se era la finestra. Se è nullo, il cursore non ha colpito nessuno dei controlli figli della griglia – in altre parole, ha colpito la regione del riquadro della finestra non occupata. Ad ogni modo, la chiave era usare il metodo VisualTreeHelper.HitTest() .

Ora, detto questo, ci sono due avvertenze che potrebbero applicarsi a te se segui i miei passi:

  1. Se non si copre l’intera finestra e invece si estende solo parzialmente il canvasio della finestra, è necessario posizionare un controllo sul rettangolo che non viene riempito dal canvasio della finestra come riempimento dell’area client.

    Nel mio caso, l’area del contenuto del mio controllo struttura a tabs si adatta bene all’area rettangular, come mostrato nei diagrammi. Nell’applicazione, potrebbe essere necessario posizionare una forma Rectangle o un controllo del Panel e dipingerlo con il colore appropriato. In questo modo verrà colpito il controllo.

    Questo problema relativo ai riempitivi dell’area cliente conduce a quello successivo:

  2. Se la griglia o un altro controllo di livello superiore ha una trama di sfondo o una sfumatura sul riquadro della finestra estesa, l’intera area della griglia risponderà al colpo, anche su qualsiasi regione completamente trasparente dello sfondo (vedere Hit Test nel livello visivo ). In tal caso, dovrai ignorare i colpi contro la griglia stessa e prestare attenzione solo ai controlli al suo interno.

Quindi:

 // In MainWindow private bool IsOnExtendedFrame(int lParam) { int x = lParam << 16 >> 16, y = lParam >> 16; var point = PointFromScreen(new Point(x, y)); // In XAML: ... var result = VisualTreeHelper.HitTest(windowGrid, point); if (result != null) { // A control was hit - it may be the grid if it has a background // texture or gradient over the extended window frame return result.VisualHit == windowGrid; } // Nothing was hit - assume that this area is covered by frame extensions anyway return true; } 

La finestra è ora mobile facendo clic e trascinando solo le aree non occupate della finestra.

Ma non è tutto. Ricordiamo nella prima illustrazione che l’area non client comprendente i bordi della finestra era influenzata anche da HTCAPTION quindi la finestra non era più ridimensionabile.

Per risolvere questo problema ho dovuto verificare se il cursore stava colpendo l’area client o l’area non client. Per verificare ciò, avevo bisogno di usare la funzione DefWindowProc() e vedere se restituiva HTCLIENT :

 // In my managed DWM API wrapper class, DwmApiInterop public static bool IsOnClientArea(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam) { if (uMsg == WM_NCHITTEST) { if (DefWindowProc(hWnd, uMsg, wParam, lParam).ToInt32() == HTCLIENT) { return true; } } return false; } // In NativeMethods [DllImport("user32.dll")] private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam); 

Infine, ecco il mio metodo di procedura finale per la finestra:

 // In MainWindow private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { switch (msg) { case DwmApiInterop.WM_NCHITTEST: if (DwmApiInterop.IsOnClientArea(hwnd, msg, wParam, lParam) && IsOnExtendedFrame(lParam.ToInt32())) { handled = true; return new IntPtr(DwmApiInterop.HTCAPTION); } return IntPtr.Zero; default: return IntPtr.Zero; } } 

Ecco qualcosa che potresti provare:

Ottieni la posizione del clic del mouse (da wParam, lParam forse?), E usalo per creare un Point (possibilmente con una sorta di trasformazione delle coordinate?).

Quindi chiama il metodo statico VisualTreeHelper.HitTest(Visual,Point) passando this e il Point appena creato. Il valore restituito indicherà il controllo con l’ordine Z più alto. Se questa è la tua finestra, allora fai il tuo voodoo HTCAPTION . Se è un altro controllo, allora … non farlo.

In bocca al lupo!

Cercando di fare la stessa cosa (rendere il mio vetro Aero esteso trascinabile nella mia app WPF), mi sono imbattuto in questo post tramite Google. Ho letto la tua risposta, ma ho deciso di continuare a cercare per vedere se c’era qualcosa di più semplice.

Ho trovato una soluzione molto meno intensiva del codice.

Basta creare un object trasparente dietro i controlli e assegnargli un gestore di eventi pulsante sinistro del mouse che chiama il metodo DragMove() della finestra.

Ecco la sezione del mio XAML che appare sul mio esteso vetro Aero:

     

E il code-behind (questo è all’interno di una class Window , quindi DragMove() è disponibile per chiamare direttamente):

 private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { DragMove(); } 

E questo è tutto! Per la tua soluzione, dovresti aggiungere più di uno di questi per ottenere la tua area trascinabile non rettangular.

modo semplice è creare uno stackpanel o qualsiasi cosa tu desideri per la tua barra del titolo XAML

   

codice

  private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { DragMove(); }