Popup 解决位置不随窗口/元素FrameworkElement 移动更新的问题

C#

浏览数:291

2019-6-14

Popup弹出后,因业务需求设置了StaysOpen=true后,移动窗口位置或者改变窗口大小,Popup的位置不会更新。

如何更新位置?

获取当前Popup的Target绑定UserControl所在窗口,位置刷新时,时时更新Popup的位置即可。

1.添加一个附加属性

1 /// <summary>
2 /// Popup位置更新
3 /// </summary>
4 public static readonly DependencyProperty PopupPlacementTargetProperty =
5     DependencyProperty.RegisterAttached("PopupPlacementTarget", typeof(DependencyObject), typeof(PopupHelper), new PropertyMetadata(null, OnPopupPlacementTargetChanged));

2.窗口移动后触发popup更新

首先,有个疑问,popup首次显示时,为何显示的位置是正确的呢?

通过查看源码,发现,其实popup也是有内置更新popup位置的!

而通过查看UpdatePosition代码,其方法确实是更新popup位置的。源码如下:


  1 private void UpdatePosition()
  2 {
  3     if (this._popupRoot.Value == null)
  4         return;
  5     PlacementMode placement = this.Placement;
  6     Point[] targetInterestPoints = this.GetPlacementTargetInterestPoints(placement);
  7     Point[] childInterestPoints = this.GetChildInterestPoints(placement);
  8     Rect bounds = this.GetBounds(targetInterestPoints);
  9     Rect rect1 = this.GetBounds(childInterestPoints);
 10     double num1 = rect1.Width * rect1.Height;
 11     int num2 = -1;
 12     Vector offsetVector1 = new Vector((double)this._positionInfo.X, (double)this._positionInfo.Y);
 13     double num3 = -1.0;
 14     PopupPrimaryAxis popupPrimaryAxis = PopupPrimaryAxis.None;
 15     CustomPopupPlacement[] customPopupPlacementArray = (CustomPopupPlacement[])null;
 16     int num4;
 17     if (placement == PlacementMode.Custom)
 18     {
 19         CustomPopupPlacementCallback placementCallback = this.CustomPopupPlacementCallback;
 20         if (placementCallback != null)
 21             customPopupPlacementArray = placementCallback(rect1.Size, bounds.Size, new Point(this.HorizontalOffset, this.VerticalOffset));
 22         num4 = customPopupPlacementArray == null ? 0 : customPopupPlacementArray.Length;
 23         if (!this.IsOpen)
 24             return;
 25     }
 26     else
 27         num4 = Popup.GetNumberOfCombinations(placement);
 28     for (int i = 0; i < num4; ++i)
 29     {
 30         bool flag1 = false;
 31         bool flag2 = false;
 32         Vector offsetVector2;
 33         PopupPrimaryAxis axis;
 34         if (placement == PlacementMode.Custom)
 35         {
 36             offsetVector2 = (Vector)targetInterestPoints[0] + (Vector)customPopupPlacementArray[i].Point;
 37             axis = customPopupPlacementArray[i].PrimaryAxis;
 38         }
 39         else
 40         {
 41             Popup.PointCombination pointCombination = this.GetPointCombination(placement, i, out axis);
 42             Popup.InterestPoint targetInterestPoint = pointCombination.TargetInterestPoint;
 43             Popup.InterestPoint childInterestPoint = pointCombination.ChildInterestPoint;
 44             offsetVector2 = targetInterestPoints[(int)targetInterestPoint] - childInterestPoints[(int)childInterestPoint];
 45             flag1 = childInterestPoint == Popup.InterestPoint.TopRight || childInterestPoint == Popup.InterestPoint.BottomRight;
 46             flag2 = childInterestPoint == Popup.InterestPoint.BottomLeft || childInterestPoint == Popup.InterestPoint.BottomRight;
 47         }
 48         Rect rect2 = Rect.Offset(rect1, offsetVector2);
 49         Rect rect3 = Rect.Intersect(this.GetScreenBounds(bounds, targetInterestPoints[0]), rect2);
 50         double num5 = rect3 != Rect.Empty ? rect3.Width * rect3.Height : 0.0;
 51         if (num5 - num3 > 0.01)
 52         {
 53             num2 = i;
 54             offsetVector1 = offsetVector2;
 55             num3 = num5;
 56             popupPrimaryAxis = axis;
 57             this.AnimateFromRight = flag1;
 58             this.AnimateFromBottom = flag2;
 59             if (Math.Abs(num5 - num1) < 0.01)
 60                 break;
 61         }
 62     }
 63     if (num2 >= 2 && (placement == PlacementMode.Right || placement == PlacementMode.Left))
 64         this.DropOpposite = !this.DropOpposite;
 65     rect1 = new Rect((Size)this._secHelper.GetTransformToDevice().Transform((Point)this._popupRoot.Value.RenderSize));
 66     rect1.Offset(offsetVector1);
 67     Rect screenBounds = this.GetScreenBounds(bounds, targetInterestPoints[0]);
 68     Rect rect4 = Rect.Intersect(screenBounds, rect1);
 69     if (Math.Abs(rect4.Width - rect1.Width) > 0.01 || Math.Abs(rect4.Height - rect1.Height) > 0.01)
 70     {
 71         Point point1 = targetInterestPoints[0];
 72         Vector vector1 = targetInterestPoints[1] - point1;
 73         vector1.Normalize();
 74         if (!this.IsTransparent || double.IsNaN(vector1.Y) || Math.Abs(vector1.Y) < 0.01)
 75         {
 76             if (rect1.Right > screenBounds.Right)
 77                 offsetVector1.X = screenBounds.Right - rect1.Width;
 78             else if (rect1.Left < screenBounds.Left)
 79                 offsetVector1.X = screenBounds.Left;
 80         }
 81         else if (this.IsTransparent && Math.Abs(vector1.X) < 0.01)
 82         {
 83             if (rect1.Bottom > screenBounds.Bottom)
 84                 offsetVector1.Y = screenBounds.Bottom - rect1.Height;
 85             else if (rect1.Top < screenBounds.Top)
 86                 offsetVector1.Y = screenBounds.Top;
 87         }
 88         Point point2 = targetInterestPoints[2];
 89         Vector vector2 = point1 - point2;
 90         vector2.Normalize();
 91         if (!this.IsTransparent || double.IsNaN(vector2.X) || Math.Abs(vector2.X) < 0.01)
 92         {
 93             if (rect1.Bottom > screenBounds.Bottom)
 94                 offsetVector1.Y = screenBounds.Bottom - rect1.Height;
 95             else if (rect1.Top < screenBounds.Top)
 96                 offsetVector1.Y = screenBounds.Top;
 97         }
 98         else if (this.IsTransparent && Math.Abs(vector2.Y) < 0.01)
 99         {
100             if (rect1.Right > screenBounds.Right)
101                 offsetVector1.X = screenBounds.Right - rect1.Width;
102             else if (rect1.Left < screenBounds.Left)
103                 offsetVector1.X = screenBounds.Left;
104         }
105     }
106     int x = DoubleUtil.DoubleToInt(offsetVector1.X);
107     int y = DoubleUtil.DoubleToInt(offsetVector1.Y);
108     if (x == this._positionInfo.X && y == this._positionInfo.Y)
109         return;
110     this._positionInfo.X = x;
111     this._positionInfo.Y = y;
112     this._secHelper.SetPopupPos(true, x, y, false, 0, 0);
113 }

