WinUI 3 踩坑记:第一个窗口
本文是 WinUI 3 踩坑记 的一部分,该系列发布于 GitHub@Scighost/WinUI3Keng,文中的代码也在此仓库中,若内容出现冲突以 GitHub 上的为准。
WinUI 3 应用的入口和 UWP 类似,也是继承自 Application
的一个类,略有不同的是没有 UWP 那么多的启动方式可供重写,只有一个 OnLaunched
可以重写。OnLaunched
中的内容很简单,就是构造一个主窗口并激活。
// App.xaml.cs
public partial class App : Application
{
public App()
{
this.InitializeComponent();
}
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
// 构造一个主窗口并激活
m_window = new MainWindow();
m_window.Activate();
}
private Window m_window;
}
本文将聚焦于主窗口 MainWindow
,介绍 设置云母或亚克力背景
、调整窗口位置大小
、自定义标题栏
等内容。
设置云母或亚克力背景
设置背景材质的方法在官方文档中有很详细的方法,不再过多介绍,本文中使用的是我个人封装的方法,源码在这。
// MainWindow.xaml.cs
using Scighost.WinUILib.Helpers;
private SystemBackdropHelper backdropHelper;
public MainWindow()
{
this.InitializeComponent();
backdropHelper = new SystemBackdropHelper(this);
// 设置云母背景,如果不支持则设置为亚克力背景
backdropHelper.TrySetMica(fallbackToAcrylic: true);
}
调整窗口位置大小
创建窗口后的第一件事儿是干什么?
没错,就是获取窗口句柄(HWND),这个流程和 WPF/UWP 截然不同,倒是和 Win32 很像。因为窗口类 Microsoft.UI.Xaml.Window
中几乎没有与窗口状态有关的方法,而所谓的 HWND 高级封装类 Microsoft.UI.Windowing.AppWindow
包含的方法也很有限,并且需要通过窗口句柄才能获取。相比之下 WPF 几乎封装了所有关于窗口的常见操作,可见 WPF 在开发体验方面更胜一筹。
// MainWindow.xaml.cs
// 命名空间真乱
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using WinRT.Interop;
private IntPtr hwnd;
private AppWindow appWindow;
public MainWindow()
{
this.InitializeComponent();
hwnd = WindowNative.GetWindowHandle(this);
WindowId id = Win32Interop.GetWindowIdFromWindow(hwnd);
appWindow = AppWindow.GetFromWindowId(id);
}
WinUI 3 不会自动保存窗口大小和位置,这个功能需要自己实现,也没有窗口最大化的方法,需要调用 Win32 Api。
// MainWindow.xaml.cs
using Vanara.PInvoke;
using Windows.Graphics;
// 窗口最大化
User32.ShowWindow(hwnd, ShowWindowCommand.SW_SHOWMAXIMIZED);
// 调整窗口位置和大小,以屏幕像素为单位
appWindow.MoveAndResize(new RectInt32(_X: 560, _Y: 280, _Width: 800, _Height: 600));
一般流程为在窗口关闭时保存位置和大小,启动时加载保存的设置,这里我们使用 应用包的设置功能,但是该 Api 能够存储的数据类型不包括 Windows.Graphics.RectInt32
,稍微对数据模型做一些调整。
注意:非打包应用不能使用应用包的设置功能
// MainWindow.xaml.cs
using Microsoft.UI.Windowing;
using Vanara.PInvoke;
using Windows.Graphics;
using Windows.Storage;
using System.Runtime.InteropServices;
public sealed partial class MainWindow : Window
{
......
public MainWindow()
{
......
// 初始化窗口大小和位置
this.Closed += MainWindow_Closed;
if (ApplicationData.Current.LocalSettings.Values["IsMainWindowMaximum"] is true)
{
// 最大化
User32.ShowWindow(hwnd, ShowWindowCommand.SW_SHOWMAXIMIZED);
}
else if (ApplicationData.Current.LocalSettings.Values["MainWindowRect"] is ulong value)
{
var rect = new WindowRect(value);
// 屏幕区域
var area = DisplayArea.GetFromWindowId(windowId: id, DisplayAreaFallback.Primary);
// 若窗口在屏幕范围之内
if (rect.Left > 0 && rect.Top > 0 && rect.Right < area.WorkArea.Width && rect.Bottom < area.WorkArea.Height)
{
appWindow.MoveAndResize(rect.ToRectInt32());
}
}
}
private void MainWindow_Closed(object sender, WindowEventArgs args)
{
// 保存窗口状态
var wpl = new User32.WINDOWPLACEMENT();
if (User32.GetWindowPlacement(hwnd, ref wpl))
{
ApplicationData.Current.LocalSettings.Values["IsMainWindowMaximum"] = wpl.showCmd == ShowWindowCommand.SW_MAXIMIZE;
var p = appWindow.Position;
var s = appWindow.Size;
var rect = new WindowRect(p.X, p.Y, s.Width, s.Height);
ApplicationData.Current.LocalSettings.Values["MainWindowRect"] = rect.Value;
}
}
/// <summary>
/// RectInt32 和 ulong 相互转换
/// </summary>
[StructLayout(LayoutKind.Explicit)]
private struct WindowRect
{
[FieldOffset(0)]
public short X;
[FieldOffset(2)]
public short Y;
[FieldOffset(4)]
public short Width;
[FieldOffset(6)]
public short Height;
[FieldOffset(0)]
public ulong Value;
public int Left => X;
public int Top => Y;
public int Right => X + Width;
public int Bottom => Y + Height;
public WindowRect(int x, int y, int width, int height)
{
X = (short)x;
Y = (short)y;
Width = (short)width;
Height = (short)height;
}
public WindowRect(ulong value)
{
Value = value;
}
public RectInt32 ToRectInt32()
{
return new RectInt32(X, Y, Width, Height);
}
}
}
到此为止已经完成了窗口状态的全部功能。
自定义标题栏
自定义标题栏是每个应用都应该做的事情,毕竟窗口顶部突然出现一个孤零零白条多少有点煞风景。
WinUI 3 提供了两种方法自定义标题栏,有关这两种方法更详细的内容,请看文档。
使用 Window 自带的属性
通过设置 Window.ExtendsContentIntoTitleBar = true
将客户区内容扩展到标题栏,用法比较简单,然后还需要调用 SetTitleBar(UIElement titleBar)
告诉系统可拖动区域的范围,这里的 titleBar
是在 xaml 文件中定义的控件,调用此 Api 后会将控件覆盖的部分设置为可拖动区域。
// MainWindow.xaml.cs
using Microsoft.UI.Xaml;
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(AppTitleBar);
<!-- MainWindow.xaml -->
<Grid>
<Border x:Name="AppTitleBar"
Height="48"
VerticalAlignment="Top">
<TextBlock VerticalAlignment="Center" Text="WinUI Desktop" />
</Border>
</Grid>
<!-- App.xaml -->
<!-- 右上角按键的背景色设置为透明 -->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush" />
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush" />
不过这个方法存在两个问题:
第一,使用 SetTitleBar
设置的可拖动区域必须是一块完整的区域,并且处于该范围内的所有控件不能再被点击,所以使用这个方法不能实现微软商店那种标题栏中嵌入搜索框的功能。
为了一探究竟,使用 Spy++ 查看窗口的属性,如下图所示。
这里的 WinUI Desktop
是主窗口,内部有两个子窗口,从名称可以看出来 DRAG_BAR_WINDOW_CLASS
和拖动功能相关,查看它的大小和位置,刚好是前面使用 SetTitleBar
设置的范围(系统缩放率 150%)。第二个 DesktopChildSiteBridge
则是托管 UI 内容的 Xaml Island。
由此可以得出结论,对自定义标题栏的鼠标操作会传递到 DRAG_BAR_WINDOW_CLASS
,而 DesktopChildSiteBridge
不会收到相关消息,所以该区域下的所以控件都无法被点击,该原理也决定了可拖动区域只能为矩形。使用 Spy++ 查看窗口消息内容也证明了这一点(图略)。
第二,点击右上角三个按键后操作无法取消,即使把鼠标移开后,松开按键时也会触发操作,具体的行为可以查看这个 issue。
使用 AppWindowTitleBar
AppwindowTitleBar
是 Windows 11 上的方法,相比前者可以设置多个可拖动区域,这使得标题栏的控件交互操作成为可能。并且如果不主动设置可拖动区域,那么原标题栏的区域则会自动成为可拖动区域。
// MainWindow.xaml.cs
// 检查是否支持此方法
if (AppWindowTitleBar.IsCustomizationSupported())
{
// 不支持时 titleBar 为 null
titleBar = appWindow.TitleBar;
titleBar.ExtendsContentIntoTitleBar = true;
// 标题栏按键背景色设置为透明
titleBar.ButtonBackgroundColor = Colors.Transparent;
titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
}
当手动设置可拖动区域时,一定要注意系统缩放率的问题,AppwindowTitleBar
设置的区域是以像素为单位,而 UI 中的控件会受到缩放率的影响变得更大,设置可拖动区域时需要手动乘上缩放率。
// MainWindow.xaml.cs
using Windows.Graphics;
using Vanara.PInvoke;
// 获取系统缩放率
var scale = (float)User32.GetDpiForWindow(hwnd) / 96;
// 48 这个值是应用标题栏的高度,不是唯一的,根据自己的 UI 设计而定
titleBar.SetDragRectangles(new RectInt32[] { new RectInt32(0, 0, 10000, (int)(48 * scale)) });
为什么要把可拖动区域的宽度设置为 10000 呢?如果设置小了标题栏右侧没有覆盖到的部分就会无法拖动,设置大了却不会影响右上角的三个键(不会有人的显示器像素宽度大于 10000 吧)。
修改可拖动区域
在很多情况下需要修改可拖动区域,最常见的就是窗口宽度变小时,NavigationView
的菜单按键会跑到上面,下图中三横线按键。
如果是使用 Window.SetTitleBar
,那么修改可拖动区域将会非常简单,直接修改 AppTitleBar
控件的边界大小就行,还无需考虑系统缩放率的影响。
AppTitleBar.Margin = new Thickness(96, 0, 0, 0);
如果是使用 AppWindowTitleBar.SetDragRectangles
,那么问题就来了,看下面这张图片,如果先把橙色方框的范围设置为可拖动区域,然后再把蓝色方框的范围设置为可拖动区域,这时候会发生什么?
答案是蓝色方框可以拖动,但是绿色方框既不能拖动,也不能点击。这是 WindowsAppSDK v1.1 版本的一个 Bug,这个 Bug 基本上断绝了在标题栏上修改控件布局的可能性。每次修改可拖动区域前可以通过调用 AppWindowTitleBar.ResetToDefault()
解决这个问题,但是那样会有系统标题栏突然出现然后消失的情况,非常影响体验。有关这个 Bug 的更详细的内容可以查看 爱奇艺 Preview 的开发者 kingcean 提出的 issue,issue 中提到了 v1.2 preview 1
解决了这个 Bug,经过我的测试确实解决了。
v1.2 Preview 1 新功能
v1.2 preview 1 的更新内容中有提到已支持在 Windows 10 中使用 AppWindowTitleBar
,在我的测试中 ExtendsContentIntoTitleBar
已可以使用并且成功将客户区扩展到了标题栏。但是无论是否调用 SetDragRectangles
都无法拖动该窗口(参考这个 issue),等后续修复吧。
总结
WinUI 3 在窗口操作上比 WPF/UWP 麻烦了不少,许多常用的操作都没有封装,比如 最大最小化
、隐藏窗口
等。又因为在窗口上设计思路的不同,使得很多功能需要通过窗口句柄这个本应该被隐藏掉的东西去实现,这就是为什么我要在前言中写下了解 Win32 窗口相关知识。
人家微软也有理由说的,我开发的是什么框架,是最新一代的框架;你让我封装的是什么东西,是 Win32 的老古董。哦哟,谢天谢地了。WinUI 3 现在什么水平,改个窗口都这么麻烦,它能火吗?火不了,没这个能力知道吗。有 WPF 珠玉在前,拿什么跟人家比,不被砍掉就算成功了。
WinUI 3 踩坑记:第一个窗口的更多相关文章
- WinUI 3 踩坑记:前言
WinUI 3 (Windows App SDK 于 2021 年 11 月发布了第一个正式版 v1.0.0 [1],最新版本是 v1.1.5 [2].我的基于 WinUI 3 的个人项目 寻空 从年 ...
- WinUI 3 踩坑记:从创建项目到发布
本文是 WinUI 3 踩坑记 的一部分,该系列发布于 GitHub@Scighost/WinUI3Keng,若内容出现冲突以 GitHub 上的为准. 创建项目 现在 WinUI 3 的入门体验比刚 ...
- Spark踩坑记——Spark Streaming+Kafka
[TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...
- Spark踩坑记——从RDD看集群调度
[TOC] 前言 在Spark的使用中,性能的调优配置过程中,查阅了很多资料,之前自己总结过两篇小博文Spark踩坑记--初试和Spark踩坑记--数据库(Hbase+Mysql),第一篇概况的归纳了 ...
- 【bug记录】OS Lab4 踩坑记
OS Lab4 踩坑记 Lab4在之前Lab3的基础上,增加了系统调用,难度增加了很多.而且加上注释不详细,开玩笑的指导书,自己做起来困难较大.也遇到了大大小小的bug,调试了一整天. 本文记录笔者在 ...
- EOS踩坑记
[EOS踩坑记] 1.每个account只能更新自己的contract,即使两个account的秘钥相同,也不允许. 如下,使用alice的权限来更新james的contract.会返回 Missin ...
- Spark踩坑记:Spark Streaming+kafka应用及调优
前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark streaming从k ...
- Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记
前言 本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 . TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript ...
- centos 7( linux )下搭建elasticsearch踩坑记
原文:https://blog.csdn.net/an88411980/article/details/83150380 概述 公司最近在做全文检索的项目,发现elasticsearch踩了不少 ...
随机推荐
- NC14731 逆序对
NC14731 逆序对 题目 题目描述 求所有长度为 \(n\) 的 \(01\) 串中满足如下条件的二元组个数: 设第 \(i\) 位和第 \(j\) 位分别位 \(a_i\) 和 \(a_j\) ...
- PTA(BasicLevel)-1094 谷歌的招聘
一.问题定义 2004 年 7 月,谷歌在硅谷的 101 号公路边竖立了一块巨大的广告牌(如下图)用于招聘.内容超级简单,就是一个以 .com 结尾的网址, 而前面的网址是一个 10 位素数,这个素数 ...
- Unity3D学习笔记7——GPU实例化(2)
目录 1. 概述 2. 详论 2.1. 实现 2.2. 解析 3. 参考 1. 概述 在上一篇文章<Unity3D学习笔记6--GPU实例化(1)>详细介绍了Unity3d中GPU实例化的 ...
- JDK的下载与安装和环境变量的配置
一.jdk下载打开浏览器在地址栏输入: http://www.oracle.com ,进入Oracle官网主页面,选择 Products-----Java---->Download Java . ...
- ooday06 内部类
笔记: 成员内部类:应用率低,了解 类中套类,外面的称为外部类,里面的称为内部类 内部类通常只服务于外部类,对外不具备可见性 内部类对象只能在外部类中创建 内部类中可以直接访问外部类的成员(包括私有的 ...
- 7 什么是dubbo
什么是dubbo 快速入门dubbo 了解什么是dubbo之前,我们得先了解什么是分布式系统? <分布式系统原理与范型>定义: 分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像 ...
- Python常用基础语法知识点大全
记得我是数学系的,大二时候因为参加数学建模,学习Python爬虫,去图书馆借了一本Python基础书,不厚,因为有matlab和C语言基础,这本书一个星期看完了,学完后感觉Python入门很快,然后要 ...
- 使用传统的方式遍历集合对集合中的数据进行过滤和使用Stream流的方式遍历集合对集合中的数据进行过滤
使用传统的方式,遍历集合,对集合中的数据进行过滤 class Test{ public static void main(String[] args){ ArrayList<String> ...
- 体验Lambda的更优写法和Lambda标准格式
体验Lambda的更优写法 借助Java8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效: public class Lambda02 { public ...
- net core天马行空系列-各大数据库快速批量插入数据方法汇总
1.前言 hi,大家好,我是三合.我是怎么想起写一篇关于数据库快速批量插入的博客的呢?事情起源于我们工作中的一个需求,简单来说,就是有一个定时任务,从数据库里获取大量数据,在应用层面经过处理后再把结果 ...