65.9K
CodeProject 正在变化。 阅读更多。
Home

编程 Windows 10 桌面:UWP 焦点 (N 中的第 7 部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2017年11月29日

CPOL

20分钟阅读

viewsIcon

16057

downloadIcon

866

UWP 入门 (告别 WinForm) 第 7 章 在文件系统中保存日记条目

引言

请查阅之前的条目,以便跟上我们通过 UWP 创建每日日记应用程序的进行中的项目。

编程 Windows 10:UWP 焦点 (N 中的第 1 部分)[^]

编程 Windows 10:UWP 焦点 (N 中的第 2 部分)[^]

编程 Windows 10:UWP 焦点 (N 中的第 3 部分)[^]

编程 Windows 10 桌面:UWP 焦点 (N 中的第 4 部分)[^]

编程 Windows 10 桌面:UWP 焦点 (N 中的第 5 部分)[^]

编程 Windows 10 桌面:UWP 焦点 (N 中的第 6 部分)[^]

背景

这是我继续尝试通过 UWP (通用 Windows 平台) 来讲述 Windows 10 桌面开发的故事。这次,在克服了一些 XAML 挑战后,我们将深入研究将文件保存到文件系统。

现在我们能够为每天创建新的日记条目,我们需要提供一种方法来保存用户输入或粘贴到 RichEditBox 中的数据。

 

组织我们的日记条目文件

我想将文件组织起来,以便每个月的条目都在同一个文件夹中。过去,我创建了我称之为 Y-M 目录(年-月),看起来像 2017-11。这种格式在文件资源管理器中排序得很好,并允许我们稍后非常轻松地找到物理文件条目。

 

在每个月文件夹中,我们需要一个方案来命名创建的每个文件。这次我们将使用 Y-M-D-N,它代表年-月-日-条目编号。由于每天可以有一个或多个条目,我们需要一种方法来区分它们,并在文件名末尾保留一个计数器可以使事情变得非常简单,并保持一切井井有条。

现在我们只需要

  • 提供一种用户可以保存条目(将按钮添加到 CommandBar)的方法

  • 提供一种用户可以删除条目(将按钮添加到 CommandBar)的方法

 

在提供用户启动所需操作的方法后,我们将需要编写实际保存或删除相关文件的代码。如果您过去在 WinForm 开发中接触过文件系统,并且这是您第一次在 UWP 范例下使用它,您将会了解到(就像我一样)一切都不同了。

 

首先,让我们更新我们的 XAML 来添加新的 AppBarButtons。

 

DailyJournal_v006.zip

如果您还没有上一篇文章的代码,请继续下载并在 Visual Studio 中打开它。

添加新按钮:复制现有代码

要添加新的保存和删除按钮,我将简单地复制我们之前添加的按钮(CreateNewEntryButton)的代码并进行修改。

但是,我们必须注意不要重用 x:Name 或 Click 值,因为这些值必须是每个按钮的唯一值。

 

 

我将首先添加新的保存按钮,为此,我将

  1. 复制整个 AppBarButton 标签

  2. 粘贴到我们现有的 AppBarButton 的关闭标签之后

  3. 将名称更改为 SaveEntryButton

  4. 删除 Click 属性及其值

  5. 将图标更改为“保存”(一个磁盘图标 - 请参阅 https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.symbol 查看所有图标)

  6. 将标签更改为“保存”

  7. 将 Tooltip(稍后详细介绍)更改为“保存 (Ctrl+S)”

 

您可以复制/粘贴到文件中的最终 XAML 是

<AppBarButton

x:Name="SaveEntryButton"

Icon="Save"

Label="Save"

ToolTipService.ToolTip="Save (Ctrl+S)" >

</AppBarButton>

添加 XAML 后,您将在设计视图中看到该按钮。

 

 

添加 XAML 并看到保存(磁盘)按钮后,双击它以将新的 Click 事件处理程序添加到您的 MainPage.xaml.cs。

这样做后,Visual Studio 将添加处理程序并为您打开 MainPage.xaml.cs。

 

确定哪个 RichEditBox 被选中

为了保存数据,我们需要知道的第一个是当前显示哪个 RichEditBox?但是,要确定当前选择的是哪个 RichEditBox,我们需要添加一些事件处理程序,以便在发生某些操作时,我们存储当前可见的 RichEditBox,以便我们可以访问和使用它。

XAML / 用户界面清理

为了完成这项工作,我们需要清理 XAML 中的一些内容,因为目前我们有一个多余的 PivotItem,我们不需要它。我们需要从 XAML 中删除第二个 PivotItem:包含 MainRichEdit2 的那个。

 

我们的最终应用程序最初每天只会显示 Entry1(而不是 Entry2),所以这个 PivotItem 不是必需的,它只会让我们感到困惑,所以让我们删除它。

删除 PivotItem XAML

我们现在就来做。选择它并按 [Delete] 键。

 


设置焦点

我们接下来要做的是确保每当我们的第一个 PivotItem 加载或获得焦点(用户正在与其交互)时,我们就将焦点设置到 RichEditBox。我们希望将焦点设置到 RichEditBox,因为这是用户实际希望与我们的应用程序交互的方式。他实际上并不关心 PivotItem,因为 PivotItem 仅仅是到达 RichEditBox 的一种方式。既然如此,我们需要确保每当用户单击 PivotItem 标题时,焦点都会被设置到相关的 RichEditBox。

外层容器

另外,请记住,当您单击 PivotItem 的标题时,您实际上首先单击的是外层容器:Pivot。操作系统会将您对 Pivot 的单击传播到 PivotItem(内层容器),但将焦点设置到相关 RichEditBox 的方法是添加一个处理程序,以便每当单击外层 Pivot 时,我们都会检查单击的是哪个 PivotItem,然后将其焦点设置到其相关的 RichEditBox。

 

当外层 Pivot 项被单击时,实际上会调用 GotFocus 事件处理程序。它不是,正如我最初假设的,Click 事件。

这要做很多工作,不是吗?这就是开发人员所做的。这就是为什么我们必须了解所有这些疯狂的细节。

添加 GotFocus 事件处理程序

让我们为我们的 Pivot 添加 GotFocus 处理程序。

我们以前已经向 XAML 元素添加了事件,但让我们再回顾一遍。

  1. 在 XAML 中单击 Pivot 元素(在查看 MainPage.xaml 时)

  2. Properties 窗口(通常位于右侧的 Solution Explorer 下方),单击 [闪电] 图标以显示此控件支持的事件处理程序列表。

  3. 在列表中向下滚动,直到您看到 GotFocus 事件。

  4. 双击 GotFocus 事件旁边的 TextBox。

 

 

当您双击 GotFocus TextBox 时,Visual Studio 会将事件处理程序添加到您的 XAML 中,并将新方法添加到 MainPage.xaml.cs。

 

 

这是添加事件处理程序的简便方法,因为 Visual Studio 通过为您完成大部分工作来提供帮助。稍后我们将看到如何通过 C# 代码添加自己的事件处理程序。

向事件处理程序添加代码:将焦点设置到 RichEditBox

让我们思考一下当用户单击 PivotItem(通过单击 Pivot)时,应用程序应该做什么。

  • 获取相关的 PivotItem

  • 将键盘焦点设置到相关的 RichEditBox

 

以下是完成这项工作的代码。它要求我们获取对所有受影响对象的引用(PivotPivotItemRichEditBox)。

Pivot p = sender as Pivot;

PivotItem pi = p.SelectedItem as PivotItem;

RichEditBox reb = pi.Content as RichEditBox;

reb.Focus(FocusState.Keyboard);

 

 

我将使用 Visual Studio 的行号来解释代码的作用。

 

rootPivot_GotFocus 事件触发时,它会提供两个参数。在我们的代码中,我们不需要使用第二个参数,但我们确实使用了第一个参数(sender)。因为我们知道 GotFocus 是从 Pivot 元素调用的,所以我们知道 sender 对象将是一个 Pivot,但是,我们需要将通用的 sender 对象转换为我们知道需要使用的类型。我们可以使用 as 关键字轻松做到这一点,这正是我们在 **第 65 行** 所做的。

 

现在我们有了一个有效的 Pivot,我们可以使用它来在 **第 66 行** 获取它包含的 PivotItem

当前选定的 Pivot 包含一个 SelectedItem 属性,它是相关的 PivotItem,所以我们获取它并将其存储在我们的本地 PivotItempi)变量中。

