[WinUI3] 如何自定义桌面应用标题栏

您所在的位置:网站首页 怎么自定义桌面应用图标图片大小 [WinUI3] 如何自定义桌面应用标题栏

[WinUI3] 如何自定义桌面应用标题栏

2024-07-06 11:46| 来源: 网络整理| 查看: 265

📢 随着 Windows App SDK 1.0 的发布,Windows 应用开发也进入到了一个新的时期。虽然前景美好,但该框架还有一些不完善的地方,下文所述即是我在折腾 WinUI 3 时遇到的标题栏的坑,分享出来以供大家参考。

P.S. 下文所展示的缺陷可能会在将来的版本中修复,本文仅针对 Windows App SDK 1.0 版本。

场景说明

先上效果图:

Untitled.png

在 UWP 中,往标题栏放控件早已不是什么新鲜事了,比如 Microsoft Store:

Untitled 1.png

自定义的标题栏往往与应用主体更为契合,在代码实现上也不困难,这些在 UWP 文档上有详细的教程:Title bar customization - Windows apps | Microsoft Docs

我在设计应用时也会延续 UWP 的设计思路,使用自定义的标题栏,在里面放上返回按钮、菜单按钮、搜索框之类的控件。

在开发 UWP 时,一切得心应手,但是在桌面应用中,一切突然变得陌生了。

下面,请新建一个空白 WinUI 3 桌面应用,我们一步步来。

❗ 官方文档给出的示例,即 CoreApplication.GetCurrentView().TitleBar 那一套在桌面应用中是不行的,由于应用模型不同,该方法不会返回正确的结果,而是抛出异常 (Element not found)。在桌面应用中,我们只能走窗口管理API这条路。

遇到的困难

双重标题栏

对于 WinUI 3 桌面应用来说,它的标题栏不止一个。

第一个标题栏(位于AppWindow)

Untitled 2.png

第二个标题栏(WindowChrome)

Untitled 3.png

当我们在 App.xaml.cs 的 OnLaunched 事件回调里加上一句

m_window.ExtendsContentIntoTitleBar = true;

第二个标题栏就会出现。此时我们调整窗口宽度,就会显示出神奇的一幕:

dragTitleBar.gif

注意到了吗?第二个标题栏不光在调整大小时有延迟,而且在它没盖住的地方还会显现出下层“真正的”标题栏,也就是位于 AppWindow 的标题栏。

这时候的问题在于,我们要在哪一个标题栏上做文章?

交互拦截

当我们调用 Window.SetTitleBar(A) 这一方法设置标题栏时,A 控件的所有内部控件及位于 A 渲染范围内的控件的交互全都会被拦截,比如下面的代码:

public MainWindow() { this.InitializeComponent(); ExtendsContentIntoTitleBar = true; SetTitleBar(AppTitleBar); }

Untitled 4.png

标题栏区域可以拖动,但两个按钮均无法点击,即便 Button B 的 ZIndex 高于 Button A。

背景覆盖

你可能注意到上面的图片中没有 Windows 应用的“三大金刚”,即最小化/最大化和关闭按钮,原因很简单,我们给 MainWindow 的根元素(Grid)加了个背景色,同时我们又设置了 ExtendsContentIntoTitleBar 为 True,所以作为内容区的 Grid 的背景色就覆盖了位于 WindowChrome 上的 TitleBar,把三大金刚给盖住了。

就TM离谱。

为了解决颜色问题,要么把自定义标题栏搞成透明的,要么覆盖默认资源,但若是碰到自定义标题栏高度和默认高度不一样,又不能覆盖默认按钮,那就有意思了,可能会这样:

Untitled 5.png

其它还有一些开发过程中会碰到的小问题,我们在后文详述。

实现方案

按照文档 Window.SetTitleBar(UIElement) Method (Microsoft.UI.Xaml) - WinUI | Microsoft Docs 的说法,使用自定义标题栏的第一步就是调用 Window.ExtendsContentIntoTitleBar = true。

如果按着文档走,接下来你会面临我上面列举的诸多恼人的问题,且基本没有解决方法,除非你改设计。

所以,让我们回到上节的第一个问题,两个标题栏,选谁?

选第一个,即 AppWindowTitleBar。

扩展标题栏

Microsoft.UI.Xaml.Window 类中有 ExtendsContentIntoTitleBar 属性,Microsoft.UI.Windowing.AppWindowTitleBar 上也有。我们要修改的就是 AppWindowTitleBar.ExtendsContentIntoTitleBar 属性。

在 App.xaml.cs 中添加如下代码:

