ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

aiogram 键盘构建器(Keyboard Builder)实战指南:动态生成 Inline 与 Reply 键盘

aiogram 键盘构建器(Keyboard Builder)实战指南:动态生成 Inline 与 Reply 键盘
  • 后端
  • 即时通讯
  • API设计

【免费下载链接】aiogram

aiogram is a modern and fully asynchronous framework for Telegram Bot API written in Python using asyncio

项目地址:https://gitcode.com/gh_mirrors/ai/aiogram
点击查看免费下载

导读

在 aiogram 3.x 中,Telegram 键盘(内联键盘与回复键盘)不再需要手工拼装list[list[Button]]数据结构,而是可以通过aiogram.utils.keyboard提供的KeyboardBuilder系列工具动态生成、调整与合并键盘布局。本文以官方文档 docs/utils/keyboard.rst 为核心,结合 keyboard.py 源码 与 测试用例,完整讲解InlineKeyboardBuilder与ReplyKeyboardBuilder的用法、参数、底层实现与常见限制,读完即可在自己的机器人中按行调整按钮布局、拼接多组按钮并直接发送。

一、何时该用 Builder?静态 vs 动态

KeyboardBuilder的核心价值在于动态生成键盘 markup。官方文档特别提醒(docs/utils/keyboard.rst):

如果你的键盘是静态的、布局固定的,最好直接显式定义InlineKeyboardMarkup,而不必使用 builder;但如果你需要根据运行时的数据动态决定按钮数量和布局,就可以放心使用 builder。

也就是说:

  • 静态场景:直接构造InlineKeyboardMarkup(inline_keyboard=[[...]])或ReplyKeyboardMarkup(keyboard=[[...]])更简洁、可读性更高;
  • 动态场景:例如根据数据库中的商品列表、用户选项数量不确定、需要按固定行宽自动换行时,builder 能大幅减少手写逻辑。

KeyboardBuilder是一个泛型抽象基类(源码见 keyboard.py),其下有两个具体实现:

类对应的 Telegram 类型单行最大按钮数(max_width)按钮总数上限(max_buttons)
InlineKeyboardBuilderInlineKeyboardMarkup8100
ReplyKeyboardBuilderReplyKeyboardMarkup10300

这两个上限定义在源码类属性中(keyboard.py 与 keyboard.py),与 Telegram Bot API 对键盘行数与按钮数的约束保持一致,超过会抛出ValueError。

二、快速上手:生成一个 10 按钮的内联键盘

官方文档给出的第一个示例是生成包含 10 个按钮的内联键盘:

from aiogram.utils.keyboard import InlineKeyboardBuilder builder = InlineKeyboardBuilder() for index in range(1, 11): builder.button(text=f"Set {index}", callback_data=f"set:{index}")

要点:

  • builder.button(...)每次添加一个按钮,内部会构造InlineKeyboardButton并追加到 builder 中(源码见 keyboard.py);
  • text是按钮显示的文本,callback_data是点击后回传给 Bot 的字符串(Telegram 限制为 1-64 字节);
  • button()支持几乎所有InlineKeyboardButton字段,具体参数见下文第四节。

三、调整布局:adjust()按行宽重排

10 个按钮如果什么都不做,默认会被add()按 max_width=8 自动换行,得到 8 + 2 两行。若希望“第一行 3 个按钮,之后每行 2 个”,官方文档给出了adjust用法:

builder.adjust(3, 2)

adjust(*sizes, repeat=False)的作用是把已添加的所有按钮按指定行宽重新排布(源码见 keyboard.py,这里修正为 keyboard.py):

  • 依次使用传入的尺寸;当传入尺寸之和小于按钮总数时,最后一个尺寸会被复用来填充剩余按钮;
  • 传repeat=True时,所有尺寸会循环使用;
  • 不传任何尺寸时,等价于adjust(max_width)(Inline 为 8,Reply 为 10);
  • 每个尺寸必须是[min_width, max_width]范围内的整数,否则抛ValueError。

测试用例对此行为做了精确验证(tests/test_utils/test_keyboard.py):

按钮数sizesrepeat结果形状
10(3, 2, 1)False[3, 2, 1, 1, 1, 1, 1]
12空False[10, 2](Reply,max_width=10)
12(3, 2, 1)True[3, 2, 1, 3, 2, 1]

源码中repeat_last生成器(keyboard.py)实现了“最后一个尺寸重复使用”的默认语义;repeat=True则借助itertools.cycle循环全部尺寸。

四、合并与拼接:attach()与from_markup()

动态构建时经常需要把“另一个 builder”或“已生成好的 markup”并入当前 builder。官方文档给出了两种方式:

4.1 拼接另一个 builder

another_builder = InlineKeyboardBuilder(...) # 另一个含按钮的 builder builder.attach(another_builder)

