在吕毅大佬的文章中已经详细介绍了什么是AppBar: WPF 使用 AppBar 将窗口停靠在桌面上,让其他程序不占用此窗口的空间(附我封装的附加属性) - walterlv
即让窗口固定在屏幕某一边,并且保证其他窗口最大化后不会覆盖AppBar占据的区域(类似于Windows任务栏)。
但是在我的环境中测试时,上面的代码出现了一些问题,例如非100%缩放显示时的坐标计算异常、多窗口同时停靠时布局错乱等。所以我重写了AppBar在WPF上的实现,效果如图:

一、AppBar的主要申请流程
主要流程如图:
(图注:ABN_POSCHANGED消息在任何需要调整位置之时都会触发,括号只是举个例子)
核心代码其实在于如何计算停靠窗口的位置,要点是处理好一下几个方面:
1. 修改停靠位置时用原窗口的大小计算,被动告知需要调整位置时用即时大小计算
2. 像素单位与WPF单位之间的转换
3. 小心Windows的位置建议,并排停靠时会得到负值高宽,需要手动适配对齐方式
4. 有新的AppBar加入时,窗口会被系统强制移动到工作区(WorkArea),这点我还没能找到解决方案,只能把移动窗口的命令通过Dispatcher延迟操作
二、如何使用
1.下载我封装好的库:AppBarTest/AppBarCreator.cs at master · TwilightLemon/AppBarTest (github.com)
2. 在xaml中直接设置:
- <Window ...>
-
- <local:AppBarCreator.AppBar>
- <local:AppBar x:Name="appBar" Location="Top" OnFullScreenStateChanged="AppBar_OnFullScreenStateChanged"/>
- </local:AppBarCreator.AppBar>
- ...
- </Window>
或者在后台创建:
- private readonly AppBar appBar=new AppBar();
- ...Window_Loaded...
- appBar.Location = AppBarLocation.Top;
- appBar.OnFullScreenStateChanged += AppBar_OnFullScreenStateChanged;
- AppBarCreator.SetAppBar(this, appBar);
3. 另外你可能注意到了,这里有一个OnFullScreenStateChanged事件:该事件由AppBarMsg注册,在有窗口进入或退出全屏时触发,参数bool为true指示进入全屏。
你需要手动在事件中设置全屏模式下的行为,例如在全屏时隐藏AppBar
- private void AppBar_OnFullScreenStateChanged(object sender, bool e)
- {
- Debug.WriteLine("Full Screen State: "+e);
- Visibility = e ? Visibility.Collapsed : Visibility.Visible;
- }
我在官方的Flag上加了一个RegisterOnly,即只注册AppBarMsg而不真的停靠窗口,可以此用来作全屏模式监听。