所有用于 RichEditBox 引用

我们进行所有这些工作只是为了能够访问 PivotItem 中包含的 RichEditBox。我们知道我们最初将 PivotItemContent 属性设置为 RichEditBox,所以我们再次使用 as 关键字在 **第 67 行** 访问相关的 RichEditBox

 

最后,我们得到了一个有效的 RichEditBox。这就是我们完成所有这些工作的全部原因,以便在 **第 68 行** 我们可以将键盘焦点设置到 RichEditBox

下载代码并尝试

现在,如果我们运行应用程序并单击 Entry1(Pivot),那么 I 빔光标将自动出现在 RichEditBox 中,用户可以立即开始键入。

 

获取 DailyJournal_v007.zip 并构建代码,运行它并单击 Entry1,您将看到这是正确的。另外,现在当您添加新条目时,该条目将获得焦点。

 

 

您是否认识到现有问题?

但是,您应该已经注意到仍然存在一个问题。

应用程序应该立即在加载时将焦点设置到 RichEditBox。但是,要解决这个问题,我们需要向 PivotItem 添加一个 Loaded 事件处理程序。

有点令人困惑

这有点令人困惑,因为我们一直在处理 Pivot,但现在我们需要向 PivotItem 添加事件。

您可能认为(正如我以前那样)可以在 MainPage 加载时将焦点设置到 RichEditBox。但是,您不能确定 PivotItem 本身已加载,如果它没有加载,那么 RichEditBox 也不会加载,这会导致应用程序崩溃。

 

