WinUI 3 踩坑记:第一个窗口

news/2024/5/3 8:42:58/文章来源:https://www.cnblogs.com/scighost/p/16703905.html

本文是 WinUI 3 踩坑记 的一部分,该系列发布于 GitHub@Scighost/WinUI3Keng,文中的代码也在此仓库中,若内容出现冲突以 GitHub 上的为准。

WinUI 3 应用的入口和 UWP 类似,也是继承自 Application 的一个类,略有不同的是没有 UWP 那么多的启动方式可供重写,只有一个 OnLaunched 可以重写。OnLaunched 中的内容很简单,就是构造一个主窗口并激活。

// App.xaml.cspublic 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);}}}

到此为止已经完成了窗口状态的全部功能。

自定义标题栏

自定义标题栏是每个应用都应该做的事情,毕竟窗口顶部突然出现一个孤零零白条多少有点煞风景。

TitleBarCompare

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++ 查看窗口的属性,如下图所示。

image-20220917180758423

这里的 WinUI Desktop 是主窗口,内部有两个子窗口,从名称可以看出来 DRAG_BAR_WINDOW_CLASS 和拖动功能相关,查看它的大小和位置,刚好是前面使用 SetTitleBar 设置的范围(系统缩放率 150%)。第二个 DesktopChildSiteBridge 则是托管 UI 内容的 Xaml Island。

image-20220917181033846

由此可以得出结论,对自定义标题栏的鼠标操作会传递到 DRAG_BAR_WINDOW_CLASS,而 DesktopChildSiteBridge 不会收到相关消息,所以该区域下的所以控件都无法被点击,该原理也决定了可拖动区域只能为矩形。使用 Spy++ 查看窗口消息内容也证明了这一点(图略)。

第二,点击右上角三个按键后操作无法取消,即使把鼠标移开后,松开按键时也会触发操作,具体的行为可以查看这个 issue。

使用 AppWindowTitleBar

AppwindowTitleBar 是 Windows 11 上的方法,相比前者可以设置多个可拖动区域,这使得标题栏的控件交互操作成为可能。并且如果不主动设置可拖动区域,那么原标题栏的区域则会自动成为可拖动区域。

