DIY智能感知






4.89/5 (77投票s)
2004年1月13日
8分钟阅读

409259

15568
DIY 智能感知/自动完成。
快速开始
下载项目,编译并运行。转到菜单并加载 %systemroot%\Microsoft.NET\Framework\v1.xxx\system.dll。在 rich text box 中键入 System 然后输入 "."。将出现一个类/命名空间的列表框。
注意:命名空间图标同时用于命名空间和没有图标的类型,这是我偷懒的表现。
引言
如果您是一名程序员,并且不熟悉智能感知,那么您过去五年很可能一直躲在与世隔绝的洞穴里,靠透析机维生。Intellisense 是微软的商标,但其概念已被数十种代码编辑器采用。简而言之,它是在代码编辑器中键入一个已识别的单词时出现的下拉列表。例如,在 Visual Studio .NET 中键入 System 然后输入 "." 会显示 System
命名空间类型的下拉列表。另一个例子是 Macromedia Dreamweaver - 键入一个特定的 HTML 标记然后输入一个空格,就会出现该 HTML 标记的属性列表。
关于智能感知历史的模糊讨论可以在 这里找到。
在这篇文章和配套的代码中,我将演示如何为 RichTextBox
实现一个类似于智能感知(我将称之为自动完成)的系统,该系统由用户按下 "." 键触发。
完整的项目具有以下功能
- 从下拉列表中自动完成
- 易于自定义出现在下拉列表框中的项的存储
我还额外添加了以下功能
- 列表框中显示图标,每个项目旁边都有图标
- 输入方法名称和 "(" 时,会出现一个简单无华的工具提示,显示参数列表
- 通过加载程序集来填充自动完成查找树。
所有这些在虚拟纸上看起来并不多,但源代码将近 900 行(包括窗体组件布局代码)。我将逐段解释代码,但首先是要求。
要求
该项目是一个简单的窗体,带有一个 RichTextBox
和一个主菜单。RichTextBox
控件没有以任何方式进行子类化,只是更改了字体为 Courier,仅此而已。
基本要求是,当用户输入一个单词,然后输入 ".",并且该单词是已知单词时,一个项目列表出现在 "." 下方和右侧的 ListBox
中。用户可以从中选择一个 ListBox
中的项目,该项目的文本将被粘贴到 RichTextBox
中,完成用户正在输入的单词。
该窗体还有一个 TreeView
,用于存储要查找的项;一个 ListBox
,用于显示项;以及一个文本字段,用作方法参数列表的廉价且实用的工具提示。
按下神奇的 "." 键
RichTextBox
的 OnKeyDown
事件检查 OemPeriod(我假设这在所有正在使用的机器上都有效)。当按下点时,使用 getLastWord()
方法检索前一个单词。
然后检查该单词是否存在于我们用于存储查找的树中(稍后介绍)。如果找到,则使用这些项目填充 ListBox
。populateListBox()
管理此过程,返回一个布尔值,表示是否填充了任何项目。之后,对 ListBox
进行定位。
幸运的是,RichTextBox
提供了一个方法使此任务变得简单。使用 RichTextBox
的 GetPositionFromCharIndex()
,我们可以获取当前插入符号位置的 Point
struct
。使用此信息,我们可以根据 "." 定位 ListBox
的 X 坐标,Y 坐标保持不变,但加上了字体的高度(我稍微调整了一下,移除了 Courier 的额外 2 像素);然后将其定位在 "." 的下方。Visual Studio .NET 会根据文本编辑器中行的位置将框置于当前行的上方或下方(如果您在文本编辑器的最后一行,则框会出现在上方)。我没有添加此功能,但我想这并不难实现。
查找树
如前所述,自动完成查找的所有项都存储在 TreeView
中。对于我实现的此类代码完成 - 命名空间/类/方法 - 将项存储在树中是完全合理的。对于 HTML 代码完成,这也将是理想的选择,根节点可以是核心 HTML 标记集,下面的节点是属性。对于平面列表,可以去掉 TreeView
,单独使用 ListBox
。
开始项目时,我开始创建自己的树实现来存储项。然后我意识到这样做没有意义,因为 .NET 提供了 TreeView
形式的结构。它有作为控件的开销,但它允许您在 Visual Studio .NET、Borland C# builder、#develop 等 IDE 中轻松添加和编辑项,从而加快了工作速度。
代码完成行为
按下 "." 并出现项目下拉列表后,将应用以下逻辑
- 焦点回到
RichTextBox
,它会捕获上下键。这些键用于在ListBox
中上下移动。 - 如果选中了一个项目,并且用户按下 Return、Space 或 Tab 键,则项目列表框将隐藏,并触发单词的自动完成。
- 当
ListBox
可见时,可以按下任何字母数字键。除了 Backspace 或 Shift 之外的任何其他键都会隐藏ListBox
。 - 按下 Delete 键会隐藏
ListBox
,前提是正在删除的字符是 "."。 - 当按下任何字母数字键时,将从输入的字符中连接一个字符串。
- 搜索
ListBox
以查看是否有任何项目以输入的组合开头,如果有,则选中该项目。 - 双击
ListBox
中的项目会触发自动完成。
所有这些都在 RichTextBox
的 OnKeyDown
事件中进行检查。
在树中查找项
找到之前输入的单词后,会搜索 TreeView
以查看是否可以找到该单词。由于单词可能包含完整的命名空间,因此会根据点将其分解,并检查节点的 FullPath
属性与连接的字符串是否匹配。这是在一个递归函数中完成的。找到节点后,将其存储在成员变量中,然后用于用所有子节点填充 ListBox
。通过创建一个实现 IComparable
的自定义类型的数组来对节点按文本排序。该数组从 TreeNode
填充,然后排序,并使用它来填充 ListBox
。
显然,每次填充时对项目进行排序是一种相当慢的方法,更可取的方法是在填充树时对其进行排序。一个可排序的 TreeView
将是另一篇文章的主题(有人实现过吗?!),但现在这样就足够了。
自动完成文本
这是一个相当简单的过程 - 点之前的所有内容存储在一个字符串中,点之后的所有内容存储在另一个字符串中,然后将选定的 ListBox
项目的文本插入到这两个字符串之间。然后,RichTextBox
的文本将被此新文本替换。我没有测试过这种方法处理大量文本的情况,如果出现闪烁,可能需要自定义 RichTextBox
。
附加功能
为了演示自动完成,源代码中还有另外 2 个方法,用于读取程序集并用其类型填充树。readAssembly()
方法加载一个程序集并遍历其所有类型,为命名空间、类、方法、字段和事件添加节点。每个类型都使用 addMembers()
来添加其字段、方法和事件作为节点。
每个 ListBox
项目都有一个图标来指示其类型 - 该类型存储在每个 TreeNode
的 Tag
属性中。Tag
属性还存储方法的参数列表作为字符串,因此如果我们假设该节点是一个方法(如果 Tag
属性是字符串)。图标在 ListBox
中的显示是使用 GListBox 类完成的。
如上所述,当键入一个方法并按下左括号 "(" 键时,会显示一个工具提示,其中包含方法的参数列表。我找不到任何免费的自定义工具提示实现,所以我使用了一个 TextBox
来显示详细信息,并将其定位方式与项目列表框相同。
结论
我不想在此文章中深入介绍每个方法,以免内容过于冗长,因此我在这里停止。希望它对大家有用。我想指出,这是概念验证代码,而不是最终产品;我没有测试 TreeView
的性能,看看它能否处理数千个节点,而且还有一些小的改进空间(如果有一支开发团队、丰厚的报酬以及世界上最大的公司支持我!)。
已知bug
- 当您转到查看窗体设计器时,可能会遇到索引超出界限的错误。这是
GListBox
的一个问题,我没有修复该错误。这仅发生在设计时,项目仍然可以正常编译。 - 使用 Tab 键进行自动完成会在
RichTextBox
中添加一个 Tab 键(即使e.Handled = true
……这是我以后需要研究的问题)。 - 工具提示未保持在窗体前面。