编程 Windows 10 桌面:UWP 焦点 (N 中的第 7 部分)
UWP 入门 (告别 WinForm) 第 7 章 在文件系统中保存日记条目
- 下载 DailyJournal_v007.zip - 123.3 KB
- 下载 DailyJournal_v008.zip - 123.6 KB
- 下载 DailyJournal_v009.zip - 123.3 KB
- 下载 DailyJournal_v010.zip - 123.6 KB
引言
请查阅之前的条目,以便跟上我们通过 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 值,因为这些值必须是每个按钮的唯一值。
我将首先添加新的保存按钮,为此,我将
-
复制整个
AppBarButton
标签 -
粘贴到我们现有的
AppBarButton
的关闭标签之后 -
将名称更改为
SaveEntryButton
-
删除
Click
属性及其值 -
将图标更改为“保存”(一个磁盘图标 - 请参阅 https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.symbol 查看所有图标)
-
将标签更改为“保存”
-
将 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 元素添加了事件,但让我们再回顾一遍。
-
在 XAML 中单击
Pivot
元素(在查看MainPage.xaml
时) -
在
Properties
窗口(通常位于右侧的 Solution Explorer 下方),单击 [闪电] 图标以显示此控件支持的事件处理程序列表。 -
在列表中向下滚动,直到您看到
GotFocus
事件。 -
双击
GotFocus
事件旁边的 TextBox。
当您双击 GotFocus TextBox 时,Visual Studio 会将事件处理程序添加到您的 XAML 中,并将新方法添加到 MainPage.xaml.cs。
这是添加事件处理程序的简便方法,因为 Visual Studio 通过为您完成大部分工作来提供帮助。稍后我们将看到如何通过 C# 代码添加自己的事件处理程序。
向事件处理程序添加代码:将焦点设置到 RichEditBox
让我们思考一下当用户单击 PivotItem(通过单击 Pivot)时,应用程序应该做什么。
-
获取相关的 PivotItem
-
将键盘焦点设置到相关的 RichEditBox
以下是完成这项工作的代码。它要求我们获取对所有受影响对象的引用(Pivot
、PivotItem
、RichEditBox
)。
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
,所以我们获取它并将其存储在我们的本地 PivotItem
(pi
)变量中。
所有用于 RichEditBox 引用
我们进行所有这些工作只是为了能够访问 PivotItem
中包含的 RichEditBox
。我们知道我们最初将 PivotItem
的 Content
属性设置为 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 事件处理程序是相同的工作。
-
在 XAML 中单击
PivotItem
元素(在查看MainPage.xaml
时) -
在
Properties
窗口(通常位于右侧的 Solution Explorer 下方),单击**[闪电]**图标以显示此控件支持的事件处理程序列表。 -
在列表中向下滚动,直到您看到
Loaded
事件。 -
双击
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 按钮时都会生成一个新的 PivotItem
和 RichEditBox
。我们需要将事件处理程序添加到生成的对象中,以便它们能够正常工作。
让我们去解决这个问题。
回到 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 何时被激活需要我们编写的代码来处理 Loaded
和 GotFocus
事件。
捕获当前选定的 RichEditBox
现在我们需要捕获当前选定的/激活的 RichEditBox,以便当用户单击 Save 按钮时,我们将获取与用户当前正在查看的 RichEditBox 相关的数据并保存其数据。
新的成员变量
这非常容易做到,只需做两件事:
-
在我们的
MainPage
类中添加一个新的私有成员变量 -
将成员变量设置为引用当前激活的(具有焦点的)
RichEditBox
由于这是一个 RichEditBox
,我们只需将以下代码添加到 MainPage
类的顶部
private RichEditBox currentRichEditBox;
注意:如果您下载了之前的源代码 zip 文件,您可能已经有了这个,因为我之前一直在摸索如何完成这项工作,并且不小心保留了我的更改。没关系,不会有什么问题,只是为您节省了一些输入。
添加了该行后,我们需要在 RichEditBox
获得焦点时的任何时候设置此引用。现在它发生在两个不同的地方:
-
rootPivot_GotFocus 方法
-
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;
}
新方法中的前两行之前分别在 Loaded
和 GetFocus
方法中,这不好。
现在,我只是将这些行移到了新方法中。当然,我也必须将 PivotItem 的引用添加为参数,以便我可以在我的新方法中使用该对象。
然后,我还将新方法的调用添加到这两个方法(GetFocus
和 Loaded
)中,这样一切仍然会正常工作。
添加的代码行以捕获 RichEditBox
最后,我将最后一行代码添加到了新方法中,这样无论 RichEditBox
如何被激活,我们都能获得对它的引用,以便我们可以使用它来获取数据并将其保存到文件中。
然而,代码目前还没有做任何不同的事情,因为我们还没有实现 **Save** 按钮。
让我们去双击我们的 **Save** 按钮,以便我们可以向它添加一个事件处理程序,以便在用户单击 **Save** 按钮时编写一些代码。
添加 SaveButton Click 事件处理程序
让我们以添加 GotFocus 和 Loaded 事件处理程序的方式来做。
打开 MainPage.xaml
并
-
在 XAML 中单击 SaveEntryButton 元素。
-
在
Properties
窗口(通常位于右侧的 Solution Explorer 下方),单击 **[闪电]** 图标以显示此控件支持的事件处理程序列表。 -
在列表中向下滚动,直到您看到
Click
事件。 -
双击
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
就会停止打扰您。
这会创建文件,但实际上不会保存任何数据。要做到这一点,我们需要
-
异步打开文件时创建一个
IRandomAccessString
-
调用
RichEditBox Document.SaveToStream
-
关闭(
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:首次发布