55 votes

Comment rendre une fenêtre WPF mobile en faisant glisser le cadre étendu de la fenêtre ?

Dans des applications comme Windows Explorer et Internet Explorer, on peut saisir les zones de cadre étendues sous la barre de titre et faire glisser Windows.

Pour les applications WinForms, les formulaires et les contrôles sont aussi proches des API Win32 natives qu'il est possible de l'être ; il suffit de remplacer la balise WndProc() dans leur formulaire, traitent le WM_NCHITTEST et faire croire au système qu'un clic sur la zone du cadre est en réalité un clic sur la barre de titre en renvoyant le message suivant HTCAPTION . Je l'ai fait dans mes propres applications WinForms avec un effet délicieux.

Dans WPF, je peux également mettre en œuvre un système similaire de WndProc() et l'accrocher au handle de ma fenêtre WPF tout en étendant le cadre de la fenêtre dans la zone client, 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, puisque je règle aveuglément handled = true et renvoyant HTCAPTION en cliquant partout mais l'icône de la fenêtre ou les boutons de contrôle provoquent le déplacement de la fenêtre. En d'autres termes, tout ce qui est mis en évidence en rouge ci-dessous provoque un glissement. Cela inclut même les poignées de redimensionnement situées sur les côtés de la fenêtre (la zone non cliente). Mes contrôles WPF, à savoir les zones de texte et le contrôle de tabulation, cessent également de recevoir des clics en conséquence :

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ées par mes commandes.

pour qu'il soit possible de le faire glisser. En d'autres termes, je veux que seules ces régions rouges puissent être déplacées (zone client + barre de titre) :

Comment puis-je modifier mon WndProc() et le reste du code XAML/code-behind de ma fenêtre, pour déterminer quelles zones doivent renvoyer HTCAPTION et lesquels ne devraient pas l'être ? Je pense à quelque chose du genre de l'utilisation de Point pour vérifier l'emplacement du clic par rapport à l'emplacement de mes contrôles, mais je ne suis pas sûr de la façon de procéder dans le monde WPF.

EDIT [4/24] : Une façon simple d'y parvenir est de faire en sorte qu'un contrôle invisible, ou même la fenêtre elle-même, réponde aux questions suivantes MouseLeftButtonDown en invoquant DragMove() sur la fenêtre (voir Réponse de Ross ). Le problème est que pour une raison quelconque DragMove() ne fonctionne pas si la fenêtre est agrandie, ce qui ne convient pas à Windows 7 Aero Snap. Comme j'opte pour l'intégration de Windows 7, ce n'est pas une solution acceptable dans mon cas.

10 votes

Cette question comprend un bref tutoriel sur la gestion des messages Windows en C#, elle comporte des illustrations bien conçues qui indiquent exactement ce qui est demandé, et il n'y a pas de fautes de frappe évidentes. +1, bébé !

0 votes

@Jeffrey L Whitledge : Mince, merci (+1 à vous) ! La seule chose que j'ai dû modifier à la fin était le titre de la question... Je jure que le titre précédent n'était pas ce que j'avais écrit avant de le poster.

31voto

BoltClock Points 249668

Exemple de code

Grâce à un courriel que j'ai reçu ce matin, j'ai été invité à créer un exemple d'application pour démontrer cette fonctionnalité. C'est maintenant chose faite ; vous pouvez la trouver sur GitHub (ou dans le CodePlex, désormais archivé ). Il suffit de cloner le dépôt ou de télécharger et d'extraire une archive, puis de l'ouvrir dans Visual Studio, de la construire et de l'exécuter.

L'application complète dans son intégralité est sous licence MIT, mais vous allez probablement la démonter et mettre des morceaux de son code autour du vôtre plutôt que d'utiliser le code de l'application dans son intégralité - pas que la licence vous empêche de le faire non plus. De plus, même si je sais que le design de la fenêtre principale de l'application n'est pas du tout similaire aux maquettes ci-dessus, l'idée est la même que celle posée dans la question.

J'espère que cela aidera quelqu'un !

Solution étape par étape

J'ai finalement résolu le problème. Merci à Jeffrey L Whitledge pour m'avoir indiqué la bonne direction ! Sa réponse a été acceptée car, sans elle, 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 prime pour son aide.

Pour la postérité, voici comment j'ai procédé (en citant la réponse de Jeffrey lorsque cela est pertinent) :

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

Ces informations peuvent être obtenues auprès du lParam de la WM_NCHITTEST message. 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 suit MSDN décrit .

Puisque les coordonnées sont relatives à l'écran entier, je dois appeler Visual.PointFromScreen() sur ma fenêtre pour convertir les coordonnées afin qu'elles soient relatives à l'espace de la fenêtre.

Ensuite, appelez la méthode statique VisualTreeHelper.HitTest(Visual,Point) le faire passer this y el Point que vous venez de faire. La valeur de retour indiquera le contrôle avec l'ordre Z le plus élevé.

