Golang实现Windows原始控制台输入输出

Golang实现Windows原始控制台输入输出 您好,

我目前正在尝试用Go语言编写一个控制台编辑器。为此,我需要能够读取用户在控制台中按下的每一个按键。幸运的是,新的 x/sys/windows 包提供了设置原始控制台模式和从控制台读取的功能。以下是我目前的代码:

in, _ :=  windows.Open("CONIN$", windows.O_RDWR, 0)
windows.SetConsoleMode(in, windows.ENABLE_WINDOW_INPUT)

buf := make([]byte, 1024)
windows.Read(in, buf)

然而,这只能读取字符,无法读取特殊按键(例如方向键、删除/擦除键、Home/End键、PgUp/PgDown键、F1-F12功能键)。

因此,我尝试改用 windows.ReadConsole(),但这个方法同样无法读取特殊按键:

buf := make([]uint16, 1024)
var iC byte = 0 // input control 指的是什么?
windows.ReadConsole(in, &buf[0], uint32(1) &read, &iC)

查阅了更多文档后,我发现了 ReadConsoleInput 函数,并使用系统调用实现了它,但这个函数“过于原始”——它还会发送按键释放事件,而且由于某些原因,我无法获取到任何可用的键码,只能得到 0x0 和 0x1。

我在我的GitHub上有一个示例,位于这个文件夹中:http://github.com/scrouthtv/termios/tree/master/win_arrows 我最初在Go问题跟踪器上报告了这个问题: #44373,但因为那里没有人能帮助我,所以被指引到了这里。

提前感谢任何帮助。


更多关于Golang实现Windows原始控制台输入输出的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

更多关于Golang实现Windows原始控制台输入输出的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我进行了更多测试。ReadConsoleInput 方法完全符合我的需求。这里有一个我可以使用的 C 示例代码:https://docs.microsoft.com/en-us/windows/console/reading-input-buffer-events

然而,如果我从 Go 中通过系统调用调用相同的方法,只会返回 0x0 和 0x1。有什么想法吗?

编辑:找到问题了。我目前读取到一个单字节中,这在这里确实没有意义。我现在创建了一个 InputRecord 结构体,正如微软文章中提到的:https://docs.microsoft.com/en-us/windows/console/input-record-str:

type InputRecord struct {
  Type  uint16
  _     [2]byte
  Event [16]byte
}

这似乎确实为我提供了更多可用的数据。

对于在Go中实现Windows原始控制台输入,特别是读取特殊按键,需要使用ReadConsoleInputW系统调用。以下是完整的解决方案:

package main

import (
    "fmt"
    "syscall"
    "unsafe"
)

const (
    ENABLE_ECHO_INPUT      = 0x0004
    ENABLE_LINE_INPUT      = 0x0002
    ENABLE_PROCESSED_INPUT = 0x0001
    ENABLE_WINDOW_INPUT    = 0x0008
    ENABLE_MOUSE_INPUT     = 0x0010
    ENABLE_INSERT_MODE     = 0x0020
    ENABLE_QUICK_EDIT_MODE = 0x0040
    ENABLE_EXTENDED_FLAGS  = 0x0080
    ENABLE_AUTO_POSITION   = 0x0100
    ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
)

const (
    KEY_EVENT                = 0x0001
    MOUSE_EVENT              = 0x0002
    WINDOW_BUFFER_SIZE_EVENT = 0x0004
    MENU_EVENT               = 0x0008
    FOCUS_EVENT              = 0x0010
)

type KEY_EVENT_RECORD struct {
    bKeyDown          int32
    wRepeatCount      uint16
    wVirtualKeyCode   uint16
    wVirtualScanCode  uint16
    UnicodeChar       uint16
    dwControlKeyState uint32
}

type INPUT_RECORD struct {
    EventType uint16
    _         [2]byte
    Event     [16]byte
}

var (
    kernel32 = syscall.NewLazyDLL("kernel32.dll")
    getStdHandle = kernel32.NewProc("GetStdHandle")
    setConsoleMode = kernel32.NewProc("SetConsoleMode")
    readConsoleInputW = kernel32.NewProc("ReadConsoleInputW")
)

func getConsoleInputHandle() (syscall.Handle, error) {
    handle, _, err := getStdHandle.Call(uintptr(^uint32(10 - 1)))
    if handle == uintptr(syscall.InvalidHandle) {
        return syscall.InvalidHandle, err
    }
    return syscall.Handle(handle), nil
}