attach()源码实现(keyboard.py)会校验:

  • 参数必须是KeyboardBuilder实例,否则抛ValueError;
  • 必须与当前 builder 的按钮类型一致——InlineKeyboardBuilder只能 attachInlineKeyboardBuilder,ReplyKeyboardBuilder只能 attachReplyKeyboardBuilder。官方文档以 warning 明确提示“不能把回复键盘 builder 附加到内联键盘 builder,反之亦然”,源码中的_button_type类型检查(keyboard.py)保证了这一点,测试用例 test_attach_invalid_button_type 也覆盖了混用会抛ValueError的场景。

4.2 拼接已有 markup

markup = InlineKeyboardMarkup(inline_keyboard=[...]) # 一些已生成好的 markup builder.attach(InlineKeyboardBuilder.from_markup(markup))

from_markup是一个类方法,接收InlineKeyboardMarkup(或ReplyKeyboardMarkup)并把它内部的行结构提取出来作为初始数据(源码见 keyboard.py 与 keyboard.py):

@classmethod def from_markup(cls, markup): return cls(markup=markup.inline_keyboard) # 或 markup.keyboard

这非常实用:当你手上有旧代码生成的静态 markup,又想继续用 builder 追加按钮时,无需手工解析二维数组。

五、导出并发送:as_markup()

最终把 builder 转成可在消息中使用的 markup:

await message.answer("Some text here", reply_markup=builder.as_markup())

as_markup()内部调用export()取出深拷贝的按钮二维数组,并构造对应的 markup 对象(keyboard.py):

  • InlineKeyboardBuilder.as_markup()返回InlineKeyboardMarkup;
  • ReplyKeyboardBuilder.as_markup(resize_keyboard=True, ...)返回ReplyKeyboardMarkup,且可以透传is_persistent、resize_keyboard、one_time_keyboard、input_field_placeholder、selective、force_reply等参数(这些字段定义见 reply_keyboard_markup.py)。

需要注意:as_markup()不会清空 builder 内部状态,多次调用会得到内容相同的 markup;而export()每次返回的是深拷贝(deepcopy,见 keyboard.py),测试 test_export 验证了修改导出结果不会影响 builder 内部数据。

六、ReplyKeyboardBuilder:同样的接口

官方文档指出“回复键盘 builder 拥有相同的接口”。示例见仓库中的问答场景 examples/quiz_scene.py:

markup = ReplyKeyboardBuilder() markup.add(*[KeyboardButton(text=answer.text) for answer in quiz.answers]) if step > 0: markup.button(text="🔙 Back") markup.button(text="🚫 Exit") await message.answer( text=QUESTIONS[step].text, reply_markup=markup.adjust(2).as_markup(resize_keyboard=True), )

这里混合使用了三种操作:

  1. add(*buttons):一次性追加多个已构造好的KeyboardButton(add会尽量把按钮填入最后一行剩余空位,再按 max_width 换新行,见 keyboard.py);
  2. button(text=...):便捷地构造并添加单个按钮;
  3. adjust(2)+as_markup(resize_keyboard=True):两列排布并导出,同时让 Telegram 客户端按需缩放键盘高度。

ReplyKeyboardBuilder.button()的专属参数(源码 keyboard.py)对应 KeyboardButton 字段:

参数类型说明
textstr(必填)按钮文本;未使用其他特殊字段时,点击会直接把文本作为消息发送
icon_custom_emoji_idstr \| None按钮文本前显示的自定义 emoji ID
stylestr \| None按钮样式:danger(红)/success(绿)/primary(蓝)
request_usersKeyboardButtonRequestUsers \| None点击后弹出用户选择器,仅私聊
request_chatKeyboardButtonRequestChat \| None点击后弹出聊天选择器,仅私聊
request_contactbool \| None请求用户电话号码,仅私聊
request_locationbool \| None请求用户当前位置,仅私聊
request_pollKeyboardButtonPollType \| None请求用户创建并发送投票,仅私聊
web_appWebAppInfo \| None点击后启动 Web App,仅私聊

七、InlineKeyboardBuilder.button() 参数全解

官方文档通过 autoclass 声明的button()方法签名(docs/utils/keyboard.rst)在源码中已扩展为更完整的参数列表(keyboard.py),对应 InlineKeyboardButton 的所有字段:

参数类型说明
textstr(必填)按钮标签文本
icon_custom_emoji_idstr \| None文本前的自定义 emoji
stylestr \| None按钮颜色样式(同上)
urlstr \| None打开的外部 HTTP(S) 或tg://链接
callback_datastr \| CallbackData \| None回传数据,1-64 字节;也接受CallbackData实例
web_appWebAppInfo \| None启动 Web App,仅私聊
login_urlLoginUrl \| NoneTelegram 登录小部件式的授权链接
switch_inline_querystr \| None切换到选中的聊天并插入内联查询
switch_inline_query_current_chatstr \| None在当前聊天输入框插入内联查询
switch_inline_query_chosen_chatSwitchInlineQueryChosenChat \| None按类型选择聊天后插入内联查询
copy_textCopyTextButton \| None点击复制指定文本到剪贴板
callback_gameCallbackGame \| None启动 HTML5 游戏
paybool \| None设为True显示支付按钮(⭐与XTR会被替换为 Star 图标)

