首页/文章列表/文章详情

WPF使用AppBar实现窗口停靠,适配缩放、全屏响应和多窗口并列(附封装好即开即用的附加属性)

编程知识3052024-07-18评论

在吕毅大佬的文章中已经详细介绍了什么是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>

或者在后台创建:

privatereadonly AppBar appBar=newAppBar();...Window_Loaded...appBar.Location=AppBarLocation.Top;appBar.OnFullScreenStateChanged+=AppBar_OnFullScreenStateChanged;AppBarCreator.SetAppBar(this, appBar);

3. 另外你可能注意到了,这里有一个OnFullScreenStateChanged事件:该事件由AppBarMsg注册,在有窗口进入或退出全屏时触发,参数bool为true指示进入全屏。

你需要手动在事件中设置全屏模式下的行为,例如在全屏时隐藏AppBar

privatevoidAppBar_OnFullScreenStateChanged(objectsender,bool e) { Debug.WriteLine("Full Screen State: "+e); Visibility = e ? Visibility.Collapsed : Visibility.Visible; }

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

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

 

以下贴出完整的代码:

1usingSystem.ComponentModel;2usingSystem.Diagnostics;3usingSystem.Runtime.InteropServices;4usingSystem.Windows;5usingSystem.Windows.Interop;6usingSystem.Windows.Threading;78namespaceAppBarTest;9publicstaticclassAppBarCreator10{11publicstaticreadonly DependencyProperty AppBarProperty =12DependencyProperty.RegisterAttached(13"AppBar",14typeof(AppBar),15typeof(AppBarCreator),16newPropertyMetadata(null, OnAppBarChanged));17privatestaticvoid OnAppBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)18{19if(dis Window window && e.NewValue is AppBar appBar)20{21 appBar.AttachedWindow =window;22}23}24publicstaticvoid SetAppBar(Window element, AppBar value)25{26if (value == null)return;27 element.SetValue(AppBarProperty, value);28}2930publicstatic AppBar GetAppBar(Window element)31{32return(AppBar)element.GetValue(AppBarProperty);33}34}3536publicclass AppBar : DependencyObject37{38///<summary>39///附加到的窗口40///</summary>41public Window AttachedWindow42{43get=>_window;44set45{46if (value == null)return;47 _window =value;48 _window.Closing +=_window_Closing;49 _window.LocationChanged +=_window_LocationChanged;50//获取窗口句柄hWnd51var handle = newWindowInteropHelper(value).Handle;52if (handle ==IntPtr.Zero)53{54//Win32窗口未创建55 _window.SourceInitialized +=_window_SourceInitialized;56}57else58{59 _hWnd =handle;60CheckPending();61}62}63}6465privatevoid_window_LocationChanged(object? sender, EventArgs e)66{67Debug.WriteLine(_window.Title+" LocationChanged: Top: "+_window.Top+"Left:"+_window.Left);68}6970privatevoid_window_Closing(object? sender, CancelEventArgs e)71{72 _window.Closing -=_window_Closing;73if (Location !=AppBarLocation.None)74DisableAppBar();75}7677///<summary>78///检查是否需要应用之前的Location更改79///</summary>80privatevoidCheckPending()81{82//创建AppBar时提前触发的LocationChanged83if(_locationChangePending)84{85 _locationChangePending = false;86LoadAppBar(Location);87}88}89///<summary>90///载入AppBar91///</summary>92///<param name="e"></param>93privatevoid LoadAppBar(AppBarLocation e,AppBarLocation? previous=null)94{9596if (e !=AppBarLocation.None)97{98if (e ==AppBarLocation.RegisterOnly)99{100//仅注册AppBarMsg101//如果之前注册过有效的AppBar则先注销,以还原位置102if (previous.HasValue && previous.Value !=AppBarLocation.RegisterOnly)103{104if (previous.Value !=AppBarLocation.None)105{106//由生效的AppBar转为RegisterOnly,还原为普通窗口再注册空AppBar107DisableAppBar();108}109RegisterAppBarMsg();110}111else112{113//之前未注册过AppBar,直接注册114RegisterAppBarMsg();115}116}117else118{119if (previous.HasValue && previous.Value !=AppBarLocation.None)120{121//之前为RegisterOnly才备份窗口信息122if(previous.Value ==AppBarLocation.RegisterOnly)123{124BackupWindowInfo();125}126SetAppBarPosition(_originalSize);127ForceWindowStyles();128}129else130EnableAppBar();131}132}133else134{135DisableAppBar();136}137}138privatevoid_window_SourceInitialized(object? sender, EventArgs e)139{140 _window.SourceInitialized -=_window_SourceInitialized;141 _hWnd = newWindowInteropHelper(_window).Handle;142CheckPending();143}144145///<summary>146/// 当有窗口进入或退出全屏时触发 bool参数为true时表示全屏状态147///</summary>148publiceventEventHandler<bool>?OnFullScreenStateChanged;149///<summary>150///期望将AppBar停靠到的位置151///</summary>152public AppBarLocation Location153{154get{return (AppBarLocation)GetValue(LocationProperty); }155set { SetValue(LocationProperty, value); }156}157158publicstaticreadonly DependencyProperty LocationProperty =159DependencyProperty.Register(160"Location",161typeof(AppBarLocation),typeof(AppBar),162new PropertyMetadata(AppBarLocation.None, OnLocationChanged));163164privatebool _locationChangePending = false;165privatestaticvoid OnLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)166{167if(DesignerProperties.GetIsInDesignMode(d))168return;169if(dis not AppBar appBar) return;170if (appBar.AttachedWindow == null)171{172 appBar._locationChangePending = true;173return;174}175appBar.LoadAppBar((AppBarLocation)e.NewValue,(AppBarLocation)e.OldValue);176}177178privateint _callbackId = 0;179privatebool _isRegistered = false;180private Window _window = null;181private IntPtr _hWnd;182private WindowStyle _originalStyle;183private Point _originalPosition;184private Size _originalSize =Size.Empty;185private ResizeMode _originalResizeMode;186privatebool_originalTopmost;187public Rect? DockedSize { get;set; } = null;188private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam,189 IntPtr lParam, refboolhandled)190{191if (msg ==_callbackId)192{193 Debug.WriteLine(_window.Title + "AppBarMsg("+_callbackId+"):" + wParam.ToInt32() + "LParam:"+lParam.ToInt32());194switch(wParam.ToInt32())195{196case(int)Interop.AppBarNotify.ABN_POSCHANGED:197Debug.WriteLine("AppBarNotify.ABN_POSCHANGED ! "+_window.Title);198if (Location !=AppBarLocation.RegisterOnly)199SetAppBarPosition(Size.Empty);200 handled = true;201break;202case(int)Interop.AppBarNotify.ABN_FULLSCREENAPP:203OnFullScreenStateChanged?.Invoke(this, lParam.ToInt32() == 1);204 handled = true;205break;206}207}208returnIntPtr.Zero;209}210211publicvoidBackupWindowInfo()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}221publicvoidRestoreWindowInfo()222{223if (_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}234publicvoidForceWindowStyles()235{236 _window.WindowStyle =WindowStyle.None;237 _window.ResizeMode =ResizeMode.NoResize;238 _window.Topmost = true;239}240241publicvoidRegisterAppBarMsg()242{243var data = newInterop.APPBARDATA();244 data.cbSize =Marshal.SizeOf(data);245 data.hWnd =_hWnd;246247 _isRegistered = true;248 _callbackId =Interop.RegisterWindowMessage(Guid.NewGuid().ToString());249 data.uCallbackMessage =_callbackId;250var success = Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_NEW,refdata);251var source =HwndSource.FromHwnd(_hWnd);252 Debug.WriteLineIf(source == null,"HwndSource is null!");253source?.AddHook(WndProc);254Debug.WriteLine(_window.Title+"RegisterAppBarMsg:"+_callbackId);255}256publicvoidEnableAppBar()257{258if(!_isRegistered)259{260//备份窗口信息并设置窗口样式261BackupWindowInfo();262//注册成为AppBar窗口263RegisterAppBarMsg();264ForceWindowStyles();265}266//成为AppBar窗口之后(或已经是)只需要注册并移动窗口位置即可267SetAppBarPosition(_originalSize);268}269publicvoid SetAppBarPosition(Size WindowSize)270{271var data = newInterop.APPBARDATA();272 data.cbSize =Marshal.SizeOf(data);273 data.hWnd =_hWnd;274 data.uEdge = (int)Location;275 data.uCallbackMessage =_callbackId;276Debug.WriteLine("\r\nWindow:"+_window.Title);277278//获取WPF单位与像素的转换矩阵279var compositionTarget = PresentationSource.FromVisual(_window)?.CompositionTarget;280if (compositionTarget == null)281thrownewException("居然获取不到CompositionTarget?!");282var toPixel =compositionTarget.TransformToDevice;283var toWpfUnit =compositionTarget.TransformFromDevice;284285//窗口在屏幕的实际大小286if(WindowSize==Size.Empty)287 WindowSize = new Size(_window.ActualWidth, _window.ActualHeight);288var actualSize = toPixel.Transform(new Vector(WindowSize.Width, WindowSize.Height));289//屏幕的真实像素290var workArea = toPixel.Transform(new Vector(SystemParameters.PrimaryScreenWidth, SystemParameters.PrimaryScreenHeight));291Debug.WriteLine("WorkArea Width: {0}, Height: {1}", workArea.X, workArea.Y);292293if(Locationis AppBarLocation.Left or AppBarLocation.Right)294{295 data.rc.top = 0;296 data.rc.bottom = (int)workArea.Y;297if (Location ==AppBarLocation.Left)298{299 data.rc.left = 0;300 data.rc.right = (int)Math.Round(actualSize.X);301}302else303{304 data.rc.right = (int)workArea.X;305 data.rc.left = (int)workArea.X - (int)Math.Round(actualSize.X);306}307}308else309{310 data.rc.left = 0;311 data.rc.right = (int)workArea.X;312if (Location ==AppBarLocation.Top)313{314 data.rc.top = 0;315 data.rc.bottom = (int)Math.Round(actualSize.Y);316}317else318{319 data.rc.bottom = (int)workArea.Y;320 data.rc.top = (int)workArea.Y - (int)Math.Round(actualSize.Y);321}322}323//以上生成的是四周都没有其他AppBar时的理想位置324//系统将自动调整位置以适应其他AppBar325Debug.WriteLine("Before QueryPos: Left: {0}, Top: {1}, Right: {2}, Bottom: {3}", data.rc.left, data.rc.top, data.rc.right, data.rc.bottom);326Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_QUERYPOS,refdata);327Debug.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不会小于0329if (data.rc.bottom - data.rc.top < 0)330{331if (Location ==AppBarLocation.Top)332 data.rc.bottom = data.rc.top + (int)Math.Round(actualSize.Y);//上对齐333elseif (Location ==AppBarLocation.Bottom)334 data.rc.top = data.rc.bottom - (int)Math.Round(actualSize.Y);//下对齐335}336if(data.rc.right - data.rc.left < 0)337{338if (Location ==AppBarLocation.Left)339 data.rc.right = data.rc.left + (int)Math.Round(actualSize.X);//左对齐340elseif (Location ==AppBarLocation.Right)341 data.rc.left = data.rc.right - (int)Math.Round(actualSize.X);//右对齐342}343//调整完毕,设置为最终位置344Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_SETPOS,refdata);345//应用到窗口346var location = toWpfUnit.Transform(new Point(data.rc.left, data.rc.top));347var dimension = toWpfUnit.Transform(new Vector(data.rc.right -data.rc.left,348 data.rc.bottom -data.rc.top));349var rect = newRect(location,new Size(dimension.X, dimension.Y));350 DockedSize =rect;351352 _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});358359Debug.WriteLine("Set {0} Left: {1} ,Top: {2}, Width: {3}, Height: {4}", _window.Title, _window.Left, _window.Top, _window.Width, _window.Height);360}361publicvoidDisableAppBar()362{363if(_isRegistered)364{365 _isRegistered = false;366var data = newInterop.APPBARDATA();367 data.cbSize =Marshal.SizeOf(data);368 data.hWnd =_hWnd;369 data.uCallbackMessage =_callbackId;370Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_REMOVE,refdata);371 _isRegistered = false;372RestoreWindowInfo();373 Debug.WriteLine(_window.Title + "DisableAppBar");374}375}376}377378publicenum AppBarLocation : int379{380 Left = 0,381Top,382Right,383Bottom,384None,385RegisterOnly=99386}387388internalstaticclassInterop389{390#region Structures & Flags391[StructLayout(LayoutKind.Sequential)]392internalstructRECT393{394publicintleft;395publicinttop;396publicintright;397publicintbottom;398}399400[StructLayout(LayoutKind.Sequential)]401internalstructAPPBARDATA402{403publicintcbSize;404public IntPtr hWnd;405publicintuCallbackMessage;406publicintuEdge;407public RECT rc;408public IntPtr lParam;409}410411internalenum AppBarMsg : int412{413 ABM_NEW = 0,414ABM_REMOVE,415ABM_QUERYPOS,416ABM_SETPOS,417ABM_GETSTATE,418ABM_GETTASKBARPOS,419ABM_ACTIVATE,420ABM_GETAUTOHIDEBAR,421ABM_SETAUTOHIDEBAR,422ABM_WINDOWPOSCHANGED,423ABM_SETSTATE424}425internalenum AppBarNotify : int426{427 ABN_STATECHANGE = 0,428ABN_POSCHANGED,429ABN_FULLSCREENAPP,430ABN_WINDOWARRANGE431}432#endregion433434#region Win32 API435[DllImport("SHELL32", CallingConvention =CallingConvention.StdCall)]436internalstaticexternuintSHAppBarMessage(intdwMessage,ref APPBARDATA pData);437438[DllImport("User32.dll", CharSet =CharSet.Auto)]439internalstaticexternintRegisterWindowMessage(stringmsg);440#endregion441}

 

三、已知问题

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和原文网址,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

 

神弓

博客园

这个人很懒...

用户评论 (0)

发表评论

captcha