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

在 Windows 控制台应用程序中使用自动完成

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (21投票s)

2017年4月17日

CPOL

6分钟阅读

viewsIcon

36585

downloadIcon

769

在 Windows 控制台应用程序中使用自动完成,同时在使用 Console.Readkey() 时保持关键功能不丢失。

引言

AutoCompleteConsole 是一个解决方案,其中包含一些用于为 Windows 控制台应用程序实现自动完成功能的工具,同时在使用 Console.Readkey() 时不会丢失关键功能。

背景

最近,我正在做一个需要简单用户界面的项目。为了节省时间,我决定使用 Windows 控制台应用程序。在实现了一些简单的命令后,我觉得允许用户使用自动完成功能(使用 Tab 键)会很棒。

经过一些研究,发现这并不像我最初想的那么简单。许多针对此问题的解决方案都包括使用 Console.ReadKey 方法。使用此方法的问题在于,它会禁用许多其他功能,例如使用向上/向下箭头键滚动浏览键入命令的历史记录。而这些是我想要保留的功能。

经过更多的谷歌搜索,我没有找到解决这个问题的方法,于是我决定自己编写。我想与社区分享我的解决方案。

我尝试了许多不同的方法,甚至直接从操作系统拦截按键。但都没有达到预期的效果。

最终,我决定逆向工程在使用 ReadKey 方法时会丢失的控制台功能(除了支持自动完成之外)。由于我希望本文侧重于问题的自动完成部分,我将把 Console.ReadKey 问题专门放在另一篇文章中。该文章可以在这里找到。

本文将解释如何实现两种类型自动完成中的任何一种。它还将解释如何将前面提到的解决方案与这些实现结合起来,因为我怀疑这正是大多数人遇到麻烦的地方。当然,自动完成算法本身也可以在不同情况下使用。因此,它在解决方案中被实现为一个单独的项目。

Using the Code

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

KeyPressResult 上的所有属性

  • ConsoleKeyInfo - 与 Console.ReadKey() 返回的相同 struct
  • 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:
        // Use result.LineBeforeKeyPress.Line to get the same result as Console.Readline()
        break;
    case ConsoleKey.Tab:
        // Tab was pressed. Handle autocomplete here
        break;
}

注意:示例中提到应该使用 LineBeforeKeyPress.Line 来获得与 Console.Readline 相同的结果。这是因为按下 Enter 键后,换行符为空。所以 LineAfterKeyPress.Line 将是一个空的 string

有关 ConsoleUtils 库的更多信息,请参阅本文

实现自动完成

尽管大多数人实际遇到的主要问题(在保留其他控制台功能的同时检测按键(Tab 键))已通过上述实现得到解决。但如果文章没有提供实现基本自动完成的方法,那就不完整了。

我发现实现自动完成有两种有效的方法:

  • 互补自动完成 - 将查看可用命令,并为用户提供所有命令共享的自动完成。
  • 循环自动完成 - 允许用户通过反复按下 Tab 键来循环浏览所有选项(在命令窗口中使用)。

互补自动完成

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

示例

var commands = new List<string>
{
    "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<string>
{
    "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;

所有示例都包含在附加的解决方案中。

关注点

尽管本文引用了本文,因为我认为它适用于大多数希望在控制台中实现自动完成的人,但本文中解释的算法也可以轻松地独立使用。因此,这两种实现都在附加的解决方案中拥有其单独的项目。

我没有实现所有默认的控制台功能。如果您有任何好的补充,请随时给我留言,我可能会将它们添加到项目中。

所有可用于自动完成的方法都有一个可选的 ignoreCase 参数。默认为 true

历史

2017 年 4 月 16 日 - 版本 1
2017 年 4 月 17 日 - 版本 2
  • 扩展了 InputResult 以包含光标位置和修饰键
  • 向库添加了 CyclingDirections ,允许用户双向循环
  • 简化了示例
  • 上传了 GIF 以供说明
2017 年 4 月 27 日 - 版本 3
  • 使整个库更加通用。以前 ConsoleExt 了解自动完成的逻辑,现在已完全分离。它现在也是一个独立的库。
  • 向 ConsoleExt 添加了 ReadLine 、 SimulateKeyPress 、 SetLine 、 ClearLine 、 StartNewLine 和 PrependLine
  • 将 InputResult 更改为 KeyPressResult 并提取了 LineState 类
  • 使 ConsoleExt 线程安全
2017 年 4 月 29 日 - 版本 3.1
  • 防止 Tab 键的正常使用导致未定义行为。
  • 修复了在预置多行字符串时 PrependLine 中的错误。
2017 年 6 月 11 日 - 版本 4.0
  • 更清晰地解释了 ConsoleExt.ReadKey 与自动完成算法之间的分离。
  • 重构了 ConsoleExt 以允许更多的单元测试,并实现了这些单元测试。
© . All rights reserved.