news 2026/9/14 18:17:52

QMK Firmware Combo(连击)功能完全指南:从基础定义到源码级高级配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QMK Firmware Combo(连击)功能完全指南:从基础定义到源码级高级配置

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 判定时间内同时按下AB,可以输出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中需要做两件事:

  1. 定义一个以COMBO_END结尾的按键序列数组(称为 chord keys,即组成该 Combo 的键);
  2. 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! };

效果:同时按下AB输出Esc;同时按下CD输出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_TAPQK_LAYER_TAPQK_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) };

上述例子中同时按下ABC时,只会触发 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; } }

效果:同时按下EM输出邮箱地址;同时按下BackspaceLeft 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 中使用以下键码随时开关:

KeycodeAliases功能
QK_COMBO_ONCM_ON开启 Combo 功能
QK_COMBO_OFFCM_OFF关闭 Combo 功能
QK_COMBO_TOGGLECM_TOGG切换 Combo 功能的开关状态

源码佐证:QK_COMBO_ON = 0x7C50QK_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
8QMK 默认
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 88(即(EXTRA_)EXTRA_LONG_COMBOS允许的最大键数)
#define COMBO_BUFFER_LENGTH 44

源码佐证:两个宏的默认值在 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_MODSCOMBO_HOLD_TERM

若 Combo 的结果是修饰键(Modifier),其处理时间窗可独立于普通 Combo 延长。默认关闭,可通过以下方式开启:

#define COMBO_MUST_HOLD_MODS #define COMBO_HOLD_TERM 150
  • COMBO_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_COMBOuint16_t get_combo_term(uint16_t combo_index, combo_t *combo)可选:按 Combo 设置独立超时窗口(默认COMBO_TERM
COMBO_MUST_HOLD_PER_COMBObool get_combo_must_hold(uint16_t combo_index, combo_t *combo)控制该 Combo 是轻点立即触发,还是必须按住(默认false
COMBO_MUST_TAP_PER_COMBObool get_combo_must_tap(uint16_t combo_index, combo_t *combo)控制该 Combo 是否仅在COMBO_HOLD_TERM内轻点才触发(默认false
COMBO_MUST_PRESS_IN_ORDER_PER_COMBObool 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_layerCOMBO_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的各个caseg/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 18:17:36

无人机协同跟踪控制:APF与MPC结合实践

1. 项目概述&#xff1a;当无人机遇上协同跟踪控制去年夏天我在调试无人机编队时&#xff0c;遇到一个典型场景&#xff1a;三架无人机需要协同跟踪一辆移动中的地面车辆。传统PID控制下&#xff0c;当目标突然转向时&#xff0c;编队会出现明显的振荡和滞后。这正是APF&#x…

作者头像 李华
网站建设 2026/9/14 18:17:11

dirsearch实战:敏感目录泄露挖掘与字典爆破原理详解

做授权渗透测试或者企业安全巡检时&#xff0c;我最深的体会是&#xff1a;信息收集这一阶段做得扎不扎实&#xff0c;直接决定后续测试能走多远。有一次目标只有一个登录框&#xff0c;常规漏扫跑了一天没结果&#xff0c;我改用 dirsearch 挂上一份中大型字典&#xff0c;扫了…

作者头像 李华
网站建设 2026/9/14 18:15:52

YOLOv10+DeepSort:工程上最稳的实时多目标跟踪组合

简介&#xff1a;面向计算机视觉与深度学习学习者的YOLOv10DeepSort视频移动目标跟踪实战项目&#xff0c;主要解决智能监控、行人跟踪、交通巡检等场景中的实时检测与稳定跟踪问题。项目将YOLOv10的高效检测与DeepSort深度特征关联有机结合&#xff0c;完整覆盖目标定位、特征…

作者头像 李华
网站建设 2026/9/14 18:15:01

Dagger TypeScript SDK 指南:EngineCacheEntrySet 缓存条目集 API 详解

Dagger TypeScript SDK 指南&#xff1a;EngineCacheEntrySet 缓存条目集 API 详解 【免费下载链接】dagger Automation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud 项目地址: https://gitcode.com/GitHub_Trending/da/da…

作者头像 李华
网站建设 2026/9/14 18:14:07

花15亿美元买下35人团队,巨头却坚决不办收购

花15亿美元买下35人团队&#xff0c;巨头却坚决不办收购 一家成立才一年多、全公司一共只有大约 35 人的初创团队&#xff0c;竟然让科技巨头心甘情愿端出了超过 15 亿美元的谈判对价。但更离奇的地方在于&#xff0c;当这笔让人咋舌的巨资落地之后&#xff0c;买方既没有把它买…

作者头像 李华