54

I have a Popup defined like this:

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

I have added event handlers to the myPopupAnchor element for the events MouseEnter and MouseLeave. The two event handlers toggle the popup's visibility.

My problem is the position of myPopupAnchor is only read when the popup is first shown, or hidden and then shown again. If the anchor moves, the popup does not.

I'm looking for ways to work around this, I want a moving Popup. Can I notify WPF that the PlacementTarget binding has changed and should be read again? Can I manually set the popup's position?

Currently, I have a very crude workaround that involves closing and then opening the popup again, which causes some repainting issues.

Dan J
  • 16,319
  • 7
  • 50
  • 82
Mizipzor
  • 51,151
  • 22
  • 97
  • 138

10 Answers10

91

I looked at a couple options and samples out there. The thing that seems to work best for me is to "bump" one of the properties that causes the Popup to reposition itself on its own. The property that I used is HorizontalOffset.

I set it to (itself + 1) and then set it back the original value. I do this in an event handler that runs when the window is repositioned.

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

When the window is moved, the popup will reposition. The subtle change in the HorizontalOffset is not noticed because the window and popup are already moving anyway.

I'm still evaluating whether a popup control is the best option in cases where the control stays open during other interaction. I'm thinking that Ray Burns suggestion to put this stuff in an Adorner layer seems like a good approach for some scenarios.

Community
  • 1
  • 1
NathanAW
  • 3,329
  • 18
  • 19
  • Any update on the adorner layer approach you were thinking of? – Sabuncu Jan 05 '12 at 20:29
  • I've used the adorner layer for a number of things and found that it works well. Adorners are a little bit challenging and require a bit of experimentation, but overall seem like a good option. – NathanAW Jan 05 '12 at 21:22
  • +1 @NathanAW I wish I could +gig your answer. Excellent. I read whole `ComboBox` implementation. But cannot figure out how it moves the `Popup`. While there is no guidance on how `ComboBox` moves `Popup`, do you have any idea? – amiry jd Aug 09 '13 at 21:33
  • @Javad_Amiry, I wish I knew, but I don't know for sure. I suspect that the mostly get away with the fact that it closes in response to most interactions that don't affect the combobox. So, if you click the parent window to move it, the combo closes. Do you have examples where it moves with the parent window? – NathanAW Aug 19 '13 at 13:18
  • @NathanAW Well I finally found the way to migrate your solution with my problem and it got fixed. Thank you again. – amiry jd Aug 25 '13 at 09:02
  • The body of the delegate can simply read `myPopup.HorizontalOffset++; myPopup.HorizontalOffset--;` – Danny Beckett Aug 01 '16 at 16:41
  • 4
    @NathanAW: Maybe it's better to add `0.001` instead of `1`, in order to make the change even smaller. `HorizontalOffset` is anyway a `double`. – Vlad May 22 '17 at 06:52
  • This also worked for me by changing the offset by 0.1, since in my scenario the popup needed to move as the mouse moved, rather than the window. An offset change of 1 was too noticeable since the window wasn't moving with it. – Sean Beanland Aug 07 '18 at 15:21
  • @user1624552 To fix it for resizing, add an event handler for the `w.SizeChanged` and/or `w.StateChanged` events and use the same code for adjusting the `Horizontal Offset` to move the popup. See answer by @Jason Frank. – Deadpikle Nov 04 '19 at 16:25
35

Just to add on to NathanAW's great solution above, I thought I'd point out some context, such as where to place the C# code in this case. I'm still pretty new to WPF so I struggled at first to figure out where to put NathanAW's code. When I tried putting that code in the constructor for the UserControl that hosted my Popup, Window.GetWindow() always returned Null (so the "bump" code never executed). So I thought that other newbies might benefit from seeing things in context.

Before showing the C# in context, here's some example XAML context to show some relevant elements and their names:

<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>

Then in the code-behind, to avoid having Window.GetWindow() return Null, wire up a handler to the Loaded event to house NathanAW's code (see Peter Walke's comment on a similar stackoverflow discussion for example). Here's exactly how it all looked in my UserControl code-behind:

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;
            };
        }
    }
}
Community
  • 1
  • 1
Jason Frank
  • 3,842
  • 31
  • 35
  • Excellent follow up.. I was getting the Window null issue too! – Craig Oct 23 '13 at 06:50
  • it works wellthanks :), however I got an issue when minimizing the app to the taskbar, the popup is still displayed on screen, how to make it disappear ? – bgcode Mar 23 '18 at 15:41
  • 2
    I know this is old, but how about when the popup target moves - as in it gets scrolled down/up the view? – Barry Franklin Jun 01 '18 at 13:35
23
    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);
        }
    }
