урок 5 из 10

Клавиатуры и кнопки

Различать обычную клавиатуру (нажатие — текст) и встроенную (нажатие — callback с данными); строить главное меню и кнопку телефона; ловить нажатия встроенных кнопок, отвечать callback.answer() и менять сообщение; держать данные кнопки в 64 байтах латинскими кодами «приставка:номер»; собирать клавиат

11 мин чтения · 4 главы ·4 задания ·домашняя работа

Обычная клавиатура

Писать команды и слова неудобно: человек ошибётся в букве, и фильтр F.text == "Цены" не сработает. Кнопки снимают эту проблему — нажать проще, чем набрать. У Телеграма два вида клавиатур, и устроены они совсем по-разному.

сообщение ботаЗаписатьсяЦенывместо клавиатуры телефонасообщение ботаДаНетпрямо под сообщениемнажатие = текстнажатие = callback
Обычная клавиатура заменяет буквы внизу экрана, и нажатие отправляет текст. Встроенная висит под сообщением бота, и нажатие приходит боту отдельным событием, без сообщения в чате.

Обычная клавиатура (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:

нажали«Да»callback_querydata = "yes"обработчикF.data == "yes"answer()edit_textбез callback.answer() кнопка «зависает»
Нажатие приходит как отдельное обновление с данными кнопки. Бот обязан ответить на него — иначе на кнопке висят часики — и обычно меняет само сообщение.
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». Поэтому бот проверяет их так же, как текст от человека: номер товара существует, время действительно свободно, у человека есть право на действие.

Задания урока

Каждое проверяется сразу: ответ сверяется с эталоном, и на неверный вариант приходит разбор.

  1. Нажатие кнопки обычной клавиатуры отправляет в чат текст с её надписью.легко
  2. Обычная клавиатура или встроенная?средне
  3. Что верно про обработчик нажатия встроенной кнопки?средне
  4. Впишите число.средне

Практика и материалы по теме курса

Пройти урок с проверкой заданий Курс «Телеграм-боты на Python» открыт и бесплатен, прогресс сохраняется.
Открыть курс