C I/O:处理 Windows 控制台上的键盘输入

问题描述 投票:0回答:1

我正在创建一个交互式控制台应用程序,该应用程序应在 Linux 和 Windows 操作系统上运行。在程序中,有提示要求从

stdin
进行键盘输入。在每次提示时,用户只能输入特定的 ASCII 字符以及特定数量的字符。如果按下
<Escape>
<Enter>
键,提示也会有特殊行为;
<Enter>
提交要由内部程序状态评估的提示,
<Escape>
触发退出程序的命令,而不需要提交提示。目前,这些提示仅适用于 Linux,因为我不知道如何在 Windows 上实现它们。

输入提示当前通过两个函数实现:通用处理函数

prompt_user_input
和平台层函数
platform_console_read_key
,目前只有 Linux 实现可用,Windows 没有。

platform_console_read_key
的工作原理如下:

  1. 立即从
    stdin
    读取最多六个字节到缓冲区中(多个字节允许解析非 ASCII 键的 ANSI 转义码)。
  2. 如果读取的第一个字节不是转义序列指示符,则第一个字节将作为常规 ASCII 字符返回。
  3. 如果第一个字节转义序列指示符,则解析缓冲区的字节2-6。
    1. 如果缓冲区的其余部分为空,则该角色是独立的,因此执行步骤 2。
    2. 如果缓冲区的后半部分有东西,那么它实际上是一个转义序列。尝试解析它并将其作为“扩展键代码”返回(对于大于 255 的整数来说,这只是一个
      #define
      )。
    3. 如果缓冲区后半部分有东西,并且是函数未处理的转义码,则返回特殊值
      0
  4. 如果存在任何类型的严重 I/O 错误,则返回特殊值
    KEY_COUNT

我之所以这样读取字符,是因为我在

platform_console_read_key
中分别处理了
prompt_user_input
返回的每个关键代码,以确定程序应该如何进行。
prompt_user_input
的工作原理如下:

  1. 采用一个参数,该参数是可以输入的最大字符数。
  2. 通过
    platform_console_read_key
    无限循环读取密钥。
  3. 如果最后一个键与
    <Enter>
    <Escape>
    匹配,则从循环中中断。
  4. 否则,请使用
    filter_user_input
    谓词查看它是否是有效键。
    1. 如果有效,则将其添加到输入缓冲区,只要写入缓冲区的字符数不超过函数参数允许的数量。还将其打印到屏幕上(即“回显”功能)。
    2. 如果达到最大写入次数,或密钥无效,请继续(即转到步骤 2)。
我的问题是如何为 Windows 平台层实现

platform_console_read_key

。如何在 Windows 终端立即读取 6 个字节的键盘输入?另外,我如何确定这 6 个字节的格式,以便我可以将 Windows 键代码映射到函数的返回值,以便函数的行为与 Linux 版本的行为相匹配?

下面是运行的Linux代码,其中平台层包括

<termios.h>

<unistd.h>
。为了清晰和易于测试,我省略了一些不相关的代码,并添加了一些 
printf
 语句。另请注意,在 
prompt_user_input
 中存在对全局 
( *state ).in
 的引用,它是输入缓冲区;假设它对于提供的 
char_count
 来说始终足够大。

