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

dans les applications comme Windows Explorer et Internet Explorer, on peut saisir les zones étendues de cadre sous la barre de titre et faire glisser les fenêtres autour.

pour WinForms applications, formulaires et contrôles sont aussi près de Win32 APIs natifs qu'ils peuvent obtenir; On outrepasserait simplement le WndProc() handler dans leur forme, le traitement du WM_NCHITTEST message de fenêtre et tromper le système en pensant qu'un clic sur la zone de cadre était vraiment un cliquez sur la barre de titre en renvoyant HTCAPTION . J'ai fait cela dans mes propres applications WinForms à l'effet délicieux.

dans WPF, je peux également mettre en œuvre une méthode similaire WndProc() et l'accrocher à la poignée 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 mets aveuglément handled = true et le retour HTCAPTION , en cliquant n'importe où mais le 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 surligné en rouge ci-dessous provoque un frottement. Cela inclut même les poignées sur les côtés de la fenêtre (la zone non client). Mes commandes WPF, à savoir les boîtes de texte et la Commande tab, arrêtent également de recevoir des clics en conséquence:

ce que je veux est pour seulement

  1. la barre de titre, et
  2. les régions de la zone client...
  3. ... qui ne sont pas occupés par mes commandes

pour être déplaçable. C'est-à-dire que je veux seulement que ces régions rouges soient drainables (zone client + barre de titre):

comment modifier ma méthode WndProc() et le reste de la fenêtre XAML / code-behind, pour déterminer quelles zones doivent retourner HTCAPTION et qui ne devrait pas? Je pense à quelque chose dans le genre d'utiliser Point s pour vérifier l'emplacement du clic par rapport à l'emplacement de mes commandes, mais je ne suis pas sûr de savoir comment le faire dans WPF land.

EDIT [4/24]: une façon simple à son sujet est d'avoir un contrôle invisible, ou même la fenêtre elle-même, répondre à 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 maximisée, de sorte qu'il ne joue pas agréable avec Windows 7 Aero Snap. Puisque je vais pour L'intégration de Windows 7, ce n'est pas une solution acceptable dans mon cas.

50
demandé sur Community 2011-03-31 02:18:51

4 réponses

code échantillon

grâce à un courriel que j'ai reçu ce matin, on m'a demandé de faire une application d'échantillon de travail démontrant cette fonctionnalité. Je l'ai fait maintenant; vous pouvez le trouver sur GitHub (ou dans le maintenant-archivée CodePlex ). 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, et de la construire et de l'exécuter.

la demande complète dans son entirety est sous licence MIT, mais vous allez probablement le démonter et mettre des morceaux de son code autour de votre propre plutôt que d'utiliser le code app en entier - pas que la licence vous empêche de le faire non plus. En outre, bien que je sais que la conception de la fenêtre principale de l'application n'est pas n'importe où semblable aux wireframes ci-dessus, l'idée est la même que posée dans la question.

Espérons que cela aide quelqu'un!

Étape-par-étape de la solution

Je l'ai finalement résolu. Merci à Jeffrey l Whitledge pour m'avoir indiqué la bonne direction! sa réponse a été acceptée parce que, sans elle, je n'aurais pas réussi à trouver une solution. MODIFIER [9/8]: cette réponse est maintenant accepté comme il est plus complet; je suis en train de donner Jeffrey une belle grosse prime à la place de son aide.

pour l'amour de la postérité, voici comment je l'ai fait (citant Jeffrey réponse le cas échéant):

obtenez l'emplacement du clic de souris (à partir du wParam, lParam peut-être?), et l'utiliser pour créer un Point (peut-être 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 de l'axe des coordonnées du curseur est son mot ordre, comme 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 relatives à l'espace de fenêtre.

puis appelez la méthode statique VisualTreeHelper.HitTest(Visual,Point) passer this et le Point que vous venez de faire. La valeur de retour indique le contrôle avec le plus grand Ordre.

j'ai dû passer au niveau supérieur Grid contrôle au lieu de this comme le visuel pour 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 zone de cadre de fenêtre inoccupée. Quoi qu'il en soit, la clé était d'utiliser la méthode VisualTreeHelper.HitTest() .

maintenant, ayant dit cela, il y a deux mises en garde qui peuvent s'appliquent à vous si vous suivez mes étapes:

  1. si vous ne couvrez pas toute la fenêtre, et au lieu de cela 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 comme un remplisseur de zone client.

    Dans mon cas, la zone de contenu de mon contrôle onglet correspond à cette zone rectangulaire parfaitement, comme indiqué dans les diagrammes. Dans votre demande, vous pourriez avoir besoin de placer un Rectangle forme ou un Panel contrôle et la peinture de la couleur appropriée. De cette façon, le contrôle sera touché.

    ce numéro sur les remplisseurs de secteurs clients mène au suivant:

  2. si votre grille ou un autre contrôle de niveau supérieur a une texture ou un gradient de fond au-dessus du cadre de fenêtre étendu, toute la zone de grille répondra au coup, même sur toute région complètement transparente de l'arrière-plan (voir Coup d'Essais dans la Couche Visual ). Dans ce cas, vous voudrez ignorer les coups contre la grille elle-même, et ne faites attention qu'aux contrôles à l'intérieur de celle-ci.

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 mobile en cliquant et en déplaçant seulement les zones inoccupées de la fenêtre.

Mais ce n'est pas tout. Rappelons dans la première illustration que la zone non-client comprenant le les bordures de la fenêtre ont aussi été affectées par HTCAPTION de sorte que la fenêtre n'était plus redimensionnable.

pour corriger cela, j'ai dû vérifier si le curseur frappait la zone client ou la zone non-client. Pour vérifier cela, j'ai dû utiliser la fonction DefWindowProc() et voir si elle retournait 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 dernière méthode de procédure 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;
    }
}
29
répondu BoltClock 2018-02-15 04:34:24

voici quelque chose que vous pourriez essayer:

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

puis appelez la méthode statique VisualTreeHelper.HitTest(Visual,Point) passer this et le Point que vous venez de faire. La valeur de retour indique le contrôle avec le plus grand Ordre. Si c'est votre fenêtre, alors faites votre HTCAPTION Voodoo. Si c'est un autre contrôle, alors...ne le fais pas.

bonne chance!

14
répondu Jeffrey L Whitledge 2011-03-30 22:53:05

cherchant à faire la même chose (rendre mon Aero glass extensible dans mon application WPF), je viens de tomber 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 exigeante en code.

il suffit de créer un élément transparent derrière vos commandes, et lui donner un bouton gauche de la souris vers le bas le gestionnaire d'événements qui appelle la fenêtre DragMove() méthode.

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

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

et le code-derrière (c'est dans une classe 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 auriez à ajouter plus d'un de ceux-ci pour réaliser votre zone non rectangulaire draggable.

6
répondu Ross 2011-11-21 22:14:16

la voie simple est créer stackpanel ou tout ce que vous voulez pour votre titlebar XAML 151930920"

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

code

  private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
     {
         DragMove();
     }
1
répondu Hady Mahmoodi 2013-11-17 11:51:55