// MainWindow.xaml.cs// 检查是否支持此方法
if (AppWindowTitleBar.IsCustomizationSupported())
{// 不支持时 titleBar 为 nulltitleBar = 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 的菜单按键会跑到上面,下图中三横线按键。

未标题-4

如果是使用 Window.SetTitleBar,那么修改可拖动区域将会非常简单,直接修改 AppTitleBar 控件的边界大小就行,还无需考虑系统缩放率的影响。

AppTitleBar.Margin = new Thickness(96, 0, 0, 0);

如果是使用 AppWindowTitleBar.SetDragRectangles,那么问题就来了,看下面这张图片,如果先把橙色方框的范围设置为可拖动区域,然后再把蓝色方框的范围设置为可拖动区域,这时候会发生什么?

未标题-5

答案是蓝色方框可以拖动,但是绿色方框既不能拖动,也不能点击。这是 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 珠玉在前,拿什么跟人家比,不被砍掉就算成功了。

image-20220917224204935

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_9294.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python计算离散积分

前言 本文是傅立叶及其python应用系列的第一篇文章对应的仓库地址为https://github.com/yuanzhoulvpi2017/tiny_python/tree/main/Fourier_Series 介绍 本篇文章将要介绍一个非常小众的scipy函数&#xff1a;simpson. 这个函数的一大功能就是可以对离散数据积分。之所以要介…

P39 事件处理

P39 事件处理1.事件模型的流程2.事件监听器2.1 动作监听器&#xff08;ActionListener&#xff09;2.2 焦点监听器&#xff08;FocusListener&#xff09;2.3 鼠标监听器&#xff08;MouseListener&#xff09;2.4 鼠标移动/拖动监听器&#xff08;MouseMotionListener&#xf…

SpringAOP的概述与实现

目录 SpringAOP的概述 什么是AOP AOP能干什么 AOP的特点 AOP底层实现 AOP基本概念 连接点 切入点 通知 切面 目标对象 织入 引入 谈谈你对AOP的理解&#xff1f; SpringAOP的实现 依赖引用 spring.xml配置 注解实现 1.定义切面 设置通知 2.开启aop 3.测试 …

金仓数据库KingbaseES客户端编程开发框架-MyBatis(2. 概述 3. MyBatis配置说明)

2. 概述 MyBatis 是一款优秀的持久层框架&#xff0c;它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息&#xff0c; 将接口和 Java 的 POJOs(Plain Old Ja…

Sourcemap 配置详解

前言 之前在脚手架工具内整合将sourcemap上传sentry能力的时候&#xff0c;考虑bundle分割对.map文件关联的限制&#xff1a;比如TerserWebpackPlugin&#xff08;JS压缩&#xff09;只对 devtool 选项的 source-map&#xff0c;inline-source-map&#xff0c;hidden-source-m…

后端研发工程师面经——JAVA语言

文章目录2. JAVA语言2.1 面向对象的三大特性2.2 JAVA异常2.2.1 异常产生的原因2.2.2 异常的分类2.2.3 异常的处理方式2.3 序列化和反序列化2.3.1 概念2.3.2 JAVA中的序列化和反序列化2.3.3 序列化和反序列化的接口2.3.4 Serialization接口详解2.3.5 Externalizable接口详解2.3.…

3D建模师做多了女人会不会找不到老婆?次世代美少女战士建模流程讲解

什么是次世代&#xff1f; 次世代是个舶来语&#xff0c;“次世代游戏”指代和同类游戏相比下更加先进的游戏&#xff0c;即“下一代游戏”。 次世代是利用高模烘焙的法线贴图回帖到低模上&#xff0c;让低模在游戏引擎里可以及时显示高模的视觉效果。模型面数比较高&#xf…

算法 - 计数排序(Counting_Sort)

目录 引言&#xff1a; 学习&#xff1a; 什么是计数排序&#xff08;Counting_sort&#xff09;&#xff1f; 定义&#xff1a; 算法思想&#xff1a; 排序过程&#xff1a; Step 1 &#xff1a; Step 2 &#xff1a; Step 3 &#xff1a; Step 4 : Step 5 &…

单片机项目式实训总篇

采取新方法&#xff0c;让自己尽快变强&#xff0c;为了更好的再次见面。停止大脑内斗。 总学习目标&#xff1a;&#xff08;完成后此文字支持跳转&#xff09; 基础知识 端口操作 显示 高级输入 时间控制 综合 Flag: 一周破解C51程序 学习内容&#xff1a; 了解单片…

DeepExploit——当Metasploit遇上机器学习

Metasploit Meets Machine Learning 文章目录Metasploit Meets Machine Learning1. Metasploit准备1.1 与外部项目的合作1.1.1 启用RPC API1.1.2 使用RPC API操作Metasploit2. 创建机器学习模型2.1 DQN2.2 A3C2.2.1 CartPole2.2.2 分布式学习机制3. 深度利用3.1 代理任务3.2 当…

JVM——GC垃圾回收机制

文章目录JVM——GC垃圾回收机制一、如何判断哪些对象应该被回收——对象判活算法引用计数算法可达性分析算法引用最终判定二、对象应该怎么被回收——垃圾回收算法分代收集理论标记-清除算法标记-复制算法标记-整理算法三、内存对象什么时候被回收——触发条件年轻代GC(Minor G…

如期而至的SVN服务器迁移引来一个大瓜XAMPP

文章目录前言方案评估前奏XAMMP搭建svn服务准备软件包安装必要环境和工具安装xampp运行xampp编辑xampp访问xampp安装subversion安装svnmanager创建svn仓库目录修改配置文件为svnmanager创建MySQL用户重启xammp服务访问svnmanager登录svnmanager可能遇到的错误查看服务器目录信息…

10 nginx 中的 slab

前言 这里主要是描述 nginx 中的 slab 内存分配相关 slab 在很多的地方都有使用, 比如 linux, nginx, netty 等等 主要的作用是 内存管理, 复用 简略 nginx 中的 slab 的流程 # slab relatedvoid* poolPtr malloc(2048);ngx_slab_pool_t *pool (ngx_slab_pool_t *)poo…

Pytorch深度学习——线性回归实现 04(未完)

文章目录1 问题假设2 步骤3 学习使用Pytorch的API来搭建模型3.1 nn.Model3.2 优化器类3.3 评估模式和训练模式3.4 使用GPUdata和item的区别1 问题假设 假设我们的基础模型就是y wxb&#xff0c;其中w和b均为参数&#xff0c;我们使用y 3x0.8来构造数据x、y,所以最后通过模型…

0.django部署(基础知识)

我们前面的代码都是在我们自己的电脑&#xff08;通常是Windows操作系统&#xff09;上面运行的&#xff0c;因为我们还处于开发过程中。 当我们完成一个阶段的开发任务后&#xff0c;就需要把我们开发的网站服务&#xff0c;给真正的用户使用了。 那就需要我们的 网站 部署在…

【二次分配问题】基于遗传算法 (GA)、粒子群优化 (PSO) 和萤火虫算法 (FA) 求解二次分配( QAP)问题(MATLAB 实现)

目录 1 概述 3 Matlab代码及文章阅读 4 运行结果 4.1 萤火虫算法 4.2 粒子群优化算法 4.3 遗传算法 5 参考文献 1 概述 目前&#xff0c;该问题已经得到深入的研究&#xff0c;进化策略(evolutionstrategies)、遗传算法(genetic algorithms)、遗传规划(geneticprogramm…

警惕利用「以太坊合并」的 3 种骗局

原文作者&#xff1a;茉莉 距离以太坊合并还有不到 6 小时&#xff0c;这条被视作下一代互联网 Web3.0 底层基础设施的区块链网络将彻底改变共识机制&#xff0c;从工作量证明的 PoW 机制转向权益证明的 PoS。 在合并即将到来前&#xff0c;去中心化安全网络市场 PolySwarm 创…

各语言转wasm-js调用

起源是 我司应该是抄袭某家player , 也用wasm做的 , 所以我也研究一下 关于标题 我估计需要大家一起完善了 , 我只会讲一下 go c 别的都不会 webassembly( wasm ) 可以编译的如图 我想起我这边应用啊 也就无非播放器~~ 本地文件压缩啊加密啊或直接就上传了, 或者在操作数据…

RestHighLevelClient创建索引时报错[299 Elasticsearch-7.12.1

RestHighLevelClient创建索引时报错[299 Elasticsearch-7.12.1出现原因 : 这是因为在使用create方法时 , 会有两个选择 , 其中一个已经过时了 client.indices().create(request, RequestOptions.DEFAULT); 其中的create方法 , 有两个版本 , 有一个显示已经过时了 , 两个方法虽然…

蜂蜜什么时候喝,才可以获得蜂蜜更大的好处?真可以治疗咳嗽?

中秋节刚过去不久&#xff0c;家里面的礼品多的是不是可以开超市了?中国人讲究一个“礼”字&#xff0c;逢年过节、探望故友病友手里不带点东西就会难受。中秋节这样带有美好祝愿的节日自然也是中国人送礼的最佳时间之一。 ​ 编辑切换为居中 添加图片注释&#xff0c;不超过…