返回文章列表
技术2026年1月30日8 分钟阅读

游戏键位映射程序详解

GamepadKeyMapper详解

代码架构与逻辑文档

本文档详细说明 GamepadKeyMapper 的代码架构、模块设计和核心逻辑。

目录结构

/
├── main.go                 # 程序入口
├── go.mod / go.sum         # Go模块定义
├── internal/
│   ├── app/                # 应用控制层
│   │   └── app.go
│   ├── config/             # 配置管理
│   │   ├── config.go
│   │   └── storage.go
│   ├── gamepad/            # 手柄输入层
│   │   ├── buttons.go
│   │   ├── listener.go
│   │   ├── xinput.go
│   │   ├── xinput_windows.go
│   │   └── xinput_stub.go
│   ├── keyboard/           # 键盘模拟层
│   │   ├── keys.go
│   │   ├── simulator_windows.go
│   │   └── simulator_stub.go
│   ├── mapper/             # 映射引擎
│   │   ├── mapper.go
│   │   └── rule.go
│   └── ui/                 # 用户界面
│       ├── window.go
│       ├── tray.go
│       ├── mapping_list.go
│       └── mapping_form.go
└── assets/                 # 静态资源

架构概览

┌─────────────────────────────────────────────────────────────┐
│                        UI Layer (Fyne)                      │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ Main Window │  │ System Tray │  │ Mapping Form/List   │  │
│  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────┘  │
└─────────┼────────────────┼────────────────────┼─────────────┘
          │                │                    │
          ▼                ▼                    ▼
┌─────────────────────────────────────────────────────────────┐
│                    App Controller (app.go)                  │
│  • 生命周期管理 (Start/Stop)                                  │
│  • 规则管理(Add/Remove/Get)                                  │
│  • 配置持久化 (Load/Save)                                     │
│  • 状态回调通知                                               │
└─────────────────────────────────────────────────────────────┘
          │                │                    │
          ▼                ▼                    ▼
┌─────────────────────────────────────────────────────────────┐
│                      Core Layer                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐   │
│  │   Gamepad    │  │   Mapper     │  │    Keyboard      │   │
│  │   Listener   │─▶│   Engine     │─▶│    Simulator     │   │
│  └──────────────┘  └──────────────┘  └──────────────────┘   │
│  • XInput轮询      • 规则匹配         • SendInput API         │
│  • 事件检测        • 循环保护         • 按键状态追踪             │
│  • 状态变化通知    • 链式触发         • 组合键支持               │
└─────────────────────────────────────────────────────────────┘
          │                                     │
          ▼                                     ▼
┌─────────────────────────────────────────────────────────────┐
│                   Platform Layer (Windows)                  │
│  ┌──────────────────────┐    ┌──────────────────────────┐   │
│  │  xinput1_4.dll       │    │  user32.dll              │   │
│  │  • XInputGetState    │    │  • SendInput             │   │
│  └──────────────────────┘    └──────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

模块详解

1. 手柄输入层 (internal/gamepad)

buttons.go - 按键定义

定义所有支持的手柄按键常量:

type Button uint32

const (
    ButtonA       Button = 0x1000   // 标准XInput按键位
    ButtonLT      Button = 0x10000  // 扳机(特殊处理)
    ButtonPaddle1 Button = 0x40000  // 精英版拨片
    ButtonLeftStickUp Button = 0x400000  // 摇杆方向(虚拟)
)

按键分类:

  • 标准按键 (0x0001-0x8000): 直接对应 XInput Buttons 位掩码
  • 扳机按键 (0x10000-0x20000): 模拟量转数字,使用阈值判断
  • 精英拨片 (0x40000-0x200000): 扩展按键
  • 摇杆方向 (0x400000+): 虚拟按键,摇杆位置转方向

xinput.go / xinput_windows.go - XInput封装

XInput状态结构:

type XInputState struct {
    PacketNumber uint32      // 状态包序号
    Gamepad      XInputGamepad
}

type XInputGamepad struct {
    Buttons      uint16   // 按键位掩码
    LeftTrigger  uint8    // 左扳机 (0-255)
    RightTrigger uint8    // 右扳机 (0-255)
    ThumbLX      int16    // 左摇杆X (-32768~32767)
    ThumbLY      int16    // 左摇杆Y
    ThumbRX      int16    // 右摇杆X
    ThumbRY      int16    // 右摇杆Y
}