4. 如果你需要在每个虚拟桌面都显示AppBar(像任务栏那样),可以尝试为窗口使用SetWindowLong添加WS_EX_TOOLWINDOW标签(自行查找)
以下贴出完整的代码:
- 1 using System.ComponentModel;
- 2 using System.Diagnostics;
- 3 using System.Runtime.InteropServices;
- 4 using System.Windows;
- 5 using System.Windows.Interop;
- 6 using System.Windows.Threading;
- 7
- 8 namespace AppBarTest;
- 9 public static class AppBarCreator
- 10 {
- 11 public static readonly DependencyProperty AppBarProperty =
- 12 DependencyProperty.RegisterAttached(
- 13 "AppBar",
- 14 typeof(AppBar),
- 15 typeof(AppBarCreator),
- 16 new PropertyMetadata(null, OnAppBarChanged));
- 17 private static void OnAppBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- 18 {
- 19 if (d is Window window && e.NewValue is AppBar appBar)
- 20 {
- 21 appBar.AttachedWindow = window;
- 22 }
- 23 }
- 24 public static void SetAppBar(Window element, AppBar value)
- 25 {
- 26 if (value == null) return;
- 27 element.SetValue(AppBarProperty, value);
- 28 }
- 29
- 30 public static AppBar GetAppBar(Window element)
- 31 {
- 32 return (AppBar)element.GetValue(AppBarProperty);
- 33 }
- 34 }
- 35
- 36 public class AppBar : DependencyObject
- 37 {
- 38 /// <summary>
- 39 /// 附加到的窗口
- 40 /// </summary>
- 41 public Window AttachedWindow
- 42 {
- 43 get => _window;
- 44 set
- 45 {
- 46 if (value == null) return;
- 47 _window = value;
- 48 _window.Closing += _window_Closing;
- 49 _window.LocationChanged += _window_LocationChanged;
- 50 //获取窗口句柄hWnd
- 51 var handle = new WindowInteropHelper(value).Handle;
- 52 if (handle == IntPtr.Zero)
- 53 {
- 54 //Win32窗口未创建
- 55 _window.SourceInitialized += _window_SourceInitialized;
- 56 }
- 57 else
- 58 {
- 59 _hWnd = handle;
- 60 CheckPending();
- 61 }
- 62 }
- 63 }
- 64
- 65 private void _window_LocationChanged(object? sender, EventArgs e)
- 66 {
- 67 Debug.WriteLine(_window.Title+ " LocationChanged: Top: "+_window.Top+" Left: "+_window.Left);
- 68 }
- 69
- 70 private void _window_Closing(object? sender, CancelEventArgs e)
- 71 {
- 72 _window.Closing -= _window_Closing;
- 73 if (Location != AppBarLocation.None)
- 74 DisableAppBar();
- 75 }
- 76
- 77 /// <summary>
- 78 /// 检查是否需要应用之前的Location更改
- 79 /// </summary>
- 80 private void CheckPending()
- 81 {
- 82 //创建AppBar时提前触发的LocationChanged
- 83 if (_locationChangePending)
- 84 {
- 85 _locationChangePending = false;
- 86 LoadAppBar(Location);
- 87 }
- 88 }
- 89 /// <summary>
- 90 /// 载入AppBar
- 91 /// </summary>
- 92 /// <param name="e"></param>
- 93 private void LoadAppBar(AppBarLocation e,AppBarLocation? previous=null)
- 94 {
- 95
- 96 if (e != AppBarLocation.None)
- 97 {
- 98 if (e == AppBarLocation.RegisterOnly)
- 99 {
- 100 //仅注册AppBarMsg
- 101 //如果之前注册过有效的AppBar则先注销,以还原位置
- 102 if (previous.HasValue && previous.Value != AppBarLocation.RegisterOnly)
- 103 {
- 104 if (previous.Value != AppBarLocation.None)
- 105 {
- 106 //由生效的AppBar转为RegisterOnly,还原为普通窗口再注册空AppBar
- 107 DisableAppBar();
- 108 }
- 109 RegisterAppBarMsg();
- 110 }
- 111 else
- 112 {
- 113 //之前未注册过AppBar,直接注册
- 114 RegisterAppBarMsg();
- 115 }
- 116 }
- 117 else
- 118 {
- 119 if (previous.HasValue && previous.Value != AppBarLocation.None)
- 120 {
- 121 //之前为RegisterOnly才备份窗口信息
- 122 if(previous.Value == AppBarLocation.RegisterOnly)
- 123 {
- 124 BackupWindowInfo();
- 125 }
- 126 SetAppBarPosition(_originalSize);
- 127 ForceWindowStyles();
- 128 }
- 129 else
- 130 EnableAppBar();
- 131 }
- 132 }
- 133 else
- 134 {
- 135 DisableAppBar();
- 136 }
- 137 }
- 138 private void _window_SourceInitialized(object? sender, EventArgs e)
- 139 {
- 140 _window.SourceInitialized -= _window_SourceInitialized;
- 141 _hWnd = new WindowInteropHelper(_window).Handle;
- 142 CheckPending();
- 143 }
- 144
- 145 /// <summary>
- 146 /// 当有窗口进入或退出全屏时触发 bool参数为true时表示全屏状态
- 147 /// </summary>
- 148 public event EventHandler<bool>? OnFullScreenStateChanged;
- 149 /// <summary>
- 150 /// 期望将AppBar停靠到的位置
- 151 /// </summary>
- 152 public AppBarLocation Location
- 153 {
- 154 get { return (AppBarLocation)GetValue(LocationProperty); }
- 155 set { SetValue(LocationProperty, value); }
- 156 }
- 157
- 158 public static readonly DependencyProperty LocationProperty =
- 159 DependencyProperty.Register(
- 160 "Location",
- 161 typeof(AppBarLocation), typeof(AppBar),
- 162 new PropertyMetadata(AppBarLocation.None, OnLocationChanged));
- 163
- 164 private bool _locationChangePending = false;
- 165 private static void OnLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- 166 {
- 167 if (DesignerProperties.GetIsInDesignMode(d))
- 168 return;
- 169 if (d is not AppBar appBar) return;
- 170 if (appBar.AttachedWindow == null)
- 171 {
- 172 appBar._locationChangePending = true;
- 173 return;
- 174 }
- 175 appBar.LoadAppBar((AppBarLocation)e.NewValue,(AppBarLocation)e.OldValue);
- 176 }
- 177
- 178 private int _callbackId = 0;
- 179 private bool _isRegistered = false;
- 180 private Window _window = null;
- 181 private IntPtr _hWnd;
- 182 private WindowStyle _originalStyle;
- 183 private Point _originalPosition;
- 184 private Size _originalSize = Size.Empty;
- 185 private ResizeMode _originalResizeMode;
- 186 private bool _originalTopmost;
- 187 public Rect? DockedSize { get; set; } = null;
- 188 private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam,
- 189 IntPtr lParam, ref bool handled)
- 190 {
- 191 if (msg == _callbackId)
- 192 {
- 193 Debug.WriteLine(_window.Title + " AppBarMsg("+_callbackId+"): " + wParam.ToInt32() + " LParam: " + lParam.ToInt32());
- 194 switch (wParam.ToInt32())
- 195 {
- 196 case (int)Interop.AppBarNotify.ABN_POSCHANGED:
- 197 Debug.WriteLine("AppBarNotify.ABN_POSCHANGED ! "+_window.Title);
- 198 if (Location != AppBarLocation.RegisterOnly)
- 199 SetAppBarPosition(Size.Empty);
- 200 handled = true;
- 201 break;
- 202 case (int)Interop.AppBarNotify.ABN_FULLSCREENAPP:
- 203 OnFullScreenStateChanged?.Invoke(this, lParam.ToInt32() == 1);
- 204 handled = true;
- 205 break;
- 206 }
- 207 }
- 208 return IntPtr.Zero;
- 209 }
- 210
- 211 public void BackupWindowInfo()
- 212 {
- 213 _callbackId = 0;
- 214 DockedSize = null;
- 215 _originalStyle = _window.WindowStyle;
- 216 _originalSize = new Size(_window.ActualWidth, _window.ActualHeight);
- 217 _originalPosition = new Point(_window.Left, _window.Top);
- 218 _originalResizeMode = _window.ResizeMode;
- 219 _originalTopmost = _window.Topmost;
- 220 }
- 221 public void RestoreWindowInfo()
- 222 {
- 223 if (_originalSize != Size.Empty)
- 224 {
- 225 _window.WindowStyle = _originalStyle;
- 226 _window.ResizeMode = _originalResizeMode;
- 227 _window.Topmost = _originalTopmost;
- 228 _window.Left = _originalPosition.X;
- 229 _window.Top = _originalPosition.Y;
- 230 _window.Width = _originalSize.Width;
- 231 _window.Height = _originalSize.Height;
- 232 }
- 233 }
- 234 public void ForceWindowStyles()
- 235 {
- 236 _window.WindowStyle = WindowStyle.None;
- 237 _window.ResizeMode = ResizeMode.NoResize;
- 238 _window.Topmost = true;
- 239 }
- 240
- 241 public void RegisterAppBarMsg()
- 242 {
- 243 var data = new Interop.APPBARDATA();
- 244 data.cbSize = Marshal.SizeOf(data);
- 245 data.hWnd = _hWnd;
- 246
- 247 _isRegistered = true;
- 248 _callbackId = Interop.RegisterWindowMessage(Guid.NewGuid().ToString());
- 249 data.uCallbackMessage = _callbackId;
- 250 var success = Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_NEW, ref data);
- 251 var source = HwndSource.FromHwnd(_hWnd);
- 252 Debug.WriteLineIf(source == null, "HwndSource is null!");
- 253 source?.AddHook(WndProc);
- 254 Debug.WriteLine(_window.Title+" RegisterAppBarMsg: " + _callbackId);
- 255 }
- 256 public void EnableAppBar()
- 257 {
- 258 if (!_isRegistered)
- 259 {
- 260 //备份窗口信息并设置窗口样式
- 261 BackupWindowInfo();
- 262 //注册成为AppBar窗口
- 263 RegisterAppBarMsg();
- 264 ForceWindowStyles();
- 265 }
- 266 //成为AppBar窗口之后(或已经是)只需要注册并移动窗口位置即可
- 267 SetAppBarPosition(_originalSize);
- 268 }
- 269 public void SetAppBarPosition(Size WindowSize)
- 270 {
- 271 var data = new Interop.APPBARDATA();
- 272 data.cbSize = Marshal.SizeOf(data);
- 273 data.hWnd = _hWnd;
- 274 data.uEdge = (int)Location;
- 275 data.uCallbackMessage = _callbackId;
- 276 Debug.WriteLine("\r\nWindow: "+_window.Title);
- 277
- 278 //获取WPF单位与像素的转换矩阵
- 279 var compositionTarget = PresentationSource.FromVisual(_window)?.CompositionTarget;
- 280 if (compositionTarget == null)
- 281 throw new Exception("居然获取不到CompositionTarget?!");
- 282 var toPixel = compositionTarget.TransformToDevice;
- 283 var toWpfUnit = compositionTarget.TransformFromDevice;
- 284
- 285 //窗口在屏幕的实际大小
- 286 if(WindowSize== Size.Empty)
- 287 WindowSize = new Size(_window.ActualWidth, _window.ActualHeight);
- 288 var actualSize = toPixel.Transform(new Vector(WindowSize.Width, WindowSize.Height));
- 289 //屏幕的真实像素
- 290 var workArea = toPixel.Transform(new Vector(SystemParameters.PrimaryScreenWidth, SystemParameters.PrimaryScreenHeight));
- 291 Debug.WriteLine("WorkArea Width: {0}, Height: {1}", workArea.X, workArea.Y);
- 292
- 293 if (Location is AppBarLocation.Left or AppBarLocation.Right)
- 294 {
- 295 data.rc.top = 0;
- 296 data.rc.bottom = (int)workArea.Y;
- 297 if (Location == AppBarLocation.Left)
- 298 {
- 299 data.rc.left = 0;
- 300 data.rc.right = (int)Math.Round(actualSize.X);
- 301 }
- 302 else
- 303 {
- 304 data.rc.right = (int)workArea.X;
- 305 data.rc.left = (int)workArea.X - (int)Math.Round(actualSize.X);
- 306 }
- 307 }
- 308 else
- 309 {
- 310 data.rc.left = 0;
- 311 data.rc.right = (int)workArea.X;
- 312 if (Location == AppBarLocation.Top)
- 313 {
- 314 data.rc.top = 0;
- 315 data.rc.bottom = (int)Math.Round(actualSize.Y);
- 316 }
- 317 else
- 318 {
- 319 data.rc.bottom = (int)workArea.Y;
- 320 data.rc.top = (int)workArea.Y - (int)Math.Round(actualSize.Y);
- 321 }
- 322 }
- 323 //以上生成的是四周都没有其他AppBar时的理想位置
- 324 //系统将自动调整位置以适应其他AppBar
- 325 Debug.WriteLine("Before QueryPos: Left: {0}, Top: {1}, Right: {2}, Bottom: {3}", data.rc.left, data.rc.top, data.rc.right, data.rc.bottom);
- 326 Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_QUERYPOS, ref data);
- 327 Debug.WriteLine("After QueryPos: Left: {0}, Top: {1}, Right: {2}, Bottom: {3}", data.rc.left, data.rc.top, data.rc.right, data.rc.bottom);
- 328 //自定义对齐方式,确保Height和Width不会小于0
- 329 if (data.rc.bottom - data.rc.top < 0)
- 330 {
- 331 if (Location == AppBarLocation.Top)
- 332 data.rc.bottom = data.rc.top + (int)Math.Round(actualSize.Y);//上对齐
- 333 else if (Location == AppBarLocation.Bottom)
- 334 data.rc.top = data.rc.bottom - (int)Math.Round(actualSize.Y);//下对齐
- 335 }
- 336 if(data.rc.right - data.rc.left < 0)
- 337 {
- 338 if (Location == AppBarLocation.Left)
- 339 data.rc.right = data.rc.left + (int)Math.Round(actualSize.X);//左对齐
- 340 else if (Location == AppBarLocation.Right)
- 341 data.rc.left = data.rc.right - (int)Math.Round(actualSize.X);//右对齐
- 342 }
- 343 //调整完毕,设置为最终位置
- 344 Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_SETPOS, ref data);
- 345 //应用到窗口
- 346 var location = toWpfUnit.Transform(new Point(data.rc.left, data.rc.top));
- 347 var dimension = toWpfUnit.Transform(new Vector(data.rc.right - data.rc.left,
- 348 data.rc.bottom - data.rc.top));
- 349 var rect = new Rect(location, new Size(dimension.X, dimension.Y));
- 350 DockedSize = rect;
- 351
- 352 _window.Dispatcher.Invoke(DispatcherPriority.ApplicationIdle, () =>{
- 353 _window.Left = rect.Left;
- 354 _window.Top = rect.Top;
- 355 _window.Width = rect.Width;
- 356 _window.Height = rect.Height;
- 357 });
- 358
- 359 Debug.WriteLine("Set {0} Left: {1} ,Top: {2}, Width: {3}, Height: {4}", _window.Title, _window.Left, _window.Top, _window.Width, _window.Height);
- 360 }
- 361 public void DisableAppBar()
- 362 {
- 363 if (_isRegistered)
- 364 {
- 365 _isRegistered = false;
- 366 var data = new Interop.APPBARDATA();
- 367 data.cbSize = Marshal.SizeOf(data);
- 368 data.hWnd = _hWnd;
- 369 data.uCallbackMessage = _callbackId;
- 370 Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_REMOVE, ref data);
- 371 _isRegistered = false;
- 372 RestoreWindowInfo();
- 373 Debug.WriteLine(_window.Title + " DisableAppBar");
- 374 }
- 375 }
- 376 }
- 377
- 378 public enum AppBarLocation : int
- 379 {
- 380 Left = 0,
- 381 Top,
- 382 Right,
- 383 Bottom,
- 384 None,
- 385 RegisterOnly=99
- 386 }
- 387
- 388 internal static class Interop
- 389 {
- 390 #region Structures & Flags
- 391 [StructLayout(LayoutKind.Sequential)]
- 392 internal struct RECT
- 393 {
- 394 public int left;
- 395 public int top;
- 396 public int right;
- 397 public int bottom;
- 398 }
- 399
- 400 [StructLayout(LayoutKind.Sequential)]
- 401 internal struct APPBARDATA
- 402 {
- 403 public int cbSize;
- 404 public IntPtr hWnd;
- 405 public int uCallbackMessage;
- 406 public int uEdge;
- 407 public RECT rc;
- 408 public IntPtr lParam;
- 409 }
- 410
- 411 internal enum AppBarMsg : int
- 412 {
- 413 ABM_NEW = 0,
- 414 ABM_REMOVE,
- 415 ABM_QUERYPOS,
- 416 ABM_SETPOS,
- 417 ABM_GETSTATE,
- 418 ABM_GETTASKBARPOS,
- 419 ABM_ACTIVATE,
- 420 ABM_GETAUTOHIDEBAR,
- 421 ABM_SETAUTOHIDEBAR,
- 422 ABM_WINDOWPOSCHANGED,
- 423 ABM_SETSTATE
- 424 }
- 425 internal enum AppBarNotify : int
- 426 {
- 427 ABN_STATECHANGE = 0,
- 428 ABN_POSCHANGED,
- 429 ABN_FULLSCREENAPP,
- 430 ABN_WINDOWARRANGE
- 431 }
- 432 #endregion
- 433
- 434 #region Win32 API
- 435 [DllImport("SHELL32", CallingConvention = CallingConvention.StdCall)]
- 436 internal static extern uint SHAppBarMessage(int dwMessage, ref APPBARDATA pData);
- 437
- 438 [DllImport("User32.dll", CharSet = CharSet.Auto)]
- 439 internal static extern int RegisterWindowMessage(string msg);
- 440 #endregion
- 441 }
三、已知问题
1.在我的github上的实例程序中,如果你将两个同进程的窗口并排叠放的话,会导致explorer和你的进程双双爆栈,windows似乎不能很好地处理这两个并排放置的窗口,一直在左右调整位置,疯狂发送ABN_POSCHANGED消息。(快去clone试试,死机了不要打我) 但是并排放置示例窗口和OneNote的Dock窗口就没有问题。
2.计算停靠窗口时,如果选择停靠位置为Bottom,则系统建议的bottom位置值会比实际的高,测试发现是任务栏窗口占据了部分空间,应该是预留给平板模式的更大图标任务栏(猜测,很不合理的设计)

自动隐藏任务栏就没有这个问题:

3. 没有实现自动隐藏AppBar,故没有处理与之相关的WM_ACTIVATE等消息,有需要的可以参考官方文档。(嘻 我懒)
参考文档:
1). SHAppBarMessage function (shellapi.h) - Win32 apps | Microsoft Learn
2). ABM_QUERYPOS message (Shellapi.h) - Win32 apps | Microsoft Learn ABM_NEW & ABM_SETPOS etc..
3). 使用应用程序桌面工具栏 - Win32 apps | Microsoft Learn
4). 判断是否有全屏程序正在运行(C#)_c# 判断程序当前窗口是否全屏如果是返回原来-CSDN博客
[打个广告] [入门AppBar的最佳实践]
看这里,如果你也需要一个高度可自定义的沉浸式顶部栏(Preview): TwilightLemon/MyToolBar: 为Surface Pro而生的顶部工具栏 支持触控和笔快捷方式 (github.com)





本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名TwilightLemon和原文网址,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。