FileSelect - 文件菜单的无忧实现
一个 WinForms 用户控件,为任何以文档为中心的应用程序实现了文件处理命令的细节。
FileSelect - 快速实现文件菜单

用途
FileSelect
是一个 WinForms 用户控件,用于默认实现“文件”菜单。您只需要实现打开、保存和关闭文档以及发送更改通知,就可以获得
- 正确处理保存、另存为和关闭的行为——无论文档是新建的还是已打开的,无论是否已修改。
这包括询问是否应保存文件、询问文件名等。 - 正确处理文件已修改/未修改的状态
- 在无法使用时禁用菜单项
- 最近使用过的文件列表——可直接显示或作为弹出菜单
- 自动更新的窗体标题,包括当前文件名和“已修改”标记
- 包含可自定义的打开文件/保存文件对话框
大部分功能都可以选择性地启用。我试图让您通过各个功能来控制。
下载内容包含一个示例 (FileSelectDemo
),它实现了一个基本的文本编辑器——或者更确切地说,是文件处理部分——您可以在其中探索可用功能。它展示了所有可用的命令,包括两种风格的最近使用过的文件。当然,在您的应用程序中,您可以只添加您需要的项。
如何添加到您的项目中
将 FileSelect 添加到您的项目: 打开工具箱面板,右键单击并选择“自定义...”。在“自定义工具箱”对话框中,在“.NET Framework 组件”选项卡上,选择“浏览”,然后选择FileSelect.dll。取消选中“RecentFileList”和“Strings”控件(只添加 FileSelect
组件本身),然后单击“确定”。
将 FileSelect 添加到您的主窗体: 从工具箱中将一个 FileSelect
组件和一个 MenuStrip
添加到您的主应用程序窗体(或您需要它们的任何地方)。将所需的命令添加到菜单中。
提示:右键单击
MenuStrip
组件,然后选择“添加默认项”。
选择“新建”菜单项,然后在“常规”类别中将“FileSelectCommand on fileSelect1”属性更改为“新建”。对所有其他您想要的命令(通常是新建、打开、保存、另存为和关闭)执行相同的操作。
同样,您可以连接工具栏按钮。
添加最近使用过的文件列表: 插入一个新的占位符菜单项(它永远不会显示),并为其分配 FileSelect
的“最近使用过的文件”命令。在运行时,它将被最近使用过的文件列表替换。
此外,如果占位符后面有一个分隔符,并且最近使用过的文件列表为空,则该分隔符将被隐藏。这允许将最近使用过的文件列表放在两个分隔符之间,而不会出现两个连续的分隔符,当没有文件时。同样,如果占位符是弹出菜单中唯一的一个项,并且最近使用过的文件列表为空,则打开弹出菜单的父项将被禁用。
实现命令: 选择 FileSelect
控件,然后转到属性面板的“事件”选项卡。为 NewDocument
、OpenDocument
、SaveDocument
、CloseDocument
事件添加处理程序。新建和打开通常可以在同一个处理程序中实现。以下示例使用一个简单的 TextDocument
来显示在 TextBox
(textbox1
) 中。TextDocument
包含在 FileSelect
中,对于其他文件格式,您需要在此处使用自己的文档类及其序列化。
处理新建和打开文档,并且
private void fileSelect1_NewOrOpenDocument(object sender, EventArgs e)
{
// Handles "NewDocument" and "OpenDocument"
FileSelectEventArgs fse = (FileSelectEventArgs)e;
DocumentInfo docInfo = fse.DocumentInfo; // additional FileSelect data
// associated with your document
// Create document instance
TextDocument textDoc;
if (fse.Command == EFSCommand.New)
textDoc = TextDocument.New(); // create an empty document
else
textDoc = TextDocument.Load(fse.Path); // ... or load from the specified path
// if that is successful, associate the document info with your document object
docInfo.InitDocument(textDoc);
// update the user interface:
textBox1.Text = textDoc.Data; // set the text
textBox1.Tag = docInfo; // remember the document info (for the dirty flag)
textBox1.ReadOnly = false; // set the text box to read only, so it can be edited
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
DocumentInfo docInfo = textBox1.Tag as DocumentInfo;
if (docInfo != null)
docInfo.IsDirty = true;
}
脏标志将文本框中的任何更改转发到文档信息,以便相应地更新用户界面。继续处理保存处理程序
private void fileSelect1_SaveDocument(object sender, EventArgs e)
{
// we update the document, and save to the path specified in the event args
FileSelectEventArgs fse = (FileSelectEventArgs)e;
DocumentInfo docInfo = fse.DocumentInfo;
TextDocument doc = (TextDocument)docInfo.Document;
// update the document with changes from the view, and save it
doc.Data = textBox1.Text;
doc.Save(fse.Path); // note: fse.Path may be different from docInfo.Path
fse.SaveComplete = true; // indicate that save was successful
}
此事件用于保存、另存为和另存副本。要保存到的文件名通过 fse.Path
传递,并且可能与文档的文件路径不同。
注意:当文档保存成功后,您需要将
SaveComplete
设置为 true。如果不是,FileSelect
将假定保存失败(例如,因为您那崭新的 1TB 硬盘又满了...)。
最后,当文档关闭时,用户界面必须更新。此外,当主窗体关闭时,我们需要处理已修改的文档
private void fileSelect1_CloseDocument(object sender, EventArgs e)
{
textBox1.Text = String.Empty; // clear text box
textBox1.ReadOnly = true; // set to read only
textBox1.Tag = null;
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e) // handler for
// FormClosing event
{
if (!fileSelect1.HandleQuit(e.CloseReason)) // ask user to close modified files, etc.
e.Cancel = true; // cancel closing the form if
// the user said "cancel".
}
其他功能和设置
控件属性
可以在 FileSelect
控件的属性中更改以下设置。(括号中的值是默认值)。
UpdateContainerTitle |
(true) 如果此标志为 true ,则 ContainerControl 的标题将被调整以显示当前文档和已修改状态。或者,如果您需要自定义处理,可以处理 ParentTitleChanged 事件。 |
AllowSaveUnmodified |
(false) 即使文档未修改,也启用“保存”命令。这不常见,但可能适用于某些应用程序。 |
AskCloseUnmodified |
(false) 关闭未修改的文档时,将询问用户确认并可以取消关闭。这不常见,但可能适用于某些应用程序。此选项不影响关闭已修改文档时显示的提示。 |
CloseCreatesNew |
(false) 不再是打开空白文档,而是创建一个新文档。这与记事本的行为相同,您永远不需要显式创建新文档。即使此选项为 true,您仍应正确处理 CloseDocument (通过清除文档 UI),因为创建新文档可能会失败。如果此选项为 true ,则创建新文档不应需要用户交互(例如选择文档类型或大小)。 |
ContainerControl |
包含 FileSelect 实例的控件。通常,您不需要更改它。它用于以下服务
|
DialogOpen 、DialogSave |
在打开或保存文档时用于询问用户文件名的文件对话框。您可以在此处自定义其设置。请注意,某些设置可能会被 FileSelect 覆盖。 |
RecentFiles |
最近使用过的文件列表的设置。 |
.ListCount |
(4) 允许您设置显示的最近项的数量 (ListCount )。 |
.PersistCount |
(10) 记住的最近文件数量。这可以大于 ListCount ——为什么?当用户注意到您刚想打开的文件刚刚从最近使用过的文件列表中消失时,他可能会去增加显示的最近文件数量。用户现在不必浏览文件,而是可以立即在最近使用过的列表中找到它。(尽管您需要提供这样的设置)。 |
.AddShortcuts |
(true) 为最近使用过的文件列表添加编号快捷键 (1, 2, ...)。 |
.DisplayLength |
(40) - 如果不为 -1,则将路径截断为选定的字符数以用于最近使用过的文件列表。 |
CustomUIStrings |
包含用于最终用户显示的 string 。您可以在此处自定义和本地化它们。 |
DocumentInfo
FileSelect
为每个文档保留一个 DocumentInfo
。它会被传递给处理程序事件,并从各种函数返回。它包含以下属性
FilePath |
文档加载或保存到的文件的路径。可能为 null / 空,当用户从未指定文件名并在保存文档时被询问时。 |
CustomTitle |
通过编程设置的自定义标题。例如,它将用于显示在容器控件标题中。 |
标题 |
文档的当前标题。如果设置了 CustomTitle ,则返回 CustomTitle ,否则标题从文件中获取,或使用默认名称。 |
DirtyFlag |
处理文档更改的接口。使用默认实现时,您必须在文档更改时将 IsDirty 属性设置为 true 。 |
IsDirty |
DirtyFlag.IsDirty 的快捷方式。 |
编程 API
HandleXxxxxDocument |
以编程方式触发相应的命令。 |
OnXxxxx |
可以在派生类中重写,而不是实现。 |
UIAskXxxxxx |
用户交互——通常是消息框,可以在派生类中重写。 |
SetCurrentDocument |
提供您自己创建或打开的文档作为当前文档。该函数将返回文档信息,通过该信息您可以设置自定义文档标题和文件路径等属性。您必须手动更新用户界面(文档的视图)。请注意,用户可能取消操作(例如,取消保存当前文档)。在这种情况下,该函数返回 null 。 |
RecentFiles.AllFiles |
包含最近使用过的文件列表,用换行符分隔。您可以将此属性保存在用户设置中,以便记住最近使用过的文件列表。 |
IDirtyFlag - 标记文档更改
脏标志会影响哪些命令已启用,以及何时显示消息框,因此为了正确的 UI 行为,需要正确实现它。
默认实现: 如果您不提供自定义实现,则需要在文档更改时调用 DocumentInfo.IsDirty = true
(这是 DocumentInfo.DirtyFlag.IsDirty
的简写)。
自定义实现: 如果您的文档类已提供脏状态或其他版本控制机制,则可以提供自定义事件。
IDirtyFlag
的 public
接口如下
bool IsDirty { get; set; }
event EventHandler DirtyChanged;
FileSelect
使用此接口来查询脏状态,修改脏状态,例如在保存文档后,或启用 NewDocumentIsDirty
时。它还绑定到更改事件以触发 UI 更新。您可以在两个地方提供自定义实现
- 在您传递给例如
FileSelectEventArgs.InitDocument
的文档类上实现IDirtyFlag
。 - 在独立对象上实现它,并与文档一起传递,例如传递给
FileSelectEventArgs.InitDocument
。
重要提示:处理脏状态更改可能成本很高,因此事件应该只在状态实际更改时触发,而不是每次赋值时。
架构
这只是对相关实体及其角色的概述。如果您有具体问题,请提问!
FileSelect
包含核心实现,并提供与 Visual Studio 的交互(设计器属性、事件等)。DocumentInfo
是与您打开的每个文档关联的对象,包含当前文件名等文档属性。EFSCommand
是一个枚举,包含可用的菜单命令。FileSelectEventArgs
是与FileSelect
事件一起发送的事件类。RecentFileList
实现文件名 MRU 缓存。RecentFiles
继承自RecentFileList
,并添加了FileSelect
使用的一些设计器属性。ICommandItem
是包装菜单或工具栏项的适配器类所需的接口,CommandItemBase
提供了一些实现它的默认值。CI_ToolStripItem
是用于WinForms
工具条(包括工具栏和菜单)的ICommandItem
实现。
未来计划
请注意 El Corazon(之前)的老鼠和天花板签名。
这只是第一个版本,还有一些粗糙的边缘和许多有待改进的功能。
多文档支持将是一个主要的增强功能,但由于这很不常见,而且我目前没有应用需求,所以我也没有计划很快添加它。但是,我做了一些考虑,应该可以实现。可以通过为每个顶级窗体提供自己的 FileSelect
实例来支持多个独立文档(每个文档在自己的顶级窗口中),尽管这还有一些不足之处。
支持多种文档类型。 打开或保存文档时,您目前区分不同格式的唯一方法是文件的扩展名以及文件对话框的 FilterIndex
属性。这可能就足够了,但可以做得更好。我最初设计了一个文档管理器类,该类处理文档的创建、打开和保存,并代表不同的文档类型,但我为了降低复杂性而将其从这个版本中删除了。这也与我使用事件的方式冲突,在这些事件中,接口或委托会更合适(但不太方便)。
自动添加应用程序设置。 我想将其作为一个选项(例如,属性“自动添加用户设置”,以及属性名称的前缀),但我还没有找到方法。现在,您需要手动将属性添加到用户设置中。
动机
我想分享一些与使用该控件不直接相关的想法。我为什么要写这个?当然,这是一个业余项目,投入的时间远远超过了商业开发所能证明的。
首先,这是一个常见的模式,我感到厌烦,当我看到不仅我不得不反复做,而且其他人也一样。在学习 Windows Forms 时,我在这里缺少一些简单性。我真的(读:“完全不”)不怀念 MFC 的文档/视图架构,因为它对我来说太不灵活和顽固了——您可以全部使用它,或者全部不使用,否则您将面临一段艰难的旅程。这就是为什么这个组件只处理提供面向文档的应用程序的“UI 端”的原因。
其次,我相信 Jan Minkovsky 所说的“UI设计的分形性质 [^]”:在他的博客中,他描述了记住窗口位置的历程。看似简单,每次他认为已经解决了,都会出现新的抱怨。最让我惊讶的是,我经历了几乎相同的磨难,尽管我以不同的方式解决了一些方面。我将其描述的方式是这样的
每一个问题——无论大小——都会填满它可用的空间。
每当您解决了那些显眼的糟糕问题后,另一个更小的问题就会填补空白,并像以前一样困扰用户。(有两个因素会对其产生影响:用户逐渐适应,以及受新问题影响的用户减少——但这些因素的出现时间相当晚)。
再加上“最好的用户界面是没有用户界面”这句俏皮话——意思是用户不应该注意到用户界面,它应该是透明的。我们通过各种方式来实现这一点——例如真实世界的隐喻、跨应用程序一致的用户界面和隐喻。然而,通过这种方式,我们正在创造任何微小的烦恼都可以填补的真空。
这就是易于使用和易于实现发生分歧的地方。易于使用描述了一种状态,即应用程序执行用户期望的操作,而我们的期望通常非常复杂。为什么呢?我不知道。
历史
- 1.2 版本 2009 年 5 月 3 日
- 重大更改:
AskCloseUnmodified
的默认值更改为false
- 重大更改:
UpdateParentTitleControl
被ContainerControl
和UpdateContainerTitle
标志替换 - 添加了选项
AllowSaveUnmodified
、CloseCreatesNew
- 添加了
FileSelect.SetCurrentDocument
- 检查文档对象是否实现了
IDirtyFlag
- 对“打开”命令的处理,当当前文档已修改时,行为类似于记事本(文件选择后关闭文档)
- 对处理程序进行了几次小的修复,代码清理
- 添加了工具条和属性网格,带有
FileSelect
设置
- 重大更改:
- 1.1 版本 2009 年 4 月 5 日
- 重大更改:更一致的命名,一些更严格的要求/验证,一些签名已修改,期望的参数更多/更少。
- 添加了对“最近使用过的”菜单的文件路径缩短
有两个选项控制默认行为,您可以通过委托指定自定义行为 RecentFilesList
现在可以以编程方式修改- 为最近使用过的占位符命令和它创建的项的
_RecentFile
命令 ID 分开 - 添加了文档
- 添加了 Sandcastle Builder 项目 + CHM 文档
- 修复了几个 bug
- 注意:一些文字字符串仍需移至
UIStrings
类
- 1.0 版本:2009 年 3 月 29 日