一、引言
控制台程序(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默認光標樣式)。可以使用函數GetConsoleCursorInfo、SetConsoleCursorInfo來獲取、設置光標外觀及顯示或隱藏屬性。
使用高層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
版權所有,請勿用於商業用途,轉載請注明原文地址。本人保留所有權利。
