zoukankan      html  css  js  c++  java
  • 【Win10 应用开发】集成文件打开选择器

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

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

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

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

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

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

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

    本文将说一说如何将当前应用程序集成到系统的文件选择器中,为啥会有这个? 因为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无法注入系统消息钩子。所以,在测试时,只能通过开始菜单来启动应用程序才能正常操作。

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

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

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

    示例代码下载

  • 相关阅读:
    POJ 1731
    POJ 1256
    POJ:1833 按字典序找到下一个排列:
    git工作流
    git 分之合并和冲突解决
    iis 7 操作 .net
    IIS7.0 Appcmd 命令详解
    SQL的注入式攻击方式和避免方法
    实例详解Django的 select_related
    django-ajax之post方式
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/4802677.html
Copyright © 2011-2022 走看看