当WPF弹出窗口的锚定元素移动时,我该如何移动它?

56

我有一个这样定义的弹出窗口:

<Popup
    Name="myPopup"
    StaysOpen="True"
    Placement="Bottom"
    PlacementRectangle="0,20,0,20"
    PlacementTarget="{Binding ElementName=myPopupAnchor}">
    <TextBlock ... />
</Popup>
我已经为myPopupAnchor元素添加了MouseEnterMouseLeave事件处理程序。这两个事件处理程序切换弹出窗口的可见性。
我的问题是当弹出窗口第一次显示或隐藏后再次显示时,仅在myPopupAnchor的位置被读取。如果锚点移动,弹出窗口将不会移动。
我正在寻找解决方法,希望能够实现弹出窗口的移动。我可以通知WPF绑定的PlacementTarget已更改并应重新读取吗?我可以手动设置弹出窗口的位置吗?
目前,我有一个非常简陋的解决方法,涉及关闭然后重新打开弹出窗口,这会导致一些重绘问题。
10个回答

92

我查看了几个选项和示例。对我来说似乎最好的方法是“推动”一个导致弹出窗口自行重新定位的属性。我使用的属性是HorizontalOffset。

我将它设置为(它本身+1),然后将其设置回原始值。我在重新定位窗口时运行的事件处理程序中执行此操作。

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

当窗口移动时,弹出控件将重新定位。由于窗口和弹出控件已经在移动,水平偏移量的微小变化不会被注意到。

我仍在评估在控件在其他交互期间保持打开状态的情况下,弹出控件是否是最佳选项。我认为Ray Burns建议将这些内容放入装饰层,对某些场景来说是一个很好的方法。


+1 @NathanAW 我希望我能够 +gig 你的回答。非常好。我已经阅读了整个 ComboBox 实现。但是我无法弄清楚它是如何移动 Popup 的。虽然没有关于 ComboBox 如何移动 Popup 的指导,但你有任何想法吗? - amiry jd
@Javad_Amiry,我希望我知道,但我不确定。我怀疑它们大多数情况下都能逃脱,因为它会在大多数不影响组合框的交互中关闭。因此,如果您单击父窗口以移动它,则组合框将关闭。您有它随父窗口移动的示例吗? - NathanAW
@NathanAW 好的,我最终找到了将你的解决方案与我的问题进行迁移的方法,问题已经解决。再次感谢你。 - amiry jd
委托的主体可以简单地读取 myPopup.HorizontalOffset++; myPopup.HorizontalOffset--; - Danny Beckett
4
也许加上0.001比1更好,这样可以使变化更小。无论如何,“HorizontalOffset”都是一个双精度浮点数。 - Vlad
显示剩余4条评论

37
只是为了补充NathanAW的优秀解决方案,我想指出一些上下文,比如在这种情况下放置C#代码的位置。我还是很新手WPF,所以起初我很难弄清楚在哪里放NathanAW的代码。当我尝试将该代码放在托管我的弹出窗口的UserControl的构造函数中时,Window.GetWindow()总是返回Null(因此“bump”代码从未执行)。所以我认为其他新手可能会从上下文中看到一些好处。
在展示C#上下文之前,这里有一些示例XAML上下文,显示一些相关元素及其名称:
<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>

然后在代码后台,为了避免Window.GetWindow()返回Null,需要将处理程序与已加载事件绑定,以容纳NathanAW的代码(例如,请参见Peter Walke的评论的类似stackoverflow讨论)。以下是我的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;
            };
        }
    }
}

非常好的跟进。我也遇到了窗口为空的问题! - Craig
它运行得很好,谢谢 :),但是当我将应用程序最小化到任务栏时,弹出窗口仍然显示在屏幕上,如何使其消失? - bgcode
2
我知道这个问题有点老了,但是如果弹出窗口的目标移动了怎么办——比如说它随着视图的滚动而上下移动? - Barry Franklin

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

2
什么?通过反射运行/调用私有方法是可能的吗?这让我感到不安...在这一点上,它对我很方便...但以某种方式,我觉得这样做很奇怪...我的意思是private存在是有原因的... - Marcel B
7
这里的“Private”并不意味着安全,并且也没有这个意图。它只是一种建立干净接口和隐藏调用者不应关心的东西(或在某些情况下潜在危险的东西)的方式。如果您的安全性依赖于成员隐私,那就有麻烦了。 - Jeff
2
我认为,在这种情况下,调用内部方法**Reposition()更为安全。根据这个链接,在调用UpdatePosition()**之前,它会进行一些检查。 - tom.maruska
通常调用私有成员是不理想的,但在 WPF 的这个领域中,API 更改的可能性很低,并且会出现故障快速情况。 在某些情况下,改变 HorizontialOffset 可能会产生可见的副作用。好的答案。 - Adam Caviness
1
@tom.maruska,您的链接已经失效,新链接为:http://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/Primitives/Popup.cs,3b989cb473173bba - lindexi
显示剩余2条评论

5

除了Jason Frank的回答,如果WPF UserControl最终托管在WinForms ElementHost中,则Window.GetWindow()方法无法使用。我需要找到包含我的UserControl的ScrollViewer,因为它是显示滚动条的元素。

这个通用的递归方法(修改自另一个答案)将帮助在逻辑树中找到特定类型的父级(也可以使用可视树),并在找到时返回它。

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

使用这个辅助方法来代替Window.GetWindow(),并按照Jason的答案订阅正确的事件。在ScrollViewer的情况下,它是ScrollChanged事件。


5
如果您想移动弹出窗口,有一个简单的技巧:更改其位置,然后设置:
IsOpen = false;
IsOpen = true;

3

我修改了Jason的代码,因为如果窗口没有激活,弹出窗口已经在前景中了。Popup类中有任何选项,还是我的解决方案可以吗?

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
我将Jason Frank提供的逻辑封装在一个类中,并从PopUp类继承。
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;
        }
    }

1

查看https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/windows/Controls/Slider.cs,我们可以看到Popup类中有一个“Reposition()”方法,它能够很好地完成工作。但问题是,该方法是内部方法,可能在未来被取消。因此,在这种情况下,可能适合使用以下不正当手段:

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

这种方法尽可能使用内部方法,如果内部实现发生变化,则使用备用方案。


-1

有多种方法可以做到这一点。请参考其他答案。 - undefined

-2

在以下网址下载弹出窗口位置示例:

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

代码示例使用了CustomPopupPlacement类和Rect对象,并绑定水平和垂直偏移量以移动弹出窗口。
<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}"

7
我尝试了这个示例,但似乎在移动窗口时无法移动弹出窗口。 - NathanAW
2
样例不再可用 :( - VitalyB
谢谢,我已经更新了链接,引用了一些 .NET 3.5 的示例。 - Zamboni
2
正如NathanAW所说,当窗口移动时,弹出窗口不会更新其位置。 - Bolu
我找不到弹出窗口的源代码,只有上述URL中的死链接。 - undefined
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接