web-dev-qa-db-fra.com

Comment déplacer un Popup WPF lorsque son élément d'ancrage se déplace?

J'ai un Popup défini comme ceci:

<Popup
    Name="myPopup"
    StaysOpen="True"
    Placement="Bottom"
    PlacementRectangle="0,20,0,20"
    PlacementTarget="{Binding ElementName=myPopupAnchor}">
    <TextBlock ... />
</Popup>

J'ai ajouté des gestionnaires d'événements à l'élément myPopupAnchor pour les événements MouseEnter et MouseLeave. Les deux gestionnaires d'événements basculent la visibilité de la fenêtre contextuelle.

Mon problème est que la position de myPopupAnchor n'est lue que lorsque la fenêtre contextuelle est d'abord affichée, ou masquée puis affichée à nouveau. Si l'ancre se déplace, la fenêtre contextuelle ne le fait pas.

Je cherche des moyens de contourner cela, je veux un Popup émouvant. Puis-je informer WPF que la liaison PlacementTarget a changé et doit être relue? Puis-je définir manuellement la position du popup?

Actuellement, j'ai une solution de contournement très grossière qui implique de fermer puis d'ouvrir à nouveau la fenêtre contextuelle, ce qui provoque des problèmes de repeinture.

42
Mizipzor

J'ai examiné quelques options et échantillons. La chose qui semble fonctionner le mieux pour moi est de "heurter" l'une des propriétés qui fait que le Popup se repositionne de lui-même. La propriété que j'ai utilisée est HorizontalOffset.

Je l'ai défini sur (lui-même + 1), puis je lui ai remis la valeur d'origine. Je le fais dans un gestionnaire d'événements qui s'exécute lorsque la fenêtre est repositionnée.

// Reference to the PlacementTarget.
DependencyObject myPopupPlacementTarget;

// Reference to the popup.
Popup myPopup; 

Window w = Window.GetWindow(myPopupPlacementTarget);
if (null != w)
{
    w.LocationChanged += delegate(object sender, EventArgs args)
    {
        var offset = myPopup.HorizontalOffset;
        myPopup.HorizontalOffset = offset + 1;
        myPopup.HorizontalOffset = offset;
    };
}

Lorsque la fenêtre est déplacée, la fenêtre contextuelle se repositionne. Le changement subtil dans l'HorizontalOffset n'est pas remarqué car la fenêtre et le popup se déplacent déjà de toute façon.

J'évalue toujours si un contrôle popup est la meilleure option dans les cas où le contrôle reste ouvert pendant une autre interaction. Je pense que suggestion de Ray Burns de mettre ces choses dans une couche Adorner semble être une bonne approche pour certains scénarios.

72
NathanAW

Juste pour ajouter à la grande solution de NathanAW ci-dessus, je pensais que je soulignerais un certain contexte , comme  pour placer le code C # dans ce cas. Je suis encore assez nouveau sur WPF, j'ai donc eu du mal au début à trouver où mettre le code de NathanAW. Quand j'ai essayé de mettre ce code dans le constructeur du UserControl qui a hébergé mon Popup, Window.GetWindow() a toujours renvoyé Null (donc le code "bump" n'a jamais été exécuté). J'ai donc pensé que d'autres débutants pourraient bénéficier de voir les choses dans leur contexte.

Avant de montrer le C # en contexte, voici un exemple de contexte XAML pour montrer quelques éléments pertinents et leurs noms:

<UserControl x:Class="MyNamespace.View1"
             xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml" >

    <TextBlock x:Name="popupTarget" />
    <Popup x:Name="myPopup"
           Placement="Bottom"
           PlacementTarget="{Binding ElementName=popupTarget}" >
         (popup content here)
    </Popup>
</UserControl>

Ensuite, dans le code-behind, pour éviter que Window.GetWindow() return Null, connectez un gestionnaire à l'événement Loaded pour héberger le code de NathanAW (voir commentaire de Peter Walke on une discussion stackoverflow similaire par exemple). Voici exactement à quoi tout cela ressemblait dans mon code-code UserControl:

public partial class View1 : UserControl
{
    // Constructor
    public View1()
    {
        InitializeComponent();

        // Window.GetWindow() will return Null if you try to call it here!             

        // Wire up the Loaded handler instead
        this.Loaded += new RoutedEventHandler(View1_Loaded);
    }

