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






4.92/5 (21投票s)
在 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
以允许更多的单元测试,并实现了这些单元测试。