AQL.exe
  • 256
  • 2
  • 2
  • 1
    Wtf? It's possible to run/invoke private methods via Reflections? That doesn't give me a good feeling in my guts… It's handy for me at this point… but somehow I feels strange doing this … I mean `private` is there for a reason… – Marcel B Jul 25 '11 at 13:14
  • @Marcel Benthin Yes, but you shouldn't use obvious names on private methods, and you can also obfuscate them. – Vercas Aug 11 '11 at 07:16
  • 7
    Private here doesn't mean secure and it's not intended that way. It's simply a way to establish clean interfaces and hide the stuff the caller shouldn't care about (or in some cases potentially dangerous stuff). If your security relies on member privacy, you're in trouble. – Jeff Dec 19 '11 at 00:27
  • 2
    I think, in this case is safer to call internal method **Reposition()**. According to [this](http://referencesource-beta.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Primitives/Popup.cs#1754), it makes some checks befor it calls **UpdatePosition()**. – tom.maruska Feb 26 '14 at 07:36
  • +1 Generally invoking private members is non-ideal but the likelihood of an API change in this area of WPF is low and a fail-fast scenario. Mutating the HorizontialOffset can have visible side effects in some scenarios. Good answer. – Adam Caviness Feb 08 '16 at 21:28
  • 1
    @tom.maruska Your link is break and the new link is:http://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/Primitives/Popup.cs,3b989cb473173bba – lindexi Jun 22 '17 at 01:32
  • @lindexi Your link is also broken. [Here's the current working link](https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Primitives/Popup.cs,f4eb387b17b9590e,references). _When will my link also be broken?_ – Nicke Manarin Jun 14 '20 at 06:07
5

If you want to move the popup, there is a simple trick : change its position,then set :

IsOpen = false;
IsOpen = true;
Aurelien Ribon
  • 7,548
  • 3
  • 43
  • 54
4

To add to Jason Frank's answer, the Window.GetWindow() approach wouldn't work if the WPF UserControl is ultimately hosted in an WinForms ElementHost. What I needed to find was the ScrollViewer that my UserControl was placed in, as that was the element showing the scrollbars.

This generic recursive method (modified off another answer) will help find the parent of a particular type in the logical tree (it's possible to use the visual tree too), and return it if found.

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

Call this helper method instead of Window.GetWindow() and continue with Jason's answer of subscribing to the right events. In the case of ScrollViewer, it's the ScrollChanged event instead.

zemien
  • 562
  • 6
  • 17
2

I modified the Code from Jason, because the Popup is already in Foreground if the Window is not Activated. Is there any Option in the Popup class or i is my solution ok?

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;
    }
Gigabyte
  • 71
  • 4
1

I encapsulated the logic provided by Jason Frank in a class and inherit from the PopUp class.

class MyPopup : Popup
    {
        private Window _root;

        public MyPopup()
        {
            Loaded += OnLoaded;
            Unloaded += OnUnloaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            _root = Window.GetWindow(this);
            _root.LocationChanged += OnRootLocationChanged;
            
        }

        private void OnRootLocationChanged(object sender, EventArgs e)
        {
            var offset = this.HorizontalOffset;
            this.HorizontalOffset = offset + 1;
            this.HorizontalOffset = offset;
        }

        private void OnUnloaded(object sender, RoutedEventArgs e)
        {
            _root.LocationChanged -= OnRootLocationChanged;
            Loaded -= OnLoaded;
            Unloaded -= OnUnloaded;
        }
    }
BrayanKn
  • 11
  • 2
0

You can not do this. When Popup is displayed on the screen, it does not reposition itself if its parent is repositioned. Thats the behavior of Popup control. check this: http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.popup.aspx

you can use a Window(with WindowStyle=None) instead of Popup that may solve your problem.

viky
  • 17,275
  • 13
  • 71
  • 90
0

Looking at https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/windows/Controls/Slider.cs we can see there is a "Reposition()" method in the Popup class, that does the job well. The problem is that the method is internal and can disappear in the future. So in this case it might be suitable to do a dirty hack like this:

internal class SampleClass
{
    // Popup's Reposition() method adapter.
    private static readonly Action<Popup> popupReposition = InitPopupReposition();

    // Reference to the PlacementTarget.
    DependencyObject myPopupPlacementTarget;

    // Reference to the popup.
    Popup myPopup;

    private void SampleMethod()
    {
        // ...

        Window w = Window.GetWindow(myPopupPlacementTarget);
        if (null != w)
        {
            w.LocationChanged += delegate (object? sender, EventArgs args)
            {
                popupReposition(myPopup);
            };
        }

        // ...
    }

    private static Action<Popup> InitPopupReposition()
    {
        var repositionMethod = typeof(Popup).GetMethod("Reposition", BindingFlags.Instance | BindingFlags.NonPublic);
        if (repositionMethod != null)
        {
            // Use internal method if possible.
            return new Action<Popup>(popup => repositionMethod.Invoke(popup, null));
        }

        // Fallback in case of internal implementation change.
        return new Action<Popup>(popup =>
        {
            var offset = popup.HorizontalOffset;
            popup.HorizontalOffset = offset + 1;
            popup.HorizontalOffset = offset;
        });
    }
}

This approach uses internal method as long as it's possible, or a fallback if internal implementation ever change.

Sinus32
  • 21
  • 3
-1

Download the Popup Popup Position Sample at:

http://msdn.microsoft.com/en-us/library/ms771558(v=VS.90).aspx

The code sample uses the class CustomPopupPlacement with a Rect object, and binds to horizontal and vertical offsets to move the Popup.

<Popup Name="popup1" Placement="Bottom" AllowsTransparency="True"
       IsOpen="{Binding ElementName=popupOpen, Path=IsChecked}"
       HorizontalOffset="{Binding ElementName=HOffset, Path=Value, Mode=TwoWay}"
       VerticalOffset="{Binding ElementName=VOffset, Path=Value, Mode=TwoWay}"
Zamboni
  • 7,897
  • 5
  • 43
  • 52