DLL加载逻辑:

func LoadXInput() error {
    // 按版本顺序尝试加载
    dllNames := []string{
        "xinput1_4.dll",   // Windows 8.1+
        "xinput1_3.dll",   // Windows 7
        "xinput9_1_0.dll", // Windows Vista
    }
    // ...
}

listener.go - 事件监听器

轮询机制:

┌─────────────────────────────────────────┐
│              pollLoop (100Hz)           │
│  ┌─────────┐                            │
│  │ ticker  │─── 10ms ───▶ poll()        │
│  └─────────┘                            │
│       │                                 │
│       ▼                                 │
│  ┌─────────────────────────────────┐    │
│  │ poll()                           │   │
│  │  ├─ GetState(controllerID)       │   │
│  │  ├─ pollButtons() 检测按键变化    │    │
│  │  ├─ pollTriggers() 检测扳机变化   │    │
│  │  └─ pollSticks() 检测摇杆方向     │    │
│  └─────────────────────────────────┘    │
│       │                                 │
│       ▼                                 │
│  ┌─────────────────────────────────┐    │
│  │ sendEvent(button, pressed)       │   │
│  │  └─ eventChan <- ButtonEvent     │   │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

状态变化检测算法:

// 按键检测 - 使用异或找出变化的位
changed := currentButtons ^ prevState
if changed&btnMask != 0 {
    pressed := currentButtons&btnMask != 0
    sendEvent(btn, pressed)
}

// 扳机检测 - 使用阈值
currentLT := state.Gamepad.LeftTrigger > TriggerThreshold // 128
if currentLT != prevLT {
    sendEvent(ButtonLT, currentLT)
}

// 摇杆方向检测 - 使用阈值
leftUp := state.Gamepad.ThumbLY > StickThreshold // 16384 (~50%)

2. 键盘模拟层 (internal/keyboard)

keys.go - 键盘按键定义

type KeyCode int

// Windows Virtual Key Codes
const (
    KeyF1    KeyCode = 0x70
    KeyA     KeyCode = 0x41
    KeySpace KeyCode = 0x20
)

type Modifiers struct {
    Ctrl  bool
    Alt   bool
    Shift bool
    Win   bool
}

simulator_windows.go - 键盘模拟器

核心数据结构:

type Simulator struct {
    mu           sync.Mutex
    pressedKeys  map[KeyCode]bool  // 当前按住的键
    pressedMods  Modifiers         // 当前按住的修饰键
}

按键保持机制:

PressKeys(keys, mods):
┌────────────────────────────────────┐
│ 1. 检查修饰键是否已按下               │
│    └─ 未按下则发送按下事件            │
│ 2. 检查目标键是否已按下               │
│    └─ 未按下则发送按下事件            │
│ 3. 更新 pressedKeys/pressedMods    │
└────────────────────────────────────┘

ReleaseKeys(keys, mods):
┌────────────────────────────────────┐
│ 1. 检查目标键是否按住                 │
│    └─ 按住则发送释放事件              │
│ 2. 检查修饰键是否需要释放              │
│    └─ 需要则发送释放事件              │
│ 3. 从 pressedKeys/pressedMods 移除  │
└────────────────────────────────────┘

SendInput调用:

type INPUT struct {
    Type uint32        // INPUT_KEYBOARD = 1
    Ki   KEYBDINPUT
}

type KEYBDINPUT struct {
    Vk        uint16   // Virtual Key Code
    Scan      uint16   // 扫描码
    Flags     uint32   // KEYEVENTF_KEYUP, KEYEVENTF_EXTENDEDKEY
    Time      uint32
    ExtraInfo uintptr
}

// 调用 user32.dll SendInput
ret, _, _ := procSendInput.Call(
    uintptr(len(inputs)),
    uintptr(unsafe.Pointer(&inputs[0])),
    uintptr(unsafe.Sizeof(INPUT{})),
)

3. 映射引擎 (internal/mapper)

rule.go - 映射规则

type TargetType int