private IntPtr _windowHandle; /// /// 应用窗口对象. /// public static AppWindow AppWindow { get; private set; } /// /// 主窗口. /// public static Window MainWindow { get; private set; } /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. /// /// Details about the launch request and process. protected override void OnLaunched(LaunchActivatedEventArgs args) { MainWindow = new MainWindow(); // 获取当前窗口句柄 _windowHandle = WindowNative.GetWindowHandle(MainWindow); var windowId = Win32Interop.GetWindowIdFromWindow(_windowHandle); // 获取应用窗口对象 AppWindow = AppWindow.GetFromWindowId(windowId); AppWindow.TitleBar.ExtendsContentIntoTitleBar = true; MainWindow.Activate(); }

此时运行应用,你会看到这样的结果:

Untitled 6.png

应用顶部与标题栏等高的区域是可以拖动的哦~

创建自定义标题栏

为了实现我们预期的设计:

Untitled.png

现在需要创建一个自定义控件,名为 AppTitleBar

Untitled 7.png

在 AppTitleBar.xaml 中创建UI:

接下来,在 MainWindow.xaml 中引入该控件。

此时运行应用,应该如下所示:

Untitled 8.png

此时我们并没有指定 AppTitleBar 为应用的标题栏,所以你能发现,一个28像素的透明标题栏依然盖在控件上方,被它覆盖的区域我们不能点击到下方的搜索框。但此时我们也可以发现,即便我们给标题栏设置了背景色,它也没有覆盖三大金刚,并且调整窗口大小也不会有奇怪的残影,这非常好!

设置拖拽区域

上一步结束之后,是不是就要把 AppTitleBar 指定为应用的标题栏呢?

非也。

你一旦在 MainWindow 中调用 SetTitleBar(AppTitleBar),你会发现……啥都没变。

因为在 MainWindow 中调用 SetTitleBar 方法,会将指定的 UIElement 设置到 WindowChrome 上,而在此之前,你必须要在 MainWindow 中设置 ExtendsContentIntoTitleBar = true 让 WindowChrome 显示出来才行。

我们既已选择了走 AppWindow 这条路,就忘了 WindowChrome 吧。

那么接下来我们怎么做?

我们现在的问题是什么?

标题栏的区域盖住了本应提供交互的区域,同时我们预期的标题栏高度要大于默认高度,所以默认的标题栏高度又不够,就像下图所示:

default_drag.png

所以说,我们要解决两个问题:

调整标题栏的高度,让它和控件一致。 不让标题栏盖住我们预期提供交互的区域(这里指搜索框)。

目前 WinUI 3 文档匮乏,没有文档告诉我们该怎么做。这里就很有意思了,我们要思考一件事,到底是什么盖住了内容区?

是标题栏吗?是,但更进一步,是标题栏的可拖拽区域盖住了内容区。

拖拽区拦截了我们所需要的交互事件,转而为窗口拖拽和窗口快捷操作(比如双击标题栏全屏)服务。

那么想到这里,我们就能把前面的问题转化成另一个问题:如何控制标题栏可拖拽区域的大小和位置?

AppWindowTitleBar.SetDragRectangles(RectInt32[])

方法名很直观的表明了该 API 的用途,所以我们的问题就可以通过该方法得到解决。

再来分析一下我们的布局:

custom_drag.png

由于三大金刚按钮始终置顶,所以我们可以忽略覆盖它们的问题,这样我们就用搜索框分割出了两个拖拽区域。

这两个拖拽区域就是我们要传给 AppWindowTitleBar.SetDragRectangles() 的参数了。

如何计算拖拽区域呢?

将下面的代码加入 AppTitleBar.xaml.cs

