1. Введение: От статики к интерактиву

В предыдущих частях нашего руководства мы последовательно построили архитектурно грамотный бот, который умеет отправлять отформатированный текст и медиафайлы. Однако наше взаимодействие с ним по-прежнему остается "статичным": пользователь вынужден запоминать и вводить все команды вручную.

Следующий ключевой этап в разработке любого удобного бота — это внедрение интерактивных элементов. Кнопки не только значительно улучшают пользовательский опыт, делая навигацию интуитивно понятной, но и позволяют создавать сложные, управляемые меню.

В Telegram существует два принципиально разных типа клавиатур, каждый из которых служит своей цели:

  1. ReplyKeyboardMarkup: Постоянные кнопки, заменяющие стандартную клавиатуру.

  2. InlineKeyboardMarkup: Встроенные в сообщение кнопки, которые отправляют боту "сигналы" (callback) и позволяют редактировать уже отправленный контент.

2. ReplyKeyboardMarkup: Создаем "пульт управления" ботом

Теория

Reply-клавиатура — это интерактивная панель с кнопками, которая появляется внизу экрана вместо стандартной клавиатуры для набора текста. Её главная особенность и принцип действия заключаются в том, что нажатие на любую из её кнопок эквивалентно отправке обычного текстового сообщения в чат.

Это делает её идеальным инструментом для создания главного меню и предоставления доступа к постоянным, часто используемым командам, таким как «Профиль», «Помощь», «Каталог» и т.д.

Практика: Реализация главного меню

Давайте реализуем команду /start, которая будет не только приветствовать пользователя, но и сразу предлагать ему главное меню с кнопками.

1. Архитектурный шаг: новый файл для обработчиков

Для поддержания порядка в проекте, всю логику, связанную с клавиатурами и интерактивными элементами, мы вынесем в отдельный файл. Создайте в папке handlers новый файл keyboard_handlers.py.

2. Реализация кода

Откройте этот файл и добавьте в него следующий код. Мы создадим роутер, хэндлер для команды /start и хэндлеры, которые будут реагировать на нажатия наших будущих кнопок.

# handlers/keyboard_handlers.py

from aiogram import Router, types, F
from aiogram.filters.command import Command
from aiogram.utils.keyboard import ReplyKeyboardBuilder

router = Router()

# Хэндлер на команду /start
@router.message(Command("start"))
async def cmd_start(message: types.message):
    # Создаем объект билдера для Reply-клавиатуры
    builder = ReplyKeyboardBuilder()
    
    # Добавляем кнопки
    builder.button(text="Помощь")
    builder.button(text="Показать картинку")

    # Указываем, сколько кнопок будет в одном ряду (в данном случае 2)
    builder.adjust(2)

    await message.answer(
        "Привет! Я бот с клавиатурами. Выбери опцию:",
        reply_markup=builder.as_markup(resize_keyboard=True)
    )

# Хэндлер для обработки нажатия на кнопку "Помощь"
@router.message(F.text == "Помощь")
async def get_help(message: types.Message):
    await message.answer("Это справка. Здесь пока ничего нет.")

# Хэндлер для обработки нажатия на кнопку "Показать картинку"
@router.message(F.text == "Показать картинку")
async def send_image_by_url(message: types.Message):
    image_url = "https://picsum.photos/seed/aiogram/800/600"
    
    await message.answer_photo(
        photo=image_url, 
        caption="Вот картинка, загруженная из интернета!"
    )

Разбор кода

  • ReplyKeyboardBuilder: Для создания клавиатуры мы используем удобный класс-конструктор. Он позволяет гибко добавлять кнопки и настраивать их расположение.

  • .button(text=...) и .adjust(...): Метод .button() добавляет новую кнопку с указанным текстом. Метод .adjust() "расставляет" уже добавленные кнопки по рядам. adjust(2) означает, что в каждом ряду будет по две кнопки.

  • reply_markup: В методе message.answer() мы используем параметр reply_markup, чтобы прикрепить нашу клавиатуру к сообщению. builder.as_markup() преобразует объект билдера в готовую клавиатуру.

  • resize_keyboard=True: Этот важный параметр делает кнопки компактными и удобными, подгоняя их размер под содержимое, а не растягивая на всю высоту экрана.

