游戏键位映射程序详解
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)
错误处理
优雅降级
- XInput 加载失败: 尝试多个DLL版本
- 手柄未连接: 静默忽略,继续轮询
- 配置文件损坏: 备份后使用默认配置
- 事件通道满: 丢弃事件,不阻塞
状态恢复
- 停止时释放按键:
mapper.ReleaseAll()确保不会卡键 - 重启时重置状态:
listener.Start()重置所有状态变量