Reply-клавиатуры
До этого урока Цыплёнок понимал только тех, кто знает команды наизусть. Сейчас мы научим его показывать кнопки — и общаться с ним сможет даже тот, кто никогда не вводил ни одного слеша.
Главная мысль урока: reply-клавиатура — это набор кнопок, который заменяет собой обычную клавиатуру под полем ввода. Нажал кнопку — и боту улетел ровно тот текст, что на ней написан. Поэтому обрабатывается такое нажатие точно так же, как если бы пользователь напечатал это слово руками.
Зачем боту кнопки, если есть команды
В прошлом уроке ты научил Цыплёнка ловить сообщения и команды: пишешь /start — он здоровается, пишешь /help — он подсказывает. Работает отлично, но есть проблема. Чтобы пользоваться таким ботом, надо помнить, какие команды он понимает. А теперь честно: ты сам помнишь наизусть команды хотя бы одного бота, которым пользуешься? Вряд ли.
Представь, что ты сделал бота-напоминалку про домашку и кинул другу. Друг открывает чат, видит пустое поле ввода и… зависает. Что писать? /add? /new? /zadanie? Он не телепат. Через минуту он закрывает бота и больше не возвращается. Обидно — бот-то хороший.
А теперь представь другой сценарий. Друг открывает чат, и прямо над клавиатурой у него вырастают три большие кнопки: «📚 Добавить домашку», «📋 Мой список», «🗑 Очистить». Думать не надо вообще — тыкай и пользуйся. Вот это и есть reply-клавиатура. К концу урока твой Цыплёнок будет показывать такое меню, а нажатия по кнопкам ты научишься ловить как обычные сообщения.
Что такое reply-клавиатура и чем она отличается от inline
Reply-клавиатура — кнопки, которые заменяют собой клавиатуру под полем ввода и при нажатии отправляют обычный текст.
Лучшая аналогия — меню в кафе быстрого питания. Над кассой висит табло с большими картинками: «Бургер», «Картошка», «Кола». Тебе не нужно знать наизусть, что у них есть в меню, и не нужно по буквам диктовать кассиру название блюда. Ты просто показываешь пальцем: «вот это». Reply-клавиатура — это такое же табло, которое бот разворачивает прямо у пользователя под полем ввода.
И вот ключевой момент, который надо понять раз и навсегда. Когда пользователь нажимает кнопку «📋 Мой список», в чат уходит обычное текстовое сообщение со словами «📋 Мой список» — ровно теми, что написаны на кнопке. Для бота это неотличимо от ситуации, когда пользователь сам напечатал «📋 Мой список» руками и нажал «отправить». То есть reply-кнопка — это просто удобный способ ввести заранее заготовленный текст, не печатая его.
В Telegram есть и второй вид кнопок — inline. Чтобы ты не путал их с самого начала, держи сравнение. Подробно про inline будет в следующем уроке, а пока запомни главное различие.
Inline-кнопка — кнопка прямо под сообщением; при нажатии она шлёт боту callback, а не текст в чат.
| Вопрос | Reply-клавиатура | Inline-кнопка |
| Где находится? | Внизу, вместо обычной клавиатуры | Прямо под конкретным сообщением |
| Что улетает боту при нажатии? | Обычный текст с надписи кнопки | Скрытый сигнал — callback |
| Видно ли нажатие в чате? | Да, появляется сообщение от пользователя | Нет, чат не засоряется |
| Как ловит бот? | Обычным хэндлером сообщений | Отдельным хэндлером callback'ов |
| Для чего удобна? | Главное меню, постоянные разделы | Голосования, кнопки под конкретным постом |
Запомни так: reply — это «напечатай за меня вот этот текст», inline — это «нажми, но текст в чате не появится». В этом уроке мы целиком про reply.
Строим первую клавиатуру через ReplyKeyboardBuilder
В aiogram 3.x клавиатуры удобнее всего собирать через специального помощника — билдер (от английского build, «строить»). Для reply-клавиатур он называется ReplyKeyboardBuilder. Работает он как конструктор: ты добавляешь кнопки по одной, а в конце говоришь «собери всё в готовую клавиатуру».
Добавим в bot.py Цыплёнка простое меню из двух кнопок. Не забудь импорты вверху файла.
from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.types import Message, KeyboardButton
from aiogram.utils.keyboard import ReplyKeyboardBuilder
import os
bot = Bot(token=os.getenv("BOT_TOKEN"))
dp = Dispatcher()
@dp.message(CommandStart())
async def start_handler(message: Message):
builder = ReplyKeyboardBuilder()
builder.add(KeyboardButton(text="📚 Добавить домашку"))
builder.add(KeyboardButton(text="📋 Мой список"))
await message.answer(
"Привет! Я Цыплёнок-помощник 🐥 Выбери, что сделать:",
reply_markup=builder.as_markup(),
)Результат: в чате бот ответит «Привет! Я Цыплёнок-помощник. Выбери, что сделать:», и под полем ввода появятся две кнопки — «📚 Добавить домашку» и «📋 Мой список». Обычная клавиатура спрячется, вместо неё будут эти кнопки.
Разберём по шагам, что мы сделали:
builder = ReplyKeyboardBuilder()— создаём пустой конструктор клавиатуры. Пока кнопок в нём нет.builder.add(KeyboardButton(text="..."))— добавляем одну кнопку.KeyboardButton— это и есть одна кнопка, аtext— надпись на ней. Именно этот текст улетит боту при нажатии.builder.as_markup()— говорим конструктору: «всё, собирай готовую клавиатуру». Метод возвращает объект разметки, который понимает Telegram.reply_markup=builder.as_markup()— передаём собранную клавиатуру вmessage.answer(...). Параметрreply_markup— это и есть «прицепи к сообщению вот такие кнопки».
Главное, что нужно усвоить: клавиатура не существует сама по себе, она прицепляется к сообщению через reply_markup. Нет сообщения — некуда вешать кнопки.
Раскладываем кнопки рядами и включаем resize_keyboard
Если ты запустил пример выше, то заметил две вещи. Во-первых, обе кнопки встали в один длинный ряд и растянулись на всю ширину. Во-вторых, кнопки получились непривычно высокими — Telegram по умолчанию делает их огромными. Чинится и то, и другое.
Сколько кнопок в ряд: метод adjust
За раскладку отвечает метод builder.adjust(...). Ты передаёшь ему числа — сколько кнопок поставить в каждый ряд. Например, adjust(1) — по одной кнопке в строке (столбиком), adjust(2) — по две, adjust(2, 1) — в первом ряду две кнопки, во втором одна.
Сделаем для Цыплёнка меню из трёх кнопок: две сверху в ряд, одна снизу во всю ширину.
@dp.message(CommandStart())
async def start_handler(message: Message):
builder = ReplyKeyboardBuilder()
builder.add(KeyboardButton(text="📚 Добавить домашку"))
builder.add(KeyboardButton(text="📋 Мой список"))
builder.add(KeyboardButton(text="🗑 Очистить всё"))
builder.adjust(2, 1)
await message.answer(
"Что сделаем? 🐥",
reply_markup=builder.as_markup(resize_keyboard=True),
)Результат: в чате бот пришлёт «Что сделаем?» и покажет аккуратное меню: в верхнем ряду рядом стоят «📚 Добавить домашку» и «📋 Мой список», а под ними на всю ширину — «🗑 Очистить всё». Кнопки будут компактными по высоте.
Что тут нового:
builder.adjust(2, 1)— раскладываем кнопки: 2 в первый ряд, 1 во второй. Билдер берёт кнопки в том порядке, в каком мы их добавляли, и режет на ряды по этим числам.builder.as_markup(resize_keyboard=True)— вот это важно. По умолчанию Telegram делает кнопки гигантскими во весь экран. resize_keyboard=True просит мессенджер ужать клавиатуру под реальный размер кнопок, чтобы она не занимала пол-экрана. Включай этот параметр почти всегда — без него меню выглядит неряшливо.
Ещё пара полезных настроек as_markup
У as_markup() есть и другие удобные параметры, которые пригодятся:
one_time_keyboard=True— клавиатура спрячется сразу после того, как пользователь нажал кнопку. Удобно, когда меню нужно показать один раз, а потом убрать с глаз.input_field_placeholder="Выбери кнопку ниже"— серая подсказка внутри пустого поля ввода. Мягко намекает пользователю, что от него хотят.
Собрать их можно вместе: builder.as_markup(resize_keyboard=True, input_field_placeholder="Выбери действие").
Ловим нажатие кнопки как обычный текст
А теперь — самое главное и самое приятное. Помнишь, мы выяснили: нажатие reply-кнопки = обычное текстовое сообщение с надписью кнопки? Значит, и ловить его надо обычным хэндлером сообщений, ровно так, как ты делал в прошлом уроке. Никакой магии.
Чтобы поймать конкретную кнопку, мы фильтруем сообщения по тексту через F.text == "...". F (магический фильтр) — это удобный способ сказать aiogram «реагируй только на сообщения, у которых текст равен вот этой строке».
from aiogram import F
@dp.message(F.text == "📋 Мой список")
async def show_list(message: Message):
await message.answer("Пока твой список домашки пуст 📭")
@dp.message(F.text == "📚 Добавить домашку")
async def add_homework(message: Message):
await message.answer("Окей! Напиши, что задали — я запомню ✍️")
@dp.message(F.text == "🗑 Очистить всё")
async def clear_all(message: Message):
await message.answer("Готово, список чистый 🧹")Результат: в чате когда пользователь нажмёт кнопку «📋 Мой список», бот ответит «Пока твой список домашки пуст». Нажмёт «📚 Добавить домашку» — Цыплёнок попросит написать задание. Нажмёт «🗑 Очистить всё» — отрапортует, что список чистый. Каждая кнопка ведёт к своему хэндлеру.
Обрати внимание на главную хитрость: текст в фильтре F.text == "📋 Мой список" должен совпадать с надписью на кнопке до последнего символа, включая эмодзи и пробелы. Если на кнопке написано «📋 Мой список», а в фильтре ты напишешь «Мой список» (без эмодзи) — хэндлер не сработает, потому что тексты не равны.
Маленький разбор: почему так важно точное совпадение
Чтобы прочувствовать, как именно сравниваются строки, давай на чистом Python смоделируем то, что aiogram делает внутри при F.text == "...". Это просто сравнение двух строк:
# текст, который пришёл боту (нажата кнопка)
incoming = "📋 Мой список"
# тексты кнопок, которые мы ждём
buttons = {
"📚 Добавить домашку": "add",
"📋 Мой список": "list",
"🗑 Очистить всё": "clear",
}
# aiogram по сути делает вот такое сравнение
for label, action in buttons.items():
if incoming == label:
print("Совпало! Вызываем действие:", action)
break
else:
print("Ни одна кнопка не подошла")
# а теперь покажем, что лишний пробел всё ломает
print("С пробелом совпадает?", incoming == "📋 Мой список ")Вывод:
Совпало! Вызываем действие: list С пробелом совпадает? False
Видишь? Один лишний пробел в конце — и строки уже не равны, сравнение даёт False. Поэтому в реальном коде надписи кнопок и тексты фильтров удобно держать в одном месте (например в переменных), чтобы не было опечаток. Об этом — в подводных камнях ниже.
Как убрать клавиатуру
Иногда меню нужно спрятать — например, диалог закончился. Для этого есть специальная разметка ReplyKeyboardRemove. Прицепляешь её к любому сообщению — и клавиатура исчезает.
from aiogram.types import ReplyKeyboardRemove
@dp.message(F.text == "🗑 Очистить всё")
async def clear_all(message: Message):
await message.answer(
"Список очищен, прячу меню 👋",
reply_markup=ReplyKeyboardRemove(),
)Результат: в чате бот напишет «Список очищен, прячу меню», и кнопки под полем ввода пропадут — у пользователя вернётся обычная клавиатура.
Частые ошибки и подводные камни
1. Текст кнопки и текст фильтра не совпадают
Самая частая беда новичков. На кнопке написано «📋 Мой список» (с эмодзи), а в хэндлере ты фильтруешь по «Мой список» (без эмодзи) — и нажатие проваливается, бот молчит. Telegram присылает ровно ту строку, что на кнопке, со всеми эмодзи и пробелами. Лечится так: заведи надписи кнопок в переменные и используй одни и те же переменные и при создании кнопки, и в фильтре. Тогда опечатка физически невозможна.
2. Забыл resize_keyboard и удивляешься гигантским кнопкам
Без resize_keyboard=True Telegram раздувает кнопки на пол-экрана — выглядит так, будто что-то сломалось. Ничего не сломалось, это поведение по умолчанию. Просто почти всегда добавляй resize_keyboard=True в as_markup(...), и меню станет аккуратным.
3. Пытаешься поймать reply-кнопку как callback
Если ты уже слышал про inline-кнопки и хэндлеры callback_query, легко перепутать. Reply-кнопка не шлёт никакого callback — она шлёт обычный текст. Поэтому ловить её надо хэндлером @dp.message(...), а не @dp.callback_query(...). Хэндлер callback'а на reply-кнопку не сработает никогда.
4. Создал клавиатуру, но забыл передать её в reply_markup
Можно идеально собрать билдер, вызвать as_markup() — и не передать результат в сообщение. Тогда кнопок пользователь не увидит: клавиатура существует в коде, но её никуда не «прицепили». Всегда проверяй, что результат builder.as_markup() ушёл в параметр reply_markup=... метода answer или send_message.
5. Ждёшь, что более широкий хэндлер перехватит кнопку
Если у тебя есть общий хэндлер «лови любое текстовое сообщение» и он объявлен выше хэндлеров кнопок, он может перехватить нажатие первым — aiogram идёт по хэндлерам сверху вниз и останавливается на первом подходящем. Поэтому конкретные хэндлеры кнопок (F.text == "...") ставь выше общего «ловлю всё», иначе кнопки перестанут работать.
Мини-практика: меню для бота-опросника
Закрепляем руками. Ты делаешь бота, который опрашивает друзей, куда пойти гулять. Сделай вот что:
- Собери клавиатуру из четырёх кнопок: «🎮 Компьютерный клуб», «🍕 Пиццерия», «🏀 Площадка», «🏠 Остаться дома». Разложи их по две в ряд через
adjust(2, 2)и не забудьresize_keyboard=True. - Покажи это меню на команду
/startс приветствием вроде «Куда сегодня?». - Напиши по хэндлеру на каждую кнопку через
F.text == "...", чтобы бот отвечал что-то в тему: на «🍕 Пиццерия» — «Отличный выбор, беру двойной сыр!», на «🏠 Остаться дома» — «Тоже вариант, врубаем сериал 🍿» и так далее. - Спрячь меню после выбора через
ReplyKeyboardRemove()— пусть после голоса клавиатура убирается.
Со звёздочкой: вынеси все четыре надписи кнопок в переменные в начале файла (например BTN_PIZZA = "🍕 Пиццерия") и используй эти переменные и в KeyboardButton(text=BTN_PIZZA), и в фильтре F.text == BTN_PIZZA. Прочувствуй, как это убивает класс ошибок с несовпадением текста.
Итоги
Соберём всё в несколько мыслей, которые надо унести с собой:
- Reply-клавиатура заменяет обычную клавиатуру под полем ввода. Нажатие кнопки = отправка обычного текста с её надписи. Это меню кафе: показал пальцем — заказал.
- Собираем через ReplyKeyboardBuilder: добавляем кнопки
add(KeyboardButton(text=...)), раскладываем по рядамadjust(...)и собираемas_markup(resize_keyboard=True). Готовую клавиатуру вешаем на сообщение черезreply_markup. - Ловим нажатие обычным хэндлером сообщений с фильтром
F.text == "надпись кнопки"— текст в фильтре обязан совпадать с надписью до последнего символа. - Главные грабли: несовпадение текстов, забытый
resize_keyboardи попытка ловить reply-кнопку как callback.
Теперь твой Цыплёнок стал по-настоящему дружелюбным: с ним справится даже тот, кто впервые видит бота. Но у reply-кнопок есть ограничение — каждое нажатие засоряет чат сообщением, да и под конкретным постом их не повесишь. В следующем уроке мы возьмёмся за inline-кнопки: они живут прямо под сообщением, не оставляют следов в переписке и шлют боту скрытый сигнал-callback. Там Цыплёнок научится показывать кнопки прямо под своими ответами. Поехали дальше 🐥