GLFW 输入系统实战指南:从事件处理到键盘、鼠标与手柄输入
【免费下载链接】glfwA multi-platform library for OpenGL, OpenGL ES, Vulkan, window and input项目地址: https://gitcode.com/GitHub_Trending/gl/glfw
本文围绕 GLFW 官方的输入指南(docs/input.md)展开,系统讲解 GLFW 的全部输入能力:事件处理三件套(轮询/等待/超时等待)、键盘按键与字符输入、鼠标位置、光标模式与光标对象、摇杆与游戏手柄(含 SDL 兼容映射格式)、高精度时间、剪贴板与文件拖放。读完后,你可以完整实现一个跨平台应用的输入层,并能对照 GLFW 源码(src/input.c、src/window.c)理解各回调与轮询函数背后的事件管线。
输入模型总览:回调与轮询并存
GLFW 提供多种输入通道,其交互方式各不相同:
- 只能轮询:如高精度时间(
glfwGetTime); - 只能回调:如鼠标滚轮(scrolling);
- 两者兼备:按键、鼠标按钮、光标位置等既可以通过回调接收,也可以用
glfwGetKey等函数轮询。
回调(callback)比轮询用起来更繁琐,但 CPU 开销更低,并且能保证你不漏掉任何状态变化。原文档给出了明确建议:优先回调,轮询作为兜底。
所有输入回调都接收一个窗口句柄GLFWwindow*。借助 窗口用户指针(glfwSetWindowUserPointer),你可以在回调中访问非全局的结构体或对象,这是处理多窗口场景的标准做法。
想要直观感受各类事件回调的行为,可以运行仓库自带的events测试程序(源码见 tests/events.c)。它注册了 GLFW 支持的全部回调,并打印每个事件的所有参数、时间戳与序号信息,是调试输入问题最快的工具。
事件处理:输入到达应用的前提
GLFW 必须定期向窗口系统轮询事件,这既是为了把输入送达到应用,也是为了向窗口系统证明应用没有锁死(很多窗口系统会据此判断无响应并允许强制关闭)。事件处理通常放在每帧渲染结束、缓冲交换(glfwSwapBuffers)之后。即使当前没有任何窗口,也必须处理事件,否则收不到显示器与摇杆的热插拔事件。
事件处理相关的全部实现集中在 src/window.c 中,四个核心 API 分别位于:
glfwPollEvents— src/window.cglfwWaitEvents— src/window.cglfwWaitEventsTimeout— src/window.cglfwPostEmptyEvent— src/window.c
glfwPollEvents:持续渲染的首选
glfwPollEvents();它只处理已经接收到的事件,处理完立即返回,不会阻塞。这是持续渲染场景(大多数游戏)的最佳选择:每帧调一次即可。
glfwWaitEvents:事件驱动的理想选择
glfwWaitEvents();当窗口内容只需要在新输入到达时更新,就应该用它。它会让当前线程挂起,直到至少收到一个事件,然后处理所有已收到的事件,节省大量 CPU 周期,适合编辑类工具等事件驱动型应用。
glfwWaitEventsTimeout:带周期任务的等待
glfwWaitEventsTimeout(0.7);如果你需要等待事件,但又有 UI 元素或其他任务需要周期性刷新(例如 HUD、网络心跳),用超时版等待:线程挂起,直到收到至少一个事件或指定秒数(如 0.7 秒)过去,然后处理期间收到的所有事件。
glfwPostEmptyEvent:从其他线程唤醒等待
glfwPostEmptyEvent();如果主线程正睡眠在glfwWaitEvents中,你可以从另一个线程向事件队列投递一个空事件,把它唤醒。
重要陷阱:回调不一定只在上述函数中触发
不要假设回调只会在这三个事件处理函数的调用中被触发。某些窗口系统要求 GLFW 自己注册回调,这些回调可能在很多窗口系统函数调用期间收到事件,GLFW 会在返回前把事件转交给你的回调。
文档给出的经典例子是 Windows:实现glfwSetWindowSize的系统函数会直接向每个窗口的过程回调投递尺寸事件。如果你设置了窗口尺寸回调,那么它会在glfwSetWindowSize调用返回之前就先被调用(此时拿到新尺寸)。这一点在实现"按尺寸重建帧缓冲"等逻辑时尤其要注意。
从源码结构看,这一机制对应平台层的_glfwPollEvents*家族:Win32 在 src/win32_window.c(_glfwPollEventsWin32)、Cocoa 在 src/cocoa_window.m(_glfwPollEventsCocoa,内部基于NSRunLoop)、X11/Wayland 在src/x11_window.c/src/wl_window.c、Null 平台在 src/null_window.c。各平台通过函数表(如 src/cocoa_init.m 中.pollEvents = _glfwPollEventsCocoa)统一挂接到window.c的公共入口。
键盘输入
GLFW 把键盘输入分为两类:
- 按键事件(key events):与真实的物理按键相关,用按键 token(如
GLFW_KEY_E)标识; - 字符事件(character events):与按键产生的文本相关,由操作系统的文本输入系统生成。
两者不是一一对应:一次按键可能产生多个字符(如 Ctrl+C),一个字符也可能需要多次按键(如死音符)。你的键盘布局、输入法与用户的可能完全不同,因此做游戏按键用 key 事件,做文本输入用 char 事件。
按键输入(Key input)
要在物理按键按下、释放或重复时收到通知,设置按键回调(实现见 src/input.c,glfwGetKey位于 src/input.c):
glfwSetKeyCallback(window, key_callback);回调函数接收键盘按键 token、平台相关的扫描码(scancode)、按键动作(action)和修饰位(modifier bits):
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { if (key == GLFW_KEY_E && action == GLFW_PRESS) activate_airship(); }action取值之一:GLFW_PRESS、GLFW_REPEAT、GLFW_RELEASE。GLFW_PRESS与GLFW_RELEASE事件对每次按键都会成对发出;大多数按键在持续按住期间还会发出GLFW_REPEAT事件。
几点必须知道的行为细节:
- 按键滚落(key rollover):许多键盘同时能识别的按键数量有上限,超限时会丢键。
- 不要依赖
GLFW_REPEAT:重复事件按用户在键盘设置中的频率发出,即使按住多个键也最多只有一个键重复。不要用GLFW_REPEAT来判断"当前按住了哪些键"或驱动动画。正确做法是基于GLFW_PRESS/GLFW_RELEASE自己维护按键状态,或调用glfwGetKey(提供基本的缓存按键状态)。 GLFW_KEY_UNKNOWN:按键是既有的 key token 之一,若 GLFW 没有对应 token(如 E-mail 键、Play 键),则为GLFW_KEY_UNKNOWN。- 扫描码可以持久化:每个物理键的 scancode 唯一,平台相关但随时间稳定,因此可以安全地存盘。可以用
glfwGetKeyScancode查询任意受支持 key token 的扫描码:
const int scancode = glfwGetKeyScancode(GLFW_KEY_X); set_key_mapping(scancode, swap_weapons);- 轮询按键状态:每个有 key token 的物理键的最后上报状态都保存在每窗口的状态数组中:
int state = glfwGetKey(window, GLFW_KEY_E); if (state == GLFW_PRESS) { activate_airship(); }返回值只有GLFW_PRESS或GLFW_RELEASE。注意glfwGetKey只返回缓存的按键事件状态,不会向系统查询物理键的当前状态,也不提供任何按键重复信息。
GLFW_KEY_LAST:保存所有 key token 中的最大值,可用来做遍历上界。
粘滞按键(GLFW_STICKY_KEYS)
轮询状态有一个固有风险:按键在被轮询前就释放了,你会错过这次按下。推荐方案是使用按键回调;若必须轮询,可开启GLFW_STICKY_KEYS输入模式:
glfwSetInputMode(window, GLFW_STICKY_KEYS, GLFW_TRUE);开启后,按键的可轮询状态会保持GLFW_PRESS,直到你用glfwGetKey轮询了该键。轮询之后,如果期间处理过该键的释放事件,状态重置为GLFW_RELEASE,否则保持GLFW_PRESS。
锁定键修饰位(GLFW_LOCK_KEY_MODS)
如果想知道 Caps Lock 和 Num Lock 在输入事件产生时的状态,设置GLFW_LOCK_KEY_MODS:
glfwSetInputMode(window, GLFW_LOCK_KEY_MODS, GLFW_TRUE);开启后,所有接收 修饰位 的回调都会带上相应位:Caps Lock 开启时置GLFW_MOD_CAPS_LOCK,Num Lock 开启时置GLFW_MOD_NUM_LOCK。
文本输入(Text input)
GLFW 支持以 Unicode 码点流 形式的文本输入,由操作系统的文本输入系统产生。与按键输入不同,文本输入受键盘布局和修饰键影响,并支持使用**死音键(dead keys)**组合字符。收到码点后,你可以自行编码为 UTF-8 或其他任意编码。由于unsigned int在 GLFW 支持的所有平台上都是 32 位,码点参数可以直接当作本机字节序的 UTF-32 处理。
glfwSetCharCallback(window, character_callback);回调函数接收产生普通文本输入的按键事件对应的 Unicode 码点,行为与该平台的普通文本框一致:
void character_callback(GLFWwindow* window, unsigned int codepoint) { }按键名称(Key names)
若要以名称指代按键,可以用glfwGetKeyName查询可打印键随键盘布局变化的名称(实现见 src/input.c):
const char* key_name = glfwGetKeyName(GLFW_KEY_W, 0); show_tutorial_hint("Press %s to move forward", key_name);该函数同时接受 key 和 scancode:如果指定的 key 是GLFW_KEY_UNKNOWN,则使用 scancode;否则 scancode 被忽略。这与按键回调的语义一致,意味着回调参数可以原封不动地传给这个函数。
鼠标输入
鼠标输入形式多样:移动、按钮按下、滚轮偏移,此外光标外观还可以改成自定义图片或系统主题标准形状。
光标位置(Cursor position)
要在光标在窗口上移动时收到通知,设置光标位置回调:
glfwSetCursorPosCallback(window, cursor_position_callback);回调接收的光标位置以屏幕坐标系度量,但相对于窗口内容区域左上角。在平台支持时,会传递完整的亚像素光标位置:
static void cursor_position_callback(GLFWwindow* window, double xpos, double ypos) { }光标位置同时按窗口保存,可以用glfwGetCursorPos轮询:
double xpos, ypos; glfwGetCursorPos(window, &xpos, &ypos);光标模式(Cursor mode)
GLFW_CURSOR输入模式提供几种针对特殊鼠标运动输入的光标模式,默认为GLFW_CURSOR_NORMAL——使用普通箭头光标(或用glfwSetCursor设置的其他光标),运动不受限制。
GLFW_CURSOR_DISABLED:实现无边界鼠标运动的关键。要实现基于鼠标运动的相机控制等需要无限鼠标运动的输入方案:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);这会隐藏光标并将其锁定到指定窗口。GLFW 接管所有光标回中和偏移计算的细节,向应用提供一个虚拟光标位置,该虚拟位置照常通过光标位置回调和轮询两种方式提供。
注意:不要试图用 GLFW 的其他特性自行实现这个功能。官方明确声明这种用法不受支持,也没有
GLFW_CURSOR_DISABLED健壮。
GLFW_CURSOR_HIDDEN:仅隐藏光标,行为正常、运动不受限:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);GLFW_CURSOR_CAPTURED:光标可见但被限制在窗口内容区域内,除非窗口失去焦点否则无法离开:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_CAPTURED);要从这两种特殊模式退出,恢复GLFW_CURSOR_NORMAL即可;若光标曾被禁用,恢复后它会移回最后可见的位置:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);原始鼠标运动(Raw mouse motion)
光标禁用时,如果平台支持,可以启用原始(不缩放、不加速)鼠标运动。原始运动更贴近鼠标在表面上的真实位移,不受桌面光标缩放与加速影响——那套处理适合光标,而原始运动更适合控制 3D 相机。正因如此,原始运动只在光标禁用时提供:
if (glfwRawMouseMotionSupported()) glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);先用glfwRawMouseMotionSupported检查当前机器是否提供原始运动。该模式默认关闭,支持时可按窗口随时开关,但只有光标处于禁用状态时才会提供原始数据。
光标对象(Cursor objects)
GLFW 支持创建自定义光标和系统主题光标,统一封装为GLFWcursor对象。创建用glfwCreateCursor/glfwCreateStandardCursor(实现分别位于 src/input.c 和 src/input.c),销毁用glfwDestroyCursor,或在glfwTerminate时一并销毁残留对象。
自定义光标
glfwCreateCursor返回创建的光标对象句柄。例如创建一个 16×16 白色方块光标,热区在左上角:
unsigned char pixels[16 * 16 * 4]; memset(pixels, 0xff, sizeof(pixels)); GLFWimage image; image.width = 16; image.height = 16; image.pixels = pixels; GLFWcursor* cursor = glfwCreateCursor(&image, 0, 0);创建失败时返回NULL,必须检查返回值。图像数据为 32 位小端、未预乘的 RGBA(每通道 8 位、红色在前),像素按常规逐行排列,从左上角开始。
标准形状光标
可以用glfwCreateStandardCursor从当前系统光标主题创建一个标准形状的光标:
GLFWcursor* url_cursor = glfwCreateStandardCursor(GLFW_POINTING_HAND_CURSOR);这类对象的行为与glfwCreateCursor创建的完全相同,只是实际图像由系统光标主题提供。部分形状并非处处可用;不可用时返回NULL(详见glfwCreateStandardCursor参考文档)。
光标销毁
glfwDestroyCursor(cursor);光标销毁总是成功。若某个窗口当前正使用该光标,该窗口会回退到默认光标(不影响光标模式)。调用glfwTerminate时所有残留光标都会被销毁。
光标设置
glfwSetCursor(window, cursor);设置后,只要系统光标位于窗口内容区域内且光标模式为GLFW_CURSOR_NORMAL,就会使用该光标图像。同一光标可设置给任意数量的窗口。传NULL恢复默认光标:
glfwSetCursor(window, NULL);光标被销毁时,所有设置为它的光标也会回退到默认光标,同样不影响光标模式。
光标进入/离开事件
glfwSetCursorEnterCallback(window, cursor_enter_callback);回调接收光标的最新分类:
void cursor_enter_callback(GLFWwindow* window, int entered) { if (entered) { // 光标进入窗口内容区域 } else { // 光标离开窗口内容区域 } }也可以用GLFW_HOVERED窗口属性查询光标当前是否位于内容区域内:
if (glfwGetWindowAttrib(window, GLFW_HOVERED)) { highlight_interface(); }鼠标按钮输入
glfwSetMouseButtonCallback(window, mouse_button_callback);GLFW_UNLIMITED_MOUSE_BUTTONS:要在回调中处理所有鼠标按钮(而非只有存在按钮 token的按钮),设置该输入模式:
glfwSetInputMode(window, GLFW_UNLIMITED_MOUSE_BUTTONS, GLFW_TRUE);开启后 GLFW 不再把上报的鼠标按钮限制为有 token 的按钮——这是出于对旧版 GLFW 的兼容性考虑(旧版本从不上报GLFW_MOUSE_BUTTON_LAST之上的按钮,用户可能依赖这一行为)。
回调接收鼠标按钮、动作和修饰位:
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) { if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS) popup_menu(); }button为鼠标按钮 token 之一;若设置了GLFW_UNLIMITED_MOUSE_BUTTONS,也可以是任意其他正整数。action为GLFW_PRESS或GLFW_RELEASE。
每个鼠标按钮 token 的最后上报状态同样保存在每窗口状态数组中,用glfwGetMouseButton轮询(不受GLFW_UNLIMITED_MOUSE_BUTTONS影响):
int state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT); if (state == GLFW_PRESS) { upgrade_cow(); }返回值为GLFW_PRESS或GLFW_RELEASE,且只返回缓存的事件状态,不向系统查询真实状态。
粘滞鼠标按钮(GLFW_STICKY_MOUSE_BUTTONS)
与按键同理,轮询可能错过"按下又释放"的短按。推荐用回调;否则可开启:
glfwSetInputMode(window, GLFW_STICKY_MOUSE_BUTTONS, GLFW_TRUE);开启后,按钮的可轮询状态保持GLFW_PRESS,直到你用glfwGetMouseButton轮询。轮询后若期间处理过该按钮的释放事件则重置为GLFW_RELEASE,否则保持GLFW_PRESS。
GLFW_MOUSE_BUTTON_LAST保存所有鼠标按钮 token 中的最大值。
滚轮输入(Scrolling)
glfwSetScrollCallback(window, scroll_callback);无论是鼠标滚轮还是触控板手势,滚动都会到达该回调。回调接收二维滚动偏移:
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { }普通(垂直)鼠标滚轮只沿 Y 轴提供偏移。滚动只能回调、不能轮询——上文输入模型总览中"只能回调"的典型。
摇杆输入(Joystick)
摇杆函数暴露已连接的摇杆与手柄(两者都称作 joystick),最多支持16 个,ID 从GLFW_JOYSTICK_1、GLFW_JOYSTICK_2一直到GLFW_JOYSTICK_16(即GLFW_JOYSTICK_LAST)。用glfwJoystickPresent测试某个摇杆是否存在:
int present = glfwJoystickPresent(GLFW_JOYSTICK_1);每个摇杆拥有:零个或多个轴(axes)、零个或多个按钮(buttons)、零个或多个帽键(hats)、一个人类可读名称、一个用户指针、以及一个 SDL 兼容的 GUID。
分配规则:检测到的摇杆加入数组开头。摇杆一旦被检测就保持其 ID 直到断开或库终止,因此随着插拔出现,ID 中可能出现空洞。
轮询与事件的分工:轴/按钮/帽状态在轮询时更新,不需要窗口、也不需要处理事件;但若要可靠地接收摇杆连接/断开事件到摇杆回调,就必须处理事件。想实时查看所有已连接摇杆的全部属性,运行 tests/joysticks.c 对应的joysticks测试程序。
轴状态
int count; const float* axes = glfwGetJoystickAxes(GLFW_JOYSTICK_5, &count);返回数组中每个元素为 -1.0 到 1.0 之间的值。注意查看参考文档中关于返回数组长度的说明(数组仅在本次调用到下一次摇杆函数调用前有效)。
按钮状态
int count; const unsigned char* buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_3, &count);每个元素为GLFW_PRESS或GLFW_RELEASE。出于向后兼容,按钮数组默认还包含所有帽键(针对没有glfwGetJoystickHats的旧版本),详见glfwGetJoystickButtons参考文档。
帽键状态
int count; const unsigned char* hats = glfwGetJoystickHats(GLFW_JOYSTICK_7, &count);每个元素取值如下:
| 名称 | 值 |
|---|---|
GLFW_HAT_CENTERED | 0 |
GLFW_HAT_UP | 1 |
GLFW_HAT_RIGHT | 2 |
GLFW_HAT_DOWN | 4 |
GLFW_HAT_LEFT | 8 |
GLFW_HAT_RIGHT_UP | GLFW_HAT_RIGHT|GLFW_HAT_UP |
GLFW_HAT_RIGHT_DOWN | GLFW_HAT_RIGHT|GLFW_HAT_DOWN |
GLFW_HAT_LEFT_UP | GLFW_HAT_LEFT|GLFW_HAT_UP |
GLFW_HAT_LEFT_DOWN | GLFW_HAT_LEFT|GLFW_HAT_DOWN |
对角方向是主方向(上下左右)的按位组合,可用按位与单独测试:
if (hats[2] & GLFW_HAT_RIGHT) { // 帽键 2 的状态可能是 right-up、right 或 right-down }同样出于向后兼容,所有帽键默认也会包含在按钮数组中。
摇杆名称
const char* name = glfwGetJoystickName(GLFW_JOYSTICK_4);返回人类可读、UTF-8 编码的名称。名称不保证唯一——两个同品牌同型号的摇杆可能同名;唯一性的只有摇杆 ID,且只在断开前有效。
摇杆用户指针
每个摇杆都有一个用户指针:
glfwSetJoystickUserPointer(jid, pointer); glfwGetJoystickUserPointer(jid);可任意用途,GLFW 不修改;值会保留直到摇杆断开或库终止。初始值为NULL。
摇杆连接/断开事件
glfwSetJoystickCallback(joystick_callback);回调接收摇杆 ID 与事件类型:
void joystick_callback(int jid, int event) { if (event == GLFW_CONNECTED) { // 摇杆已连接 } else if (event == GLFW_DISCONNECTED) { // 摇杆已断开 } }要点:
- 要在所有平台上收到连接/断开事件,必须调用某个事件处理函数;
- 断开也可能由摇杆函数调用时被动检测到并触发回调,该函数随后返回"已断开摇杆"对应的值;
- 只有
glfwGetJoystickName和glfwGetJoystickUserPointer对已断开的摇杆仍返回有用值,且仅限于回调返回之前。
手柄输入(Gamepad)
摇杆函数提供的是无标签的轴、按钮和帽键,无法指示它们在设备上的位置,顺序甚至可能随平台变化。为解决该问题,SDL 社区众包维护了SDL_GameControllerDB项目——一个把各种设备映射到类 Xbox 手柄的映射数据库。
GLFW 支持该映射格式,并在发布时内置了当时可用的映射副本(生成产物即 src/mappings.h)。映射会在摇杆连接或映射更新时自动分配。
基本查询与状态
判断摇杆既存在又有手柄映射:
if (glfwJoystickIsGamepad(GLFW_JOYSTICK_2)) { // 作为手柄使用 }如果只关心手柄输入,可直接用它代替glfwJoystickPresent。
查询映射提供的人类可读名称(可能与摇杆名称相同也可能不同):
const char* name = glfwGetGamepadName(GLFW_JOYSTICK_7);获取手柄状态(实现见 src/input.c):
GLFWgamepadstate state; if (glfwGetGamepadState(GLFW_JOYSTICK_3, &state)) { if (state.buttons[GLFW_GAMEPAD_BUTTON_A]) { input_jump(); } input_speed(state.axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER]); }GLFWgamepadstate包含两个数组:按钮状态与轴状态。取值与glfwGetJoystickButtons/glfwGetJoystickAxes一致:按钮为GLFW_PRESS/GLFW_RELEASE,轴为 -1.0 到 1.0(含端点)。数组大小与下标位置是固定的:
- 按钮下标:
GLFW_GAMEPAD_BUTTON_A、_B、_X、_Y、_LEFT_BUMPER、_RIGHT_BUMPER、_BACK、_START、_GUIDE、_LEFT_THUMB、_RIGHT_THUMB、_DPAD_UP、_DPAD_RIGHT、_DPAD_DOWN、_DPAD_LEFT;另有用法等价别名GLFW_GAMEPAD_BUTTON_CROSS、_CIRCLE、_SQUARE、_TRIANGLE分别对应 A、B、X、Y。 - 轴下标:
GLFW_GAMEPAD_AXIS_LEFT_X、_LEFT_Y、_RIGHT_X、_RIGHT_Y、_LEFT_TRIGGER、_RIGHT_TRIGGER。 GLFW_GAMEPAD_BUTTON_LAST与GLFW_GAMEPAD_AXIS_LAST等于各数组的最大可用下标。
手柄映射(Gamepad mappings)
运行时可用glfwUpdateGamepadMappings追加新映射(实现见 src/input.c):
const char* mappings = load_file_contents("game/data/gamecontrollerdb.txt"); glfwUpdateGamepadMappings(mappings);该函数从单行到未修改的整个gamecontrollerdb.txt文件内容都支持。
源码构建时更新内置映射:用 CMake 从源码构建 GLFW 时,可构建update_mappings目标,它会运行 CMake/GenerateMappings.cmake 脚本,下载gamecontrollerdb.txt并重新生成 src/mappings.h 头文件。
映射格式(由 SDL 与 SDL_GameControllerDB 项目定义,其文档与代码具有最终解释权):
- 每条映射是一个逗号分隔值的单行:GUID、名称、布局;不以十六进制数字开头的行会被忽略;
- 第一个值始终是手柄 GUID:32 字符的十六进制字符串,通常标识厂商、型号、修订版本和连接类型;信息不可得时用手柄名称生成。GLFW 使用 SDL 2.0.5+ 的 GUID 格式,但可转换旧格式;
- 第二个值始终是人可读名称;
- 其后为
<字段>:<值>形式的布局描述,字段可缺省且顺序任意。
按钮字段:a、b、x、y、back、start、guide、dpup、dpright、dpdown、dpleft、leftshoulder、rightshoulder、leftstick、rightstick。轴字段:leftx、lefty、rightx、righty、lefttrigger、righttrigger。
字段取值可以是摇杆按钮(bN,如b2表示第 3 个按钮)、摇杆轴(aN,如a7)、帽键位掩码(hN.N,如h0.8表示第一个帽键的左方向,可设多个位)或空。
修饰符:
- 轴前可加
+或-范围修饰,如+a3只取第 4 轴的负半区之外的正半区,把输入限制在轴的正/负半区; - 轴或半轴后可加
~反转修饰,如a2~或-a7~,对该手柄轴的值取反。
另有特殊的platform字段,指定映射生效的平台,取值为Windows、Mac OS X、Linux。
下面是 GLFW 内置的 Windows 上经 XInput 访问的 Xbox 手柄映射(为排版断成了多行,实际映射必须是单行):
78696e7075740100000000000000000000,XInput Gamepad (GLFW),platform:Windows,a:b0, b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8, rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4, righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,注意:GLFW 尚不支持 SDL 新近加入的输出范围与修饰符
+、-;输入修饰符+、-、~是支持的,如上所述。
时间输入
GLFW 提供以秒为单位的高精度时间输入:
double seconds = glfwGetTime();返回自glfwInit初始化库以来的秒数。各平台底层计时源通常具有微秒或纳秒级分辨率。
可以用glfwSetTime修改基准时间:
glfwSetTime(4.0);设置为指定秒数后从该点继续计数。
还可以访问实现上述函数的原始计时器:
uint64_t value = glfwGetTimerValue();该值以 1/频率 秒为单位。频率取决于操作系统与硬件,用glfwGetTimerFrequency(单位 Hz)查询:
uint64_t frequency = glfwGetTimerFrequency();各平台的原始计时器实现可参见 src/win32_time.c、src/posix_time.c、src/cocoa_time.c 等平台源文件。
剪贴板输入与输出
若系统剪贴板包含 UTF-8 字符串(或可转换为 UTF-8),用glfwGetClipboardString获取:
const char* text = glfwGetClipboardString(NULL); if (text) { insert_text(text); }剪贴板为空或内容无法转换时返回NULL。
用glfwSetClipboardString把系统剪贴板设置为 UTF-8 字符串:
glfwSetClipboardString(NULL, "A string with words in it");路径拖放输入(Path drop)
要接收拖放到窗口上的文件/目录路径,设置文件拖放回调:
glfwSetDropCallback(window, drop_callback);回调接收以 UTF-8 编码的路径数组:
void drop_callback(GLFWwindow* window, int count, const char** paths) { int i; for (i = 0; i < count; i++) handle_dropped_file(paths[i]); }数组及其字符串只在文件拖放回调返回前有效——它们可能是为该事件专门生成的。若需保留路径,必须深拷贝数组。
小结与调试建议
- 事件是输入到达应用的唯一通道:每帧
glfwPollEvents(持续渲染)或glfwWaitEvents/glfwWaitEventsTimeout(事件驱动),并注意回调可能在其他 GLFW 调用内被触发; - 按键与字符是两套输入:游戏控制用 key 回调 + 自维护状态(或
glfwGetKey缓存),文本输入用 char 回调;GLFW_REPEAT不可靠、轮询会丢事件,必要时开GLFW_STICKY_KEYS/GLFW_STICKY_MOUSE_BUTTONS; - 相机类控制用
GLFW_CURSOR_DISABLED+GLFW_RAW_MOUSE_MOTION,不要用其他方式自行模拟; - 手柄统一走
glfwJoystickIsGamepad+glfwGetGamepadState,需要新设备时用glfwUpdateGamepadMappings或构建update_mappings目标更新 src/mappings.h; - 调试输入行为首选 tests/events.c(全量事件打印)与 tests/joysticks.c(手柄实时属性),延迟类问题还可参考 tests/inputlag.c。
【免费下载链接】glfwA multi-platform library for OpenGL, OpenGL ES, Vulkan, window and input项目地址: https://gitcode.com/GitHub_Trending/gl/glfw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考