Новый подход: Отправка медиа по URL

Обратите внимание на хэндлер send_image_by_url. Вместо использования FSInputFile для отправки файла с диска, мы просто передаем в параметр photo строку с URL-адресом изображения. aiogram достаточно умен, чтобы определить, что это ссылка, самостоятельно скачать файл "на лету" и отправить его пользователю. Это чрезвычайно удобный способ для работы с динамическим контентом из интернета.

3. Интеграция роутера

Наконец, не забудьте зарегистрировать наш новый роутер в файле main.py, чтобы диспетчер знал о его существовании.

# main.py
# ... (импорты)
from handlers import basic_handlers, menu_handlers, keyboard_handlers

# ... (инициализация)

async def main():
    dp.include_router(basic_handlers.router)
    dp.include_router(menu_handlers.router)
    dp.include_router(keyboard_handlers.router) # <-- Наш новый роутер

    # ... (запуск)

Теперь запустите бота и отправьте ему команду /start. Вы увидите приветственное сообщение и две удобные кнопки внизу экрана.

3. InlineKeyboardMarkup: Интерактивность внутри сообщения

Теория

В отличие от Reply-клавиатуры, которая является элементом интерфейса чата, Inline-клавиатура — это неотъемлемая часть самого сообщения. Она прикрепляется непосредственно к тексту, фото или другому контенту и "путешествует" вместе с ним (например, при пересылке).

Ключевое отличие в принципе действия: нажатие на Inline-кнопку не отправляет новое текстовое сообщение в чат. Вместо этого оно посылает боту фоновый запрос-сигнал, называемый CallbackQuery. Это позволяет создавать сложные интерактивные меню, проводить опросы, реализовывать навигацию и запрашивать подтверждения действий, не засоряя историю чата.

Практика: Два типа Inline-кнопок

Давайте добавим логику для Inline-клавиатур в наш файл handlers/keyboard_handlers.py.

Тип 1: Кнопки-ссылки (url)

Это простейший вид Inline-кнопок. Их единственная задача — перенаправить пользователя по указанному URL-адресу. Они не отправляют боту никаких callback-сигналов.

Добавим в наш файл новую команду /links:

# handlers/keyboard_handlers.py
# ... (предыдущий код) ...

# Не забываем импортировать InlineKeyboardBuilder
from aiogram.utils.keyboard import InlineKeyboardBuilder

# ...

@router.message(Command("links"))
async def cmd_links(message: types.Message):
    # Создаем объект билдера для Inline-клавиатуры
    builder = InlineKeyboardBuilder()

    # Добавляем кнопки-ссылки
    builder.button(text="Документация aiogram", url="https://aiogram.dev/")
    builder.button(text="Мой GitHub", url="https://github.com/")
    
    # Расставляем кнопки в один ряд
    builder.adjust(1)
    
    await message.answer(
        "Вот полезные ссылки:",
        reply_markup=builder.as_markup()
    )

Здесь мы используем InlineKeyboardBuilder и его метод .button(), но вместо параметра text указываем url.

Тип 2: Callback-кнопки (callback_data)

Это самый мощный и часто используемый тип кнопок. Параметр callback_data содержит строку — "секретный код", который будет отправлен боту при нажатии. Мы сами придумываем этот код и используем его для идентификации того, какая именно кнопка была нажата.

Процесс работы с ними состоит из двух шагов: создание кнопок и обработка нажатий.

Шаг 1: Создание кнопок

Добавим команду /actions, которая будет отправлять сообщение с двумя callback-кнопками.

