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
更多关于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)
}
}
这个实现的关键点:
- 使用
ReadConsoleInputW而不是ReadConsole - 正确解析
INPUT_RECORD结构体获取键盘事件 - 通过
wVirtualKeyCode识别特殊按键 - 通过
UnicodeChar获取字符输入 - 使用
bKeyDown区分按键按下和释放事件
对于控制台编辑器,还需要处理控制键状态(Shift、Ctrl、Alt等),可以通过dwControlKeyState字段获取。

