首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC 教學

讓控制台程序支持命令歷史、快捷鍵與自動補全 (ReadConsoleW 進階應用)

     編輯:VC 教學

前言

Windows cmd.exe 擁有命令歷史 (上下鍵)、功能鍵 (F1-F9)、路徑自動補全 (Tab) 等強大編輯功能。本文揭示 ReadConsoleW 通過 lpReserved 參數傳遞 READ_CONSOLE_CONTROL 結構體即可啟用這些原生功能的機制,並提供完整可運行的 C++ 示例。

一、核心秘密:ReadConsoleW 的 lpReserved 參數

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');

二、完整實現:支持歷史、補全、行編輯的迷你 Shell

#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 版

四、工程化補全引擎設計建議

  1. 狀態機解析:解析 buffer 當前光標前的詞法上下文 (命令名、參數標志、路徑前綴)。
  2. 多源補全:合並 文件系統 (FindFirstFile)、內建命令、歷史命令、環境變量、別名。
  3. 模糊匹配:支持前綴、子序列、拼音首字母匹配。
  4. 菜單選擇:多匹配項時,WriteConsoleW 繪製下拉列表 (需保存/恢復光標位置 GetConsoleScreenBufferInfo/SetConsoleCursorPosition),Tab/Shift+Tab 切換,Enter 確認。
  5. 異步補全:耗時操作 (網絡路徑) 放入工作線程,主線程顯示「載入中...」,完成後 PostConsoleMessage (自定義) 觸發刷新。

五、常見問題排查

  • ReadConsoleW 返回 FALSE, GetLastError=87 (ERROR_INVALID_PARAMETER):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) 統一介面。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved