在最近的项目开发中,需要为一个简单的用户界面添加自动补全功能,以提高用户体验。考虑到时间成本,选择了Windows控制台应用程序作为开发平台。在实现了一些基础命令后,决定添加Tab键触发的自动补全功能。经过一番研究,发现实现这一功能并不像最初想象的那样简单。许多解决方案都建议使用Console.ReadKey()方法,但这种方法会禁用其他一些功能,比如使用上下箭头键浏览命令历史,而这些功能希望能够保留。
在进行了更多的搜索后,没有找到满意的解决方案,因此决定自己编写一个。希望通过分享解决方案,能够帮助社区中的其他开发者。尝试了多种不同的方法,甚至直接从操作系统拦截按键,但都没有达到预期的效果。最终,决定逆向工程,恢复使用ReadKey方法时丢失的控制台功能(同时支持自动补全)。由于本文主要关注自动补全问题,已经将关于Console.ReadKey问题的讨论放在了另一篇文章中,感兴趣的读者可以点击查看。
本文将介绍两种类型的自动补全实现方法,并解释如何将上述解决方案与这些实现方法结合起来,因为认为这是大多数人遇到问题的地方。当然,自动补全算法本身也可以在不同的情况下使用。因此,它被实现在一个单独的项目中。
ConsoleUtils库(也在附带的解决方案中)提供了自己的ConsoleExt.ReadKey方法,它与.NET提供的Console.ReadLine方法非常相似。不同之处在于新方法保留了大多数键功能(例如,上下箭头仍然可以滚动浏览之前的命令等)。它还返回一个KeyPressResult对象,而不是ConsoleKeyInfo实体。这个对象不仅告诉程序员哪个键被按下,还包含有关完整行和按键前后光标位置的信息。
KeyPressResult的所有属性:
LineState的所有属性:
使用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)的同时保留其他控制台功能)已经通过上述实现解决,但如果文章中没有提供实现基本自动补全的方法,那么这篇文章就不完整。
发现有两种有用的自动补全实现方法:
实现补充性自动补全是最简单的,因为它不需要程序有状态。要实现这一点,可以使用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;
}
}
需要注意三点:
没有必要使用拦截。在得到一个不想要的字符(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;
所有示例都在附带的解决方案中。
尽管本文引用了,因为认为它适用于大多数希望在控制台中实现自动补全的人,但本文中解释的算法可以很容易地在没有它的情况下使用。因此,两种实现都有它们在附带解决方案中的单独项目。