bool prompt_user_input ( const u8 char_count ) { printf ( "Type your response: " ); // Clear any previous user input. memset ( ( *state ).in , 0 , sizeof ( ( *state ).in ) ); KEY key; u8 indx = 0; for (;;) { key = platform_console_read_key (); if ( key == KEY_COUNT ) { // ERROR return false; } // Submit response? Y/N if ( key == KEY_ENTER ) { return true; } // Quit signal? Y/N if ( key == KEY_ESCAPE ) { //... return true; } // Handle backspace. if ( key == KEY_BACKSPACE ) { if ( !indx ) { continue; } indx -= 1; ( *state ).in[ indx ] = 0; } // Response too long? Y/N else if ( indx >= char_count ) { indx = char_count; continue; } // Invalid keycode? Y/N else if ( !filter_user_input ( key ) ) { continue; } // Write to input buffer. else { ( *state ).in[ indx ] = key; indx += 1; } // Render the character. printf ( "%s" , ( key == KEY_BACKSPACE ) ? "\b \b" : ( char[] ){ key , 0 } ); } } KEY platform_console_read_key ( void ) { KEY key = KEY_COUNT; // Configure terminal for non-canonical input. struct termios tty; struct termios tty_; tcgetattr ( STDIN_FILENO , &tty ); tty_ = tty; tty_.c_lflag &= ~( ICANON | ECHO ); tcsetattr ( STDIN_FILENO , TCSANOW , &tty_ ); fflush ( stdout ); // In case echo functionality desired. // Read the key from the input stream. char in[ 6 ]; // Reserve up to six bytes to handle special keys. platform_memory_clear ( in , sizeof ( in ) ); i32 result = read ( STDIN_FILENO , in , sizeof ( in ) ); // I/O error. if ( result < 0 ) { key = KEY_COUNT; goto platform_console_read_key_end; } // End of transmission (I/O error). if ( in[ 0 ] == 4 || in[ 1 ] == 4 || in[ 2 ] == 4 || in[ 3 ] == 4 || in[ 4 ] == 4 || in[ 5 ] == 4 ) { key = KEY_COUNT; goto platform_console_read_key_end; } // ANSI escape sequence. if ( *in == '\033' ) { // Standalone keycode. if ( !in[ 1 ] ) { key = KEY_ESCAPE; goto platform_console_read_key_end; } // Composite keycode. else { if ( in[ 1 ] == '[' ) { // ... } else { key = 0; } goto platform_console_read_key_end; } } // Standalone ASCII character. else { // Backspace key is mapped to ASCII 'delete' (for some reason). key = ( *in == KEY_DELETE ) ? KEY_BACKSPACE : *in; goto platform_console_read_key_end; } // Reset terminal to canonical input mode. platform_console_read_key_end: tcsetattr ( STDIN_FILENO , TCSANOW , &tty ); fflush ( stdout ); // In case echo functionality desired. return key; }

编辑

已解决,感谢已接受的答案。这是

platform_read_console_key

 的最小 Windows 实现,它保持 
prompt_user_input
 的行为相同。它使用 
<conio.h>
 标头。

KEY platform_console_read_key ( void ) { i32 getch; getch = _getch (); // Error. if ( getch < 0 ) { return KEY_COUNT; } // Standalone ASCII keycode. if ( getch != 0 && getch != 224 && getch < 0x100 ) { return newline ( getch ) ? KEY_ENTER : ( getch == '\033' ) ? KEY_ESCAPE : getch ; } // Extended keycode. getch = _getch (); switch ( getch ) { default: return 0; // Unknown keycode. } }
    
c windows io cross-platform keyboard-input
1个回答
1
投票
这是用户

Weather Vane

暗示的一个简单的解决方案,我已经使用了多年。运行此代码还可以让您发现并定义更多的 F 键——您知道,
F1
F2
,还有 
arrow keys
,等等。

注意:您希望将 256 添加到

_getch()

 作为 
GetKey()
 中的扩展键。这会将扩展键值置于公共键的范围之外,例如 
A thru Z
....注意 
F10
 本身只能是值 
68
——并且 
thatSHIFT + 'D'
 的键值匹配。不好,看到了吗?

此代码也适用于

CTRL

 + 组合键。也适用于限制内的 
ALT
 组合键。 
ALT
 组合键有点复杂,因为 
ALT+TAB key
 例如可以在 MS Windows 中的应用程序之间切换。因此,当您的应用程序开始与操作系统竞争击键操作时,行为可能会变得不确定。而且 - 根据经验 - 您可以添加 100 行代码来解决这些问题,但只会名义上提高性能和范围。

代码在 Visual Studio 中编译,并在发布到 SO 之前在 Win10 上进行测试。

#include <stdio.h> #include <conio.h> #define KEY_ESCAPE 27 #define KEY_BACKSPACE 8 #define KEY_ENTER 13 #define KEY_F10 324 /*--------------------------------------------- GetKey() Thanks John Wagner *---------------------------------------------*/ int GetKey(void) { int c = _getch(); if(c ==0 || c == 224) c = 256 + _getch(); /* If extended key (like F10), add 256. */ return c; } int main () { int key; printf("Hit a key -- ESC key quits\n"); do{ key = GetKey(); switch(key) { case KEY_ENTER: printf("ENTER key detected.\n"); break; case KEY_BACKSPACE: printf("BACKSPACE key detected.\n"); break; case KEY_F10: printf("MENU(F10) key detected.\n"); break; case 'y': case 'Y': printf("User says yes!\n"); break; case 'n': case 'N': printf("User says No!\n"); break; default: printf("Key value: %d\n", key); break; } }while (key != KEY_ESCAPE); return 0; }
    
© www.soinside.com 2019 - 2024. All rights reserved.