func setConsoleModeRaw(handle syscall.Handle) error {
    // 禁用行缓冲、回显和特殊按键处理
    mode := uint32(0)
    // 启用窗口输入以获取窗口大小变化事件
    mode |= ENABLE_WINDOW_INPUT
    // 启用扩展标志
    mode |= ENABLE_EXTENDED_FLAGS
    // 启用虚拟终端输入以支持ANSI转义序列
    mode |= ENABLE_VIRTUAL_TERMINAL_INPUT
    
    _, _, err := setConsoleMode.Call(
        uintptr(handle),
        uintptr(mode),
    )
    if err != nil && err.Error() != "The operation completed successfully." {
        return err
    }
    return nil
}

func readConsoleInput(handle syscall.Handle) ([]INPUT_RECORD, error) {
    var records [256]INPUT_RECORD
    var numRead uint32
    
    _, _, err := readConsoleInputW.Call(
        uintptr(handle),
        uintptr(unsafe.Pointer(&records[0])),
        uintptr(len(records)),
        uintptr(unsafe.Pointer(&numRead)),
    )
    
    if err != nil && err.Error() != "The operation completed successfully." {
        return nil, err
    }
    
    return records[:numRead], nil
}

func main() {
    // 获取控制台输入句柄
    handle, err := getConsoleInputHandle()
    if err != nil {
        panic(err)
    }
    
    // 设置原始模式
    err = setConsoleModeRaw(handle)
    if err != nil {
        panic(err)
    }
    
    fmt.Println("按任意键开始(按ESC退出)...")
    
    for {
        records, err := readConsoleInput(handle)
        if err != nil {
            panic(err)
        }
        
        for _, record := range records {
            switch record.EventType {
            case KEY_EVENT:
                // 解析键盘事件
                keyEvent := (*KEY_EVENT_RECORD)(unsafe.Pointer(&record.Event[0]))
                
                if keyEvent.bKeyDown == 1 {
                    // 按键按下事件
                    switch keyEvent.wVirtualKeyCode {
                    case 0x1B: // ESC键
                        fmt.Println("\n退出程序")
                        return
                    case 0x25: // 左箭头
                        fmt.Print("[LEFT]")
                    case 0x26: // 上箭头
                        fmt.Print("[UP]")
                    case 0x27: // 右箭头
                        fmt.Print("[RIGHT]")
                    case 0x28: // 下箭头
                        fmt.Print("[DOWN]")
                    case 0x2E: // Delete键
                        fmt.Print("[DELETE]")
                    case 0x24: // Home键
                        fmt.Print("[HOME]")
                    case 0x23: // End键
                        fmt.Print("[END]")
                    case 0x21: // Page Up
                        fmt.Print("[PGUP]")
                    case 0x22: // Page Down
                        fmt.Print("[PGDN]")
                    case 0x70: // F1
                        fmt.Print("[F1]")
                    case 0x71: // F2
                        fmt.Print("[F2]")
                    // 添加更多功能键...
                    default:
                        // 普通字符
                        if keyEvent.UnicodeChar != 0 {
                            fmt.Printf("%c", keyEvent.UnicodeChar)
                        }
                    }
                }
                // 忽略按键释放事件
            }
        }
    }
}

对于更简洁的封装,可以使用以下辅助函数:

func readKey() (uint16, uint16, error) {
    handle, err := getConsoleInputHandle()
    if err != nil {
        return 0, 0, err
    }
    
    for {
        records, err := readConsoleInput(handle)
        if err != nil {
            return 0, 0, err
        }
        
        for _, record := range records {
            if record.EventType == KEY_EVENT {
                keyEvent := (*KEY_EVENT_RECORD)(unsafe.Pointer(&record.Event[0]))
                if keyEvent.bKeyDown == 1 {
                    return keyEvent.wVirtualKeyCode, keyEvent.UnicodeChar, nil
                }
            }
        }
    }
}

// 使用示例
func main() {
    fmt.Println("按方向键测试(按ESC退出)")
    
    for {
        vkCode, unicodeChar, err := readKey()
        if err != nil {
            panic(err)
        }
        
        if vkCode == 0x1B {
            break
        }
        
        fmt.Printf("VirtualKey: 0x%04X, Unicode: 0x%04X\n", vkCode, unicodeChar)
    }
}

这个实现的关键点:

  1. 使用ReadConsoleInputW而不是ReadConsole
  2. 正确解析INPUT_RECORD结构体获取键盘事件
  3. 通过wVirtualKeyCode识别特殊按键
  4. 通过UnicodeChar获取字符输入
  5. 使用bKeyDown区分按键按下和释放事件

对于控制台编辑器,还需要处理控制键状态(Shift、Ctrl、Alt等),可以通过dwControlKeyState字段获取。

回到顶部