让我们将 Loaded 事件处理程序添加到 XAML 中存在的 PivotItem。之后,我们将开始了解如何通过 C# 代码将事件添加到以编程方式添加的元素(PivotItems 和相关的 RichEditBoxes)。

向 PivotItem 添加 Loaded 事件处理程序

在另一个元素上添加 Loaded 事件处理程序是相同的工作。

 

 

  1. 在 XAML 中单击 PivotItem 元素(在查看 MainPage.xaml 时)

  2. Properties 窗口(通常位于右侧的 Solution Explorer 下方),单击**[闪电]**图标以显示此控件支持的事件处理程序列表。

  3. 在列表中向下滚动,直到您看到 Loaded 事件。

  4. 双击 Loaded 事件旁边的 TextBox。

 

我们将添加到 PivotItem Loaded 事件中的代码将非常相似,但不需要我们从 Pivot 中获取 PivotItem 引用。相反,我们将通过 sender 对象(PivotItem_Loaded 方法的第一个参数)获得对相关 PivotItem 的引用。

以下是我们将在 Loaded 事件中添加的代码

PivotItem pi = sender as PivotItem;

           RichEditBox reb = pi.Content as RichEditBox;

           reb.Focus(FocusState.Keyboard);

 

 

这表明 sender 对象变成了引发事件的类型。在这种情况下,当 PivotItem Loaded 事件处理程序触发时,sender 是一个 PivotItem

所以,在这种情况下,我们只需要获取 PivotItem 的 Content (RichEditBox),以便我们可以将其焦点设置上去。

当 PivotItem 加载时:将焦点设置到 RichEditBox

现在,当我们运行应用程序时,RichEditBox 将立即获得焦点(即使在用户单击 Pivot 之前)。这还将确保每当 PivotItem 加载时,其相关的 RichEditBox 都会获得焦点。这很重要,因为这是用户与应用程序交互的方式。

获取代码并尝试

如果您正在跟进,请继续构建和运行。或者下载 DailyJournal_v008.zip 并构建、运行并尝试。

