zl程序教程

您现在的位置是:首页 >  其他

当前栏目

【Win10 应用开发】集成文件打开选择器

2023-03-20 14:47:53 时间

有朋友给老周提出建议:老周,能不能在写博客时讲一下有深度的小故事?技术文章谁不会写。讲一下对人生有启发性的故事会更好。

哎呀,这要求真是越来越高了。好吧,尽量吧,如果有小故事的话,老周在就每次写博客时写出来;如果没有故事可讲,那只能请您原谅了,呵呵。 

有人问老周,你每天都玩手机的吗?答案是肯定的,与时俱进嘛,玩是肯定的。不过,老周从不做低头族,虽然玩,但不会一整天都低着头看手机,这样做让人觉得你很没礼貌(如果一个人独处就没关系),也很没情趣。尤其是一堆人在说话时,你再不喜欢讲话也应该插上一两句,老低着头在那里,一来对身体不好,二来也显得不尊重别人。

其实,老周在家独处时,也不会总拿着手机的。我觉得现在的人很奇怪,似乎大家都知道某些事情对身心不好,但就是不知道为什么,明知道有害也要沉迷其中。这大概就是佛家所说的过度执迷了。执着本没什么不好,但执迷就有点物极必反了。

要说现代人到底懂不懂什么是爱,这真的难说,如果对自己都负不起责任的话,不懂得惜爱自己的人,估计也很难去爱别人。生活中很多东西(比如手机)都是我们的工具,我们是要做工具的主人,还是成为工具的奴隶。唉,只有自己心里明白了。究其根本,可能就是因为很多人的精神家园一片苍白的原因吧。

总之,适可而止就不会有什么后患。

 

=================================================================

本文将说一说如何将当前应用程序集成到系统的文件选择器中,为啥会有这个? 因为Windows App不同于传统的桌面应用,大概是为了数据安全的需要,在应用安装后,操作系统会为每一个应用程序分配独立的注册表项,以及独立的存储目录。严格上说,这些属于某个应用程序的“隐私”,是不应该让其他应用程序去访问的。

不过,有时候真的希望某个应用可以将它的本地文件提供给其他应用使用。其实有一种思路就是可以把共用的文件直接存到系统的图片、音乐、视频、文档等库中。当然,如果可以把当前应用程序集成到系统的文件选择器中的话,会让我们处理起来比较灵活,因为从界面到文件,我们开发者都可以自行控制,也可以操作哪些文件希望公开给其他应用程序,或只留给自己使用。

SDK提供了这些集成功能,这个功能在Win 8的时候就有,到了Win 10,就与传统的系统文件对话框融合到一起了。以前在Win 8/8.1的应用里面,是使用独立的全屏的文件选择器的,现在是把新的呈现引擎与传统的Shell窗口结合到一起了。

SDK提供了打开文件对话框和保存文件对话框的集成支持,而且实现原理相近。本文老周只以集成打开文件对话框为例,至于保存文件对话框的集成,有空的话,老周再补写,因为原理相近。

 

老规矩,先介绍一下要点:

1、要让应用程序可以集成到文件选择器中,需要重写Application类的OnFileOpenPickerActivated方法,当文件选择器激活当前应用时,会调用该方法。

2、从OnFileOpenPickerActivated方法的参数对象的FileOpenPickerUI属性可以获取到一个FileOpenPickerUI实例,后面的所有操作都是在这个FileOpenPickerUI对象上做文章了。因为我们的应用需要提供一个可视化的界面来让用户操作的,所以通常会导航到一个页面,并把FileOpenPickerUI实例作为参数传递过去。

3、要把某个文件添加到选择器的选择结果中,可以调用AddFile方法,方法的第一个参数为文件的标识,这个标识在整个选择列表中必须是唯一的,通常可以用文件名来标识;第二个参数就是要加入到选择结果列表中的文件实例。如果文件选择器是多选,则整个结果列表会返回给调用方,如果是单选,就只返回一个文件实例给调用方。在AddFile之前,请调用CanAddFile方法来检查一下某个文件到底能不能添加到结果列表中,能就返回true,不能就返回false。“命里无时莫强求”,如果不能添加,那你就省省事吧。

4、RemoveFile方法可以从结果列表中删除一个文件,注意只是从选择结果列表中删除而已,不会真的把文件从硬盘中删除。删除时指定文件的标识,这个标识就是刚才AddFile时的标识,为什么标识要唯一,原因就在这里。在删除前,可以用ContainsFile方法查看一下结果列表中有没有要删除的项。

