控制台基础概念

一、引言

控制台程序(Console Application)相信是很多人接触编程的第一个界面,比如C/C++入门的Hello World程序,黑色的字符界面窗口,windows最经典的控制台程序是cmd(命令行窗口),如下图。

MSDN上对控制台描述如下:用于管理基于字符的应用程序(不提供GUI界面)的输入输出。

本文主要参考MSDN上关于Console的资料,并加以整理扩充。原始资料可参考:http://msdn.microsoft.com/en-us/library/windows/desktop/ms682010(v=vs.85).aspx

本系列文章会回答如下问题:

  • 控制台程序是有哪些元素构成的?
  • 通过什么方式控制控制台显示字符和接收用户输出?
  • 基于字符的编辑器是如何实现的?
  • 控制台程序的窗口、句柄如何获取?
  • 控制台程序如何响应窗口大小改变、关闭、退出等操作?

本文首先回答第一个问题,控制台基础概念,控制台的构成、创建及销毁。

二、控制台基本构成

控制台(Console)是为字符模式应用程序提供输入和输出的。由于控制台程序无界面,独立于特定的硬件平台及操作系统之上,使得我们很容易扩展或者移植现有的控制台程序。

控制台有一个输入缓冲、至少一个屏幕缓冲(输出缓冲)构成。输入缓冲是有一个输入记录的队列,其中输入记录包含输入事件的相关信息,包括键盘按下和弹起事件、鼠标事件(鼠标移动、左右键按下弹起)以及影响可见字符宽度的事件。输出缓冲可以认为是一个字符信息的二维数组,每个元素包括实际在控制台显示的字符和颜色。

多个进程可共享一个控制台。

控制台函数提供两个级别的访问机制(后续会介绍高层访问控制和底层访问控制的划分)。

高层访问控制:应用程序可通过标准输入STDIN读取控制台的输入缓冲中事件;可使用标准输出STDOUT、标准错误STDERR向控制台的输出缓冲写数据,用于显示字符;控制台支持重定向标准输入、标准输出、标准错误。

底层访问控制:支持应用程序直接接收并处理键盘输入、鼠标操作以及其他用户交互的控制;有更大自由度控制字符屏幕输出的方式。

1. 控制台的输入缓冲 Input Buffer

控制台缓冲中保存了所有输入事件的队列。当控制台拥有键盘输入的焦点时,控制台会将键盘按键、鼠标移动、鼠标按键等事件格式化保存到输入事件队列中。

控制台的输入缓冲可以通过高层输入函数访问,也可以通过底层输入函数访问。高层输入函数会自动过滤并处理输入缓冲中的事件,仅返回处理之后的输入字符流。底层输入函数提供读取和写入输入缓冲的机制。

输入事件使用INPUT_RECORD结构体表示:

typedef struct _INPUT_RECORD {
  WORD  EventType;
  union {
    KEY_EVENT_RECORD          KeyEvent;
    MOUSE_EVENT_RECORD        MouseEvent;
    WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
    MENU_EVENT_RECORD         MenuEvent;
    FOCUS_EVENT_RECORD        FocusEvent;
  } Event;
} INPUT_RECORD;

其中EventType表示事件类型(事件类型包括:鼠标、键盘、窗口缩放、获得焦点、菜单事件),其他数据表示事件对应的详细信息。

获得焦点和菜单事件是给操作系统使用的,应用程序在读取时必须直接忽略。

键盘事件

 键盘事件是由键盘上任意一个键被按下或弹起触发的(包括控制键)。不过也有例外:ALT键单独按下或者弹起对操作系统有特殊意义,在控制台输入缓冲区不会记录;当输入缓冲正在被处理的情况下,CTRL+C组合键也会被忽略掉。

对于键盘事件INPUT_RECORD结构体中的KEY_EVENT_RECORD结构定义如下:

