在吕毅大佬的文章中已经详细介绍了什么是AppBar:
WPF 使用 AppBar 将窗口停靠在桌面上,让其他程序不占用此窗口的空间(附我封装的附加属性) - walterlv

即让窗口固定在屏幕某一边,并且保证其他窗口最大化后不会覆盖AppBar占据区域(类似于Windows任务栏)。

但是在我的环境中测试时,上面的代码出现了一些问题,例如非100%缩放显示时的坐标计算异常、多窗口同时停靠时布局错乱等。所以我重写了AppBar在WPF上的实现,效果如图:


一、AppBar的主要申请流程

主要流程如图:

核心代码其实在于如何计算停靠窗口的位置,要点是处理好一下几个方面:

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:AppBarx:Name="appBar"Location="Top"OnFullScreenStateChanged="AppBar_OnFullScreenStateChanged"/>
</local:AppBarCreator.AppBar>...</Window>

或者在后台创建:

private readonly AppBar appBar=newAppBar();

...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, boole)
{
Debug.WriteLine(
"Full Screen State:"+e);
Visibility
= e ?Visibility.Collapsed : Visibility.Visible;
}

我在官方的Flag上加了一个RegisterOnly,即只注册AppBarMsg而不真的停靠窗口,可以此用来作全屏模式监听。

4. 如果你需要在每个虚拟桌面都显示AppBar(像任务栏那样),可以尝试为窗口使用SetWindowLong添加WS_EX_TOOLWINDOW标签(自行查找)

以下贴出完整的代码:

1 usingSystem.ComponentModel;2 usingSystem.Diagnostics;3 usingSystem.Runtime.InteropServices;4 usingSystem.Windows;5 usingSystem.Windows.Interop;6 usingSystem.Windows.Threading;7 
8 namespaceAppBarTest;9 public static classAppBarCreator10 {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 voidOnAppBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)18 {19         if (d is Window window && e.NewValue isAppBar appBar)20 {21             appBar.AttachedWindow =window;22 }23 }24     public static voidSetAppBar(Window element, AppBar value)25 {26         if (value == null) return;27 element.SetValue(AppBarProperty, value);28 }29 
30     public staticAppBar GetAppBar(Window element)31 {32         return(AppBar)element.GetValue(AppBarProperty);33 }34 }35 
36 public classAppBar : DependencyObject37 {38     /// <summary>
39     ///附加到的窗口40     /// </summary>
41     publicWindow AttachedWindow42 {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 = newWindowInteropHelper(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 voidCheckPending()81 {82         //创建AppBar时提前触发的LocationChanged
83         if(_locationChangePending)84 {85             _locationChangePending = false;86 LoadAppBar(Location);87 }88 }89     /// <summary>
90     ///载入AppBar91     /// </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                 //仅注册AppBarMsg101                 //如果之前注册过有效的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 = newWindowInteropHelper(_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     publicAppBarLocation Location153 {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             newPropertyMetadata(AppBarLocation.None, OnLocationChanged));163 
164     private bool _locationChangePending = false;165     private static voidOnLocationChanged(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     privateIntPtr _hWnd;182     privateWindowStyle _originalStyle;183     privatePoint _originalPosition;184     private Size _originalSize =Size.Empty;185     privateResizeMode _originalResizeMode;186     private bool_originalTopmost;187     public Rect? DockedSize { get; set; } = null;188     private IntPtr WndProc(IntPtr hwnd, intmsg, IntPtr wParam,189                                     IntPtr lParam, ref boolhandled)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         returnIntPtr.Zero;209 }210 
211     public voidBackupWindowInfo()212 {213         _callbackId = 0;214         DockedSize = null;215         _originalStyle =_window.WindowStyle;216         _originalSize = newSize(_window.ActualWidth, _window.ActualHeight);217         _originalPosition = newPoint(_window.Left, _window.Top);218         _originalResizeMode =_window.ResizeMode;219         _originalTopmost =_window.Topmost;220 }221     public voidRestoreWindowInfo()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 voidForceWindowStyles()235 {236         _window.WindowStyle =WindowStyle.None;237         _window.ResizeMode =ResizeMode.NoResize;238         _window.Topmost = true;239 }240 
241     public voidRegisterAppBarMsg()242 {243         var data = newInterop.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, refdata);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 voidEnableAppBar()257 {258         if (!_isRegistered)259 {260             //备份窗口信息并设置窗口样式
261 BackupWindowInfo();262             //注册成为AppBar窗口
263 RegisterAppBarMsg();264 ForceWindowStyles();265 }266         //成为AppBar窗口之后(或已经是)只需要注册并移动窗口位置即可
267 SetAppBarPosition(_originalSize);268 }269     public voidSetAppBarPosition(Size WindowSize)270 {271         var data = newInterop.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 = newSize(_window.ActualWidth, _window.ActualHeight);288         var actualSize = toPixel.Transform(newVector(WindowSize.Width, WindowSize.Height));289         //屏幕的真实像素
290         var workArea = toPixel.Transform(newVector(SystemParameters.PrimaryScreenWidth, SystemParameters.PrimaryScreenHeight));291         Debug.WriteLine("WorkArea Width: {0}, Height: {1}", workArea.X, workArea.Y);292 
293         if (Location isAppBarLocation.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, refdata);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, refdata);345         //应用到窗口
346         var location = toWpfUnit.Transform(newPoint(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, newSize(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 voidDisableAppBar()362 {363         if(_isRegistered)364 {365             _isRegistered = false;366             var data = newInterop.APPBARDATA();367             data.cbSize =Marshal.SizeOf(data);368             data.hWnd =_hWnd;369             data.uCallbackMessage =_callbackId;370             Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_REMOVE, refdata);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 classInterop389 {390     #region Structures & Flags
391 [StructLayout(LayoutKind.Sequential)]392     internal structRECT393 {394         public intleft;395         public inttop;396         public intright;397         public intbottom;398 }399 
400 [StructLayout(LayoutKind.Sequential)]401     internal structAPPBARDATA402 {403         public intcbSize;404         publicIntPtr hWnd;405         public intuCallbackMessage;406         public intuEdge;407         publicRECT rc;408         publicIntPtr 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_SETSTATE424 }425     internal enum AppBarNotify : int
426 {427         ABN_STATECHANGE = 0,428 ABN_POSCHANGED,429 ABN_FULLSCREENAPP,430 ABN_WINDOWARRANGE431 }432     #endregion
433 
434     #region Win32 API
435     [DllImport("SHELL32", CallingConvention =CallingConvention.StdCall)]436     internal static extern uint SHAppBarMessage(int dwMessage, refAPPBARDATA pData);437 
438     [DllImport("User32.dll", CharSet =CharSet.Auto)]439     internal static extern int RegisterWindowMessage(stringmsg);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和原文网址,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

标签: none

添加新评论