Comment créer une fenêtre WPF en faisant glisser le cadre de la fenêtre étendue?

Dans les applications telles que l’Explorateur Windows et Internet Explorer, il est possible de saisir les zones d’image étendues sous la barre de titre et de faire glisser les fenêtres.

Pour les applications WinForms, les formulaires et les contrôles sont aussi proches que possible des API Win32 natives; il suffit de remplacer le gestionnaire WndProc() dans son formulaire, de traiter le message de la fenêtre WM_NCHITTEST et de faire croire au système qu’un clic sur la zone du cadre était réellement un clic sur la barre de titre en renvoyant HTCAPTION . Je l’ai fait dans mes propres applications WinForms pour un effet délicieux.

Dans WPF, je peux également implémenter une méthode WndProc() similaire et l’accrocher au handle de ma fenêtre WPF tout en étendant le frame de fenêtre dans la zone cliente, comme ceci:

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

Le problème est que, comme je règle aveuglément HTCAPTION handled = true et que HTCAPTION retourne HTCAPTION , cliquez n’importe où, mais l’icône de la fenêtre ou les boutons de contrôle font glisser la fenêtre. C’est-à-dire que tout ce qui est mis en évidence en rouge provoque un glissement. Cela inclut même les poignées de redimensionnement sur les côtés de la fenêtre (la zone non-client). Mes contrôles WPF, à savoir les zones de texte et le contrôle onglet, cessent également de recevoir des clics:

Ce que je veux c’est pour seulement

  1. la barre de titre et
  2. les régions de l’espace client …
  3. … qui ne sont pas occupés par mes contrôles

être draggable. Autrement dit, je veux seulement que ces régions rouges soient déplaçables (zone client + barre de titre):

Comment modifier ma méthode WndProc() et le rest de ma fenêtre XAML / code-behind, pour déterminer quelles zones doivent renvoyer HTCAPTION et lesquelles ne devraient pas l’être? Je pense à quelque chose comme l’utilisation de Point s pour vérifier l’emplacement du clic par rapport aux emplacements de mes contrôles, mais je ne suis pas sûr de savoir comment s’y prendre dans WPF land.

EDIT [4/24]: un moyen simple est d’avoir un contrôle invisible, ou même la fenêtre elle-même, qui répond à MouseLeftButtonDown en DragMove() sur la fenêtre (voir la réponse de Ross ). Le problème est que pour une raison quelconque, DragMove() ne fonctionne pas si la fenêtre est agrandie, donc il ne fonctionne pas bien avec Windows 7 Aero Snap. Comme je vais pour l’intégration de Windows 7, ce n’est pas une solution acceptable dans mon cas.

Code exemple

Grâce à un e-mail que j’ai reçu ce matin, j’ai été invité à créer un exemple d’application illustrant cette fonctionnalité. Je l’ai fait maintenant; vous pouvez le trouver sur GitHub (ou dans CodePlex maintenant archivé ). Clonez simplement le référentiel ou téléchargez et extrayez une archive, puis ouvrez-le dans Visual Studio et comstackz-le et exécutez-le.

L’ensemble de l’application dans son intégralité est sous licence MIT, mais vous allez probablement le démonter et mettre des bits de son code autour de vous plutôt que d’utiliser le code de l’application en entier – pas que la licence vous empêche de le faire non plus. De plus, bien que je sache que la conception de la fenêtre principale de l’application n’est pas similaire à celle des structures filaires ci-dessus, l’idée est la même que celle posée dans la question.

J’espère que cela aide quelqu’un!

Solution pas à pas

Je l’ai finalement résolu. Merci à Jeffrey L Whitledge de m’avoir orienté dans la bonne direction! Sa réponse a été acceptée car sinon, je n’aurais pas réussi à trouver une solution. EDIT [9/8]: cette réponse est maintenant acceptée car elle est plus complète. Je donne à Jeffrey une belle grosse prime pour son aide.

Pour l’amour de la postérité, voici comment je l’ai fait (en citant la réponse de Jeffrey lorsque cela est pertinent):

Obtenir l’emplacement du clic de la souris (à partir du wParam, lParam peut-être?), Et l’utiliser pour créer un Point (éventuellement avec une sorte de transformation de coordonnées?).

Cette information peut être obtenue à partir du lParam du message WM_NCHITTEST . La coordonnée x du curseur est son mot de poids faible et la coordonnée y du curseur est son mot de poids fort , comme décrit dans MSDN .