Loaded 事件的结果

现在,即使您在单击 Entry1 PivotItem 标题之前,您也应该在 RichEditBox 中看到 I 빔光标。

但是,仍然存在会导致用户交互错误的问��。

仍然存在什么问题?

这有点棘手,因为如果您启动应用程序并单击 New Entry 按钮,您将看到新条目上的 RichEditBox 会获得焦点。但是,这只是因为您在 Entry1 RichEditBox 中有焦点,我们很幸运。

重现问题

如果您首先单击 CalendarView 控件(将焦点设置到 CalendarView),然后单击 New Entry 按钮,您将看到 RichEditBox 没有 I 빔光标,因为它没有获得焦点。这将在将来给我们带来问题。

为什么会发生这个问题?

这个问题发生的原因是生成的 PivotItem 和相关的生成 RichEditBoxes 没有激活事件处理程序。请记住,我们将事件处理程序添加到了 XAML 中存在的特定 PivotItem,但是每次我们单击 New Entry 按钮时都会生成一个新的 PivotItemRichEditBox。我们需要将事件处理程序添加到生成的对象中,以便它们能够正常工作。

 

让我们去解决这个问题。

回到 CreateNewEntryButton 方法

为了解决这个问题,我们需要向每次用户单击 New Entry 按钮时生成的 PivotItem 添加一个事件处理程序。

这并不难,但如果您以前从未做过,可能有点棘手。

事实上,这只有一行代码,因为我们已经实际编写了该方法(PivotItem_Loaded)。

 

首先,看一下当前的 CreateNewEntryButton_Click 方法来了解我们的方向

 

显示所有代码

我还显示了其余代码,因为我们将使用 PivotItem_Loaded 方法来解决我们的问题,并且我希望它可见。

仔细检查我列出的 CreateNewEntryButton_Click 代码,您会看到第 59 行有一个空行(图像中显示 Visual Studio 行号)。

那是我将要添加代码行来解决我们问题的地方。

我们需要告诉每个生成的 PivotItem (pi),它应该注册 Loaded 事件的处理程序。我们还希望告诉它,事件处理程序方法将是我们已经编写的方法:PivotItem_Loaded。

 

如果您开始在**第 59 行**键入并开始输入 pi.L,您会看到 Intellisense 会尝试帮助您。

 

 

Intellisense 弹出时,您会看到一个对象支持的事件处理程序(**[闪电]**图标)和属性(**扳手**图标),以及(此处未显示)方法(函数)(**框**图标)列表。

在我们的例子中,我们想要 Loaded 事件,所以如果您愿意,可以双击列表中的该项,它将被添加到您的代码中。Loaded 设置的值是一个委托(在 Loaded 事件触发时将被调用的方法)。语法有点不同,因为我们将我们的方法添加到可能已有的任何其他方法中,所以我们使用 += 来做到这一点。

这是完整的代码行

pi.Loaded += PivotItem_Loaded;

问题已修复

就是这样。现在问题解决了。每当您添加一个新的日记条目时,Loaded 事件都会将焦点设置到 RichEditBox。

即使您先单击了其他控件,例如 CalendarView,然后单击 New Entry 按钮,您也会发现 RichEditBox 获得了焦点。

构建、运行、尝试

继续构建、运行并尝试一下。如果您需要代码,请在此文章顶部获取 DailyJournal_v009.zip

我们为什么走了这么远?

所有这些工作都是为了让我们能够

  • 确定当前正在使用哪个 RichEditBox,以便我们可以保存其数据。

要做到这一点,我们需要知道 RichEditBox 何时被激活。要知道 RichEditBox 何时被激活需要我们编写的代码来处理 LoadedGotFocus 事件。

捕获当前选定的 RichEditBox

现在我们需要捕获当前选定的/激活的 RichEditBox,以便当用户单击 Save 按钮时,我们将获取与用户当前正在查看的 RichEditBox 相关的数据并保存其数据。

新的成员变量

