Обычная клавиатура
Писать команды и слова неудобно: человек ошибётся в букве, и фильтр F.text == "Цены" не сработает. Кнопки снимают эту проблему — нажать проще, чем набрать. У Телеграма два вида клавиатур, и устроены они совсем по-разному.
Обычная клавиатура (reply-клавиатура) появляется вместо букв внизу экрана. Нажатие на её кнопку ничем не отличается от того, что человек сам набрал этот текст: в чат уходит обычное сообщение «Записаться». Поэтому ловят его привычным фильтром по тексту.
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove
menu = ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="Записаться"), KeyboardButton(text="Цены")],
[KeyboardButton(text="Контакты")],
],
resize_keyboard=True,
)
@dp.message(CommandStart())
async def start(message: Message):
await message.answer("Выберите, что нужно:", reply_markup=menu)
@dp.message(F.text == "Цены")
async def prices(message: Message):
await message.answer("Стрижка — 1500 ₽")
Клавиатура описывается рядами: список списков, каждый внутренний список — одна строка кнопок. Здесь в первом ряду две кнопки, во втором одна. resize_keyboard=True делает кнопки по размеру текста; без него они растягиваются на пол-экрана.
Клавиатура остаётся на экране, пока бот её не уберёт. Убирают её, отправив сообщение с ReplyKeyboardRemove(). У обычной клавиатуры есть и особые кнопки: с request_contact=True она отправит номер телефона человека (после его подтверждения), с request_location=True — место на карте. Это единственный честный способ получить телефон: вводить его руками дольше, и в нём ошибаются.
Встроенная клавиатура
Встроенная клавиатура (inline-клавиатура) крепится прямо к сообщению бота. У каждой её кнопки есть надпись, которую видит человек, и скрытые данные — callback_data, которые получит бот. Нажатие не отправляет в чат никакого текста: боту приходит отдельное обновление с этими данными.
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
confirm = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Да, записаться", callback_data="book_yes"),
InlineKeyboardButton(text="Нет", callback_data="book_no")],
[InlineKeyboardButton(text="Как добраться", url="https://example.com/map")],
])
await message.answer("Записать вас на завтра, 15:00?", reply_markup=confirm)
Почему это удобно. Во-первых, чат остаётся чистым: нет сообщений «Да», «Нет», «Да». Во-вторых, надпись и данные разные: человек видит «Да, записаться», а бот получает короткий код book_yes, и надпись можно переписать, не трогая обработчик. В-третьих, кнопка привязана к конкретному сообщению: если у человека в чате три предложения записи, бот знает, под каким нажали.
Кнопка может вместо данных вести на ссылку (параметр url) — тогда нажатие откроет сайт или карту, а боту ничего не придёт.
Что выбрать? Обычная клавиатура хороша для постоянного главного меню: оно всегда под рукой. Встроенная — для вопросов и действий под конкретным сообщением: подтвердить, выбрать время, перелистнуть страницу, оценить. В одном сообщении может быть только одна клавиатура — либо обычная, либо встроенная.
Нажатие и данные
Нажатие встроенной кнопки ловят не обработчиком сообщений, а обработчиком callback_query:
from aiogram.types import CallbackQuery
@dp.callback_query(F.data == "book_yes")
async def book_yes(callback: CallbackQuery):
await callback.message.edit_text("Готово! Ждём вас завтра в 15:00.")
await callback.answer()
@dp.callback_query(F.data == "book_no")
async def book_no(callback: CallbackQuery):
await callback.message.edit_text("Хорошо, не записываю.")
await callback.answer("Отменено")
В callback.data лежат данные кнопки, в callback.from_user — кто нажал, в callback.message — сообщение, под которым была кнопка. Обычно бот меняет это сообщение через edit_text: вопрос превращается в ответ, кнопки исчезают, и человек не нажмёт «Да» второй раз.
callback.answer() обязателен. Пока бот не ответил на нажатие, на кнопке у человека крутятся часики, и через несколько секунд кажется, что бот завис. Ответ можно дать пустым, а можно с коротким текстом — он всплывёт уведомлением вверху экрана и исчезнет сам. Вызывают answer в каждом обработчике нажатий, даже если больше ничего делать не нужно.
У данных кнопки есть предел — 64 байта. Латинская буква и цифра занимают байт, а русская буква — два. Поэтому в данные пишут короткие латинские коды, а не фразы: «book_yes», а не «да, записать меня». Если кнопок много однотипных — товары, время, страницы, — данные составляют из приставки и номера: «buy:42», «slot:1500», «page:3». Ловят их по приставке, а номер достают разбором строки.
@dp.callback_query(F.data.startswith("buy:"))
async def buy(callback: CallbackQuery):
item_id = int(callback.data.split(":")[1])
await callback.answer(f"Товар {item_id} в корзине")
Построитель и раскладка
Когда кнопок много и они берутся из данных — список товаров, свободное время, — писать ряды руками неудобно. Для этого есть построитель клавиатуры:
from aiogram.utils.keyboard import InlineKeyboardBuilder
slots = ["10:00", "11:30", "13:00", "15:00", "16:30"]
builder = InlineKeyboardBuilder()
for s in slots:
builder.button(text=s, callback_data=f"slot:{s.replace(':', '')}")
builder.adjust(3) # по три кнопки в ряд
await message.answer("Свободное время:", reply_markup=builder.as_markup())
builder.button добавляет кнопку, adjust(3) раскладывает их по три в ряд (последний ряд — сколько останется), as_markup() превращает всё в готовую клавиатуру. Двоеточие из времени убрано нарочно: оно служит разделителем в данных, и «slot:15:00» разобралось бы неверно.
Несколько правил раскладки, которые делают бота удобным:
| правило | почему |
|---|---|
| 1–3 кнопки в ряд | на телефоне узкий экран, длинные надписи обрезаются |
| короткие надписи | «15:00», а не «Записаться на 15:00» |
| кнопка «Назад» | человек должен выйти из любого экрана |
| не больше 8–10 кнопок | дальше — страницы «‹ 1/3 ›» |
И одно правило безопасности: данным кнопки нельзя слепо верить. Изменённое приложение может прислать любые данные, в том числе такие, каких в ваших кнопках не было, — «buy:-5» или «slot:9999». Поэтому бот проверяет их так же, как текст от человека: номер товара существует, время действительно свободно, у человека есть право на действие.