5、重点,很容易忘记,就是要在清单文件中配置相关扩展声明,让应用程序支持文件选择器的集成。

 

好,抽象的话讲完了,下面就给大伙儿来点不抽象的东东,以缓和一下性情。

我定义了这么个页面,用ListView来显示待用户选择的文件。

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <ListView Name="lvFiles" SelectionMode="Extended" SelectionChanged="OnSelectionChanged" IsMultiSelectCheckBoxEnabled="True">
            <ItemsControl.ItemTemplate>
                <DataTemplate x:DataType="local:FileItem">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition />
                            <RowDefinition Height="auto"/>
                        </Grid.RowDefinitions>
                        <Image Margin="2" Width="85" Height="85" Source="{x:Bind Icon}" x:Phase="1"/>
                        <TextBlock Grid.Row="1" Margin="3" HorizontalAlignment="Center" Text="{x:Bind Name}" x:Phase="0"/>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <ItemsWrapGrid Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ListView>
    </Grid>

这里我用到了新的绑定扩展标记x:Bind。它与Binding的不同在于,Binding是在运行阶段完成绑定;Bind是在编译时完成绑定。所以这两个家伙的区别就在于开始绑定的一刹那,也就是说,性能的提升在于开始绑定的一瞬间,如果界面上的数据不需要运态改变,后续的运行就不会因为频繁取值而占用CPU时间。

这里要弄清楚的是,Bind只是省去了动态绑定消耗的性能,并不表示它能压缩内存。如果数据量非常大,那没办法了,因为数据在内存中它肯定需要空间来存放的,谁叫你把那么数据放到内存中呢。再说了,大批量数据的加载是考验硬件性能的,像很多国产平板,尤其是那些100块钱以下的山寨板,配置不会高到哪里去,因此,别动不动就上一大堆数据,这很不厚道。如果数据条数很大,可以实现分段加载(预提取)功能,这个功能在SDK有提供,有时间老周给大家演示演示。

 

上面页面中是使用了绑定,注意在DataTemplate中使用x:Bind时,一定要加上x:DataType,以指定要绑定的数据源的类型,因为Bind默认的相对点是UserControl或者Page,而Binding的相对点是DataContext。因此,在DataTemplate中使用的话,如果不指定DataType,那么Bind们就找不到源对象,因为它是编译时绑定的,所以是强类型的,不能使用动态类型(dynamic),要用动态类型,请用Binding。

x:Phase表示分阶段提取数据,默认为0,即第一阶段,为1表示第二阶段,依此类推。不指定时表明默认值0。在本例中,Image中的图标可能加载得较慢,为了让数据可以马上显示,让TextBlock的文本在第一阶段加载,然后在第二阶段来加载图标。

ListView绑定的是我自定义的类,我用它来封装文件信息。

    public class FileItem : INotifyPropertyChanged
    {
        StorageFile m_file = null;
        BitmapImage m_icon = null;

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propn = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propn));
        }

        public FileItem(StorageFile file)
        {
            m_file = file;
            GetIconAsync();
        }

        /// <summary>
        /// 文件名
        /// </summary>
        public string Name => m_file?.Name;
        /// <summary>
        /// 关联的文件
        /// </summary>
        public StorageFile File => m_file;
        /// <summary>
        /// 图标
        /// </summary>
        public BitmapImage Icon
        {
            get { return m_icon; }
            private set
            {
                if (value != m_icon)
                {
                    m_icon = value;
                    OnPropertyChanged();
                }
            }
        }

        private async void GetIconAsync()
        {
            IRandomAccessStream stream = await m_file.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode.SingleItem);
            Icon = new BitmapImage();
            Icon.DecodePixelWidth = 100;
            await Icon.SetSourceAsync(stream);
            stream.Dispose();
        }
    }

GetIconAsync方法是取得文件的图标。

 

 

重写页面的OnNavigatedTo方法,从参数中取得App传递过来的FileOpenPickerUI对象。这里我是在本地目录中生成20个文件文件,来作为演示文件。

        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            // 获取参数
            pickerUI = e.Parameter as FileOpenPickerUI;
            // 获取本地文件列表
            var files = await ApplicationData.Current.LocalFolder.GetFilesAsync(Windows.Storage.Search.CommonFileQuery.DefaultQuery);
            if (files.Count == 0)
            {
                await CreateFilesAsync();
                // 重新获取
                files = await ApplicationData.Current.LocalFolder.GetFilesAsync(Windows.Storage.Search.CommonFileQuery.DefaultQuery);
            }

            List<FileItem> items = new List<FileItem>();
            foreach (var f in files)
            {
                items.Add(new FileItem(f));
            }
            lvFiles.ItemsSource = items;
        }

 

