Windows cmd.exe 擁有命令歷史 (上下鍵)、功能鍵 (F1-F9)、路徑自動補全 (Tab) 等強大編輯功能。本文揭示 ReadConsoleW 通過 lpReserved 參數傳遞 READ_CONSOLE_CONTROL 結構體即可啟用這些原生功能的機制,並提供完整可運行的 C++ 示例。
MSDN 稱 lpReserved 必須為 NULL,但 Unicode 版 ReadConsoleW 內部識別非 NULL 指針並解析擴展控制結構 (ANSI 版 ReadConsoleA 不支持)。
struct READ_CONSOLE_CONTROL {
DWORD cbSize; // 結構大小,必須為 sizeof(READ_CONSOLE_CONTROL) = 16
DWORD dwInitLen; // 初始緩衝區已有字符數 (續行編輯用)
DWORD dwWakeMask; // 喚醒掩碼:Ctrl+@ (bit0) ~ Ctrl+Z (bit25),置位對應 Ctrl 鍵觸發返回
DWORD dwUnknown; // 保留,置 0
};
// Ctrl+鍵 對應位位置: Ctrl+@(0) Ctrl+A(1) ... Ctrl+Z(25)
#define CTRL_KEY_BIT(ch) (1u << ((ch) & 0x1F)) // 'A'=1, 'F'=5, 'D'=3...
// 常用組合:Tab(完成)、Ctrl+C(中斷)、Ctrl+D(補全)、Enter(換行默認喚醒)
DWORD dwWakeMask = CTRL_KEY_BIT('F') | CTRL_KEY_BIT('D') | CTRL_KEY_BIT('C');
#include
#include
#include
#include
#define CTRL_BIT(ch) (1u << ((ch) & 0x1F))
struct READ_CONSOLE_CONTROL {
DWORD cbSize; DWORD dwInitLen; DWORD dwWakeMask; DWORD dwUnknown;
};
int wmain() {
// 1. 設置區域支持中文顯示
_wsetlocale(LC_ALL, L".ACP"); // 或 L"zh_CN.UTF-8"
// 2. 獲取標準輸入輸出句柄
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hIn == INVALID_HANDLE_VALUE || hOut == INVALID_HANDLE_VALUE) return 1;
// 3. 啟用虛擬終端序列 (Win10+ 支持 ANSI 色彩/光標控制)
DWORD dwMode;
GetConsoleMode(hOut, &dwMode);
SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
// 4. 準備讀取控制結構
READ_CONSOLE_CONTROL rcc = {0};
rcc.cbSize = sizeof(rcc);
rcc.dwWakeMask = CTRL_BIT('F') | CTRL_BIT('D') | CTRL_BIT('C'); // Ctrl+F, Ctrl+D, Ctrl+C
WCHAR buffer[256] = {0};
DWORD charsRead = 0;
while (true) {
// 5. 顯示提示符
DWORD written;
WriteConsoleW(hOut, L"\n$ ", 3, &written, NULL);
// 6. 重置緩衝區與控制結構
buffer[0] = L'\0';
rcc.dwInitLen = 0; // 無初始內容
// 7. 調用 ReadConsoleW (關鍵:傳遞 &rcc)
BOOL ok = ReadConsoleW(hIn, buffer, 255, &charsRead, &rcc);
if (!ok) {
wprintf(L"ReadConsoleW 失敗: %lu\n", GetLastError());
break;
}
// 8. 處理喚醒鍵 (緩衝區最後一字符為觸發鍵)
if (charsRead > 0) {
WCHAR lastChar = buffer[charsRead - 1];
// Ctrl+C (3) - 中斷/退出
if (lastChar == 3) { // Ctrl+C
WriteConsoleW(hOut, L"^C\n", 3, &written, NULL);
continue; // 或 break 退出
}
// Ctrl+F (6) - 自動補全 "FILE_PATH"
if (lastChar == 6) { // Ctrl+F
const WCHAR* completion = L"FILE_PATH";
// 替換觸發鍵
wcscpy(&buffer[charsRead - 1], completion);
DWORD compLen = wcslen(completion);
// 回顯補全內容
WriteConsoleW(hOut, completion, compLen, &written, NULL);
// 更新 dwInitLen 使光標停在補全內容後,繼續編輯
rcc.dwInitLen = charsRead - 1 + compLen;
// 關鍵:再次調用 ReadConsoleW 繼續讀取 (模擬 cmd 連續補全)
// 這裡簡化:直接 goto 再次讀取邏輯,實際需循環處理
// 為演示簡單,這裡直接提交該行
buffer[charsRead - 1 + compLen] = L'\0';
}
// Ctrl+D (4) - 自動補全 "DIR_PATH"
if (lastChar == 4) { // Ctrl+D
const WCHAR* completion = L"DIR_PATH";
wcscpy(&buffer[charsRead - 1], completion);
DWORD compLen = wcslen(completion);
WriteConsoleW(hOut, completion, compLen, &written, NULL);
rcc.dwInitLen = charsRead - 1 + compLen;
buffer[charsRead - 1 + compLen] = L'\0';
}
}
// 9. 去除結尾換行符 (\r\n 或 \n)
while (charsRead > 0 && (buffer[charsRead-1] == L'\n' || buffer[charsRead-1] == L'\r'))
buffer[--charsRead] = L'\0';
if (charsRead == 0) continue; // 空行
// 10. 處理內建命令
if (wcscmp(buffer, L"quit") == 0 || wcscmp(buffer, L"exit") == 0) break;
if (wcscmp(buffer, L"history") == 0) {
// 歷史功能由 ReadConsoleW 內部自動管理 (上下鍵),無需應用層代碼
WriteConsoleW(hOut, L"(歷史由系統自動提供,按 ↑/↓ 查看)\n", 40, &written, NULL);
continue;
}
// 11. 執行外部命令 (簡化:直接 system)
// 實際應用 CreateProcess + 匿名管道捕獲輸出
char cmdAnsi[512];
WideCharToMultiByte(CP_ACP, 0, buffer, -1, cmdAnsi, 512, NULL, NULL);
system(cmdAnsi);
}
return 0;
}
| 功能 | 實現方式 | 備註 |
|---|---|---|
| 命令歷史 (↑/↓) | ReadConsoleW 內建 | 無需代碼,自動記錄本次會話輸入 |
| 功能鍵 (F1-F9) | ReadConsoleW 內建 | F1:逐字複製上一命令, F3:複製全部, F7:彈出歷史菜單... |
| 行編輯 (Home/End/←/→/Insert/Del) | ReadConsoleW 內建 | 完整支持 |
| 自動補全 (Tab/Ctrl+鍵) | dwWakeMask + 回顯 + dwInitLen | 需應用層實現補全邏輯 (文件系統/命令/參數) |
| Unicode 支持 | ReadConsoleW + wchar_t | 完美支持中文輸入顯示 |
| ANSI 版 ReadConsoleA | 不支持 dwWakeMask/dwInitLen | 必須用 Unicode 版 |
buffer 當前光標前的詞法上下文 (命令名、參數標志、路徑前綴)。文件系統 (FindFirstFile)、內建命令、歷史命令、環境變量、別名。WriteConsoleW 繪製下拉列表 (需保存/恢復光標位置 GetConsoleScreenBufferInfo/SetConsoleCursorPosition),Tab/Shift+Tab 切換,Enter 確認。PostConsoleMessage (自定義) 觸發刷新。cbSize 未設置或結構體對齊錯誤 (需 #pragma pack(4) 或確保 4 字節對齊)。dwInitLen 必須精確等於「提示符長度 + 緩衝區有效字符數」。提示符由應用層 WriteConsoleW 輸出,不計入 ReadConsoleW 管理範圍。ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT (默認即開啟)。ENABLE_VIRTUAL_TERMINAL_PROCESSING 並設置正確代碼頁 (chcp 65001 或 _wsetlocale)。ReadConsoleW 的 lpReserved 擴展結構是 Windows 控制台編程的「隱藏寶藏」。僅需 16 字節結構體,即可解鎖 原生級命令行編輯體驗 (歷史、功能鍵、行編輯),並為 自定義自動補全 提供標準化鉤子。結合虛擬終端序列 (VT100) 實現彩色提示符、進度條、交互菜單,即可構建媲美 cmd.exe/PowerShell 的專業控制台應用。建議封裝為 CConsoleLineEditor 類,隔離底層細節,提供 ReadLine(prompt, completer) 統一介面。