经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
WPF使用AppBar实现窗口停靠,适配缩放、全屏响应和多窗口并列(附封装好即开即用的附加属性)
来源:cnblogs  作者:TwilightLemon  时间:2024/7/19 10:46:05  对本文有异议

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

  1. <Window ...>
  2.  
  3. <local:AppBarCreator.AppBar>
  4. <local:AppBar x:Name="appBar" Location="Top" OnFullScreenStateChanged="AppBar_OnFullScreenStateChanged"/>
  5. </local:AppBarCreator.AppBar>
  6. ...
  7. </Window>

或者在后台创建:

  1. private readonly AppBar appBar=new AppBar();
  2. ...Window_Loaded...
  3. appBar.Location = AppBarLocation.Top;
  4. appBar.OnFullScreenStateChanged += AppBar_OnFullScreenStateChanged;
  5. AppBarCreator.SetAppBar(this, appBar);

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

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

  1. private void AppBar_OnFullScreenStateChanged(object sender, bool e)
  2. {
  3. Debug.WriteLine("Full Screen State: "+e);
  4. Visibility = e ? Visibility.Collapsed : Visibility.Visible;
  5. }

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

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

 

以下贴出完整的代码:

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

 

原文链接:https://www.cnblogs.com/TwilightLemon/p/18309927

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号