CreateFilesAsync方法是我定义的,用来生成演示的20个文本文件。

        private async Task CreateFilesAsync()
        {
            int n = 20; //文件个数
            StorageFolder localfolder = ApplicationData.Current.LocalFolder;
            // 创建文件
            for (int x = 0; x < n; x++)
            {
                StorageFile file = await localfolder.CreateFileAsync($"{x + 1}.txt", CreationCollisionOption.ReplaceExisting);
                Guid g = Guid.NewGuid();
                // 写入内容
                await FileIO.WriteTextAsync(file, g.ToString());
            }
        }

随便弄个GUID,写到文本文件中。

 

因为文件选择操作我交给ListView控件来干活,所以要处理它的SelectionChanged事件,在选择项发生变化后及时管理文件选择结果列表。

        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // 移除列表
            if (e.RemovedItems.Count > 0)
            {
                if (pickerUI.SelectionMode == FileSelectionMode.Multiple)
                {
                    for (int i = 0; i < e.RemovedItems.Count; i++)
                    {
                        FileItem item = e.RemovedItems[i] as FileItem;
                        // 移除前先判断是否存在目标项
                        if (pickerUI.ContainsFile(item.Name))
                        {
                            pickerUI.RemoveFile(item.Name);
                        }
                    }
                }
                else
                {
                    FileItem item = e.RemovedItems[0] as FileItem;
                    if (pickerUI.ContainsFile(item.Name))
                    {
                        pickerUI.RemoveFile(item.Name);
                    }
                }
            }

            // 添加列表
            if (e.AddedItems.Count > 0)
            {
                // 如果是多选
                if (pickerUI.SelectionMode == FileSelectionMode.Multiple)
                {
                    for (int i = 0; i < e.AddedItems.Count; i++)
                    {
                        FileItem item = e.AddedItems[i] as FileItem;
                        // 将项添加到被选文件列表
                        if (pickerUI.CanAddFile(item.File))
                        {
                            pickerUI.AddFile(item.Name, item.File);
                        }
                    }
                }
                else //如果是单选
                {
                    FileItem item = e.AddedItems[0] as FileItem;
                    if (pickerUI.CanAddFile(item.File))
                    {
                        pickerUI.AddFile(item.Name, item.File);
                    }
                }
            }

        }

 

接下来,就轮到App类上面做手脚了。重写OnFileOpenPickerActivated方法,取得UI引用,然后导航到我们上面定义的页面。

        protected override void OnFileOpenPickerActivated(FileOpenPickerActivatedEventArgs args)
        {
            FileOpenPickerUI UI = args.FileOpenPickerUI;
            Frame f = Window.Current.Content as Frame;
            if (f == null)
            {
                f = new Frame();
                Window.Current.Content = f;
            }

            f.Navigate(typeof(FileListPage), UI);

            Window.Current.Activate();
        }


别忘了,清单文件。打开清单文件,切换到“声明”选项卡。

添加一个“文件打开选取器”,然后在右边配置所支持的文件类型,你可以直接勾选支持任意类型的文件,就像我这样。当然,你可以单独配置所支持的文件类型。文件类型输入时不要带星号,直接.jpg、.txt、.doc这样就行了,不要漏了前面的“.”。

 

为了让应用程序可以测试,可以在主页面上调用FileOpenPicker来选取一个文件,然后显示文件的内容。

            FileOpenPicker picker = new FileOpenPicker();
            picker.FileTypeFilter.Add(".txt");

            StorageFile file = await picker.PickSingleFileAsync();
            if (file == null)
            {
                return;
            }

            string msg = null;
            msg += $"文件名:{file.Name}
";
            // 读出内容
            string str = await FileIO.ReadTextAsync(file);
            msg += $"文件内容:{str}";

            tb.Text = msg;

 

这里面有个奇怪的现象,就是如果用VS来调试运行时,你在文件选择器上无法用鼠标操作,不知道什么原因,可能是VS无法注入系统消息钩子。所以,在测试时,只能通过开始菜单来启动应用程序才能正常操作。

 

运行后,点击主页上的按钮,打开文件选择器,然后在左边的导航栏中找到你的程序,点击后会激活。

 

选择文件后,确定,回到应用程序,就能看到选取的文件的内容了。

 

好,今天的牛皮就吹到这里。

示例代码下载