这非常容易做到,只需做两件事:

  1. 在我们的 MainPage 类中添加一个新的私有成员变量

  2. 将成员变量设置为引用当前激活的(具有焦点的)RichEditBox

 

由于这是一个 RichEditBox,我们只需将以下代码添加到 MainPage 类的顶部

private RichEditBox currentRichEditBox;

 

 

注意:如果您下载了之前的源代码 zip 文件,您可能已经有了这个,因为我之前一直在摸索如何完成这项工作,并且不小心保留了我的更改。没关系,不会有什么问题,只是为您节省了一些输入。

 

添加了该行后,我们需要在 RichEditBox 获得焦点时的任何时候设置此引用。现在它发生在两个不同的地方:

  1. rootPivot_GotFocus 方法

  2. PivotItem_Loaded

 

实际上,现在我查看这两个方法时,我发现我在重复一些代码。这违反了一个称为 DRY (Don't Repeat Yourself) 的 SOLID 软件开发原则(SOLID (面向对象设计) - Wikipedia[^]),所以我将把代码重构到它自己的方法中,以便我们只需要在一个地方设置我们的 currentRichEditBox 引用。

这是更改后的 C# 代码

private void rootPivot_GotFocus(object sender, RoutedEventArgs e)

       {

           Pivot p = sender as Pivot;

           PivotItem pi = p.SelectedItem as PivotItem;

           RichEditBox_SetFocus(pi);

       }


       private void PivotItem_Loaded(System.Object sender, RoutedEventArgs e)

       {

           PivotItem pi = sender as PivotItem;

           RichEditBox_SetFocus(pi);

       }


       private void RichEditBox_SetFocus(PivotItem pi)

       {

           RichEditBox reb = pi.Content as RichEditBox;

           reb.Focus(FocusState.Keyboard);

           currentRichEditBox = reb;

       }

 

 

新方法中的前两行之前分别在 LoadedGetFocus 方法中,这不好。

现在,我只是将这些行移到了新方法中。当然,我也必须将 PivotItem 的引用添加为参数,以便我可以在我的新方法中使用该对象。

然后,我还将新方法的调用添加到这两个方法(GetFocusLoaded)中,这样一切仍然会正常工作。

添加的代码行以捕获 RichEditBox

最后,我将最后一行代码添加到了新方法中,这样无论 RichEditBox 如何被激活,我们都能获得对它的引用,以便我们可以使用它来获取数据并将其保存到文件中。

然而,代码目前还没有做任何不同的事情,因为我们还没有实现 **Save** 按钮。

让我们去双击我们的 **Save** 按钮,以便我们可以向它添加一个事件处理程序,以便在用户单击 **Save** 按钮时编写一些代码。

 

添加 SaveButton Click 事件处理程序

让我们以添加 GotFocus 和 Loaded 事件处理程序的方式来做。

打开 MainPage.xaml

  1. 在 XAML 中单击 SaveEntryButton 元素。

  2. Properties 窗口(通常位于右侧的 Solution Explorer 下方),单击 **[闪电]** 图标以显示此控件支持的事件处理程序列表。

  3. 在列表中向下滚动,直到您看到 Click 事件。

  4. 双击 Click 事件旁边的 TextBox。

 

当然,当您双击 TextBox 时,Visual Studio 会打开 MainPage.xaml.cs 并添加新方法。

章节很长:快速保存

由于本章变得很长,我们将暂时将 Save 按钮保存到单个文件中。这意味着当前显示哪个 RichEditBox 将决定文件中的数据。如果您切换到另一个 RichEditBox 并再次保存,它将覆盖您之前的数据。

 

未来功能

然后,下次我们将开始解决真正的问��:将数据存储在多个文件中,然后将文件重新加载到它们相关的 RichEditBoxes 中。

 

文件存储:保存数据

以下是我们将在 SaveEntryButton_Click 方法中使用的代码

       

    Windows.Storage.StorageFolder storageFolder =

               Windows.Storage.ApplicationData.Current.LocalFolder;

           Windows.Storage.StorageFile sampleFile =

               await storageFolder.CreateFileAsync("FirstRichEdit.rtf",

                   Windows.Storage.CreationCollisionOption.ReplaceExisting);

 

我从以下示例中获取了该代码:

https://docs.microsoft.com/en-us/windows/uwp/files/quickstart-reading-and-writing-files

我更改了它保存的文件名,但其余部分相同。

当您将代码添加到 MainPag.xaml.cs 时,Visual Studio 的 Intellisense 会警告您一个问题。

 

 

它之所以这样告诉我们,是因为我们正在从一个同步方法(SaveEntryButton_Click)调用一个异步方法(CreateFileAsync)。

这与我们在**第 2 章**中尝试弹出对话框时看到的情况类似。

我们可以不借助 Intellisense 来解决这个问题。只需在 SaveEntryButton_Click 方法中(紧跟在 private 关键字之后)添加 async 关键字。

您这样做后,Intellisense 就会停止打扰您。

 


这会创建文件,但实际上不会保存任何数据。要做到这一点,我们需要

  1. 异步打开文件时创建一个 IRandomAccessString

  2. 调用 RichEditBox Document.SaveToStream

  3. 关闭(Dispose)流

 

以下是您需要添加的代码

IRandomAccessStream documentStream = await sampleFile.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);

           currentRichEditBox.Document.SaveToStream(TextGetOptions.FormatRtf, documentStream);

           documentStream.Dispose();

 

