本文涉及的某些概念在前文中有所提及,如果有不太清楚的描述,建议参考前文。
本文属于控制台编程第三篇文章,前两篇链接如下。
一、内容概述
针对不同的控制需求和灵活度,控制台提供了高层和底层两种不同的输入输出接口。
高层输入输出接口简化了字符输入/输出的复杂性,但限制了对输入缓冲和屏幕输出的访问控制。
底层输入输出接口需要更多的编码及针对不同输入输出函数的选择,但具有更大的灵活性。
控制台提供的高层、底层输入输出接口不是互斥的,同一个应用程序可以按照实际需求同时调用高层、底层输入输出函数。
本文主要参考msdn上的Input and Output Methods一文,并结合我自己的理解,加以整理扩充。
本文主要涉及控制台输入、输出函数的功能,不同的输入输出模式对函数的影响等。
二、控制台模式
控制台模式按照操作划分,可分为输入模式、输出模式。输入模式主要与控制台的输入缓冲相关,主要影响输入操作及调用;输出模式与控制台的屏幕缓冲有关,主要影响输出操作及调用。输入模式可分为影响高层输入接口的模式、影响底层输入接口的模式。输出模式仅包括影响高层输出接口的模式,对底层输出函数无影响。
应用程序可以使用 GetConsoleMode函数和SetConsoleMode函数来获取、设置输入缓冲的输入模式或者屏幕缓冲的输出模式。对于用于多个屏幕缓冲的控制台程序,每个屏幕缓冲的输出模式可以不同。应用程序可以随时调用SetConsoleMode来修改控制台的输入输出模式。
1. 高层控制台模式(输入模式、输出模式)
控制台在创建时会默认启用以下输入模式:
- 行输入模式(ENABLE_LINE_INPUT)
- 自动处理输入模式(ENABLE_PROCESSED_INPUT)
- 输入回显模式(ENABLE_ECHO_INPUT)
自动启用以下输出模式:
- 自动处理输出模式(ENABLE_PROCESSED_OUTPUT)
- 自动换行模式(ENABLE_WRAP_AT_EOL_OUTPUT)
除了自动换行模式外,其他模式都是一起使用。也就是说要么全部启用行输入模式、自动处理输入模式、输入回显模式、自动处理输出模式,要么全部禁用。全部启用的情况下,应用程序本身需要做的处理很少,仅需要处理简单的字符I/O即可。全部禁用的情况,应用程序需要处理所有的输入输出事件。各个输入输出模式的详细含义可参考下表:
模式 | 描述 |
---|---|
ENABLE_PROCESSED_INPUT 自动处理输入模式 |
用于控制是否将系统控制键及系统事件放到输出缓冲中。 在自动换行输出模式下,退格backspace和换行会被系统自动处理。退格键引起光标向前移动一个字符,并且不影响光标当前所在位置的字符。回车键被作为行结束的标志。 |
ENABLE_LINE_INPUT 自动换行模式 |
用于控制台输入函数响应方式。 启用的情况下,在用户输出回车换行时会触发ReadFile和ReadConsole函数返回。 禁用的情况下,当输入缓冲中有字符时,ReadFile和ReadConsole函数会直接返回。 |
ENABLE_ECHO_INPUT 输入回显模式 |
用于控制用户输入字符是否回显。主要回显会把输入回显到活动屏幕缓冲。 输出回显模式只能在自动换行模式下使用。 活动屏幕缓冲的输出模式会影响回显字符效果。 |
ENABLE_PROCESSED_OUTPUT 自动处理输出模式 |
用于控制是否自动处理写入到屏幕缓冲中的ANSI控制字符(比如回退backspace、tab键、/a蜂鸣声、回车换行符、/t填充字符等)。 实际处理下:TAB键会引起光标移动到下一个tab位置(相邻两个tag间隔为8个字符)。/a转义字符会引起系统蜂鸣声。 |
ENABLE_WRAP_AT_EOL_OUTPUT 输出自动换行模式 |
用于控制输出到达行尾时是否自动切换到下一行的起始位置。 如果到达控制台窗口区域的下边界,则会引起窗口自动下移一行。 如果到达控制台屏幕缓冲的底部,则会引起屏幕缓冲向上滚动一行,屏幕缓冲会丢掉最上面的一行数据,窗口区域不变。 禁用的情况下,行尾的字符会被后续暑促胡字符覆盖掉。 |
2. 底层控制台输入模式
控制台输入缓冲中的事件类型取决于控制台的输入模式。
控制台的输入模式决定了控制台如何处理CTRL+C组合键。
由于底层控制台输出函数都会指定输出字符的属性和操作,所以控制台输出模式对底层输出函数无效。
控制台底层输入模式包括以下几种:
模式 | 描述 |
---|---|
ENABLE_MOUSE_INPUT 使能鼠标输入 |
用于控制是否在输入缓冲中记录鼠标事件。 默认情况下,控制台输入缓冲是记录鼠标事件,不记录窗口输入事件。 改变这些输入模式,仅会对后续输入操作有影响。输出缓冲中的未处理的鼠标事件和窗口事件时不会被刷新的。 是否使能对鼠标的显示没有影响。 |
ENABLE_WINDOW_INPUT 使能窗口输入 |
用于控制是否在输入缓冲中记录窗口缩放事件。 默认情况下,控制台输入缓冲是记录鼠标事件,不记录窗口输入事件。 改变这些输入模式,仅会对后续输入操作有影响。输出缓冲中的未处理的鼠标事件和窗口事件时不会被刷新的。 |
ENABLE_PROCESSED_INPUT 自动处理输入 |
用于控制高层控制台输入输出函数。 但在该模式启用的情况下,CTRL+C组合键不会记录在控制台的输入缓冲中,会直接调用已注册的控制回调函数,可参考Console Control Handlers。 |
三、高层控制台输入输出接口
高层控制台输出输出接口简化了从控制台读写字符流的方式。高层输入操作从控制台输入缓冲中读入字符,并保存在指定的缓冲区中。高层输出操作将指定缓冲区的字符输出到屏幕缓冲的光标所在位置,并按照实际输入字符数目移动光标。高层控制台I/O仅有四个函数:ReadFile和WriteFile 、ReadConsole和WriteConsole。这两组函数功能相似,主要有两个区别。
- ReadConsole和WriteConsole函数支持ANSI和UNICODE;而ReadFile和WriteFile函数仅支持ANSI。
- ReadFile和WriteFile函数支持文件、管道、串口通信等机制;而ReadConsole和WriteConsole函数仅支持控制台句柄,不能使用重定向之后的句柄。对于需要重定向输入输出的应用建议慎重选择。
使用高层控制台I/O时,会依赖于屏幕缓冲的属性来显示字符属性。也可使用控制台模式影响高层I/O处理逻辑。
使用高层控制台输入函数(ReadConsole、ReadFile)接收的键盘输入仅包括可符号化的输入(转化为ANSI或者UNICODE字符)、某些控制键,但不包括键盘上的功能键、方向键。由鼠标、窗口、获取焦点、菜单输入的事件均会被忽略。
如果行输入模式启用的情况下(默认设置),高层控制台输入函数在未遇到回车键Enter之前时不会返回;在行输入模式禁用的情况下,缓冲区中只要有数据就会返回。不管行输出模式是否启用,除非遇到给定输入缓冲区已满或者无输出字符的清下,输入函数会尽可能多的读入字符。未读取的字符会被缓冲,直到下次读取为止。函数返回值表示实际读入的字符数目。在回显输入模式启用的情况下,这些函数读入的字符都会回显到活动屏幕缓冲的当前光标位置。
高层控制台输出函数(WriteFile、WriteConsole)可以向活动或非活动屏幕缓冲写字符流。
使用(WriteFile、WriteConsole)或者ReadConsole、ReadFile回显的字符都会插入到屏幕缓冲光标的当前位置。光标移动位置受控制台输出模式控制。
四、底层控制台输入输出接口
控制台底层I/O提供更加强大的机制来访问输入缓冲和屏幕缓冲。应用程序可以使用控制台底层I/O来完成下面的几个功能:
- 接收=鼠标事件及缓冲区变化事件
- 接收键盘输入事件的扩展信息
- 向输入缓冲添加输入记录
- 从输入缓冲中读取输入记录,但保留处理之后的输入记录
- 读取输入缓冲待处理事件的数目
- 清空输入缓冲
- 从屏幕缓冲的指定光标位置读写ANSI或UNICODE字符
- 从屏幕缓冲的指定光标位置读写自定义字符串(前景色、背景色等)
- 在屏幕缓冲指定位置读写矩形区域的字符数据(字符、颜色属性)
- 在屏幕缓冲指定位置写连续多个重复的ANSI或UNICODE字符(字符、颜色属性)
1. 底层输入函数
控制台的底层输入函数的缓冲区中包含键盘、鼠标、缓冲变化、获得焦点及菜单消息等事件的一系列输入记录。底层输入函数可直接访问控制台的输入缓冲。控制台主要提供以下几个底层输入函数:
函数名 | 说明 |
ReadConsoleInput |
从输入缓冲中读取并移除输入记录。 本函数仅在输入缓冲中有输入记录时返回。系统会将所有的输入记录保存到函数调用给定的缓冲区中,在以下两种情况下函数会返回:输入缓冲中无新的输入记录、给定的缓冲区已满。未读入的输入记录会保留在输入缓冲中,等待下次输入一并处理。 函数最后一个参数返回实际读入的输入记录个数。 |
PeekConsoleInput |
从输入缓冲中读取输入记录,但不移除输入记录。 输入缓冲的所有输入记录都会复制到给定的缓冲区中。如果输入缓冲中无输入记录,本函数直接返回。 功能、参数、返回值,均和ReadConsoleInput类似。 |
GetNumberOfConsoleInputEvents | 获取输入缓冲中未读取的输入记录的个数。 |
WriteConsoleInput |
在输入缓冲尾部添加一个新的输入记录。 输入缓冲会按照实际需要自动扩充大小,以保存所有输入记录。 |
FlushConsoleInputBuffer | 清空输入缓冲中所有的输入记录。 |
控制台的输入缓冲的句柄可用于线程同步等待函数。如果输入缓冲中有未读取的输入记录时,该输入缓冲句柄就会变为“signaled”(标志)状态,可用于同步线程的等待处理,当输入缓冲清空之后,该句柄的标志状态会自动清除;若输入缓冲中无输入记录,使用该句柄调用线程同步等待函数是高效的,cpu占用很低。也就是说应用程序可以使用单独的线程等待并处理输入缓冲中的事件。
2. 底层输出函数
底层输出函数可以直接访问控制台屏幕缓冲的字符。主要包含以下几个函数:
函数 | 描述 |
---|---|
ReadConsoleOutputCharacter | 从屏幕缓冲中读入ANSI或UNICODE字符串。 |
WriteConsoleOutputCharacter | 向屏幕缓冲中写入ANSI或UNICODE字符串。 |
ReadConsoleOutputAttribute | 从屏幕缓冲中复制包含文本属性的字符串。 |
WriteConsoleOutputAttribute | 向屏幕缓冲中写入包含文本属性的字符串。 |
FillConsoleOutputCharacter | 向屏幕缓冲中连续写入多个ANSI或UNICODE字符。 |
FillConsoleOutputAttribute | 向屏幕缓冲中连续写入多个包含文本属性的字符。 |
ReadConsoleOutput | 从屏幕缓冲中读取指定区域的字符(包含文本属性)。 |
WriteConsoleOutput | 向屏幕缓冲中写入指定区域的字符(包含文本属性)。 |
其中包含文本属性的字符包括要显示的字符、前景色、背景色等。上面函数中读取指定区域的函数,把屏幕缓冲、源缓冲、目标缓冲当成是由CHAR_INFO结构构成的二维数组,区域以字符宽度为单位长度,区域使用SMALL_RECT结构体标记。
五、控制台其他属性
1. Code Page
Code Page指的是从256个字符码到不同字符的映射。不同的Code Page包含不同的特殊字符,这些字符可能被一种或多种语言共享。
和控制台相关的Code主要有两类:用于输入的和用于输出的。控制台使用输入Code Page将键盘输入映射成相关的字符,并使用输出Code Page将输出字符转化为控制台上可显示的数据。应用程序可使用SetConsoleCP、GetConsoleCP函数来设置并获取控制台的输入Code Page,使用 SetConsoleOutputCP、GetConsoleOutputCP函数来设置获取控制台的输出Code Page。
当前主机可用的Code Page保存在如下注册表中:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage。
2. 控制处理机制
当控制台收到CTRL+C、CTRL+BREAK、CTRL+CLOSE信号时会自动调用默认控制处理函数,通常该控制处理函数会直接动用ExitProcess退出当前进程。如果关心这些事件,可以使用SetConsoleCtrlHandler函数添加或者移除额外的HandlerRoutine回调函数,来完成用户自定义的额外处理。
当进程收到对应的控制信号,会按照注册事件相反的次序依次调用回调函数,直到其中某个控制回调函数返回TRUE(表示事件已经处理)为止。如果所有控制回调函数都未返回TRUE,则调用默认的控制处理函数。
CTRL+C、CTRL+BREAK信号
CTRL+C、CTRL+BREAK组合键会被系统特殊处理。当某个控制台在获取输入焦点之后,CTRL+C、CTRL+BREAK不会被当做键盘输入,而是作为终止信号(SIGINT、SIGBREAK),这个信号会传递给所有与控制关联的进程。
CTRL+C组合键可以通过以下方式修改控制台的响应处理:
- 使用函数SetConsoleMode禁用ENABLE_PROCESSED_INPUT输入模式,可以让系统把CTRL+C作为键盘输入而不是终止信号。
- 使用SetConsoleCtrlHandler 函数可让程序忽略CTRL+C的终止信号。
CTRL+CLOSE信号
用户关闭控制台时,所有与控制台关联的进程均会收到CTRL+CLOSE信号,应用程序可以在程序退出前释放相关资源。当应用程序收到CTRL+CLOSE信号,在执行完相应的资源释放后可以做如下三种处理:
- 调用ExitProcess退出当前进程
- 返回FALSE,让系统调用默认的控制处理函数,退出当前进程。
- 返回TRUE,屏蔽其他回调处理函数,并退出当前进程。