Popup 解决位置不随窗口/元素FrameworkElement 移动更新的问题
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 //旧值取消LocationChanged监听 5 if (e.OldValue is DependencyObject previousPlacementTarget) 6 { 7 Window window = Window.GetWindow(previousPlacementTarget); 8 var element = previousPlacementTarget as FrameworkElement; 9 if (window != null) 10 { 11 CancelEventsListeningInWindow(window); 12 } 13 if (element != null) 14 { 15 element.SizeChanged -= ElementSizeChanged; 16 element.LayoutUpdated -= ElementLayoutUpdated; 17 } 18 } 19 20 //新值添加LocationChanged监听 21 if (e.NewValue is DependencyObject newPlacementTarget) 22 { 23 Window window = Window.GetWindow(newPlacementTarget); 24 var element = newPlacementTarget as FrameworkElement; 25 //窗口已加载 26 if (window != null) 27 { 28 RegisterEventsInWindow(window); 29 } 30 //窗口未加载,则等待控件初始化后,再获取窗口 31 else if (element != null) 32 { 33 element.Loaded -= ElementLoaded; 34 element.Loaded += ElementLoaded; 35 } 36 //元素大小变换时,变更Popup位置 37 if (element != null) 38 { 39 element.SizeChanged -= ElementSizeChanged; 40 element.SizeChanged += ElementSizeChanged; 41 element.LayoutUpdated -= ElementLayoutUpdated; 42 element.LayoutUpdated += ElementLayoutUpdated; 43 } 44 void ElementLoaded(object sender, RoutedEventArgs e3) 45 { 46 element.Loaded -= ElementLoaded; 47 window = Window.GetWindow(newPlacementTarget); 48 if (window != null) 49 { 50 RegisterEventsInWindow(window); 51 } 52 } 53 } 54 void RegisterEventsInWindow(Window window) 55 { 56 window.LocationChanged -= WindowLocationChanged; 57 window.LocationChanged += WindowLocationChanged; 58 window.SizeChanged -= WindowSizeChanged; 59 window.SizeChanged += WindowSizeChanged; 60 } 61 void CancelEventsListeningInWindow(Window window) 62 { 63 window.LocationChanged -= WindowLocationChanged; 64 window.SizeChanged -= WindowSizeChanged; 65 } 66 void WindowLocationChanged(object s1, EventArgs e1) 67 { 68 UpdatePopupLocation(); 69 } 70 void WindowSizeChanged(object sender, SizeChangedEventArgs e2) 71 { 72 UpdatePopupLocation(); 73 } 74 void ElementSizeChanged(object sender, SizeChangedEventArgs e3) 75 { 76 UpdatePopupLocation(); 77 } 78 void ElementLayoutUpdated(object sender, EventArgs e4) 79 { 80 UpdatePopupLocation(); 81 } 82 void UpdatePopupLocation() 83 { 84 if (pop != null && pop.IsOpen) 85 { 86 //通知更新相对位置 87 var method = typeof(Popup).GetMethod("UpdatePosition", BindingFlags.NonPublic | BindingFlags.Instance); 88 method?.Invoke(pop, null); 89 } 90 } 91 }
值得注意的是,原有的绑定目标源要记得取消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>