Windows控制台应用程序的自动补全功能实现

在最近的项目开发中,需要为一个简单的用户界面添加自动补全功能,以提高用户体验。考虑到时间成本,选择了Windows控制台应用程序作为开发平台。在实现了一些基础命令后,决定添加Tab键触发的自动补全功能。经过一番研究,发现实现这一功能并不像最初想象的那样简单。许多解决方案都建议使用Console.ReadKey()方法,但这种方法会禁用其他一些功能,比如使用上下箭头键浏览命令历史,而这些功能希望能够保留。

在进行了更多的搜索后,没有找到满意的解决方案,因此决定自己编写一个。希望通过分享解决方案,能够帮助社区中的其他开发者。尝试了多种不同的方法,甚至直接从操作系统拦截按键,但都没有达到预期的效果。最终,决定逆向工程,恢复使用ReadKey方法时丢失的控制台功能(同时支持自动补全)。由于本文主要关注自动补全问题,已经将关于Console.ReadKey问题的讨论放在了另一篇文章中,感兴趣的读者可以点击查看。

本文将介绍两种类型的自动补全实现方法,并解释如何将上述解决方案与这些实现方法结合起来,因为认为这是大多数人遇到问题的地方。当然,自动补全算法本身也可以在不同的情况下使用。因此,它被实现在一个单独的项目中。

使用代码

ConsoleUtils库(也在附带的解决方案中)提供了自己的ConsoleExt.ReadKey方法,它与.NET提供的Console.ReadLine方法非常相似。不同之处在于新方法保留了大多数键功能(例如,上下箭头仍然可以滚动浏览之前的命令等)。它还返回一个KeyPressResult对象,而不是ConsoleKeyInfo实体。这个对象不仅告诉程序员哪个键被按下,还包含有关完整行和按键前后光标位置的信息。

KeyPressResult的所有属性:

  • ConsoleKeyInfo - 与Console.ReadKey()返回的结构体相同
  • Key - ConsoleKeyInfo中的ConsoleKey
  • KeyChar - ConsoleKeyInfo中的键字符
  • Modifiers - 输入时按下的修饰键(例如Shift,Ctrl)
  • LineBeforeKeyPress - 包含按键前行信息的LineState类
  • LineAfterKeyPress - 包含按键后行信息的LineState类

LineState的所有属性:

  • Line - 行
  • CursorPosition - 控制台光标位置
  • LineBeforeCursor - 光标前的行部分
  • LineAfterCursor - 光标后的行部分

使用ReadKey的示例:

KeyPressResult result = ConsoleExt.ReadKey(); switch (result.Key) { case ConsoleKey.Enter: // 使用result.LineBeforeKeyPress.Line获取与Console.ReadLine()相同的结果 break; case ConsoleKey.Tab: // Tab键被按下。在这里处理自动补全 break; }

注意:示例中提到应该使用LineBeforeKeyPress.Line来获取与Console.ReadLine()相同的结果。这是因为按下Enter键后,新行是空的。因此,LineAfterKeyPress.Line将是一个空字符串。

更多关于ConsoleUtils库的信息可以在中找到。

实现自动补全

尽管主要问题(检测按键(Tab)的同时保留其他控制台功能)已经通过上述实现解决,但如果文章中没有提供实现基本自动补全的方法,那么这篇文章就不完整。

发现有两种有用的自动补全实现方法:

  • 补充性自动补全- 会查看可用命令,并为用户提供所有命令共有的自动补全。
  • 循环自动补全 - 允许用户通过重复按Tab键循环浏览所有选项(在命令窗口中使用)。

实现补充性自动补全是最简单的,因为它不需要程序有状态。要实现这一点,可以使用AutoComplete.GetComplimentaryAutoComplete方法自动完成整个句子。

var commands = new List { "Exit", "The green ball.", "The red ball.", "The red block.", "The round ball." }; var running = true; while (running) { var result = ConsoleExt.ReadKey(); switch (result.Key) { case ConsoleKey.Enter: // ... break; case ConsoleKey.Tab: var autoCompletedLine = AutoComplete.GetComplimentaryAutoComplete( result.LineBeforeKeyPress.LineBeforeCursor, commands); ConsoleExt.SetLine(autoCompletedLine); break; } }

需要注意三点:

  • commands - 这是一个包含可能命令的String列表,用于自动补全
  • result.LineBeforeKeyPress用于因为实际上不想要Tab字符时查找自动补全。
  • LineBeforeCursor用于这个示例中的自动补全。这意味着如果用户使用左箭头键回到他的行中,只有光标前的部分用于自动补全。

没有必要使用拦截。在得到一个不想要的字符(Tab字符)的唯一情况下,已经使用ConsoleExt.SetLine来覆盖整行,包括Tab。

GIF显示了结果(下面有解释):

当用户在输入"t"后按下Tab键时,行自动补全为"The"。当用户然后输入"re"(使其成为"The re"),并按下Tab键时,行变为"The red b"。只有在提供"l"之后,系统才能自动补全为"The red block."。

示例还展示了用户如何决定回到行中输入一个"g"。因为代码使用了LineBeforeCursor,现在它只使用"The g"进行自动补全。将行变为"The green ball."。

要实现循环自动补全,提供了CyclingAutoComplete类。

var commands = new List { "Exit", "The green ball.", "The red ball.", "The red block.", "The round ball." }; var running = true; var cyclingAutoComplete = new CyclingAutoComplete(); while (running) { var result = ConsoleExt.ReadKey(); switch (result.Key) { case ConsoleKey.Enter: // ... break; case ConsoleKey.Tab: var autoCompletedLine = cyclingAutoComplete.AutoComplete( result.LineBeforeKeyPress.LineBeforeCursor, commands); ConsoleExt.SetLine(autoCompletedLine); break; } }

结果:

当用户在输入"T"后按下Tab键时,行自动补全为"The green ball."。当用户然后再次按下Tab键时,行变为"The red ball."。每次按下Tab键,行都会继续循环。

当用户然后将光标移动到"red"后面时,循环更改为只包含"The red ball."和"The red block."。再次因为使用了LineBeforeCursor。

在大多数控制台中,用户可以使用Shift+Tab组合键向后循环。CyclingAutoComplete类已经支持这一点,使用CyclingDirections参数可以轻松实现:

case ConsoleKey.Tab: var shiftPressed = (result.Modifiers & ConsoleModifiers.Shift) != 0; var cyclingDirection = shiftPressed ? CyclingDirections.Backward : CyclingDirections.Forward; var autoCompletedLine = cyclingAutoComplete.AutoComplete( result.LineBeforeKeyPress.LineBeforeCursor, commands, cyclingDirection); ConsoleExt.SetLine(autoCompletedLine); break;

所有示例都在附带的解决方案中。

感兴趣的要点

尽管本文引用了,因为认为它适用于大多数希望在控制台中实现自动补全的人,但本文中解释的算法可以很容易地在没有它的情况下使用。因此,两种实现都有它们在附带解决方案中的单独项目。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485