Popup 解决置顶显示问题
前言
Popup显示时会置顶显示。尤其是 Popup设置了StayOpen=true时,会一直置顶显示,问题更明显。
置顶显示问题现象:
解决方案
怎么解决问题?
获取绑定UserControl所在的窗口,窗口层级变化时,通知更新当前Popup的Tostmost属性。
1. 添加附加属性
在属性变更中,监听Loaded/UnLoaded事件,在加载后处理相应的逻辑。
1 private static readonly DependencyProperty TopmostInCurrentWindowProperty = DependencyProperty.RegisterAttached("TopmostInCurrentWindow", 2 typeof(bool), typeof(Popup), new FrameworkPropertyMetadata(false, OnTopmostInCurrentWindowChanged)); 3 4 public static bool GetTopmostInCurrentWindow(DependencyObject obj) 5 { 6 return (bool)obj.GetValue(TopmostInCurrentWindowProperty); 7 } 8 9 public static void SetTopmostInCurrentWindow(DependencyObject obj, bool value) 10 { 11 obj.SetValue(TopmostInCurrentWindowProperty, value); 12 } 13 14 private static void OnTopmostInCurrentWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 15 { 16 if (d is Popup popup) 17 { 18 _popup = popup; 19 popup.Loaded -= OnPopupLoaded; 20 popup.Unloaded -= OnPopupUnloaded; 21 if ((bool)e.NewValue) 22 { 23 popup.Loaded += OnPopupLoaded; 24 popup.Unloaded += OnPopupUnloaded; 25 } 26 } 27 }
2. 添加事件监听
- 在Popup.Loaded事件中,监听Popup所在窗口的唤醒事件。同时,Unloaded事件中注销所在窗口的事件监听。
- 在窗口唤醒事件监听逻辑中,设置当前popup选择性的置顶显示
- 添加Popup的MouseDown事件监听,点击Popup的内容后,Popup置顶显示、窗口层级也发相应的变化。
1 static void OnPopupLoaded(object sender, RoutedEventArgs e) 2 { 3 if (!(sender is Popup popup)) 4 return; 5 6 popup.Child?.AddHandler(UIElement.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(OnChildPreviewMouseLeftButtonDown), true); 7 8 _parentWindow = Window.GetWindow(popup); 9 if (_parentWindow == null) 10 return; 11 12 _parentWindow.Activated -= OnParentWindowActivated; 13 _parentWindow.Deactivated -= OnParentWindowDeactivated; 14 _parentWindow.Activated += OnParentWindowActivated; 15 _parentWindow.Deactivated += OnParentWindowDeactivated; 16 } 17 18 static void OnPopupUnloaded(object sender, RoutedEventArgs e) 19 { 20 if (_parentWindow == null) 21 return; 22 _parentWindow.Activated -= OnParentWindowActivated; 23 _parentWindow.Deactivated -= OnParentWindowDeactivated; 24 } 25 26 private static void OnParentWindowActivated(object sender, EventArgs e) 27 { 28 SetTopmostState(true); 29 } 30 31 private static void OnParentWindowDeactivated(object sender, EventArgs e) 32 { 33 //Parent Window Deactivated 34 if (IsTopmost == false) 35 { 36 SetTopmostState(IsTopmost); 37 } 38 } 39 40 static void OnChildPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 41 { 42 SetTopmostState(true); 43 44 if (!_parentWindow.IsActive && IsTopmost == false) 45 { 46 _parentWindow.Activate(); 47 } 48 }
3. 选择性置顶显示
- 记录/设置当前Popup的置顶显示状态
- 选择性置顶显示-可以显示在最顶层,也可以只显示在当前窗口的上层。
1 private static bool IsTopmost 2 { 3 get => _isTopmost; 4 set 5 { 6 _isTopmost = value; 7 SetTopmostState(value); 8 } 9 } 10 11 private static void SetTopmostState(bool isTop) 12 { 13 if (_appliedTopMost.HasValue && _appliedTopMost == isTop) 14 { 15 return; 16 } 17 18 if (_popup?.Child == null) 19 return; 20 21 var hwndSource = (PresentationSource.FromVisual(_popup.Child)) as HwndSource; 22 23 if (hwndSource == null) 24 return; 25 var hwnd = hwndSource.Handle; 26 27 RECT rect; 28 29 if (!GetWindowRect(hwnd, out rect)) 30 return; 31 32 if (isTop) 33 { 34 SetWindowPos(hwnd, HWND_TOPMOST, rect.Left, rect.Top, (int)_popup.Width, (int)_popup.Height, TOPMOST_FLAGS); 35 } 36 else 37 { 38 // 重新激活Topmost,需要bottom->top->notop 39 SetWindowPos(hwnd, HWND_BOTTOM, rect.Left, rect.Top, (int)_popup.Width, (int)_popup.Height, TOPMOST_FLAGS); 40 SetWindowPos(hwnd, HWND_TOP, rect.Left, rect.Top, (int)_popup.Width, (int)_popup.Height, TOPMOST_FLAGS); 41 SetWindowPos(hwnd, HWND_NOTOPMOST, rect.Left, rect.Top, (int)_popup.Width, (int)_popup.Height, TOPMOST_FLAGS); 42 } 43 44 _appliedTopMost = isTop; 45 }
以下是窗口消息处理、私有字段:
通过user32.dll的GetWindowRect和SetWindowPos函数,处理Popup层级问题

1 #region 窗口消息 2 3 [StructLayout(LayoutKind.Sequential)] 4 public struct RECT 5 6 { 7 public int Left; 8 public int Top; 9 public int Right; 10 public int Bottom; 11 } 12 13 [DllImport("user32.dll")] 14 [return: MarshalAs(UnmanagedType.Bool)] 15 private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); 16 17 [DllImport("user32.dll")] 18 private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); 19 20 static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); 21 static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); 22 static readonly IntPtr HWND_TOP = new IntPtr(0); 23 static readonly IntPtr HWND_BOTTOM = new IntPtr(1); 24 25 private const UInt32 SWP_NOSIZE = 0x0001; 26 const UInt32 SWP_NOMOVE = 0x0002; 27 const UInt32 SWP_NOREDRAW = 0x0008; 28 const UInt32 SWP_NOACTIVATE = 0x0010; 29 30 const UInt32 SWP_NOOWNERZORDER = 0x0200; 31 const UInt32 SWP_NOSENDCHANGING = 0x0400; 32 33 //很重要,窗口切换等需要将popup显示层级重新刷新 34 const UInt32 TOPMOST_FLAGS = 35 SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSENDCHANGING; 36 37 38 #endregion 39 40 #region private fileds 41 42 private static bool? _appliedTopMost; 43 private static bool _alreadyLoaded; 44 private static Window _parentWindow; 45 private static Popup _popup; 46 private static bool _isTopmost; 47 48 #endregion
下载 此Demo
注:也可以通过自定义用户控件Popup实现,逻辑一样:

1 /// <summary> 2 /// 解决StayOpen=true时,永远置顶的问题 3 /// </summary> 4 public class MyPopup : Popup 5 { 6 public static readonly DependencyProperty IsTopmostProperty = DependencyProperty.Register("IsTopmost", typeof(bool), typeof(MyPopup), new FrameworkPropertyMetadata(false, OnIsTopmostChanged)); 7 8 private bool? _appliedTopMost; 9 private bool _alreadyLoaded; 10 private Window _parentWindow; 11 12 public bool IsTopmost 13 { 14 get { return (bool)GetValue(IsTopmostProperty); } 15 set { SetValue(IsTopmostProperty, value); } 16 } 17 18 /// <summary> 19 /// ctor 20 /// </summary> 21 public MyPopup() 22 { 23 Loaded += OnPopupLoaded; 24 Unloaded += OnPopupUnloaded; 25 } 26 27 28 void OnPopupLoaded(object sender, RoutedEventArgs e) 29 { 30 if (_alreadyLoaded) 31 return; 32 33 _alreadyLoaded = true; 34 35 if (Child != null) 36 { 37 Child.AddHandler(PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(OnChildPreviewMouseLeftButtonDown), true); 38 } 39 40 _parentWindow = Window.GetWindow(this); 41 42 if (_parentWindow == null) 43 return; 44 45 _parentWindow.Activated += OnParentWindowActivated; 46 _parentWindow.Deactivated += OnParentWindowDeactivated; 47 } 48 49 private void OnPopupUnloaded(object sender, RoutedEventArgs e) 50 { 51 if (_parentWindow == null) 52 return; 53 _parentWindow.Activated -= OnParentWindowActivated; 54 _parentWindow.Deactivated -= OnParentWindowDeactivated; 55 } 56 57 void OnParentWindowActivated(object sender, EventArgs e) 58 { 59 SetTopmostState(true); 60 } 61 62 void OnParentWindowDeactivated(object sender, EventArgs e) 63 { 64 Debug.WriteLine("Parent Window Deactivated"); 65 66 if (IsTopmost == false) 67 { 68 SetTopmostState(IsTopmost); 69 } 70 } 71 72 void OnChildPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 73 { 74 75 SetTopmostState(true); 76 77 if (!_parentWindow.IsActive && IsTopmost == false) 78 { 79 _parentWindow.Activate(); 80 } 81 } 82 83 private static void OnIsTopmostChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) 84 { 85 var thisobj = (MyPopup)obj; 86 87 thisobj.SetTopmostState(thisobj.IsTopmost); 88 } 89 90 protected override void OnOpened(EventArgs e) 91 { 92 SetTopmostState(IsTopmost); 93 base.OnOpened(e); 94 } 95 96 private void SetTopmostState(bool isTop) 97 { 98 if (_appliedTopMost.HasValue && _appliedTopMost == isTop) 99 { 100 return; 101 } 102 103 if (Child == null) 104 return; 105 106 var hwndSource = (PresentationSource.FromVisual(Child)) as HwndSource; 107 108 if (hwndSource == null) 109 return; 110 var hwnd = hwndSource.Handle; 111 112 RECT rect; 113 114 if (!GetWindowRect(hwnd, out rect)) 115 return; 116 117 if (isTop) 118 { 119 SetWindowPos(hwnd, HWND_TOPMOST, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS); 120 } 121 else 122 { 123 // 重新激活Topmost,需要bottom->top->notop 124 SetWindowPos(hwnd, HWND_BOTTOM, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS); 125 SetWindowPos(hwnd, HWND_TOP, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS); 126 SetWindowPos(hwnd, HWND_NOTOPMOST, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS); 127 } 128 129 _appliedTopMost = isTop; 130 } 131 132 [StructLayout(LayoutKind.Sequential)] 133 public struct RECT 134 135 { 136 public int Left; 137 public int Top; 138 public int Right; 139 public int Bottom; 140 } 141 142 [DllImport("user32.dll")] 143 [return: MarshalAs(UnmanagedType.Bool)] 144 private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); 145 146 [DllImport("user32.dll")] 147 private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, 148 int Y, int cx, int cy, uint uFlags); 149 150 static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); 151 static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); 152 static readonly IntPtr HWND_TOP = new IntPtr(0); 153 static readonly IntPtr HWND_BOTTOM = new IntPtr(1); 154 155 private const UInt32 SWP_NOSIZE = 0x0001; 156 const UInt32 SWP_NOMOVE = 0x0002; 157 const UInt32 SWP_NOZORDER = 0x0004; 158 const UInt32 SWP_NOREDRAW = 0x0008; 159 const UInt32 SWP_NOACTIVATE = 0x0010; 160 161 const UInt32 SWP_FRAMECHANGED = 0x0020; /* The frame changed: send WM_NCCALCSIZE */ 162 const UInt32 SWP_SHOWWINDOW = 0x0040; 163 const UInt32 SWP_HIDEWINDOW = 0x0080; 164 const UInt32 SWP_NOCOPYBITS = 0x0100; 165 const UInt32 SWP_NOOWNERZORDER = 0x0200; /* Don’t do owner Z ordering */ 166 const UInt32 SWP_NOSENDCHANGING = 0x0400; /* Don’t send WM_WINDOWPOSCHANGING */ 167 168 //很重要,窗口切换等需要将popup显示层级重新刷新 169 const UInt32 TOPMOST_FLAGS = 170 SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSENDCHANGING; 171 }
解决方案总结
添加如上附加属性或者用户控件Popup后,能解决置顶问题,Popup只会出现在所在窗口上层。
截图如下:
分类:
WPF/Silverlight
标签:
Popup
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)