那么,我们有什么办法调用这个私有方法呢?我相信大家都想,找到popup源码开发者,爆了他Y的!

有一种方法,叫反射,反射可以获取类的任一个字段或者属性。

反射,可以参考:https://www.cnblogs.com/vaevvaev/p/6995639.html

通过反射,我们获取到UpdatePosition方法,并调用执行。

1 var mi = typeof(Popup).GetMethod("UpdatePosition", BindingFlags.NonPublic | BindingFlags.Instance);
2 mi.Invoke(pop, null);

下面是详细的属性更改事件实现:

 1 private static void OnPopupPlacementTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
 2 {
 3     Popup pop = d as Popup;
 4 
 5     //旧值取消LocationChanged监听
 6     if (e.OldValue is DependencyObject previousPlacementTarget)
 7     {
 8         Window window = Window.GetWindow(previousPlacementTarget);
 9         if (window != null)
10         {
11             window.LocationChanged -= WindowLocationChanged;
12         }
13     }
14 
15     //新值添加LocationChanged监听
16     if (e.NewValue is DependencyObject newPlacementTarget)
17     {
18         Window window = Window.GetWindow(newPlacementTarget);
19         if (window != null)
20         {
21             window.LocationChanged -= WindowLocationChanged;
22             window.LocationChanged += WindowLocationChanged;
23         }
24     }
25     void WindowLocationChanged(object s1, EventArgs e1)
26     {
27         if (pop != null && pop.IsOpen)
28         {
29             //通知更新相对位置
30             var mi = typeof(Popup).GetMethod("UpdatePosition", BindingFlags.NonPublic | BindingFlags.Instance);
31             mi.Invoke(pop, null);
32         }
33     }
34 }