Comme les coordonnées sont relatives à la totalité de l’écran, je dois appeler Visual.PointFromScreen() sur ma fenêtre pour convertir les coordonnées relatives à l’espace de la fenêtre.

Ensuite, appelez la méthode statique VisualTreeHelper.HitTest(Visual,Point) passant this et le Point que vous venez de faire. La valeur de retour indiquera le contrôle avec le Z-Order le plus élevé.

J’ai dû passer le contrôle Grid haut niveau au lieu de this comme visuel pour tester le point. De même, je devais vérifier si le résultat était nul au lieu de vérifier s’il s’agissait de la fenêtre. Si la valeur est NULL, le curseur n’a touché aucun des contrôles enfants de la grid – en d’autres termes, il a atteint la zone du cadre de la fenêtre inoccupée. Quoi qu’il en soit, la clé était d’utiliser la méthode VisualTreeHelper.HitTest() .

Maintenant, cela dit, il y a deux mises en garde qui peuvent s’appliquer à vous si vous suivez mes étapes:

  1. Si vous ne couvrez pas la totalité de la fenêtre et que vous étendez seulement partiellement le cadre de la fenêtre, vous devez placer un contrôle sur le rectangle qui n’est pas rempli par le cadre de la fenêtre en tant que remplissage de la zone client.

    Dans mon cas, la zone de contenu de mon contrôle onglet s’adapte parfaitement à cette zone rectangular, comme indiqué dans les diagrammes. Dans votre application, vous devrez peut-être placer une forme Rectangle ou un contrôle Panel et peindre la couleur appropriée. De cette façon, le contrôle sera frappé.

    Cette question sur les remplisseurs de zone client conduit à la suivante:

  2. Si votre grid ou un autre contrôle de niveau supérieur possède une texture d’arrière-plan ou un dégradé sur le cadre de la fenêtre étendue, la zone de grid entière répondra à l’appel, même sur les régions totalement transparentes de l’arrière-plan (voir Test de la couche visuelle ). Dans ce cas, vous devez ignorer les access à la grid elle-même et ne faire attention qu’aux commandes qui s’y trouvent.

Par conséquent:

 // 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 fenêtre est maintenant mobile en cliquant et en faisant glisser uniquement les zones inoccupées de la fenêtre.

Mais ce n’est pas tout. Rappelez-vous dans la première illustration que la zone non-client comprenant les bordures de la fenêtre était également affectée par HTCAPTION sorte que la fenêtre n’était plus redimensionnable.

Pour résoudre ce problème, je devais vérifier si le curseur frappait la zone client ou la zone non-client. Pour vérifier cela, je devais utiliser la fonction DefWindowProc() et voir si HTCLIENT renvoyé:

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

Enfin, voici ma dernière méthode de procédure de fenêtre:

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

Voici quelque chose que vous pourriez essayer:

Obtenir l’emplacement du clic de la souris (à partir du wParam, lParam peut-être?), Et l’utiliser pour créer un Point (éventuellement avec une sorte de transformation de coordonnées?).

Ensuite, appelez la méthode statique VisualTreeHelper.HitTest(Visual,Point) passant this et le Point que vous venez de faire. La valeur de retour indiquera le contrôle avec le Z-Order le plus élevé. Si c’est votre fenêtre, alors faites votre vaudou HTCAPTION . Si c’est un autre contrôle, alors … ne le faites pas.

Bonne chance!

En cherchant à faire la même chose (faire glisser mon verre Aero étendu dans mon application WPF), je suis tombé sur ce post via Google. J’ai lu votre réponse, mais j’ai décidé de continuer à chercher pour voir s’il y avait quelque chose de plus simple.

J’ai trouvé une solution beaucoup moins intensive en code.

Créez simplement un élément transparent derrière vos contrôles et donnez-lui un gestionnaire d’événements du bouton gauche de la souris qui appelle la méthode DragMove() la fenêtre.

Voici la section de mon XAML qui apparaît sur mon verre Aero étendu:

     

Et le code-behind (ceci se trouve dans une classe Window et DragMove() est disponible pour appeler directement):

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

Et c’est tout! Pour votre solution, il vous faudrait en append plus d’un pour réaliser votre zone de déplacement non rectangular.

manière simple est de créer un stackpanel ou tout ce que vous voulez pour votre barre de titre XAML

   

code

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