在UWP中实现窗口多开,以及关联文件

2018/5/23 13:59:12


最近需要使用Markdown写博文,但是找不到好用的UWP软件可以用,于是自己就写了个简单的Markdown编辑器MarkV,并且现在已经发布到UWP应用商店。虽然这是个小软件,但在写的过程中也学到了不少东西。


1.实现MarkV多窗口多开

在win10 16299和以前,虽然也支持多窗口导航,但那些窗口都是在同一个App对象中生成,也就是每个窗口都对应一个CoreApplicationView
但17069及以上现在才算真正支持了多窗口,因为在新版本中,增加了AppInstance这个类,也就是类似于Win32下的窗口句柄,可以实现每个窗口对应一个App对象,或者每个窗口对应一个窗口句柄。当然,以前的CoreApplicationView窗口功能也没有改变。
在这个新功能的前提下,在MarkV中,才可以实现这个功能:要实现每次运行一个.md文件,都要打开一个新窗口;再次运行这个文件时,激活之前打开对应的窗口。但这又不能在App.xaml.cs中解决,因为每次生成窗口,都对应一个App对象,也就是说在App.xaml.cs中的App对象是在这个窗口中生成。这该怎么解决呢?
看看App类中的构造函数:

    /// <summary>
    /// 初始化单一实例应用程序对象。这是执行的创作代码的第一行,
    /// 已执行,逻辑上等同于 main() 或 WinMain()。
    /// </summary>
    public App()
    {
        this.InitializeComponent();
    }

肯定会有不少人认为,这个就是UWP的入口,因为官方文档就这样介绍,而且注释说的也很清楚。其实不然,学C#的都知道,程序入口就在Main()函数,UWP也不例外,但UWP的Main()在哪呢?如果你尝试在解决方案下新建一个Program类,编译器就会提示你已经有过该类,于是用F12找到Program的定义处,是在App.g.i.cs文件中,并且该类中还有Main()函数,这就是UWP程序真正的入口点了,主函数如下:

    static void Main(string[] args)
    {
        Windows.UI.Xaml.Application.Start((p) => new App());
    }

可以看到,每次程序运行,主函数都新建一个App对象。因此我们如果想要实现打开新.md文件就新建窗口、打开已经打开的.md文件就激活对应窗口,需要在Main函数中做文章。但是这又出现新的问题:App.g.i.cs是编译器自动创建和修改,我们怎么才能控制Main函数呢?那么我们就让编译器不要自动生成,由我们自己生成。这需要做以下操作:

  1. 右键单击解决方案
  2. 点击属性
  3. 条件编译符号中加上DISABLE_XAML_GENERATED_MAIN;。注意每项之间用”;"分隔。 这样就可以让编译器不自动生成主函数了。所以我们需要自己写主函数:

  4. 在解决方案下新建Program类

  5. 在该类中写上主函数,如下

    static void Main(string[] args)
    {
        //获得所有窗口句柄
        instances = AppInstance.GetInstances();
        //程序被激活
    
        IActivatedEventArgs activatedArgs = AppInstance.GetActivatedEventArgs();
        //如果程序由文件激活(即打开关联文件)
        if (activatedArgs is FileActivatedEventArgs fileArgs)
        {
            //如果运行多个文件,在这里仅打开第一个。也可以用循环打开多个
            IStorageItem file = fileArgs.Files.FirstOrDefault();
            if (file != null)
            {
                //查询或注册句柄,这里用文件路径作为Key,可以查询文件是否已经被打开
                var instance = AppInstance.FindOrRegisterInstanceForKey(file.Path);
                if (instance.IsCurrentInstance)
                {
                    //如果文件没有被打开,则新建一个App对象。对应就打开一个新的窗口
                    Windows.UI.Xaml.Application.Start((p) => new App());
                }
                else
                {
                    // 如果文件已经被打开,则利用这个句柄来激活打开该文件的窗口
                    instance.RedirectActivationTo();
                }
            }
        }
        else
        {
            //如果程序不是由文件激活,即直接运行程序。先注册一个句柄,在MainPage.cs中,要为打开的文件修改窗口句柄,以实现再次打开文件激活窗口
            AppInstance.FindOrRegisterInstanceForKey("REUSABLE" + App.Id.ToString());
            Windows.UI.Xaml.Application.Start((p) => new App());
        }
    }
    

在App.cs中还要加上一个函数:

    protected override void OnFileActivated(FileActivatedEventArgs args)
    {
        Frame rootFrame = Window.Current.Content as Frame;
        if (rootFrame == null)
        {
            rootFrame = new Frame();
            Window.Current.Content = rootFrame;
        }

        StorageFile file = args.Files.FirstOrDefault() as StorageFile;
        if (rootFrame.Content == null)
        {
            rootFrame.Navigate(typeof(MainPage), file);
        }
        Window.Current.Activate();
    }

这个函数和OnLaunched类似,也是在构建App对象之后运行该函数。不同之处在于当程序由文件激活时(也就是打开关联文件,并新建App对象时),程序运行OnFileActivated而不是OnLaunched。所以在这个函数中,把文件传给MainPage并在MainPageOnNavigatedTo中打开对应文件。


2. 实现关联文件

这个比较简单,也并不是17*版本所特有的功能。 只需做以下操作:

  1. 在vs中双击Package.appxmanifest
  2. 打开声明选项卡
  3. 下拉可用声明列表,找到文件类型关联并点击
  4. 填写徽标路径,可以填相对路径,也可以浏览图标图片文件,vs会自动添加到解决方案中
  5. 填写名称,这个是必填的,一般写软件名称就好
  6. 支持的文件类型中的文件类型填写想要关联的文件类型,比如.md
  7. 重新部署UWP软件并运行,就可以使用自己的软件打开想要打开的文件类型了

3. 请教一个问题

我在MarkV中为了让编辑即时保存,并且不是反复打开文件,为了运行效率我选择了一直打开文件,并在MainPage中有IRandomAccessStream全局对象。这样会造成一个问题:

如果在WPF中,可以在窗口关闭时响应关闭事件,并Dispose IRandomAccessStream。但在UWP中,似乎没有关闭事件(很有可能有,只是我没找到),也不能在关闭时做任何处理。所以每次MarkV退出后,打开的那个文件还会被占用一会。请懂这个的大佬教一下小弟,联系邮箱