# handlers/keyboard_handlers.py
# ... (предыдущий код) ...

@router.message(Command("actions"))
async def cmd_actions(message: types.Message):
    builder = InlineKeyboardBuilder()

    builder.button(text="Показать уведомление", callback_data="show_alert")
    builder.button(text="Сменить это сообщение", callback_data="edit_message")
    
    await message.answer(
        "Нажми на кнопку, чтобы выполнить действие:",
        reply_markup=builder.as_markup()
    )

Шаг 2: Обработка нажатий

Чтобы "поймать" callback_data, нам нужен новый тип хэндлера, который регистрируется с помощью декоратора @router.callback_query(). Он слушает не сообщения в чате, а именно те самые CallbackQuery-сигналы.

Добавим в конец файла keyboard_handlers.py два таких хэндлера:

# handlers/keyboard_handlers.py
# ... (предыдущий код) ...

# Хэндлер для обработки нажатия на кнопку "Показать уведомление"
@router.callback_query(F.data == "show_alert")
async def handle_show_alert(callback: types.CallbackQuery):
    await callback.answer(
        "Это всплывающее уведомление!",
        show_alert=True # Делает уведомление модальным окном
    )

# Хэндлер для обработки нажатия на кнопку "Сменить это сообщение"
@router.callback_query(F.data == "edit_message")
async def handle_edit_message(callback: types.CallbackQuery):
    # Редактируем текст исходного сообщения
    await callback.message.edit_text("Сообщение было изменено!")
    # Отвечаем на callback, чтобы убрать "часики" на кнопке
    await callback.answer()

Разбор нового кода:

  • @router.callback_query(...): Декоратор для регистрации обработчика CallbackQuery.

  • F.data == "...": Магический фильтр, который проверяет содержимое поля data в пришедшем CallbackQuery. Так мы различаем, какая кнопка была нажата.

  • callback: types.CallbackQuery: В хэндлер передается объект CallbackQuery, содержащий всю информацию о нажатии, включая ссылку на исходное сообщение (callback.message) и на пользователя (callback.from_user).

  • await callback.answer(): Это обязательный метод! Его вызов сообщает клиенту Telegram, что нажатие обработано. Если этого не сделать, на кнопке будут вечно висеть "часики". Можно передать в него текст для всплывающего уведомления, а параметр show_alert=True превратит его в модальное окно.

  • await callback.message.edit_text(...): Один из самых мощных методов. Он позволяет боту редактировать то самое сообщение, к которому была прикреплена нажатая кнопка, создавая динамический интерфейс без отправки новых сообщений.

5. Заключение и домашнее задание

В этом уроке мы сделали огромный шаг вперед, превратив нашего бота из простого исполнителя команд в интерактивного собеседника. Вы освоили один из самых важных аспектов в разработке ботов — создание и обработку клавиатур. Теперь вы умеете создавать главное меню с помощью ReplyKeyboardMarkup, а также сложные интерактивные элементы, кнопки-ссылки, и даже редактировать сообщения "на лету" с помощью InlineKeyboardMarkup и CallbackQuery.

Домашнее задание к Уроку "Все виды клавиатур"

Задание 1: "Главное меню v2.0"

Что нужно сделать: Усовершенствуйте Reply-клавиатуру, которая появляется по команде /start.

  • Добавьте в нее третью кнопку: "Скрыть клавиатуру".

  • Напишите текстовый хэндлер, который будет реагировать на сообщение "Скрыть клавиатуру".

  • В этом хэндлере отправьте пользователю сообщение (например, "Клавиатура скрыта.") и в reply_markup передайте специальный объект ReplyKeyboardRemove.

Подсказка: from aiogram.types import ReplyKeyboardRemove

Ожидаемый результат: При нажатии на новую кнопку Reply-клавиатура должна исчезнуть.

Цель: Научиться не только показывать, но и убирать Reply-клавиатуру, а также добавлять кнопки в существующий ReplyKeyboardBuilder.