const (
    TargetKeyboard TargetType = iota  // 目标是键盘
    TargetGamepad                      // 目标是手柄按键
)

type MappingRule struct {
    ID            string
    SourceKey     gamepad.Button
    TargetType    TargetType
    
    // 键盘目标
    TargetKeys    []keyboard.KeyCode
    Modifiers     keyboard.Modifiers
    
    // 手柄目标
    TargetButtons []gamepad.Button
    
    Enabled       bool
}

mapper.go - 映射引擎核心

事件处理流程:

HandleEvent(event):
┌──────────────────────────────────────────────────────┐
│ 1. 循环保护检查                                        │
│    if processing[event.Button] → return              │
│                                                      │
│ 2. 查找匹配规则                                        │
│    for rule in rules:                                │
│        if rule.SourceKey == event.Button             │
│                                                      │
│ 3. 根据目标类型处理                                     │
│    ├─ TargetKeyboard:                                │
│    │   if pressed: PressKeys()                       │
│    │   else: ReleaseKeys()                           │
│    │                                                 │
│    └─ TargetGamepad:                                 │
│        handleGamepadMapping()                        │
└──────────────────────────────────────────────────────┘

手柄映射递归处理:

handleGamepadMapping(rule, pressed):
┌──────────────────────────────────────────────────────┐
│ 1. 标记源按键正在处理 (循环保护)                          │
│    processing[rule.SourceKey] = true                 │
│                                                      │
│ 2. 遍历目标手柄按键                                     │
│    for targetBtn in rule.TargetButtons:              │
│                                                      │
│ 3. 查找目标按键的映射规则                                │
│    for targetRule in rules:                          │
│        if targetRule.SourceKey == targetBtn          │
│                                                      │
│ 4. 执行目标规则                                        │
│    ├─ TargetKeyboard: PressKeys/ReleaseKeys          │
│    └─ TargetGamepad: 递归 handleGamepadMapping        │
│       (有循环保护,不会无限递归)                         │
│                                                      │
│ 5. 清除处理标记                                        │
│    delete(processing, rule.SourceKey)                │
└──────────────────────────────────────────────────────┘

循环保护机制:

配置: A → B, B → A

执行: 按下A
┌─────────────────────────────────────┐
│ HandleEvent(A pressed)              │
│   processing[A] = true              │
│   → 查找A的规则: A → B                │
│   → handleGamepadMapping for B      │
│       processing[B] = true          │
│       → 查找B的规则: B → A            │
│       → 检查 processing[A] = true    │
│       → 跳过 (防止循环)               │
│       processing[B] = false         │
│   processing[A] = false             │
└─────────────────────────────────────┘

4. 应用控制层 (internal/app)

app.go - 应用控制器

状态管理:

type State int

const (
    StateStopped State = iota
    StateRunning
)

type App struct {
    mapper   *mapper.Mapper
    listener *gamepad.Listener
    state    State
    mu       sync.RWMutex
    
    // 回调
    onStateChange func(State)
    onRulesChange func()
    onError       func(error)
}

启动流程:

Start():
┌─────────────────────────────────────┐
│ 1. 检查是否已运行                     │
│ 2. 启动手柄监听器                     │
│    listener.Start()                 │
│ 3. 启动事件处理协程                    │
│    go eventLoop()                   │
│ 4. 更新状态为 Running                 │
│ 5. 触发状态变更回调                    │
└─────────────────────────────────────┘

停止流程:

Stop():
┌─────────────────────────────────────┐
│ 1. 检查是否已停止                      │
│ 2. 释放所有按住的键                    │
│    mapper.ReleaseAll()              │
│ 3. 停止手柄监听器                     │ 
│    listener.Stop()                  │
│ 4. 更新状态为 Stopped                 │
│ 5. 触发状态变更回调                    │
└─────────────────────────────────────┘

事件循环:

func (a *App) eventLoop() {
    for event := range a.listener.Events() {
        a.mapper.HandleEvent(event)
    }
    // 通道关闭后自动退出
}

5. 配置管理 (internal/config)

config.go - 配置结构

type Config struct {
    Rules          []*mapper.MappingRule
    MinimizeToTray bool
    StartMinimized bool
}

storage.go - 持久化

配置路径:

func GetConfigPath() (string, error) {
    // 优先: %APPDATA%\GamepadKeyMapper\
    configDir, _ := os.UserConfigDir()
    appDir := filepath.Join(configDir, "GamepadKeyMapper")
    return filepath.Join(appDir, "gamepad-key-mapper.json"), nil
}

加载/保存:

// 加载 - 容错处理
func Load() (*Config, error) {
    data, err := os.ReadFile(path)
    if os.IsNotExist(err) {
        return NewDefault(), nil  // 返回默认配置
    }
    
    if json.Unmarshal(data, &cfg) != nil {
        os.Rename(path, path+".backup")  // 备份损坏文件
        return NewDefault(), nil
    }
    return &cfg, nil
}

// 保存
func Save(cfg *Config) error {
    data, _ := json.MarshalIndent(cfg, "", "  ")
    return os.WriteFile(path, data, 0644)
}

6. 用户界面 (internal/ui)

window.go - 主窗口

布局结构:

┌─────────────────────────────────────────┐
│ 状态: 已停止  │  [启动]  [停止]          │ ← 控制栏
├─────────────────────────────────────────┤
│ 按键映射列表                             │
│ ┌─────────────────────────────────────┐ │
│ │ A → ⌨️ Ctrl+F1            [删除]   │ │
│ │ RB → 🎮 X+B               [删除]   │ │
│ │ ...                                 │ │
│ └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│            [添加映射]                    │ ← 操作栏
└─────────────────────────────────────────┘

tray.go - 系统托盘

托盘菜单结构:

右键菜单:
├─ 启动
├─ 停止
├─ ────────
├─ 显示窗口
├─ ────────
└─ 退出

窗口关闭行为:

func (t *Tray) SetupWindowClose() {
    t.window.SetCloseIntercept(func() {
        t.window.Hide()  // 隐藏而不是关闭
    })
}

数据流

完整事件流

[手柄按下LB]
    │
    ▼
XInput DLL (xinput1_4.dll)
    │ XInputGetState()
    ▼
Listener.poll()
    │ 检测到 Buttons 变化
    ▼
Listener.sendEvent(ButtonLB, true)
    │ eventChan <- ButtonEvent
    ▼
App.eventLoop()
    │ for event := range events
    ▼
Mapper.HandleEvent(event)
    │ 查找规则: LB → Ctrl+F1
    ▼
Simulator.PressKeys([F1], {Ctrl:true})
    │ SendInput()
    ▼
[系统收到 Ctrl+F1 按键]

链式映射流

配置:
  RT → 🎮 A+B (手柄映射)
  A  → ⌨️ F1  (键盘映射)
  B  → ⌨️ F2  (键盘映射)

执行:
[按下RT]
    │
    ▼
HandleEvent(RT, pressed)
    │ 规则: RT → 手柄 A+B
    ▼
handleGamepadMapping()
    │
    ├──▶ 查找A的规则 → 键盘 F1
    │    └─ PressKeys([F1])
    │
    └──▶ 查找B的规则 → 键盘 F2
         └─ PressKeys([F2])
    │
    ▼
[F1和F2同时被按下]

线程安全

锁使用

组件 锁类型 保护对象
Mapper.mu RWMutex rules 列表
Mapper.processingMu Mutex processing map
Simulator.mu Mutex pressedKeys, pressedMods
Listener.mu Mutex running, cancel
App.mu RWMutex state

并发模型

Main Goroutine (UI)
    │
    ├──▶ App.Start() ──▶ Listener Goroutine (轮询)
    │                        │
    │                        └──▶ eventChan
    │                                │
    └──▶ App.eventLoop() ◀──────────┘
              │
              └──▶ Mapper.HandleEvent()
                        │
                        └──▶ Simulator (SendInput)

错误处理

优雅降级

  1. XInput 加载失败: 尝试多个DLL版本
  2. 手柄未连接: 静默忽略,继续轮询
  3. 配置文件损坏: 备份后使用默认配置
  4. 事件通道满: 丢弃事件,不阻塞

状态恢复

  • 停止时释放按键: mapper.ReleaseAll() 确保不会卡键
  • 重启时重置状态: listener.Start() 重置所有状态变量