typedef struct _KEY_EVENT_RECORD {
  BOOL  bKeyDown; // 键位是被按下还是释放
  WORD  wRepeatCount; // 重复按键次数,大于1表示键位被按下未释放
  WORD  wVirtualKeyCode; // 设备无关的虚拟键码
  WORD  wVirtualScanCode; // 设备相关的虚拟扫描码
  union { // 转换后的ASCII码或者UNICODE码
    WCHAR UnicodeChar; 
    CHAR  AsciiChar;
  } uChar;
  DWORD dwControlKeyState; // 控制键状态,ctrl、shift、alt、caps Lock等
} KEY_EVENT_RECORD;

鼠标事件

 鼠标事件是由于鼠标移动、鼠标键位按下或者释放触发的。鼠标事件通常不会记录到控制台的输入缓冲,如果需要启用控制台输入缓冲接收鼠标事件需要做如下设置:

控制台输入模式设为 ENABLE_MOUSE_INPUT (默认模式,后续会有介绍)

控制台用于输入焦点

鼠标在控制台窗口范围内

对于鼠标事件,

typedef struct _MOUSE_EVENT_RECORD {
  COORD dwMousePosition; // 基于字符的屏幕坐标位置
  DWORD dwButtonState; // 鼠标按键标志
  DWORD dwControlKeyState; // 控制键状态标志
  DWORD dwEventFlags; // 鼠标事件标志,单击、双击、按下、弹起、鼠标移动、滚轮事件
} MOUSE_EVENT_RECORD;

缓冲区缩放事件

 用户调整控制台大小会引起活动屏幕缓冲大小的改变,这样会触发缓冲区缩放事件。缓冲区屏幕缩放事件必须在缓冲区输入模式为ENABLE_WINDOW_INPUT 时有效。

对于缓冲区缩放事件,INPUT_RECORD结构体中的 WINDOW_BUFFER_SIZE_RECORD 结构定义如下:

typedef struct _WINDOW_BUFFER_SIZE_RECORD {
  COORD dwSize;
} WINDOW_BUFFER_SIZE_RECORD;

其中包含以字符为单位的行数和列数。

若缩小屏幕缓冲区大小,缓冲区外的数据会丢失。

需要注意的是调用SetConsoleScreenBufferSize函数不会触发缓冲区缩放事件。

2. 控制台的屏幕缓冲 Screen Buffer

 屏幕缓冲可认为一个矩形区域,每个元素包含显示字符信息和颜色信息。一个控制台可以有多个屏幕缓冲。活动屏幕缓冲指的是当前显示于屏幕上的屏幕缓冲。

系统在创建控制台时会自动创建一个屏幕缓冲,可以使用CreateFile函数获得当前活动屏幕缓冲的句柄。可以通过CreateConsoleScreenBuffer函数为控制台创建新的屏幕缓冲。可以通过SetConsoleActiveScreenBuffer 函数将屏幕缓冲设置为活动屏幕缓冲,达到显示的目的。不管是什么类型的屏幕缓冲(活动或者非活动)都可以直接读写。

我们把屏幕缓冲视为矩形区域,每个元素的信息是由CHAR_INFO 结构体定义的,如下:

typedef struct _CHAR_INFO {
  union { // 显示字符
    WCHAR UnicodeChar;
    CHAR  AsciiChar;
  } Char;
  WORD  Attributes; // 字符前景色、背景色、样式
} CHAR_INFO, *PCHAR_INFO;

