UWP 平台中的 Window 和 View 都是什么鬼?

UWP 支持一个 App 打开多个窗口,使得 App 更像传统的桌面应用,使用更加灵活,但是由于文档提及的内容实在太少,于是自己摸索了一阵,遇到了很多问题,也总结了一些经验。

UWP API 中,关于一个窗口的一共有四个类,分别是:

  • Window class,表示一个 Windows 下可见的窗口,负责 host 窗口内容
  • CoreWindow class,负责接收和翻译 Windows 窗口消息并在 CoreApplicationView.Dispatcher 上 dispatch(类似 WindowProc)
  • ApplicationView class,表示窗口的状态,比如大小和全屏状态
  • CoreApplicationView class,抽象的窗口,可以 host 在各种父窗口里,负责 dispatch 窗口消息和事件。如果是启动的第一个 View(MainView),App 的启动代码也运行在此。

然后是关闭窗口时的操作:

当用户对着你的 App 的一个窗口,也就是一个 Window,按下右上角的 X 的时候,实际被关闭的是关联的 CoreApplicationView,换句话说,窗口被“关闭”之后,它的 Content 还在继续运行。对于单窗口的 App,最后一个 View 被关闭的时候 App 就退出了;但对于多窗口的 App,必须自行维护窗口,一般用户不会注意到,但是会造成内存占用,正在播放的媒体也会继续播放。

解决方法:在 ApplicationView 被关闭时把关联的 Window.Content 设置成 null。**不要使用 Window.Current.Close()**,首先 App 的主要代码运行在启动的第一个 Window 里(其实是 CoreApplicationView 里),所以这个 Window 是无法 Close 的(会丢出异常);而对于其它 App 自己打开的 CoreApplicationViewClose() 会导致 Dispatcher 立即被终结,任何排队的操作都会丢出异常,对于一个复杂的程序,特别是调用了第三方 UI 控件的 App 来说,很可能导致崩溃。

如何创建一个新窗口

关键 API:

  • CoreApplicationView CoreApplication.CreateNewView()
    创建一个新的 View

  • IAsyncAction CoreApplicationView.Dispatcher.RunAsync(CoreDispatcherPriority, DispatchedHandler)
    在新的 View 的 Dispatcher 上执行代码,这里需要对新窗口设置内容(任何 UIElement 都可以,比如 FramePage,甚至是 UserControl),然后 Window.Current.Activate()必须!否则无法显示内容),然后获得 ApplicationView.Id

  • IAsyncOperation<bool> ApplicationViewSwitcher.TryShowAsStandaloneAsync(int)
    把上面的 ApplicationView.Id 对应的 ApplicationView 作为独立窗口显示

组合起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static async void CreateNewViewAsync(Action initialize)
{
var view = CoreApplication.CreateNewView();
var viewId = 0;
await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(initialize));
await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => viewId = InitializeView());
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId);
}

static int InitializeView()
{
var currentView = ApplicationView.GetForCurrentView();
currentView.Consolidated += View_Consolidated;

Window.Current.Activate();
return currentView.Id;
}

private static void View_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
{
Window.Current.Content = null;
}

如何实现像计算器一样每次启动都是新窗口

启动时显示新窗口并不像上面一样使用 ApplicationViewSwitcher.TryShowAsStandaloneAsync,而是需要使用 OnLaunched()LaunchActivatedEventArgsViewSwitcher

正常启动时这个属性总是为 null,第一次必须以正常方式启动,并且调用 ApplicationViewSwitcher.DisableSystemViewActivationPolicy() 声明接下来的启动会由 App 自行维护窗口激活逻辑,之后的启动时就会提供一个 ActivationViewSwitcher 用于显示新的窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
if (ApiInformation.IsTypePresent("Windows.UI.ViewManagement.StatusBar"))
{
StatusBar.GetForCurrentView().BackgroundOpacity = 1;
}

#if DEBUG
if (System.Diagnostics.Debugger.IsAttached)
{
DebugSettings.EnableFrameRateCounter = true;
}
#endif

if (e.ViewSwitcher == null)
{
ApplicationViewSwitcher.DisableSystemViewActivationPolicy();

// Initialize rootFrame normally.
}
else
{
// Create new view, use e.ViewSwitcher.ShowAsStandaloneAsync(int) to display.
}
}

上面的代码还包括一个设置 StatusBar.BackgroundOpacity 的部分,是为了解决 Win10 Mobile 上 StatusBar 全黑或全白的 bug(需要给 project 添加 Mobile Extension 的 Reference)。原因是系统在启动 App 的时候将这个属性设成了 0,这样 SplashScreen 的时候 StatusBar 就是透明的,然而 App 完成启动后并没有自动改回来。至今这个 bug 也没有修好。