有朋友给老周提出建议:老周,能不能在写博客时讲一下有深度的小故事?技术文章谁不会写。讲一下对人生有启发性的故事会更好。
哎呀,这要求真是越来越高了。好吧,尽量吧,如果有小故事的话,老周在就每次写博客时写出来;如果没有故事可讲,那只能请您原谅了,呵呵。
有人问老周,你每天都玩手机的吗?答案是肯定的,与时俱进嘛,玩是肯定的。不过,老周从不做低头族,虽然玩,但不会一整天都低着头看手机,这样做让人觉得你很没礼貌(如果一个人独处就没关系),也很没情趣。尤其是一堆人在说话时,你再不喜欢讲话也应该插上一两句,老低着头在那里,一来对身体不好,二来也显得不尊重别人。
其实,老周在家独处时,也不会总拿着手机的。我觉得现在的人很奇怪,似乎大家都知道某些事情对身心不好,但就是不知道为什么,明知道有害也要沉迷其中。这大概就是佛家所说的过度执迷了。执着本没什么不好,但执迷就有点物极必反了。
要说现代人到底懂不懂什么是爱,这真的难说,如果对自己都负不起责任的话,不懂得惜爱自己的人,估计也很难去爱别人。生活中很多东西(比如手机)都是我们的工具,我们是要做工具的主人,还是成为工具的奴隶。唉,只有自己心里明白了。究其根本,可能就是因为很多人的精神家园一片苍白的原因吧。
总之,适可而止就不会有什么后患。
=================================================================
本文将说一说如何将当前应用程序集成到系统的文件选择器中,为啥会有这个? 因为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 = ;
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 == )
{
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 = ; //文件个数
StorageFolder localfolder = ApplicationData.Current.LocalFolder;
// 创建文件
for (int x = ; 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 > )
{
if (pickerUI.SelectionMode == FileSelectionMode.Multiple)
{
for (int i = ; 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[] as FileItem;
if (pickerUI.ContainsFile(item.Name))
{
pickerUI.RemoveFile(item.Name);
}
}
} // 添加列表
if (e.AddedItems.Count > )
{
// 如果是多选
if (pickerUI.SelectionMode == FileSelectionMode.Multiple)
{
for (int i = ; 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[] 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}\n";
// 读出内容
string str = await FileIO.ReadTextAsync(file);
msg += $"文件内容:{str}"; tb.Text = msg;
这里面有个奇怪的现象,就是如果用VS来调试运行时,你在文件选择器上无法用鼠标操作,不知道什么原因,可能是VS无法注入系统消息钩子。所以,在测试时,只能通过开始菜单来启动应用程序才能正常操作。
运行后,点击主页上的按钮,打开文件选择器,然后在左边的导航栏中找到你的程序,点击后会激活。
选择文件后,确定,回到应用程序,就能看到选取的文件的内容了。
好,今天的牛皮就吹到这里。