值得注意的是,原有的绑定目标源要记得取消LocationChanged事件订阅,新的绑定目标源保险起见,也要提前注销再添加事件订阅。

另:通知popup位置更新,也可能通过如下的黑科技:

1     //通知更新相对位置
2     var offset = pop.HorizontalOffset;
3     pop.HorizontalOffset = offset + 1;
4     pop.HorizontalOffset = offset;

 为何改变一下HorizontalOffset就可行呢?因为上面最终并没有改变HorizontalOffset的值。。。

原来。。。好吧,先看源码

 1     /// <summary>获取或设置目标原点和弹出项对齐之间的水平距离点。</summary>
 2     /// <returns>
 3     ///   目标原点和 popup 对齐点之间的水平距离。
 4     ///    有关目标原点和 popup 对齐点的信息,请参阅 Popup 放置行为。
 5     ///    默认值为 0。
 6     /// </returns>
 7     [Bindable(true)]
 8     [Category("Layout")]
 9     [TypeConverter(typeof (LengthConverter))]
10     public double HorizontalOffset
11     {
12       get
13       {
14         return (double) this.GetValue(Popup.HorizontalOffsetProperty);
15       }
16       set
17       {
18         this.SetValue(Popup.HorizontalOffsetProperty, (object) value);
19       }
20     }
21 
22     private static void OnOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
23     {
24       ((Popup) d).Reposition();
25     }

是的,最终调用了Reposition,而Reposition方法中有调用UpdatePosition更新popup位置。

 所以以上,更新HorizontalOffset,是更新popup位置的一种捷径。

 3. 元素移动/大小变化后,触发更新

当popup的PlaceTarget绑定一个控件或者一个Grid后,FrameworkElement大小变化/位置变化时,popup位置更新(同上)

元素大小变化时:

1     else if (newPlacementTarget is FrameworkElement frameworkElement)
2     {
3         frameworkElement.SizeChanged -= ElementOnSizeChanged;
4         frameworkElement.SizeChanged += ElementOnSizeChanged;
5     }

也可以直接监听LayoutUpdated事件,元素大小/位置变化时,LayoutUpdated都会触发。注意:LayoutUpdated触发有点频繁。

1     else if (newPlacementTarget is FrameworkElement frameworkElement)
2     {
3         frameworkElement.LayoutUpdated -= ElementOnLayoutUpdated;
4         frameworkElement.LayoutUpdated += ElementOnLayoutUpdated;
5     }

4.界面设置绑定目标源

1     <Popup x:Name="FirstShowPopup" PlacementTarget="{Binding ElementName=TestButton}" Placement="Custom"
2         CustomPopupPlacementCallback="{easiUi:Placement Align=RightCenter,OutOfScreenEnabled=True}" PopupAnimation="Fade"
3         AllowsTransparency="True" StaysOpen="True" HorizontalOffset="-16" VerticalOffset="4"
4         helper:PopupHelper.LocationUpdatedOnTarget="{Binding ElementName=TestButton}"
5         helper:PopupHelper.TopmostInCurrentWindow="True">
6     </Popup>

 

作者:唐宋元明清2188