J'ai dû passer dans le niveau supérieur Grid au lieu de this comme le visuel à tester contre le point. De même, je devais vérifier si le résultat était nul au lieu de vérifier si c'était la fenêtre. S'il est nul, le curseur n'a touché aucun des contrôles enfants de la grille - en d'autres termes, il a touché la région inoccupée du cadre de la fenêtre. Quoi qu'il en soit, la clé était d'utiliser la fonction VisualTreeHelper.HitTest() méthode.

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, mais que vous n'étendez que 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 pour remplir la zone client.

    Dans mon cas, la zone de contenu de mon contrôle de tabulation s'adapte parfaitement à cette zone rectangulaire, comme le montrent les diagrammes. Dans votre application, vous devrez peut-être placer un contrôle de tabulation Rectangle ou une Panel et le peindre de la couleur appropriée. De cette façon, le contrôle sera touché.

    Cette question sur les produits de comblement de l'espace client mène à la suivante :

  2. Si votre grille ou autre contrôle de niveau supérieur a une texture ou un dégradé d'arrière-plan sur le cadre de la fenêtre étendue, toute la zone de la grille répondra à la frappe, même sur les régions entièrement transparentes de l'arrière-plan (cf. Test d'impact dans la couche visuelle ). Dans ce cas, vous voudrez ignorer les frappes contre la grille elle-même et ne prêter attention qu'aux contrôles qui s'y trouvent.

D'où :

// In MainWindow
private bool IsOnExtendedFrame(int lParam)
{
    int x = lParam << 16 >> 16, y = lParam >> 16;
    var point = PointFromScreen(new Point(x, y));

    // In XAML: <Grid x:Name="windowGrid">...</Grid>
    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 déplaçable 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 cliente comprenant les bords de la fenêtre était également affectée par HTCAPTION donc la fenêtre n'était plus redimensionnable.

Pour résoudre ce problème, j'ai dû vérifier si le curseur touchait la zone client ou la zone non-client. Pour ce faire, j'ai dû utiliser la fonction DefWindowProc() et voir si elle renvoie 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);

Enfin, voici ma méthode finale 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;
    }
}

3 votes

Le cerveau grillé, le cœur largué. Quelle bonne solution vous avez trouvée. Je prie le Seigneur de ne jamais avoir à faire face à une exigence similaire. +1.

0 votes

@Markust : Tu as fait mon après-midi avec le jeu de mots "core-dump".

16voto

Jeffrey L Whitledge Points 27574

Voici quelque chose que vous pourriez essayer :

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

Ensuite, appelez la méthode statique VisualTreeHelper.HitTest(Visual,Point) le faire passer this y el Point que vous venez de faire. La valeur de retour indiquera le contrôle avec l'ordre Z le plus élevé. Si c'est votre fenêtre, alors faites votre HTCAPTION vaudou. Si c'est un autre contrôle, alors... ne le faites pas.

Bonne chance !

0 votes

Voici la partie délicate : WinForms' Control a un PointToClient() qui effectue la transformation nécessaire. Mais la méthode de WPF Visual ne dispose pas de cette méthode. Probablement parce que c'est, eh bien, WPF.

2 votes

Par souci d'exhaustivité, je fais de ma réponse la réponse acceptée. En guise de remerciement, ayez une prime !

6voto

Ross Points 2138

Je cherche à faire la même chose (rendre ma vitre Aero étendue déplaçable dans mon application WPF) et je viens de tomber sur cet article 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 gourmande en code.

Il suffit de créer un élément transparent derrière vos contrôles, et donnez-lui un gestionnaire d'événement du bouton gauche de la souris vers le bas qui appelle l'événement DragMove() méthode.

Voici la section de mon XAML qui apparaît au-dessus de mon verre Aero étendu :

<Grid DockPanel.Dock="Top">
    <Border MouseLeftButtonDown="Border_MouseLeftButtonDown" Background="Transparent" />
    <Grid><!-- My controls are in here --></Grid>
</Grid>

Et le code-behind (qui se trouve à l'intérieur d'un fichier Window et donc DragMove() est disponible pour appeler directement) :

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

Et c'est tout ! Pour votre solution, vous devriez ajouter plus d'un de ces éléments pour obtenir votre zone de dragage non rectangulaire.

1 votes

J'ai considéré DragMove() également, mais le problème est qu'il ne fonctionne pas lorsque la fenêtre est agrandie, et qu'il n'est donc pas vraiment compatible avec Aero Snap.

0 votes

Vous avez raison. Je viens de tester Aero Snap avec ma solution et bien que cela ne pose aucun problème pour la maximisation et l'arrimage à gauche/droite, cela ne fonctionne pas pour le "désarrimage" à partir d'un état maximisé.

1 votes

Mais encore une fois, Aero Snap est une fonctionnalité de Windows 7, et si cela n'a pas d'importance que Aero Snap ne fonctionne pas, DragMove() est bien.

1voto

Hady Mahmoodi Points 49

Le moyen le plus simple est créer un stackpanel ou tout ce que vous voulez pour votre barre de titre. XAML

 <StackPanel Name="titleBar" Background="Gray" MouseLeftButtonDown="titleBar_MouseLeftButtonDown" Grid.ColumnSpan="2"></StackPanel>

code

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

0 votes

En quoi cela diffère-t-il de la réponse de Ross publiée il y a deux ans et demi (à part l'utilisation d'un mot de passe) ? StackPanel au lieu d'un Border ) ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X