QMK Firmware Combo(连击)功能完全指南:从基础定义到源码级高级配置
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
导读
Combo(连击/和弦键)是 QMK Firmware 提供的一种"按键和弦"方案:通过在同一时间窗口内按下多个按键,触发一个完全不同的输出或自定义动作。例如在 Combo 判定时间内同时按下A和B,可以输出Esc,甚至执行更复杂的宏。本文以官方文档 docs/features/combo.md 为主体,结合 quantum/process_keycode/process_combo.c 与 quantum/process_keycode/process_combo.h 的源码实现,系统讲解 Combo 的启用、定义、重叠处理、运行时开关、全套高级配置(定时器、缓冲区、按 Combo 独立时序、层引用等)以及用户回调接口,帮助你从"会用"进阶到"理解其底层机制"。
一、启用与最小实现
1.1 在rules.mk中启用功能
Combo 属于可选编译特性,需要先在键盘(或 keymap)的rules.mk中加入:
COMBO_ENABLE = yes该选项在 builddefs/show_options.mk 中被登记为可选特性,并在 docs/config_options.md 中有对应说明。编译时,COMBO_ENABLE = yes会引入 quantum/process_keycode/process_combo.c,其中process_combo()会在按键处理链中与其他功能串联执行(参见 quantum/quantum.c 中对process_combo的调用)。
1.2 在keymap.c中定义第一个 Combo
启用后,在keymap.c中需要做两件事:
- 定义一个以
COMBO_END结尾的按键序列数组(称为 chord keys,即组成该 Combo 的键); - 在
combo_t key_combos[]数组中登记该序列与它触发的动作。
最小示例:
const uint16_t PROGMEM test_combo1[] = {KC_A, KC_B, COMBO_END}; const uint16_t PROGMEM test_combo2[] = {KC_C, KC_D, COMBO_END}; combo_t key_combos[] = { COMBO(test_combo1, KC_ESC), COMBO(test_combo2, LCTL(KC_Z)), // keycodes with modifiers are possible too! };效果:同时按下A与B输出Esc;同时按下C与D输出Ctrl+Z。
1.3 源码中的数据结构与宏
从 process_combo.h 可以看到底层数据结构与宏的真实定义:
typedef struct combo_t { const uint16_t *keys; uint16_t keycode; #ifdef EXTRA_SHORT_COMBOS uint8_t state; #else bool disabled; bool active; # if defined(EXTRA_EXTRA_LONG_COMBOS) uint32_t state; # elif defined(EXTRA_LONG_COMBOS) uint16_t state; # else uint8_t state; # endif #endif } combo_t; #define COMBO(ck, ca) {.keys = &(ck)[0], .keycode = (ca)} #define COMBO_ACTION(ck) {.keys = &(ck)[0]} #define COMBO_END 0要点解读:
COMBO(ck, ca)只是结构体初始化宏:keys指向你的按键序列数组,keycode是触发结果;COMBO_END的值为0,作为序列终止哨兵,源码在遍历时通过pgm_read_word(&combo->keys[idx]) != COMBO_END判断结束(见 process_combo.c);combo_t内部维护disabled(被禁用/放弃)与active(已被触发激活)状态位,以及state位掩码(记录该 Combo 的哪些键当前处于按下状态)。
二、与高级按键码协同工作
Combo 不仅支持普通键码,还支持 QMK 的高级按键码,例如 Mod-Tap(LSFT_T/LT等)与 Tap Dance。若你的 keymap 中使用了这类按键码,需要在 Combo 定义中直接放入完整的按键码,而非其基础键:
const uint16_t PROGMEM test_combo1[] = {LSFT_T(KC_A), LT(1, KC_B), COMBO_END}; const uint16_t PROGMEM test_combo2[] = {TD(TD_ESC_CAPS), KC_F1, COMBO_END};源码层面,Combo 匹配使用的是_find_key_index_and_count()逐键比对combo->keys数组中的完整键码(见 process_combo.c),因此序列中既可以出现QK_MOD_TAP、QK_LAYER_TAP、QK_MOMENTARY(即MO())这类"高级键",也可以混合普通键码。
三、重叠 Combo(Overlapping Combos)
多个 Combo 可以共享部分按键。当所有相关键都被按下时,QMK 会优先触发键数更多的那个 Combo:
const uint16_t PROGMEM test_combo1[] = {LSFT_T(KC_A), LT(1, KC_B), COMBO_END}; const uint16_t PROGMEM test_combo2[] = {LSFT_T(KC_A), LT(1, KC_B), KC_C, COMBO_END}; combo_t key_combos[] = { COMBO(test_combo1, KC_ESC) COMBO(test_combo2, KC_TAB) };上述例子中同时按下A、B、C时,只会触发 3 键的test_combo2(输出Tab),2 键的test_combo1会被放弃。
源码佐证:overlaps()函数(process_combo.c)逐个比对两个 Combo 的按键数组,判断是否存在交集;存在交集时返回"键数更少"的那个(键数相同则返回前者),随后process_single_combo在将新 Combo 写入缓冲队列前,会把与之重叠的已就绪 Combo 标记为DISABLE_COMBO(见 process_combo.c)。这就是"三键组合优先于二键组合"这一行为的实现根源。
四、实战示例
4.1 用enum管理大量 Combo
Combo 数量多时,建议用enum为每个 Combo 命名,并利用数组指定下标进行初始化:
enum combos { AB_ESC, JK_TAB, QW_SFT, SD_LAYER }; const uint16_t PROGMEM ab_combo[] = {KC_A, KC_B, COMBO_END}; const uint16_t PROGMEM jk_combo[] = {KC_J, KC_K, COMBO_END}; const uint16_t PROGMEM qw_combo[] = {KC_Q, KC_W, COMBO_END}; const uint16_t PROGMEM sd_combo[] = {KC_S, KC_D, COMBO_END}; combo_t key_combos[] = { [AB_ESC] = COMBO(ab_combo, KC_ESC), [JK_TAB] = COMBO(jk_combo, KC_TAB), [QW_SFT] = COMBO(qw_combo, KC_LSFT), [SD_LAYER] = COMBO(sd_combo, MO(_LAYER)), };此例同时展示了 Combo 可以触发修饰键(KC_LSFT)与层切换(MO(_LAYER))。
4.2 使用process_combo_event执行自定义逻辑
对于更复杂的动作,可以用COMBO_ACTION(x)定义"无预置动作"的 Combo(其效果等同于COMBO(x, KC_NO)),然后通过process_combo_event(combo_index, pressed)回调在触发/释放时执行任意代码:
enum combo_events { EM_EMAIL, BSPC_LSFT_CLEAR, }; const uint16_t PROGMEM email_combo[] = {KC_E, KC_M, COMBO_END}; const uint16_t PROGMEM clear_line_combo[] = {KC_BSPC, KC_LSFT, COMBO_END}; combo_t key_combos[] = { [EM_EMAIL] = COMBO_ACTION(email_combo), [BSPC_LSFT_CLEAR] = COMBO_ACTION(clear_line_combo), }; /* COMBO_ACTION(x) is same as COMBO(x, KC_NO) */ void process_combo_event(uint16_t combo_index, bool pressed) { switch(combo_index) { case EM_EMAIL: if (pressed) { SEND_STRING("john.doe@example.com"); } break; case BSPC_LSFT_CLEAR: if (pressed) { tap_code16(KC_END); tap_code16(S(KC_HOME)); tap_code16(KC_BSPC); } break; } }效果:同时按下E与M输出邮箱地址;同时按下Backspace与Left Shift清空当前行。你还可以把它扩展为播放声音、修改设置等任意动作。
从源码看,process_combo_event是一个__attribute__((weak))弱符号,默认空实现(process_combo.c),你在 keymap 中定义的同名函数会覆盖它。当 Combo 的keycode为 0(即COMBO_ACTION)时,触发与释放都会回调该函数(见release_combo(),process_combo.c)。
提示:自 PR#8591 之后,
COMBO_ACTION已不再是必需方案。你可以自定义一个按键码,在process_record_user中实现其功能,然后用COMBO(<key_array>, <your_custom_keycode>)让 Combo 触发它,示例可参考 Macros 文档。
五、运行时开关:Combo 功能快捷键
游戏或临时场景下需要暂时关闭 Combo 时,可在 keymap 中使用以下键码随时开关:
| Keycode | Aliases | 功能 |
|---|---|---|
QK_COMBO_ON | CM_ON | 开启 Combo 功能 |
QK_COMBO_OFF | CM_OFF | 关闭 Combo 功能 |
QK_COMBO_TOGGLE | CM_TOGG | 切换 Combo 功能的开关状态 |
源码佐证:QK_COMBO_ON = 0x7C50、QK_COMBO_TOGGLE = 0x7C52定义于 quantum/keycodes.h,别名CM_ON/CM_TOGG在 quantum/keycodes.h。process_combo()在处理到这些键码且为按下事件时,分别调用combo_enable()、combo_disable()、combo_toggle()(process_combo.c)。
对应的 C 函数回调接口如下:
| 函数 | 说明 |
|---|---|
combo_enable() | 启用 Combo 功能 |
combo_disable() | 禁用 Combo 功能,并清空 Combo 缓冲 |
combo_toggle() | 切换 Combo 功能开关状态 |
is_combo_enabled() | 返回 Combo 功能当前是否启用(true/false) |
实现位于 process_combo.c:全局状态位b_combo_enable默认为true(启用);combo_disable()会清零定时器、清空缓冲队列并调用clear_combos()与dump_key_buffer(),把缓冲中的按键恢复为普通按键处理。
六、高级配置(config.h)
以下配置均写在config.h中。
6.1 Combo 判定时间窗:COMBO_TERM
默认 Combo 识别超时时间为50ms(宏定义见 process_combo.h)。如果经常误触发,或觉得同时按键困难,可调整:
#define COMBO_TERM 40将判定窗口缩短为 40ms。源码中_get_combo_term()默认返回COMBO_TERM,且combo_task()通过timer_elapsed(timer) > longest_term判断是否超时并强制执行缓冲中的 Combo(process_combo.c)。
6.2 缓冲与状态尺寸
长 Combo 或大量重叠 Combo 可能导致缓冲不足。可调整以下配置,但注意:更大的 Combo 尺寸与缓冲会占用更多内存。
单个 Combo 允许的最大按键数:
| 按键数 | 需要定义的宏 |
|---|---|
| 6 | #define EXTRA_SHORT_COMBOS |
| 8 | QMK 默认 |
| 16 | #define EXTRA_LONG_COMBOS |
| 32 | #define EXTRA_EXTRA_LONG_COMBOS |
对应源码:MAX_COMBO_LENGTH在 process_combo.h 中按上述宏分别取 6/8/16/32,同时combo_t.state的位宽随之切换为uint8_t/uint16_t/uint32_t。
EXTRA_SHORT_COMBOS会把 Combo 的内部状态压缩进一个字节(disabled/active标志挤占state的高两位,见 process_combo.c 的条件编译分支),某些情况下能节省内存;若没有节省效果则没必要使用,且此时 Combo 的按键数绝不能超过 6 个。
处理 Combo 存在两个缓冲:一个存放按键按下记录(key buffer),一个存放已被激活的 Combo(combo buffer)。可分别配置:
| 宏 | 默认值 |
|---|---|
#define COMBO_KEY_BUFFER_LENGTH 8 | 8(即(EXTRA_)EXTRA_LONG_COMBOS允许的最大键数) |
#define COMBO_BUFFER_LENGTH 4 | 4 |
源码佐证:两个宏的默认值在 process_combo.h 定义,缓冲数组在 process_combo.c 中声明(key_buffer[COMBO_KEY_BUFFER_LENGTH]、combo_buffer[COMBO_BUFFER_LENGTH]),combo_buffer为环形队列(INCREMENT_MOD取模推进)。
6.3 修饰键 Combo:COMBO_MUST_HOLD_MODS与COMBO_HOLD_TERM
若 Combo 的结果是修饰键(Modifier),其处理时间窗可独立于普通 Combo 延长。默认关闭,可通过以下方式开启:
#define COMBO_MUST_HOLD_MODS #define COMBO_HOLD_TERM 150COMBO_MUST_HOLD_MODS:要求"按住"修饰键类 Combo 才会触发;开启后该 Combo 不再支持轻点触发,误触发更少;COMBO_HOLD_TERM:修饰键 Combo 的保持时间窗,默认值为TAPPING_TERM(见 process_combo.h)。
源码佐证:_get_combo_must_hold()在未定义按 Combo 独立钩子时,会通过KEYCODE_IS_MOD(combo->keycode) || (combo->keycode >= QK_MOMENTARY && combo->keycode <= QK_MOMENTARY_MAX)判断(process_combo.c);_get_wait_time()对需要保持/轻点的 Combo 返回COMBO_HOLD_TERM(process_combo.c)。KEYCODE_IS_MOD宏在 process_combo.h 定义,覆盖纯修饰键码以及带修饰但无基础键的键码。
6.4 严格按键顺序:COMBO_MUST_PRESS_IN_ORDER
#define COMBO_MUST_PRESS_IN_ORDER开启后,Combo 只有在按键按下顺序与其在数组中定义的顺序一致时才会触发。源码中keys_pressed_in_order()通过位运算(1 << key_index) == (COMBO_STATE(combo) + 1)判断当前按下的键是否为"下一个期望键"(process_combo.c),注释中给出了形象的二进制示例(两个键已按下时state == 11,下一个按键位应为100)。
6.5 按 Combo 独立配置:时序、保持、轻点与按键顺序
针对每个 Combo,可独立配置其时间窗、是否需要按住、是否只能轻点、是否必须按键有序。典型场景:当底层按键是 Mod-Tap 或 Layer-Tap 键时,tap-only(仅轻点)Combo非常有用——轻点 Combo 触发结果;按下并保持则不触发 Combo,各键按普通逻辑分别处理。
使用这些特性,需要定义以下配置开关并实现对应函数(默认值见各表):
| 配置开关 | 需实现的函数 | 说明 |
|---|---|---|
COMBO_TERM_PER_COMBO | uint16_t get_combo_term(uint16_t combo_index, combo_t *combo) | 可选:按 Combo 设置独立超时窗口(默认COMBO_TERM) |
COMBO_MUST_HOLD_PER_COMBO | bool get_combo_must_hold(uint16_t combo_index, combo_t *combo) | 控制该 Combo 是轻点立即触发,还是必须按住(默认false) |
COMBO_MUST_TAP_PER_COMBO | bool get_combo_must_tap(uint16_t combo_index, combo_t *combo) | 控制该 Combo 是否仅在COMBO_HOLD_TERM内轻点才触发(默认false) |
COMBO_MUST_PRESS_IN_ORDER_PER_COMBO | bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo) | 控制该 Combo 是否要求按键按顺序按下才触发(默认true) |
这些钩子函数在 process_combo.c 中均以弱符号形式提供默认实现。完整示例:
#ifdef COMBO_TERM_PER_COMBO uint16_t get_combo_term(uint16_t combo_index, combo_t *combo) { // 按结果键码判断 switch (combo->keycode) { case KC_X: return 50; } // 或用 Combo 下标(即 enum 名称)判断 switch (combo_index) { case COMBO_NAME_HERE: return 9001; } // 还可以按和弦中的按键判断 // 当多个 Combo 共用一个键、想给它们统一设置时间窗时很有用 if (combo->keys[0] == KC_ENT) { // 若数组第一个键是 Enter return 150; } return COMBO_TERM; } #endif #ifdef COMBO_MUST_HOLD_PER_COMBO bool get_combo_must_hold(uint16_t combo_index, combo_t *combo) { // 与上面一样,可按结果键码、Combo 下标或和弦按键判断 if (KEYCODE_IS_MOD(combo->keycode) || (combo->keycode >= QK_MOMENTARY && combo->keycode <= QK_MOMENTARY_MAX) // MO(kc) 键码 ) { return true; } switch (combo_index) { case COMBO_NAME_HERE: return true; } return false; } #endif #ifdef COMBO_MUST_TAP_PER_COMBO bool get_combo_must_tap(uint16_t combo_index, combo_t *combo) { // 若想让所有 Combo 都仅轻点触发,取消下一行注释 // return true // 若想让所有"和弦中含有 Mod-Tap/Layer-Tap/Momentary 键"的 Combo 都仅轻点触发: uint16_t key; uint8_t idx = 0; while ((key = pgm_read_word(&combo->keys[idx])) != COMBO_END) { switch (key) { case QK_MOD_TAP...QK_MOD_TAP_MAX: case QK_LAYER_TAP...QK_LAYER_TAP_MAX: case QK_MOMENTARY...QK_MOMENTARY_MAX: return true; } idx += 1; } return false; } #endif #ifdef COMBO_MUST_PRESS_IN_ORDER_PER_COMBO bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo) { switch (combo_index) { /* 在这里列出要求"按键按下顺序与数组定义一致"的 Combo */ case COMBO_NAME_HERE: return true; default: return false; } } #endif说明:
- tap-only Combo(
get_combo_must_tap返回true)在源码中不会像普通 Combo 一样在缓冲队列中等待,而是在首个按键释放时立即apply_combo()(见 process_combo.c 分支); - 若组合了
COMBO_PROCESS_KEY_RELEASE,tap-only Combo 释放时还会调用process_combo_key_release(); - "设计合理的时间与配置"需要结合你的输入习惯自行调优。
6.6 通用触发钩子:COMBO_SHOULD_TRIGGER
定义COMBO_SHOULD_TRIGGER并实现
bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record)即可按你的条件放行或拦截 Combo 的触发。例如:在基础层禁用某些 Combo 而在另一层启用;或在计时器运行期间禁用 home row 上的 Combo。
bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) { /* 在 _LAYER_A 层禁用 SOME_COMBO */ switch (combo_index) { case SOME_COMBO: if (layer_state_is(_LAYER_A)) { return false; } } return true; }源码中该钩子以弱符号默认返回true(process_combo.c),并在process_single_combo()的key_is_part_of_combo判定链中生效(process_combo.c)。
6.7 Combo 定时器行为
默认行为:定时器在第一个键按下时启动,并在COMBO_TERM内的每次按键按下时重置。这种"宽松"输入方式容错高,但也略微更易误触发。以下两个选项可以改变定时器行为:
#define COMBO_STRICT_TIMER
定时器仅在第一个键按下时启动,之后不再重置。输入不再宽松:整个和弦必须在COMBO_TERM内按完。误触发更少,但若快速连续输入多个 Combo,后面的 Combo 可能无法正确激活。
源码佐证:COMBO_STRICT_TIMER分支中if (!timer) { timer = timer_read(); },即只在定时器未启动时设置一次(process_combo.c)。
#define COMBO_NO_TIMER
完全禁用定时器:Combo 在首个键释放时被激活。同时,"must hold(必须按住)"类功能将被禁用(它们依赖定时器,见_get_combo_must_hold()中#ifdef COMBO_NO_TIMER return false;分支,process_combo.c)。
6.8 自定义按键释放处理:COMBO_PROCESS_KEY_RELEASE
定义COMBO_PROCESS_KEY_RELEASE并实现
bool process_combo_key_release(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode)可在 Combo 激活后的每次按键释放时运行自定义代码,例如修改 RGB 颜色、触发触觉反馈或调整修饰键状态。若该函数返回true,则 Combo 会被提前释放。
以下示例中,Combo 的结果是双修饰键Ctrl+Shift,释放时根据具体释放的键逐个取消修饰键:
enum combos { AB_MODS }; const uint16_t PROGMEM ab_combo[] = {KC_A, KC_B, COMBO_END}; combo_t key_combos[] = { [AB_MODS] = COMBO(ab_combo, LCTL(KC_LSFT)), }; bool process_combo_key_release(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) { switch (combo_index) { case AB_MODS: switch(keycode) { case KC_A: unregister_mods(MOD_MASK_CTRL); break; case KC_B: unregister_mods(MOD_MASK_SHIFT); break; } return false; // 不提前释放 Combo } return false; }源码佐证:该钩子同样以弱符号默认返回false(process_combo.c),在process_single_combo()的"首键/中间键释放""最后键释放"分支中被调用(process_combo.c)。
6.9 自定义按键重按处理:COMBO_PROCESS_KEY_REPRESS
定义COMBO_PROCESS_KEY_REPRESS并实现
bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode)可在"重按一个刚释放的 Combo 键"时运行自定义代码。结合自定义的process_combo_event,可以实现 Alt+Tab 切窗特例:F+G 激活时注册Alt并按下Tab,之后释放G再重按可向前切窗、重按F向后切窗。完整示例:
enum combos { CMB_ALTTAB }; const uint16_t PROGMEM combo_alttab[] = {KC_F, KC_G, COMBO_END}; combo_t key_combos[COMBO_LENGTH] = { [CMB_ALTTAB] = COMBO(combo_alttab, KC_NO), // KC_NO 将处理权留给 process_combo_event }; void process_combo_event(uint16_t combo_index, bool pressed) { switch (combo_index) { case CMB_ALTTAB: if (pressed) { register_mods(MOD_LALT); tap_code(KC_TAB); } else { unregister_mods(MOD_LALT); } break; } } bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) { switch (combo_index) { case CMB_ALTTAB: switch (keycode) { case KC_F: tap_code16(S(KC_TAB)); return true; case KC_G: tap_code(KC_TAB); return true; } } return false; }说明:
key_combos使用了COMBO_LENGTH作为数组大小(即 Combo 总数,由process_combo.c依据key_combos数组自动推导);- 重按处理返回
true时,该键会被记为"重按状态"(COMBO_KEY_REPRESSED),触发process_combo_key_repress对应的动作; - 该钩子的弱符号默认实现见 process_combo.c,调用点在 process_combo.c。
6.10 层无关 Combo:COMBO_ONLY_FROM_LAYER
如果你有多套基础层(例如 QWERTY 与 Colemak),希望 Combo 在不同层上从相同的按键位置触发,而不必为每套布局重复定义:
#define COMBO_ONLY_FROM_LAYER 0开启后,Combo 的按键始终从层 0读取键码进行匹配,即使其他层处于激活状态。
源码佐证:process_combo.c 中,COMBO_ONLY_FROM_LAYER定义后直接keycode = keymap_key_to_keycode(COMBO_ONLY_FROM_LAYER, record->event.key);否则走combo_ref_from_layer()引用层逻辑。
6.11 按层指定 Combo 引用层:combo_ref_from_layer与COMBO_REF_LAYER
如果不使用COMBO_ONLY_FROM_LAYER,还可以通过combo_ref_from_layer钩子为任意层指定 Combo 引用层。该函数返回"当前层对应的引用层":若未匹配到任何规则则返回默认引用层(若设置了),否则返回当前层。默认引用层用DEFAULT_REF_LAYER(_MY_COMBO_REF_LAYER)设置;未设置时,自动生成的combo_ref_from_layer()默认引用层就是当前层本身。
下面的手写实现让_DVORAK层引用_QWERTY层的按键位置、_NAV层引用自身,其余层使用默认引用层_MY_COMBO_LAYER:
#define COMBO_REF_DEFAULT _MY_COMBO_LAYER uint8_t combo_ref_from_layer(uint8_t layer){ switch (get_highest_layer(layer_state)){ case _DVORAK: return _QWERTY; case _NAV: return _NAV; default: return _MY_COMBO_LAYER; } return layer; // 当默认层不在 case 中时很重要 }等价的宏写法:
COMBO_REF_LAYER(_DVORAK, _QWERTY) COMBO_REF_LAYER(_NAV, _NAV) DEFAULT_REF_LAYER(_MY_COMBO_LAYER).源码佐证:combo_ref_from_layer的弱符号默认实现为return layer;(process_combo.c);在process_combo中,若引用层与当前最高层不同,则用引用层解析键码keymap_key_to_keycode(ref_layer, record->event.key)(process_combo.c)。
七、用户回调接口汇总
除上述按需定义的钩子函数外,process_combo.c还提供以下可直接调用的状态接口:
| 函数 | 说明 |
|---|---|
combo_enable() | 启用 Combo 功能 |
combo_disable() | 禁用 Combo 功能,并清空 Combo 缓冲 |
combo_toggle() | 切换 Combo 功能的开关状态 |
is_combo_enabled() | 返回 Combo 功能当前是否启用(true/false) |
实现见 process_combo.c,底层基于静态变量b_combo_enable(默认true)。combo_disable()除置位外,还会清零定时器、将缓冲读写指针对齐并清空各 Combo 状态,保证关闭后缓冲中的按键能按普通键正常送出。
另外,combo_task()(process_combo.c)由 QMK 主循环周期调用,负责在超时后统一应用缓冲中的 Combo——这是"窗口结束即触发"机制的心脏。
八、字典管理:用combos.def高效维护大量 Combo
当 Combo 数量很多时,"新增/修改一个 Combo 要同步更新三处"会变得繁琐。可以借助 gboards 的字典魔法(本质上是 C 宏)来简化。
首先,在rules.mk中加入:
VPATH += keyboards/gboards然后在keymap.c中包含头文件:
#include "g/keymap_combo.h"::: warning 该功能复用了COMBO_ACTION宏使用的同一个process_combo_event函数,因此你不能再在自己的 keymap 中定义该函数。你需要在inject.h中单独定义switch的各个case,g/keymap_combo.h会把它 include 进函数体。 :::
随后在combos.def文件中以声明式写法定义所有 Combo:
// 按层指定引用层 // 层 引用层 COMBO_REF_LAYER(_DVORAK, _QWERTY) // 让 dvorak 层引用 qwerty 层 COMBO_REF_LAYER(_NAV, _NAV) // 显式引用自身,而不是默认层 // 名称 结果 和弦按键 COMB(AB_ESC, KC_ESC, KC_A, KC_B) COMB(JK_TAB, KC_TAB, KC_J, KC_K) COMB(JKL_SPC, KC_SPC, KC_J, KC_K, KC_L) COMB(BSSL_CLR, KC_NO, KC_BSPC, KC_LSFT) // 结果键码用 KC_NO 等价于以前的 COMBO_ACTION COMB(QW_UNDO, C(KC_Z), KC_Q, KC_W) SUBS(TH_THE, "the", KC_T, KC_H) // SUBS 使用 SEND_STRING 输出给定字符串 ...要点:
COMB(name, result, ...keys):一行定义一个 Combo,name同时充当 enum 名称与combo_index;- 结果键码为
KC_NO时等同于传统COMBO_ACTION,动作需在process_combo_event中实现; SUBS(name, "string", ...keys):基于SEND_STRING的字符串输出便捷宏;COMBO_REF_LAYER/DEFAULT_REF_LAYER与 6.11 节的手写combo_ref_from_layer完全等价,宏会自动生成该函数。
对于现成的中小型乃至大型 Combo 字典,可以参考 gboards 维护的公开 Combo 字典(项目文档中给出的参考资源,可根据自身键盘与需求挑选合适的组合使用)。
九、总结
QMK 的 Combo 特性把"多键同时按下"变为一种通用的输入抽象:
- 最小接入成本低:
COMBO_ENABLE = yes+ 一段combo_t key_combos[]即可使用; - 表达能力强:可与 Mod-Tap、Tap Dance、层切换、修饰键组合,并通过
process_combo_event承载任意自定义逻辑; - 可调维度丰富:全局
COMBO_TERM、按 Combo 独立的时序/保持/轻点/顺序钩子、严格定时器、无定时器模式、键释放/重按钩子、层引用机制,配合 process_combo.c 中的位掩码状态机与环形缓冲实现,能够在误触发率与输入宽容度之间精细取舍; - 规模化维护有方案:
combos.def字典式写法大幅降低大量 Combo 的维护成本。
无论你是想给 40% 小配列键盘补上缺失的快捷键,还是构建复杂的自定义输入方案,Combo 都是 QMK 生态中极其高效的一块拼图。建议动手时从COMBO_TERM与缓冲尺寸入手调试,再逐步尝试 per-combo 钩子与层引用等进阶能力。
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考