从上面定义可以看出,屏幕缓冲可以通过修改矩形区域内的任意元素的配置信息来改变控制台显示效果。屏幕缓冲显示效果与以下几个属性相关:

  • 屏幕缓冲大小,行数x列数,以字符为单位
  • 字符属性(前景色、背景色)
  • 窗口大小及位置
  • 光标位置、外观及是否可见
  • 输出模式(ENABLE_PROCESSED_OUTPUT、ENABLE_WRAP_AT_EOL_OUTPUT参考High-Level Console Modes

屏幕缓冲在创建时是空的,其光标位于左上角屏幕缓冲区原点(0,0)的位置,窗口位于屏幕缓冲原点位置。屏幕缓冲的大小、窗口大小以及显示字符属性都使用系统默认设置。

应用程序在修改屏幕缓冲样式之前,必须保存系统的屏幕缓冲样式,并在程序退出时恢复系统默认配置。

光标外观及位置

 屏幕缓冲的光标可以显示,也可以隐藏。其外观可变,变化范围从占用一个字符区域到缩小到水平线上(cmd默认光标样式)。可以使用函数GetConsoleCursorInfoSetConsoleCursorInfo来获取、设置光标外观及显示或隐藏属性。

使用高层I/O函数写入的字符被保存在屏幕缓冲的光标当前位置,并会将光标移动到下一个字符位置。可以使用函数GetConsoleScreenBufferInfo、 SetConsoleCursorPosition来获取和设置屏幕缓冲的光标位置。需要注意的是光标移动之后,光标所在位置的字符会被下一次输入或者输出覆盖掉。

每个屏幕缓冲的光标位置、外观是可以独立设置的,相互不会影响。

字符属性

 字符属性用于设置显示字符前景色、背景色和双字节字符集(DBCS)属性。默认的前景色和背景色都是黑色。

可使用函数GetConsoleScreenBufferInfo获取屏幕缓冲当前的字符属性,函数SetConsoleTextAttribute设置字符属性。修改屏幕缓冲的字符属性并不会影响已经显示的字符。另外,修改屏幕缓冲的字符属性,对底层访问机制中的输入输出函数无效,因为这些函数在使用时都设置了字符属性。

字体属性

控制台当前显示的字体属性可以通过函数GetCurrentConsoleFont获取。返回字体属性结构体定义如下:

typedef struct _CONSOLE_FONT_INFO {
  DWORD nFont; // 系统默认字体索引
  COORD dwFontSize; //字符大小,宽x高
} CONSOLE_FONT_INFO, *PCONSOLE_FONT_INFO;

 3. 屏幕缓冲大小及窗口大小

屏幕缓冲大小是一个矩形区域,其宽度是以实际显示字符个数为单位的列数,其高度是以实际显示字符个数为单位的行数。屏幕缓冲大小是没有上限的,只要内存足够,屏幕缓冲可以任意大。windows下屏幕缓冲坐标系与实际的GUI窗口坐标系一直,坐标原点位于左上角(0,0)的位置,向右方向为x轴正向,向下的方向为y轴正向,以实际可显示字符个数为单位。

屏幕缓冲的窗口的概念是指控制台程序中可见字符区域。屏幕缓冲窗口包含两个主要的信息,起始位置及窗口大小,实际表示使用类似RECT的定义,给出左上角和右下角的坐标。屏幕缓冲窗口大小是不能超过屏幕缓冲大小的,也不能超过屏幕最大可显示字符数目。

使用GetConsoleScreenBufferInfo函数可以获得屏幕缓冲大小及窗口大小的属性,该函数返回CONSOLE_SCREEN_BUFFER_INFO结构体,定义如下:

typedef struct _CONSOLE_SCREEN_BUFFER_INFO {
  COORD      dwSize; // 以字符为单位的屏幕缓冲宽x高
  COORD      dwCursorPosition; // 光标默认位置
  WORD       wAttributes; // 屏幕缓冲字符属性
  SMALL_RECT srWindow; // 屏幕缓冲窗口RECT
  COORD      dwMaximumWindowSize; // 最大屏幕缓冲窗口大小
} CONSOLE_SCREEN_BUFFER_INFO;

可使用 SetConsoleScreenBufferSize函数来设置屏幕缓冲大小,但是需要注意调用该函数必须保证设置的屏幕缓冲大小不小于屏幕缓冲窗口大小。

可使用SetConsoleWindowInfo函数设置屏幕缓冲窗口位置及大小。注意调整活动屏幕缓冲的窗口大小会影响实际显示的控制台窗口大小。

4. 控制台的标记和选择

可以使用GetConsoleSelectionInfo函数来获取控制台中实际选择的字符区域和位置,具体用法可参考msdn上说明。

 

本文作者:Tocy

版权所有,请勿用于商业用途,转载请注明原文地址。本人保留所有权利。

posted @ 2014-11-21 21:39  Tocy  阅读(6744)  评论(0编辑  收藏  举报