    /// Provides a way to "dock" the Popup control to the Window
    ///  so that the popup "sticks" to the window while the window is dragged around.
    void View1_Loaded(object sender, RoutedEventArgs e)
    {
        Window w = Window.GetWindow(popupTarget);
        // w should not be Null now!
        if (null != w)
        {
            w.LocationChanged += delegate(object sender2, EventArgs args)
            {
                var offset = myPopup.HorizontalOffset;
                // "bump" the offset to cause the popup to reposition itself
                //   on its own
                myPopup.HorizontalOffset = offset + 1;
                myPopup.HorizontalOffset = offset;
            };
            // Also handle the window being resized (so the popup's position stays
            //  relative to its target element if the target element moves upon 
            //  window resize)
            w.SizeChanged += delegate(object sender3, SizeChangedEventArgs e2)
            {
                var offset = myPopup.HorizontalOffset;
                myPopup.HorizontalOffset = offset + 1;
                myPopup.HorizontalOffset = offset;
            };
        }
    }
}
24
Jason Frank
    private void ppValues_Opened(object sender, EventArgs e)
    {
        Window win = Window.GetWindow(YourControl);
        win.LocationChanged += new EventHandler(win_LocationChanged);            
    }
    void win_LocationChanged(object sender, EventArgs e)
    {
        if (YourPopup.IsOpen)
        {                
            var mi = typeof(Popup).GetMethod("UpdatePosition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            mi.Invoke(YourPopup, null);
        }
    }
21
AQL.exe

Pour ajouter à la réponse de Jason Frank, l'approche Window.GetWindow() ne fonctionnerait pas si le WPF UserControl est finalement hébergé dans un WinForms ElementHost. Ce que je devais trouver était le ScrollViewer dans lequel mon UserControl était placé, car c'était l'élément montrant les barres de défilement.

Cette méthode récursive générique (modifiée à partir d'une autre réponse) aidera à trouver le parent d'un type particulier dans l'arborescence logique (il est également possible d'utiliser l'arborescence visuelle) et de le renvoyer s'il est trouvé.

public static T FindLogicalParentOf<T>(DependencyObject child) where T: FrameworkElement
    {
        DependencyObject parent = LogicalTreeHelper.GetParent(child);

        //Top of the tree
        if (parent == null) return null;

        T parentWindow = parent as T;
        if (parentWindow != null)
        {
            return parentWindow;
        }

        //Climb a step up
        return FindLogicalParentOf<T>(parent);
    }

Appelez cette méthode d'assistance au lieu de Window.GetWindow() et continuez avec la réponse de Jason de s'abonner aux bons événements. Dans le cas de ScrollViewer, c'est plutôt l'événement ScrollChanged.

3
zemien

Si vous voulez déplacer le popup, il y a une astuce simple: changez sa position, puis définissez:

IsOpen = false;
IsOpen = true;
3
Aurelien Ribon

J'ai modifié le code de Jason, car le Popup est déjà au premier plan si la fenêtre n'est pas activée. Y a-t-il une option dans la classe Popup ou ma solution est-elle correcte?

private void FullLoaded(object sender, RoutedEventArgs e) {
Window CurrentWindow = Window.GetWindow(this.Popup);
if (CurrentWindow != null) {

    CurrentWindow.LocationChanged += (object innerSender, EventArgs innerArgs) => {
        this.RedrawPopup();
    };

    CurrentWindow.SizeChanged += (object innerSender, SizeChangedEventArgs innerArgs) => {
        this.RedrawPopup();
    };

    CurrentWindow.Activated += (object innerSender, EventArgs innerArgs) => {
        if (this.m_handleDeActivatedEvents && this.m_ShowOnActivated) {
            this.Popup.IsOpen = true;
            this.m_ShowOnActivated = false;
        }
    };

    CurrentWindow.Deactivated += (object innerSender, EventArgs innerArgs) => {
        if (this.m_handleDeActivatedEvents && this.Popup.IsOpen) {
            this.Popup.IsOpen = false;
            this.m_ShowOnActivated = true;
        }
    };

}
}

    private void RedrawPopup() {
        double Offset = this.Popup.HorizontalOffset;
        this.Popup.HorizontalOffset = Offset + 1;
        this.Popup.HorizontalOffset = Offset;
    }
2
Gigabyte

Tu ne peux pas faire ça. Lorsque Popup est affiché à l'écran, il ne se repositionne pas si son parent est repositionné. C'est le comportement du contrôle Popup. vérifiez ceci: http://msdn.Microsoft.com/en-us/library/system.windows.controls.primitives.popup.aspx

vous pouvez utiliser une fenêtre (avec WindowStyle = None) au lieu de Popup qui peut résoudre votre problème.

0
viky