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 }
View Code

 那么,我們有什么辦法調用這個私有方法呢?我相信大家都想,找到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>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM