【Win 10 应用开发】UI Composition 札记(一):视图框架的实现
在开始今天的内容之前,老周先说一个问题,这个问题记得以前有人提过的。
设置 Windows.ApplicationModel.Core.CoreApplicationView.TitleBar.ExtendViewIntoTitleBar 属性可以让应用窗口中的内容扩展到标题栏。简单地说,就是你的UI区域可以扩大,并填充到标题栏,这在开发自定义标题栏或弄个什么毛玻璃效果时很有用。
不过,这个 ExtendViewIntoTitleBar 属性有个“八阿哥”,一旦你设置之后,系统会对其进行记录,很难还原回默认的窗口标题栏。这是为啥呢,这个问题其实老周早已找到答案,只是写博客的时睺没有写出来,要是单独写这个内容,好像也太单调了。
今天忽然想起,就在本文中顺便说一下吧。
其实这个标题栏状态是保存在注册表中的,路径在
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ApplicationFrame\TitleBar\<你的应用程序包ID>
解决方法就是你找到这个键,然后在 TitleBar 下面找到你的应用ID,再把你的应用ID键删除就可以了。注意,这个TitleBar子键默认是不会出现的,只有当前用户安装过自定义了标题栏的应用后才会由系统创建。
===================================================================
好,以上内容完结,如果你在自定义标题栏时遇到问题,可以参考上述方法,当然,那是针对 UWP 应用的,桌面应用无需理会,也不受这个影响。
从今天开始,老周给大伙伴们讲一个不少朋友觉得很复杂的内容——UI Composition 。这个我不知道怎么翻译,干脆用原词了,当然你可以翻译为“用户界面构件”,或者“用户界面组建”,反正怪怪的。
这个玩意儿在开发的时候还是挺有用的,尤其是处理图像,或做一些界面效果时,还是蛮不错,有时候反而比 WPF 中的方便,当然,WPF 中有 Render 方法重写,UWP中没有。所以要借助 Composition 。
要在 UWP 中绘图,要么你直接用 XAML 的类型去组建 UI 树,要么你用 D2D,但这个是要用 C++ 去写的,也确实挺复杂的,尤其是像老周这种对 Direct Show 不熟悉的菜鸟来说,那就更难了。
如果你使用 Composition API 进行绘图或实现滤镜功能,除了 SDK 中的内容,你还应该借助一个组件—— Win 2D,这个东东对 D2D 做了封装,用起来很简单,经常可以与 Composition 结合使用。这个东东也是微软发布的,在项目中右击“引用”,然后从右键菜单中选择【Nuget 管理器】,然后你搜索 Win 2D,看到有.uwp 字样的你就安装即可。
UI Composition 很复杂吗?其实也不一定,我们可以抽丝剥茧,许多东西,不要一下子就往很复杂的方向上想,也不要想得太多,不然是学不明白的。只要把其结构搞清楚,后面就好进入状态了,一旦进入状态,你两个小时就能学会。
关于 UI Composition 的结构,本篇先不讲,咱们留到下一篇再讲,因为我们得先了解一下应用程序的视图框架。当然了,这个东西,在常规的应用程序中是用不到的,可能在游戏或者Direct相关的程序中会用到,不过啊,我们了解一下也是好的。
具体说来,我们要实现两个接口。
a、IFrameworkView:这是组装界面的重要类,在实现这个接口时,可以完成UI的组装,也可以处理应用窗口与用户的各种交互,比如键盘输入、鼠标或其他指针设备操作等。该接口提供了与生命周期相关的一些方法,我们通过实现这些方法来完成应用逻辑的。
b、IFrameworkViewSource:实现这个接口,只需要实现一个方法即可:
IFrameworkView CreateView()
这个方法会返回一个实现了 IFrameworkView接口的对象实例。
所以,实现 IFrameworkView 接口是核心,然后通过 IFrameworkViewSource 接口的 CreateView 方法把应用视图返回。
下面,我们来完成一个简单的例子,一边动手一边学习,效果可以提升 120 倍,信不信由你。
在VS中新建一个空白的应用程序项目。待项目新建完成后,你可以把项目生成的App.xaml、MainPage.xaml 以及相关的代码文件删除,因为本示例中我们用不上。
清单文件还是留着吧,不必删。
现在,我们新建一个代码文件,然后实现自己的应用视图类。
public class MyCustView : IFrameworkView { public void Initialize(CoreApplicationView applicationView) { } public void SetWindow(CoreWindow window) { } public void Load(string entryPoint) { } public void Run() { } public void Uninitialize() { } }
我们看到,即将要实现的几个方法。
* Initialize:在初始化时调用,在这个方法中,可以实例化一些要用的类型或资源。
* SetWindow:在该方法中可以对应用窗口做相应处理,比如,你要跟踪用户鼠标指针的坐标,就可以在这里附加 PointerMoved 事件的处理方法,比如你要跟踪窗口大小(可以动态重绘界面元素),可以处理 SizeChanged 事件。
* Load:在应用程序开始执行之前加载一些外部资源,或者加载一些文件数据之类的。注意,这个方法会接收一个字符串参数,它是要调用的应用类型的入口点。在标准的基于 XAML 的应用程序中,一般是 App 类。这个值来自清单文件的配置,如没有特殊需要,可以忽略该参数。
* Run:这个时候,应用程序就开始执行了,消息循环也启动。
* Uninitialize:应用结束时会调用这个方法,所以,在这个方法里面可以做一些清理工作。
那么,这几个方法的调用顺序如何呢?很简单,我们写一些调试代码跟踪一下就明白了。
public class MyCustView : IFrameworkView { public void Initialize(CoreApplicationView applicationView) { Debug.WriteLine($"-- 正在调用 {nameof(Initialize)} 方法。"); } public void SetWindow(CoreWindow window) { Debug.WriteLine($"-- 正在调用 {nameof(SetWindow)} 方法。"); } public void Load(string entryPoint) { Debug.WriteLine($"-- 正在调用 {nameof(Load)} 方法。"); } public void Run() { Debug.WriteLine($"-- 正在调用 {nameof(Run)} 方法。"); } public void Uninitialize() { Debug.WriteLine($"-- 正在调用 {nameof(Uninitialize)} 方法。"); } }
别急着运行,现在应用程序是无法运行的,我们还有一个接口没有实现。
public class MyCustViewSource : IFrameworkViewSource { public IFrameworkView CreateView() { return new MyCustView(); } }
在实现 CreateView 方法时,返回刚刚上面定义的那个视图类的实例。
好,现在该实现的接口都实现了,但是,很遗憾,应用还是无法运行的。我且问你,Windows 上,要执行一个应用程序,首先要找到什么位置?对啊,入口点,也就是 Main 方法。UWP 应用也是一样的,你得有 Main 方法才行。其实基于XAML 的标准应用程序也是有 Main 方法的,只是它由开发工具帮你生成了。
因此,我们还要为应用程序写一个入口点,这个 Main 方法如果你觉得另起一类来写麻烦,你可以直接写到刚刚上面的 MyCustViewSource 类里面。不过,老周向来的习惯都是会单独写一个类来放 Main 方法的,因为那样代码看起来更明了。
class Program { static void Main(string[] args) { CoreApplication.Run(new MyCustViewSource()); } }
这其实也很像我们以前写 WinForm 和 WPF 应用程序,在 Main 方法中会调用 Application.Run 方法。这里我们调用的是 CoreApplication 类的方法,它需要的参数正是一个实现了 IFrameworkViewSource 接口的类型实例。所以我们前面才要写 MyCustViewSource类,就是在这个地方用的。
经过上面这一番练习,你会了解到实现应用视图框架的过程:实现 IFrameworkView 接口编写核心应用代码 ---> 实现 IFrameworkViewSource 接口以返回视图实例 ---> 再把 source 实例传给 Run 方法来执行(在 Main 方法中)。
现在,你可以运行了,你会发现,应用窗口并没有出现,然后就结束了。那是因为实现 IFrameworkView 接口时,Run 方法中并没有启动任何消息循环,所以,应用程序运行后收不到任何消息,自然就结束了。
现在,我们看看”输出“窗口中的内容。
从结果中可知,应用程序先进行初始化,然后设置窗口参数,接着加载相关资源,随后开始执行,等到退出时再用 Uninitialize 方法去清理。
为了让大伙能看到窗口,我们在 Run 方法执行时启动一下消息循环。
public class MyCustView : IFrameworkView { CoreWindow theWindow = null; public void Initialize(CoreApplicationView applicationView) { Debug.WriteLine($"-- 正在调用 {nameof(Initialize)} 方法。"); } public void SetWindow(CoreWindow window) { Debug.WriteLine($"-- 正在调用 {nameof(SetWindow)} 方法。"); theWindow = window; } public void Load(string entryPoint) { Debug.WriteLine($"-- 正在调用 {nameof(Load)} 方法。"); } public void Run() { Debug.WriteLine($"-- 正在调用 {nameof(Run)} 方法。"); theWindow.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessUntilQuit); } public void Uninitialize() { Debug.WriteLine($"-- 正在调用 {nameof(Uninitialize)} 方法。"); } }
UWP 应用传统了 WPF 的优良传统,也是由 Dispatcher 对象来负责消息调度的,所以我常说,WPF能学好,UWP基本可以旁通。
现在,我们再运行一下,就能看到窗口了,而且它会等待你关闭了窗口,应用才会退出。
有大伙伴会问,怎么只有个初始屏幕?那当然了,因为这窗口是空白的,应用只是启动了消息循环,却没有任何界面元素,当然只能看到初始屏幕了。
至于说怎么组建 UI 元素,咱们下一篇再讨论吧,今天就写到这里,别写太长了,学习要一点点积累。