Задание 2: "Бот-визитка"

Что нужно сделать: Создайте новую команду /socials, которая будет отправлять Inline-клавиатуру с кнопками-ссылками на различные ресурсы.

  • Напишите хэндлер для команды /socials.

  • Внутри создайте InlineKeyboardBuilder.

  • Добавьте как минимум 3 кнопки-ссылки (например, на канал в Telegram, на YouTube, на ваш сайт). Каждая кнопка должна вести на свой URL.

  • Используйте builder.adjust(1), чтобы каждая кнопка была на новой строке.

Ожидаемый результат: По команде /socials бот присылает сообщение с тремя вертикально расположенными кнопками, каждая из которых открывает свою ссылку.

Цель: Закрепить навык создания Inline-клавиатур с url-кнопками.

Задание 3: "Запрос подтверждения"

Что нужно сделать: Создайте команду /confirm, которая запрашивает у пользователя подтверждение действия с помощью Inline-кнопок.

  • Напишите хэндлер для команды /confirm. Он должен отправлять сообщение "Вы уверены, что хотите продолжить?".

  • К этому сообщению прикрепите Inline-клавиатуру с двумя кнопками: "Да" и "Нет".

  • Для кнопки "Да" установите callback_data="confirm_yes".

  • Для кнопки "Нет" установите callback_data="confirm_no".

  • Напишите два callback_query хэндлера, которые будут ловить confirm_yes и confirm_no.

  • В каждом хэндлере используйте callback.answer() с параметром show_alert=True, чтобы показать пользователю всплывающее окно с его выбором (например, await callback.answer("Вы подтвердили действие!", show_alert=True)).

Цель: Научиться создавать Inline-кнопки с callback_data и обрабатывать их с помощью callback_query хэндлеров, используя модальные окна.

Задание 4: "Динамическое сообщение"

Что нужно сделать: Усовершенствуйте задание 3. Теперь, после нажатия на кнопку "Да" или "Нет", исходное сообщение должно изменяться.

  • Возьмите код из предыдущего задания.

  • В callback_query хэндлере для confirm_yes, после callback.answer(), добавьте редактирование исходного сообщения: await callback.message.edit_text("Действие успешно подтверждено! ✅").

  • Аналогично, в хэндлере для confirm_no измените сообщение на: await callback.message.edit_text("Действие отменено. ❌").

Ожидаемый результат: После нажатия на кнопку Inline-клавиатура исчезает, а текст сообщения "Вы уверены...?" заменяется на результат выбора.

Цель: Освоить ключевой метод callback.message.edit_text() для создания интерактивных интерфейсов.

Задание 5 (со звездочкой): "Интерактивный счётчик"*

Что нужно сделать: Создайте команду /counter, которая отправляет сообщение с числом "0" и двумя Inline-кнопками: "+1" и "-1". При нажатии на кнопки число в сообщении должно изменяться.

  • Напишите хэндлер для команды /counter, который отправляет сообщение "Текущее значение: 0" и Inline-клавиатуру с кнопками "+1" (callback_data="increment") и "-1" (callback_data="decrement").

  • Создайте один или два callback_query хэндлера для обработки этих нажатий.

  • Внутри хэндлера вам нужно:

    1. Получить текущее число из текста сообщения (callback.message.text).

    2. Преобразовать его в целое число (int).

    3. Увеличить или уменьшить его на 1.

    4. С помощью callback.message.edit_text() обновить текст сообщения новым значением.

Подсказка: Чтобы из строки "Текущее значение: 5" получить число 5, можно использовать int(callback.message.text.split()[-1]).

Цель: Решить нестандартную задачу, комбинируя чтение данных из сообщения (callback.message.text) и его редактирование (edit_text), чтобы создать полноценный интерактивный элемент.

Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.

Уверен, у вас все получится. Вперед, к практике!