使用 Console.ReadKey() 时保留(并修改)关键函数






4.20/5 (5投票s)
在使用 Console.ReadKey() 时保留默认函数并自定义控制台按键操作
引言
ReadKeyConsole
是一个解决方案,它提供了一种在
使用Console.ReadKey()
时保留按键函数、覆盖这些函数以及其他一些有用的控制台功能的方法。
背景
在开发控制台应用程序时,我希望在按下Tab 键时实现自定义操作。
经过一些研究,发现这并不像我最初想象的那么简单。许多针对此问题的解决方案都提到了使用 Console.ReadKey
方法。使用此方法的问题在于,它会禁用许多其他功能,例如使用向上/向下箭头键来滚动浏览输入的命令历史记录。而这些功能是我想要保留的。
下面的示例说明了这一点
bool running = true; string line = string.Empty; while (running) { var key = Console.ReadKey(); if (key.Key == ConsoleKey.Enter) { if (line.ToLower() == "exit") running = false; Console.WriteLine(line); line = string.Empty; } else line += key.KeyChar; }
注意:在此示例中,我已经花时间修复了 Enter 键的功能,因为默认行为只会将光标返回到其原始位置,而不会开始新的一行。
当我在第二行按下向上键时,应用程序会插入一个空格,而不是我之前输入的命令(“Test”)。然后,当我按 Escape(两次)清除该行时,它只会显示一个“?”,这意味着它正在写入字符而不是执行预期操作。
显然,当我想要为单个按键实现功能时,我不希望失去控制台提供的其他所有功能。
经过更多谷歌搜索,我没有找到解决此问题的方法,于是我决定自己编写。我想与社区分享我的解决方案。
我尝试了许多不同的方法,甚至直接从操作系统拦截按键。但都没有达到预期的效果。
最终,我决定逆向工程在使用 ReadKey
方法时丢失的控制台函数。我不确定这样做的好处是否大于成本。但这确实是一个不错的项目,也是一次宝贵的学习经历。
默认控制台按键函数
在我开始之前,我必须弄清楚控制台应用程序支持哪些默认函数。同样,Google 提供的帮助比我希望的要少。我找到了不少解释各种函数的页面,但没有一个完整的。
通过结合这些页面的信息,并通过自己实际尝试,我决定支持以下函数:
不按下 Ctrl 键的按键组合
默认值 | 插入字符 |
F1 | 自动完成先前输入条目中的单个字符 |
F3 | 使用先前输入的条目自动完成该行的其余部分 |
F5 | 循环向上浏览先前输入的条目 |
F6 | 什么都不做 |
F8 | 使用先前输入的条目自动完成 |
转义 | 清除行 |
删除 | 删除光标后的字符 |
退格键 | 删除光标前的字符 |
左箭头 | 光标左移 |
右箭头 | 光标右移 |
Home | 将光标移动到行首 |
End | 将光标移动到行尾 |
上箭头 | 循环向上浏览先前输入的条目 |
Page Up | 循环到第一个条目 |
下箭头 | 循环向下浏览先前输入的条目 |
Page Down | 循环到最后一个条目 |
按下 Ctrl 键的按键组合
默认值 | 什么都不做 |
H | 删除光标前的字符 |
退格键 | 删除光标前所有字符 |
左箭头 | 将光标移动到行首 |
右箭头 | 将光标移动到行尾 |
Home | 删除光标前所有字符 |
End | 删除光标后所有字符 |
注意:如前所述,此列表并非详尽无遗。有些函数的好处与其成本相比太小(例如,F2 和 F4 打开菜单以启用功能),而有些函数我可能只是错过了。但正如您将在下一章中看到的:此解决方案将使添加您可能发现缺少的任何功能变得非常容易。甚至可以覆盖这些表中显示的默认功能。
使用代码
该库(已附加)提供了自己的 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
的示例
bool running = true;
while (running)
{
var result = ConsoleExt.ReadKey();
if (result.Key == ConsoleKey.Enter && result.LineBeforeKeyPress.Line.ToLower() == "exit")
running = false;
}
通过使用 ConsoleExt.ReadKey,将保留默认函数。下面的 GIF 显示了与背景章节中示例完全相同的按键组合(向上键后跟Escape 键)。现在具有预期的行为。
注意:该示例指出应使用 LineBeforeKeyPress.Line
来获得与 Console.Readline
相同的结果。这是因为按下Enter 键后,新行是空的。因此,LineAfterKeyPress.Line
将是一个空的字符串
。
在某些情况下,拦截按下的按键可能很有用。 .NET 和此库都通过在 ReadKey
方法中提供 intercept
参数来支持这一点。
对标准控制台功能的有用补充是,程序员现在可以使用 SimulateKeyPress
方法模拟按键。这使得在检查后使用被拦截的按键,甚至提供用户未输入的按键成为可能。
在下面的示例中,出于某种原因,我们想拦截空格键。使用 intercept 和 SimulateKeyPress
功能可以轻松实现这一点。
KeyPressResult result = ConsoleExt.ReadKey(true);
switch (result.Key)
{
case ConsoleKey.Enter:
ConsoleExt.SimulateKeyPress(result.ConsoleKeyInfo);
// Extra logic for return. E.g. check for a command.
break;
case ConsoleKey.Spacebar:
// Handle space, without it being entered in console,
// as we intercept and do not simulate.
break;
default:
// In all other cases, call SimulateKeyPress.
ConsoleExt.SimulateKeyPress(result.ConsoleKeyInfo);
break;
}
}
注意:使用 intercept 参数时,LineAfterKeyPressed
将包含与 LineBeforeKeyPressed
相同的信息,因为控制台中没有任何更改。
ConsoleExt
上所有可用的 `public` 属性/方法
LineState
- 返回当前行/光标状态的属性。
ReadLine
- 行为与Console.Readline
相同的函数。但是,为了使某些其他函数正常工作,必须使用此函数而不是Console.Readline
。这些函数用星号(*)标记。ReadKey
- 阻塞直到用户按下某个键的函数。可以从控制台拦截按键。SimulateKeyPress
- 模拟用户按键的函数。SetLine
- 设置当前行的函数。将覆盖用户已键入的任何字符。*ClearLine
- 将清除当前行。用户已键入的所有字符都将被删除。*StartNewLine
- 将开始新的一行。就像按下 Enter 键一样。*PrependLine
- 将在用户当前使用的行上方添加一行。* 这对于多线程控制台应用程序可能很有用。(我将很快撰写一篇关于该主题的文章)- 自定义控制台函数 - 这些将在下一章中讨论。
* 如果使用 Console.ReadLine/ReadKey
而不是 ConsoleExt.ReadLine/ReadKey
,则这些函数将不起作用。
自定义控制台操作
自定义控制台操作可用于修改默认的控制台按键行为。例如,默认情况下附加到Home 键的控制台操作会将光标设置到行的第一个位置。ConsoleExt 提供了覆盖此默认行为或完全创建新行为的方法。
在分配自定义控制台操作时,修饰键(Shift、Ctrl 和 Alt)组合会与操作键组合。对于单个按键,所有不同的修饰键组合都可以具有不同的控制台操作。这使库尽可能灵活。
例如:Ctrl + A 可以表示全选,而 Ctrl + Alt + A 可以具有完全不同的功能。
以下是这些组合的列表
- 无修饰键
- Control
- Shift
- Alt
- Shift + Alt
- Ctrl + Shift
- Ctrl + Alt
- Ctrl + Shift + Alt
为了方便使用,所有与控制台操作相关的函数都有几个变体:
- 一个影响所有修饰键组合(包括无修饰键)的控制台操作。
- 一个影响不带 Ctrl 键的所有组合(无修饰键、Shift、Alt 和 Shift + Alt)的控制台操作。
- 一个影响带 Ctrl 键的所有组合(Ctrl、Ctrl + Shift、Ctrl + Alt 和 Ctrl + Shift + Alt)的控制台操作。
- 以及一个影响单个修饰键组合操作的函数。
每个修饰键组合都有一个默认操作。这是如果在特定按键上没有分配操作时将使用的操作。如默认控制台按键功能章节中每个表的 first 行所示,非 Ctrl 组合的默认是向控制台写入字符,而 Ctrl 组合的默认是不执行任何操作。该库还允许覆盖默认设置。
ConsoleExt
支持控制台操作的以下功能:
- 设置控制台操作
- 删除控制台操作
- 设置默认控制台操作
- 重置所有控制台行为
将这些函数与上面提到的修饰键变体相结合,可得到 ConsoleExt
实现的控制台操作函数列表:
SetConsoleAction
- 将控制台操作分配给一个特定的按键 + 修饰键组合。SetConsoleActionForNonCtrlModifierCombinations
- 将控制台操作分配给一个特定的按键 + 所有非 Ctrl 修饰键组合。SetConsoleActionForCtrlModifierCombinations
- 将控制台操作分配给一个特定的按键 + 所有 Ctrl 修饰键组合。SetConsoleActionForAllModifierCombinations
- 将控制台操作分配给一个特定的按键 + 所有修饰键组合。RemoveConsoleAction
- 从一个特定的按键 + 修饰键组合中删除控制台操作。RemoveConsoleActionForNonCtrlModifierCombinations
- 从一个特定的按键 + 所有非 Ctrl 修饰键组合中删除控制台操作。RemoveConsoleActionForCtrlModifierCombinations
- 从一个特定的按键 + 所有 Ctrl 修饰键组合中删除控制台操作。RemoveConsoleActionForAllModifierCombinations
- 从一个特定的按键 + 所有修饰键组合中删除控制台操作。SetDefaultConsoleAction
- 为特定的修饰键设置默认控制台操作。SetDefaultConsoleActionForNonCtrlModifierCombinations
- 为所有非 Ctrl 修饰键组合设置默认控制台操作。SetDefaultConsoleActionForCtrlModifierCombinations
- 为所有 Ctrl 修饰键组合设置默认控制台操作。SetDefaultConsoleActionForAllModifierCombinations
- 为所有修饰键组合设置默认控制台操作。ResetConsoleBehaviour
- 重置所有已分配的控制台操作。
创建自定义控制台操作非常简单。它要求您的类实现 IConsoleAction
,然后可以使用上述方法之一将其传递给 ConsoleExt。
在下面的示例中,我们希望 Tab 键重复行中的前一个字符。
首先,我们实现控制台操作类:
public class RepeatPreviousCharacterAction : IConsoleAction
{
public void Execute(IConsole console, ConsoleKeyInfo consoleKeyInfo)
{
if (console.CursorPosition <= 0)
return;
var previousChar = console.CurrentLine[console.CursorPosition - 1].ToString();
console.CurrentLine = console.CurrentLine.Insert(console.CursorPosition, previousChar);
console.CursorPosition += 1;
}
}
如果光标位于第一个位置,则操作无效。在其他情况下,它将获取光标前面的字符,并在光标位置插入它。之后,我们将光标向右移动一个位置。
现在,我们要做的就是将此操作分配给正确的按键,在这种情况下是Tab。
ConsoleExt.SetConsoleActionForNonCtrlModifierCombinations(
ConsoleKey.Tab, new RepeatPreviousCharacterAction());
就是这样。当应用程序用户按下 Tab 键而不按住 Ctrl 键时,前一个字符将再次插入。
注意:因为我们使用了方法的 Non-Control 变体,所以控制台操作会自动分配给Tab、Shift + Tab、Alt + Tab 和 Shift + Alt + Tab。
仅为Shift + Alt + Tab 分配控制台操作的示例将是:
ConsoleExt.SetConsoleAction( ConsoleModifiers.Shift | ConsoleModifiers.Alt, ConsoleKey.Tab, new RepeatPreviousCharacterAction());
设计选择
本章将解释我在创建库时做出的一些设计选择。为了便于理解,在阅读本文时最好打开源代码。
上一行缓冲区
默认按键函数的一个重要部分是使用先前输入的条目。例如,使用向上和向下键来滚动浏览这些先前输入的条目,以及使用功能键通过这些条目进行自动完成。
由于此功能包含在所有函数中使用的状态(条目和条目索引),因此我决定为此创建一个单独的类:PreviousLineBuffer
。
使用单独的类也有助于轻松进行单元测试,并使 ConsoleExt
类更简洁。
控制台操作
为了使测试更容易,并使 IConsoleAction
更通用,我希望将控制台传递给接口,而不是让所有实现都直接使用 ConsoleExt
。为了实现这一点,使用了 IConsole
。
由于 ConsoleExt
是静态的,无法直接实现接口,因此创建了一个包装类(ConsoleExtInstance
),以便 ConsoleExt
能够将自身传递到控制台操作类中。
使用 IConsole
还可以更轻松地进行单元测试,因为程序员现在可以创建一个存根类,而不必使用实际的 ConsoleExt
。附加的解决方案也包含这方面的示例(ConsoleStub
)。
关注点
如默认控制台按键功能章节所述,我尚未实现所有功能。如果您有任何好的建议,请随时给我留言,我可能会将其添加到项目中。
此版本的库尚不支持 Tab 等键。目前,该库设计为屏幕字符与行字符一一对应,而 Tab 键则表示一个行字符对应多个屏幕字符。
给纯粹主义者的一个小免责声明:是的,在诸如 CycleDownActionTests
之类的测试中,最好模拟(甚至存根)PreviousLineBuffer
,但我不想为这样一个孤立的库使用模拟框架,或以其他方式使事情复杂化。所以是的,CycleDownActionTests
(以及类似的测试)也测试了 PreviousLineBufferTests
中已经测试过的一些代码。
为了使 ReadLine
等函数可预测,我选择始终让Enter 键成为新行键,而不为此使用控制台操作。当然,您可以在 ConsoleExt
类中轻松地自己修改它。