第一行是另一个异步调用,因为应用程序不知道需要多长时间才能获得磁盘访问权限,它不想冻结。

 

 

一旦我们拥有一个有效的 Stream,我们就可以使用它来写入文件数据,然后我们可以调用 SaveToStream 方法。请注意,第一个参数是一个 SDK(软件开发工具包)枚举,它告诉方法将文件保存为标准的 RTF(富文本格式)。这样,即使您有一些图片,它也会保存它们。

 

最后,我们 Dispose 我们的流以关闭文件,我们就完成了。

获取代码并尝试

构建代码,运行它,然后在 RichEditBox 中输入一些内容并单击 Save 按钮。如果一切正常,您甚至不会注意到它已保存。

 

但是,文件保存在哪里?

啊,64 美元的问题。现在我们使用特殊的 Windows.Storage.ApplicationData 及其 LocalFolder 值,应用程序让操作系统告诉它应该在哪里保存文件。

 

再次,这是因为 UWP 架构师正在努力确保无论应用程序运行在什么设备上,它都不会因保存数据而失败。

在我们的例子中,在桌面上,您可以通过转到

%localappdata%\Packages\116d1010-a1e4-452a-a1d2-c84aea07af6d_gw4zt26480tv8

您应该能够复制最后一行,将其粘贴到文件资源管理器中,它将带您到该位置。

这不是很奇怪吗?!嗯,%localappdata% 是一个指向您的 Windows SpecialFolder LocalAppData 的环境变量,通常位于:c:\users\<username>\local\AppData\local\

\Packages 文件夹中,UWP 应用程序存储其数据。

这是我目录中的文件

 

Package.appxmanifest

最后一项是 **GUID**(全局唯一 ID),它是 Visual Studio 工具在您首次创建应用程序时生成的,用于唯一标识您的应用程序。

事实上,如果我们回到 Visual Studio,在 Solution Explorer 中双击 Package.appxmanifest,然后选择 Packaging 部分,您会看到 Package 系列名称包含此值。

 

 

如果您在文件中保存了一些数据,您可以导航到文件系统中的文件(使用前面的线索,并使用 **MS-Word** 或 **WordPad** 查看它。

这是我保存的数据(甚至包含一张快照图像)。

 

 

然后我在 **WordPad** 中打开了该文档

注意:Windows 10 认为图像是危险的,所以如果您在那里保存图像,它可能不愿意让您查看该数据。

 

 

我就此结束,因为这是一个非常长的章节。

但是,我很快就会回来修复文件保存方案并添加删除条目(及其文件)的功能。

历史

 

2017-11-29:首次发布

© . All rights reserved.