public AppTitleBar() { this.InitializeComponent(); this.Loaded += OnLoaded; this.SizeChanged += OnSizeChanged; } private void OnSizeChanged(object sender, SizeChangedEventArgs e) => UpdateDragRects(); private void OnLoaded(object sender, RoutedEventArgs e) => UpdateDragRects(); private void UpdateDragRects() { var titleBar = App.AppWindow.TitleBar; // 当前控件的实际宽度. var totalSpace = ActualWidth; var height = ActualHeight; // 搜索框的左边界相对于整个控件左边界的偏移值. var searchLeftOffset = SearchBox.ActualOffset.X; // 搜索框的右边界相对于整个控件左边界的偏移值. var searchRightOffset = searchLeftOffset + SearchBox.ActualWidth; var leftSpace = searchLeftOffset; var rightSpace = totalSpace - searchLeftOffset - SearchBox.ActualWidth; var leftRect = new RectInt32(0, 0, Convert.ToInt32(leftSpace), Convert.ToInt32(height)); var rightRect = new RectInt32(Convert.ToInt32(searchRightOffset), 0, Convert.ToInt32(rightSpace), Convert.ToInt32(height)); titleBar.SetDragRectangles(new RectInt32[] { leftRect, rightRect }); }

📌 UpdateDragRects 方法表明了计算过程。由于作为示例,这里的矩形计算相对简单,如果你的标题栏中包含更多控件,需要划分更多区域,请按照这个思路继续。如果你的应用有更高的设计规范要求,也别忘了考虑 AppWindowTitleBar.LeftInset 和 AppWindowTitleBar.RightInset 造成的影响,这里就不展开了。

DPI 问题

如果你不在 100% 标准比例下运行应用,你会发现一件很坑的事情,即你写的算法没有问题,但是拖拽区域就是对不上。

比如你在 125% 放大的环境中运行上面的代码,你会发现搜索框后面半截依然被拖拽区域覆盖,且搜索框左侧的空白区域有一段不可拖拽,看上去像是我给错区域了。

在我踩坑时,我并没有意识到这是 DPI 的问题。我想从 UWP 转过来的开发者脑子里面可能都不会想到是 DPI,谁让在开发 UWP 的时候完全不用考虑这种事呢?

直到我做匹配测试(即在传入矩形区域前手动调整矩形参数),得出的多组数值都显示预期数值是传入数值的1.25倍左右我才意识到可能是放大比例的问题。

所以,同志们,我们需要修改上面的计算方法,以考虑 DPI 的影响。

先引入 PInvoke.User32 nuget 包,再加一个转换方法:

/// /// 在设置拖动区域时,需要考虑到系统缩放比例对像素的影响. /// /// 像素值. /// 转换后的结果. private static int GetActualPixel(double pixel) { var windowHandle = WindowNative.GetWindowHandle(App.MainWindow); var currentDpi = PInvoke.User32.GetDpiForWindow(windowHandle); return Convert.ToInt32(pixel * (currentDpi / 96.0)); } private void UpdateDragRects() { var titleBar = App.AppWindow.TitleBar; // 当前控件的实际宽度. var totalSpace = ActualWidth; var height = ActualHeight; // 搜索框的左边界相对于整个控件左边界的偏移值. var searchLeftOffset = SearchBox.ActualOffset.X; // 搜索框的右边界相对于整个控件左边界的偏移值. var searchRightOffset = searchLeftOffset + SearchBox.ActualWidth; var leftSpace = searchLeftOffset; var rightSpace = totalSpace - searchLeftOffset - SearchBox.ActualWidth; var leftRect = new RectInt32(0, 0, GetActualPixel(leftSpace), GetActualPixel(height)); var rightRect = new RectInt32(GetActualPixel(searchRightOffset), 0, GetActualPixel(rightSpace), GetActualPixel(height)); titleBar.SetDragRectangles(new RectInt32[] { leftRect, rightRect }); }

P.S. 96 是一个参考的标准数值,指在 100% 缩放下的DPI,但该值并不是固定不变的,只能说适用于绝大多数情况。

这样,拖拽区域的问题就解决啦!你也不必担心设置拖拽区域会影响到正常标题栏的功能。在设置的拖拽区域内,标题栏的快捷操作依然正常进行。

修改按钮颜色

解决了最大的拖拽问题后,还有一个小问题,就是三大金刚按钮的颜色。

这个反而是好解决的,因为 API 很完备,和 UWP 几乎一致。

我们可以把设置方式整理成一个方法,里面包含扩展标题栏的设置:

public static void InitializeTitleBar(AppWindowTitleBar bar, ApplicationTheme theme) { bar.ExtendsContentIntoTitleBar = true; if (theme == ApplicationTheme.Light) { // 设置成自己预期的颜色即可 bar.ButtonBackgroundColor = Colors.Wheat; bar.ButtonForegroundColor = Colors.DarkGray; bar.ButtonHoverBackgroundColor = Colors.LightGray; bar.ButtonHoverForegroundColor = Colors.DarkGray; bar.ButtonPressedBackgroundColor = Colors.Gray; bar.ButtonPressedForegroundColor = Colors.DarkGray; bar.ButtonInactiveBackgroundColor = Colors.Wheat; bar.ButtonInactiveForegroundColor = Colors.Gray; } else { // 暗黑模式自行设置 } }

在 App.xaml.cs 的 OnLaunched 事件回调中调用即可。

遗留问题

在开发中,我还碰到一个棘手的问题,到现在还没有找到合适的解决方法,也可能是 bug。

在上述代码完成后,启动应用,一切正常,但是当我调整窗口大小到一个较小值后,我发现无法再点击搜索框了,即便回到较大的窗口大小也一样。

通过简单的点击拖拽判断,此时的拖拽区域也并没有覆盖搜索框。

我被迫写了一个重置方法:

private void ResetTitleBar() { var titleBar = App.AppWindow.TitleBar; titleBar.ResetToDefault(); App.InitializeTitleBar(titleBar); UpdateDragRects(); }

在检查到窗口大小更改时延迟调用来处理,但是标题栏会有闪烁,降低用户体验。

希望以后可以解决该问题。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3