其中url、callback_data、web_app、login_url、switch_inline_query*、copy_text、callback_game、pay这些字段互斥——Telegram 要求一个按钮除text、icon_custom_emoji_id、style外只能指定一种行为类型(见 inline_keyboard_button.py 的类文档)。

7.1 与 CallbackData 工厂的集成

callback_data参数除了接收原始字符串,还可以直接传入CallbackData实例。底层_button()方法会检测到CallbackData类型并自动调用其pack()方法生成回调字符串(keyboard.py):

def _button(self, **kwargs): if isinstance(callback_data := kwargs.get("callback_data"), CallbackData): kwargs["callback_data"] = callback_data.pack() button = self._button_type(**kwargs) return self.add(button)

CallbackData的工厂机制定义在 callback_data.py:子类需声明prefix,字段会按声明顺序以:分隔拼装,并强制校验最终长度不超过 64 字节。官方文档 callback_data.rst 给出了完整配合示例:

class AdminAction(CallbackData, prefix="adm"): action: Action # 字符串枚举 chat_id: int user_id: int # 在处理器内部 builder = InlineKeyboardBuilder() for action in Action: builder.button( text=action.value.title(), callback_data=AdminAction(action=action, chat_id=chat_id, user_id=user_id), ) await bot.send_message(admins_chat, f"...", reply_markup=builder.as_markup())

之后用AdminAction.filter(F.action == Action.ban)过滤器即可精准命中对应回调(测试 test_button 验证了callback_data=MyCallback(value="test")会被打包为字符串"test:test"存入按钮)。

八、其他常用方法:buttons、copy、export、row

buttons属性

以生成器形式展平所有按钮(keyboard.py),常用于遍历或统计按钮总数:

for btn in builder.buttons: print(btn.text)

copy()

返回一个与当前 builder 内容完全相同的新实例(深拷贝),用于在不污染原 builder 的前提下继续调整(源码 keyboard.py)。测试 test_copy 验证了两个副本相互独立:对副本执行add/row不会影响原 builder。

export()

返回深拷贝的list[list[Button]],可手工交给InlineKeyboardMarkup/ReplyKeyboardMarkup构造(keyboard.py):

markup = InlineKeyboardMarkup(inline_keyboard=builder.export())

row(*buttons, width=None)

显式添加一整行按钮;行宽默认取max_width,也可用width参数指定。当传入按钮超过行宽时会自动拆成多行(keyboard.py)。与adjust的区别在于:row是“边构建边定行”,adjust是“先全部添加、最后统一重排”。

九、底层实现原理与数据校验

KeyboardBuilder在构造时即对 markup 结构做严格校验(keyboard.py):

  • button_type必须是InlineKeyboardButton或KeyboardButton的子类,否则抛ValueError;
  • 初始 markup 必须是list[list[Button]],每行长度不得超过max_width,按钮总数不得超过max_buttons,否则抛ValueError(对应_validate_row/_validate_markup,见 keyboard.py);
  • adjust/row的行宽必须落在[min_width, max_width]区间内且为整数(_validate_size,见 keyboard.py)。

这些校验在 tests/test_utils/test_keyboard.py 中都有对应用例:非法的按钮类型、超长的行、超量的按钮总数、非整数行宽等都会抛出ValueError,保证最终生成的 markup 一定符合 Telegram API 的约束。

十、注意事项小结

  1. 静态优先:布局固定的键盘直接写InlineKeyboardMarkup/ReplyKeyboardMarkup,动态内容再使用 builder;
  2. 类型不可混搭:attach()只允许同类型 builder 互相拼接,内联与回复键盘互不兼容(keyboard.py);
  3. 行宽与总数上限:Inline 每行最多 8 个、总数最多 100;Reply 每行最多 10 个、总数最多 300,违反会抛ValueError;
  4. adjust的尾部复用规则:默认最后一个尺寸重复使用,repeat=True才循环全部尺寸;
  5. export()返回深拷贝:可以放心修改导出结果而不影响 builder 内部状态。

结语

InlineKeyboardBuilder与ReplyKeyboardBuilder是 aiogram 3.x 处理动态键盘的标准工具:button()负责便捷地造按钮,add/row/adjust负责布局,attach/from_markup负责合并,as_markup()负责交付给消息发送接口。配合CallbackData工厂还可以获得类型安全的回调数据链路。更多 API 细节可查阅 keyboard.py 源码、官方文档 docs/utils/keyboard.rst 以及完整的 test_keyboard.py 测试套件。

  • 后端
  • 即时通讯
  • API设计

【免费下载链接】aiogram

aiogram is a modern and fully asynchronous framework for Telegram Bot API written in Python using asyncio

项目地址:https://gitcode.com/gh_mirrors/ai/aiogram
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进