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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (5投票s)

2017年6月5日

CPOL

11分钟阅读

viewsIcon

17336

downloadIcon

172

在使用 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 类中轻松地自己修改它。

历史

05-06-2017 - 版本 1
© . All rights reserved.