Как сделать кнопки в aiogram



Скачай курс
в приложении
Перейти в приложение
Открыть мобильную версию сайта
© 2013 — 2023. Stepik
Наши условия использования и конфиденциальности

Public user contributions licensed under cc-wiki license with attribution required
Урок 5. Клавиатуры и кнопки
В сегодняшнем уроке мы разберём шаблоны сообщений и инлайн-клавиатуры и научимся применять встроенные в aiogram методы для составления сложных клавиатур.
Не забывайте своевременно обновлять библиотеку командой python3.6 -m pip install aiogram -U ! Урок проводится с использованием версии 1.2.3
Весь код, использованный в уроке, как обычно доступен на GitHub
Для начала стоит понять, в чем основное различие ReplyKeyboardMarkup и InlineKeyboardMarkup :
ReplyKeyboardMarkup — это шаблоны сообщений. К примеру, ваш бот задаёт пользователю вопрос и предлагает варианты ответа. Пользователь может самостоятельно напечатать ответ, либо нажать на готовую кнопку. Такая клавиатура показывается вместо основной и не привязана ни к какому сообщению. В кнопки такой клавиатуры нельзя заложить никакой информации, нельзя запрограммировать для неё подобный если пользователь нажимает кнопку с текстом «abc» отправить текст «qwerty» алгоритм, отправлено будет только то, что написано на кнопке (есть два исключения, о которых ниже).
InlineKeyboardMarkup — это уже настоящая кастомная клавиатура. С её помощью мы можем выполнять более сложные действия. Она привязывается к сообщению, с которым была отправлена. В кнопки можно заложить любой текст размером от 1 до 64 байт (будьте осторожны, недобросовестные клиенты позволяют изменять эти данные). Инлайн кнопки позволяют скрыть в себе внутреннюю телеграм ссылку, ссылку на внешний ресурс, а также шорткат для инлайн запроса (об инлайн режиме в одном из следующих уроков).
И ту и другую клавиатуру можно редактировать, но разными способами. Первая обновляется при отправке сообщения с новой клавиатурой типа ReplyKeyboardMarkup , вторую можно редактировать вместе с сообщением, к которому она прикреплена (или только саму разметку).
Переходим к коду
Создадим отдельный модуль с клавиатурами, которые будем использовать в основной программе.
В первую очередь импортируем необходимые нам модули и создаём первую клавиатуру:
from aiogram.types import ReplyKeyboardRemove, \ ReplyKeyboardMarkup, KeyboardButton, \ InlineKeyboardMarkup, InlineKeyboardButton button_hi = KeyboardButton('Привет! ') greet_kb = ReplyKeyboardMarkup() greet_kb.add(button_hi)
При инициализации класса KeyboardButton необходимо передать один обязательный параметр — текст, который пользователь будет отправлять по нажатию на эту кнопку. У объекта класса ReplyKeyboardMarkup есть несколько методов, позволяющих добавить кнопку, в данном случае мы используем add . Таким образом мы получили первую готовую клавиатуру.
Создаём обработчик, который будет отправлять нам сообщение и наш шаблон (напомню, что отправить отдельно клавиатуру никак нельзя, она является параметром к сообщению).
@dp.message_handler(commands=['start']) async def process_start_command(message: types.Message): await message.reply("Привет!", reply_markup=kb.greet_kb)
Запускаем и проверяем:

Отлично, клавиатура появилась! Но эта одна кнопка с маленьким текстом занимает очень много места. Телеграм позволяет автоматически уменьшить размер, для этого необходимо передать в инициализатор класса ReplyKeyboardMarkup параметру resize_keyboard значение True . Создадим новую клавиатуру:
greet_kb1 = ReplyKeyboardMarkup(resize_keyboard=True).add(button_hi)
Мы передали параметр в инициализатор и следом сразу добавили уже существующую кнопку. Отправляем новую версию клавиатуры:
@dp.message_handler(commands=['hi1']) async def process_hi1_command(message: types.Message): await message.reply("Первое - изменяем размер клавиатуры", reply_markup=kb.greet_kb1)

Очевидно, так как у нас нет обработчика обычных сообщений, текст, отправляемый нажатием на эту кнопку, остаётся без ответа. При желании можно добавить функцию эхо из первого урока. А что ещё можно было заметить при использовании этой кнопки? Что она остаётся всё там же, даже если мы после нажатия хотели бы вернуться к привычной клавиатуре. Да, нажать одну кнопку, чтобы переключиться, не сложно. Но если клавиатура в принципе не подразумевает повторного нажатия прямо сейчас? И для этого тоже есть решение:
# keyboards.py greet_kb2 = ReplyKeyboardMarkup( resize_keyboard=True, one_time_keyboard=True ).add(button_hi) # bot.py @dp.message_handler(commands=['hi2']) async def process_hi2_command(message: types.Message): await message.reply("Второе - прячем клавиатуру после одного нажатия", reply_markup=kb.greet_kb2)
Эти параметры можно комбинировать. Вследствие чего мы получили маленькую клавиатуру, которая скрывается после одной отправки. Но пользователь может снова её открыть нажатием на соответствующую кнопку в поле ввода.
Добавляем больше кнопок
Рассмотрим подробно работу встроенных методов для создания более сложных клавиатур, а именно row , insert и add . Создаём кнопки, которые мы сможем использовать повторно и генерируем несколько разных клавиатур:
button1 = KeyboardButton('1️⃣') button2 = KeyboardButton('2️⃣') button3 = KeyboardButton('3️⃣') markup3 = ReplyKeyboardMarkup().add( button1).add(button2).add(button3) markup4 = ReplyKeyboardMarkup().row( button1, button2, button3 ) markup5 = ReplyKeyboardMarkup().row( button1, button2, button3 ).add(KeyboardButton('Средний ряд')) button4 = KeyboardButton('4️⃣') button5 = KeyboardButton('5️⃣') button6 = KeyboardButton('6️⃣') markup5.row(button4, button5) markup5.insert(button6)
Важно: сейчас мы отталкиваемся от того, что клавиатура по умолчанию имеет 3 кнопки в ряд. О том, как изменить это значение, будет в примере с инлайн клавиатурами — этот параметр идентичен для обоих видов.
- Метод add принимает в себя любое количество кнопок, всегда начинает добавление с новой строки и переносит ряд при достижении значения установленной ширины.
- Метод row тоже принимает любое количество, но при этом не переносит кнопки на новый ряд, а добавляет всё полученное в одну строчку.
- Метод insert работает по схеме схожей с add , но только начинает добавлять к последнему ряду. И только если там уже достигнута максимальная ширина, начинает новую строку. Взглянем на него ещё раз при создании следующей клавиатуры.
Отправляем все готовые кнопки и смотрим на результат:
@dp.message_handler(commands=['hi3']) async def process_hi3_command(message: types.Message): await message.reply("Третье - добавляем больше кнопок", reply_markup=kb.markup3)

@dp.message_handler(commands=['hi4']) async def process_hi4_command(message: types.Message): await message.reply("Четвертое - расставляем кнопки в ряд", reply_markup=kb.markup4)

@dp.message_handler(commands=['hi5']) async def process_hi5_command(message: types.Message): await message.reply("Пятое - добавляем ряды кнопок", reply_markup=kb.markup5)

Думаю, здесь достаточно наглядно видно работу методов добавления кнопок в разметку.
И последнее по этому виду клавиатур. ReplyKeyboardMarkup позволяет запросить у пользователя его контакт или локацию. Это те самые два исключения из правила, когда при нажатии кнопки будет отправлено не то, что написано на ней. Их можно отправлять как по одной, так и в составе более сложной клавиатуры. Добавим обе кнопки и посмотрим, что будет (обращу внимание читателя на то, что нельзя одной кнопкой запросить сразу и то и то):
# keyboards.py markup_request = ReplyKeyboardMarkup(resize_keyboard=True).add( KeyboardButton('Отправить свой контакт ☎️', request_contact=True) ).add( KeyboardButton('Отправить свою локацию ️', request_location=True) ) #bot.py @dp.message_handler(commands=['hi6']) async def process_hi6_command(message: types.Message): await message.reply("Шестое - запрашиваем контакт и геолокацию\nЭти две кнопки не зависят друг от друга", reply_markup=kb.markup_request)
При нажатии на каждую из этих кнопок клиент (приложение Телеграм) спросит, уверены ли вы, что хотите поделиться с ботом этими данными и при утвердительном ответе отправит их.

Рассмотрим подробнее последний метод для составления клавиатур — insert . Он похож на метод add , но начинает добавлять кнопки не с нового ряда, а сначала проверяет, заполнен ли до конца последний ряд. И если нет, то сначала добавляет кнопки туда, а переносит строку только при достижении указанного лимита.
# keyboards.py markup_big = ReplyKeyboardMarkup() markup_big.add( button1, button2, button3, button4, button5, button6 ) markup_big.row( button1, button2, button3, button4, button5, button6 ) markup_big.row(button4, button2) markup_big.add(button3, button2) markup_big.insert(button1) markup_big.insert(button6) markup_big.insert(KeyboardButton('9️⃣')) # bot.py @dp.message_handler(commands=['hi7']) async def process_hi7_command(message: types.Message): await message.reply("Седьмое - все методы вместе", reply_markup=kb.markup_big)
После каждой отправки ботом пользователю клавиатуры, последняя заменяет предыдущую. Поэтому пользователь всегда может открыть её, даже когда по контексту она не нужна. Для того, чтобы у пользователя в клиенте клавиатура убралась совсем, нужно отправить ему ReplyKeboardRemove :
@dp.message_handler(commands=['rm']) async def process_rm_command(message: types.Message): await message.reply("Убираем шаблоны сообщений", reply_markup=kb.ReplyKeyboardRemove())
Получив сообщение с такой клавиатурой, клиент уберёт шаблоны полностью.
Инлайн клавиатуры
Теперь перейдем к инлайн клавиатурам. Они имеют больше параметров, поэтому позволяют нам делать больше разных вещей. Самое популярное использование — как кнопка, являющаяся шорткатом для какого-то действия. То есть «если пользователь нажал кнопку X, сделать Y». И под Y можно понимать вообще что угодно, так как это уже не ограничивается даже API. Рассмотрим наглядно, для этого передадим в инициализатор значение callback_data :
# keyboards.py inline_btn_1 = InlineKeyboardButton('Первая кнопка!', callback_data='button1') inline_kb1 = InlineKeyboardMarkup().add(inline_btn_1) #bot.py @dp.message_handler(commands=['1']) async def process_command_1(message: types.Message): await message.reply("Первая инлайн кнопка", reply_markup=kb.inline_kb1)
Нажимаем кнопку и.. ничего не происходит! Почему? Если у бота было включено логгирование, то вы могли заметить, что приходит обновление типа CallbackQuery . Так вот именно его нам и нужно отлавливать. Добавляем нужный хэндлер (я предпочитаю ставить их выше, но относительно хэндлеров обычных сообщений значения это не имеет):
@dp.callback_query_handler(func=lambda c: c.data == 'button1') async def process_callback_button1(callback_query: types.CallbackQuery): await bot.answer_callback_query(callback_query.id) await bot.send_message(callback_query.from_user.id, 'Нажата первая кнопка!')
Здесь мы делаем проверку прямо в хэндлере. Аргумент func ожидает функцию, которая принимает один параметр (туда передаётся апдейт), и хэндлер срабатывает, если возвращается истина. Этот аргумент можно использовать и в других хэндлерах, мы уже использовали его в уроке по FSM.
Внутри мы можем делать вообще что угодно, так как работаем с обычным питоном. Подача корма для кошки? Управление светом? Пределом является только ваша фантазия.
Хорошим тоном будет отвечать на все колбеки — для этого есть метод answerCallbackQuery . В документации сказано, что ответ ожидается клиентом, и дать его нужно обязательно, даже если вы не собираетесь передавать что-либо. Этого времени предостаточно, чтобы собрать необходимые данные и прислать их. При этом во время ожидания на кнопке будут крутиться часики, показывающие, то клиент ждёт ответа, поэтому если мы не хотим, чтобы пользователь наблюдал их, то нужно не забывать отвечать. Обязательный аргумент — айди запроса, на который мы отвечаем. Остальные параметры рассмотрим ниже. А в данном случае мы для наглядности отправим сообщение с информацией о том, что нажата первая кнопка. Запускаем снова, жмём кнопку и таки получаем ответ:

В этот раз установим ширину клавиатуры 2 и посмотрим, что будет.
Ну и так как методы добавления новых элементов мы уже разобрали, добавим сразу все доступные кнопки и отправим получившуюся клавиатуру, не забыв добавить хэндлер кнопок:
# keyboards.py inline_kb_full = InlineKeyboardMarkup(row_width=2).add(inline_btn_1) inline_kb_full.add(InlineKeyboardButton('Вторая кнопка', callback_data='btn2')) inline_btn_3 = InlineKeyboardButton('кнопка 3', callback_data='btn3') inline_btn_4 = InlineKeyboardButton('кнопка 4', callback_data='btn4') inline_btn_5 = InlineKeyboardButton('кнопка 5', callback_data='btn5') inline_kb_full.add(inline_btn_3, inline_btn_4, inline_btn_5) inline_kb_full.row(inline_btn_3, inline_btn_4, inline_btn_5) inline_kb_full.insert(InlineKeyboardButton("query=''", switch_inline_query='')) inline_kb_full.insert(InlineKeyboardButton("query='qwerty'", switch_inline_query='qwerty')) inline_kb_full.insert(InlineKeyboardButton("Inline в этом же чате", switch_inline_query_current_chat='wasd')) inline_kb_full.add(InlineKeyboardButton('Уроки aiogram', url='https://surik00.gitbooks.io/aiogram-lessons/content/')) # bot.py @dp.callback_query_handler(func=lambda c: c.data and c.data.startswith('btn')) async def process_callback_kb1btn1(callback_query: types.CallbackQuery): code = callback_query.data[-1] if code.isdigit(): code = int(code) if code == 2: await bot.answer_callback_query(callback_query.id, text='Нажата вторая кнопка') elif code == 5: await bot.answer_callback_query( callback_query.id, text='Нажата кнопка с номером 5.\nА этот текст может быть длиной до 200 символов ', show_alert=True) else: await bot.answer_callback_query(callback_query.id) await bot.send_message(callback_query.from_user.id, f'Нажата инлайн кнопка! code=') @dp.message_handler(commands=['2']) async def process_command_2(message: types.Message): await message.reply("Отправляю все возможные кнопки", reply_markup=kb.inline_kb_full)

Пройдёмся по строчкам по порядку, чтобы не осталось вопросов:
- мы создаём клавиатуру типа InlineKeyboardMarkup , указываем, что ширина строки должна быть не больше двух (напомню, что это не распространяется на метод row ) и сразу добавляем туда уже готовую кнопку
- далее добавляем кнопку, у которой указываем другие данные в параметре callback_data
- следом генерируем три новые кнопки и добавляем их дважды. Сначала методом add , затем через row . И так как ширина клавиатуры равна двум, то в первом случае происходит перенос третьей кнопки, во втором случае нет
- затем добавляем кнопки, у которых указываем уже не callback_data , а другие параметры. То, что мы добавим в switch_inline_query , будет автоматически использовано при нажатии кнопки: пользователю предложат выбрать чат, а там вызовется инлайн режим этого бота (в поле ввода сообщения добавится юзернейм бота), следом через пробел будет прописано то, что мы указали. Параметр может принимать пустую строку, тогда инлайн режим запустится без какого-либо запроса, но если будет указан текст, то он и добавится
- при использовании switch_inline_query_current_chat произойдёт ровно то же, что и в предыдущем пункте, но без выбора чата, а запустится в текущем (было сложно догадаться по названию, я знаю)
- ну и последний параметр url - добавляем ссылку на внешний ресурс, либо диплинк в самом Телеграме
Так как параметр клавиатуры row_width равен двум, то кнопки автоматически расставились соответствующе. Рассмотрим реакцию на кнопки по порядку: При нажатии первой срабатывает наш первый колбек, так как не важно, в какую клавиатуру добавлена кнопка, важно, какая у неё callback_data ☝️. Поэтому добавлять инлайн кнопку можно сколько угодно раз в любые инлайн клавиатуры.
Кнопки со второй по пятую имеют схожую структуру в callback_data , поэтому внутри хэндлера проверяем, какой код у нажатой кнопки и:
- если 2, то отвечаем на запрос и передаем информационное сообщение. Аргумент text это текст ответа на запрос. По умолчанию он будет показан вверху чата и сам скроется через пару секунд.
- если 5, то отвечаем так же, но указываем show_alert=True , таким образом мы сообщаем клиенту, что нужно показать окошко с текстом
- в ином случае просто отвечаем на колбек
И во всех случаях шлем сообщение пользователю:

Ещё можно отправить кнопку с игрой или платежом, но первое мы разберем в одном из следующих уроков, а о втором я уже упоминал в одном из предыдущих. При ответе на колбек можно ещё передать в ответ ссылку формата t.me/your_bot?start=XXXX , чтобы пользователь запустил вашего бота с определенным параметром, но диплинкинг мы тоже оставим для другого урока, так как здесь только о кнопках.
На последок не забываем добавить обработку команы /help и запускаем:
help_message = text( "Это урок по клавиатурам.", "Доступные команды:\n", "/start - приветствие", "\nШаблоны клавиатур:", "/hi1 - авто размер", "/hi2 - скрыть после нажатия", "/hi3 - больше кнопок", "/hi4 - кнопки в ряд", "/hi5 - больше рядов", "/hi6 - запрос локации и номера телефона", "/hi7 - все методы" "/rm - убрать шаблоны", "\nИнлайн клавиатуры:", "/1 - первая кнопка", "/2 - сразу много кнопок", sep="\n" ) @dp.message_handler(commands=['help']) async def process_help_command(message: types.Message): await message.reply(help_message) if __name__ == '__main__': executor.start_polling(dp)
Вот и всё!
В этом уроке мы разобрали как работать с клавиатурами в Телеграм
Кнопки¶
В этой главе мы познакомимся с такой замечательной фичей Telegram-ботов как кнопки. Прежде всего, чтобы избежать путаницы, определимся с названиями. То, что цепляется к низу экрана вашего устройства, будем называть обычными кнопками, а то, что цепляется непосредственно к сообщениям, назовём инлайн-кнопками. Ещё раз картинкой:

Обычные кнопки¶
Кнопки как шаблоны¶
Этот вид кнопок появился вместе с Bot API в далёком 2015 году и представляет собой не что иное, как шаблоны сообщений (за исключением нескольких особых случаев, но о них позже). Принцип простой: что написано на кнопке, то и будет отправлено в текущий чат. Соответственно, чтобы обработать нажатие такой кнопки, бот должен распознавать входящие текстовые сообщения.
Напишем хэндлер, который будет при нажатии на команду /start отправлять сообщение с двумя кнопками:
# from aiogram import types @dp.message_handler(commands="start") async def cmd_start(message: types.Message): keyboard = types.ReplyKeyboardMarkup() button_1 = types.KeyboardButton(text="С пюрешкой") keyboard.add(button_1) button_2 = "Без пюрешки" keyboard.add(button_2) await message.answer("Как подавать котлеты?", reply_markup=keyboard)
Обратите внимание, что т.к. обычные кнопки суть шаблоны сообщений, то их можно создавать не только как объекты KeyboardButton , но и как обычные строки.
Что ж, запустим бота и обалдеем от громадных кнопок:

Как-то некрасиво. Во-первых, хочется сделать кнопки поменьше, а во-вторых, расположить их горизонтально.
Почему вообще они такие большие? Дело в том, что по умолчанию «кнопочная» клавиатура должна занимать на смартфонах столько же места, сколько и обычная буквенная. Для уменьшения кнопок к объекту клавиатуры надо указать дополнительный параметр resize_keyboard=True .
Но как заменить вертикальные кнопки на горизонтальные? С точки зрения Bot API, клавиатура — это массив массивов кнопок, а если говорить проще, массив строк. Метод add() при каждом вызове создаёт новую строку (ряд) и принимает произвольное число аргументов по количеству желаемых кнопок в строке. Перепишем наш код, чтобы было красиво:
@dp.message_handler(commands="start") async def cmd_start(message: types.Message): keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) buttons = ["С пюрешкой", "Без пюрешки"] keyboard.add(*buttons) await message.answer("Как подавать котлеты?", reply_markup=keyboard)
Обратите внимание на конструкцию *buttons . Здесь вам не C++ и звёздочка используется для распаковки списка. Подробнее об операторах * и ** можно прочитать здесь.
Смотрим — действительно красиво:

Осталось научить бота реагировать на нажатие таких кнопок. Как уже было сказано выше, необходимо делать проверку на полное совпадение текста. Сделаем это двумя способами: через специальный фильтр Text и обычной лямбдой:
# from aiogram.dispatcher.filters import Text @dp.message_handler(Text(equals="С пюрешкой")) async def with_puree(message: types.Message): await message.reply("Отличный выбор!") @dp.message_handler(lambda message: message.text == "Без пюрешки") async def without_puree(message: types.Message): await message.reply("Так невкусно!")

Чтобы удалить кнопки, необходимо отправить новое сообщение со специальной «удаляющей» клавиатурой типа ReplyKeyboardRemove . Например: await message.reply("Отличный выбор!", reply_markup=types.ReplyKeyboardRemove())
У объекта обычной клавиатуры есть ещё две полезных опции: one_time_keyboard для скрытия кнопок после нажатия и selective для показа клавиатуры лишь некоторым участникам группы. Их использование остаётся для самостоятельного изучения.
Помимо стандартных опций, описанных выше, aiogram немного расширяет функциональность клавиатур параметром row_width . При его использовании, фреймворк автоматически разобьёт массив кнопок на строки по N элементов в каждой, где N — значение row_width , например, row_width=2 . Попробуйте!
Специальные обычные кнопки¶
По состоянию на конец ужасного 2020 года в Telegram существует три специальных вида обычных кнопок, не являющихся шаблонами: для отправки текущей геолокации, для отправки своего номера телефона и ярлык для создания опроса/викторины. Для первых двух типов достаточно установить булевый флаг, а для опросов и викторин нужно передать специальный тип KeyboardButtonPollType и, по желанию, указать тип создаваемого объекта.
Впрочем, проще один раз увидеть код:
@dp.message_handler(commands="special_buttons") async def cmd_special_buttons(message: types.Message): keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) keyboard.add(types.KeyboardButton(text="Запросить геолокацию", request_location=True)) keyboard.add(types.KeyboardButton(text="Запросить контакт", request_contact=True)) keyboard.add(types.KeyboardButton(text="Создать викторину", request_poll=types.KeyboardButtonPollType(type=types.PollType.QUIZ))) await message.answer("Выберите действие:", reply_markup=keyboard)
Инлайн-кнопки¶
URL-кнопки и колбэки¶
В отличие от обычных кнопок, инлайновые цепляются не к низу экрана, а к сообщению, с которым были отправлены. В этой главе мы рассмотрим два типа таких кнопок: URL и Callback. Ещё один — Switch — будет рассмотрен в главе про инлайн-режим.
Login- и Pay-кнопки в книге рассматриваться не будут вообще. Если у кого-то есть желание помочь хотя бы с рабочим кодом для авторизации или оплаты, пожалуйста, создайте Pull Request на GitHub. Спасибо!
Самые простые инлайн-кнопки относятся к типу URL, т.е. «ссылка». Поддерживаются только протоколы HTTP(S) и tg://
@dp.message_handler(commands="inline_url") async def cmd_inline_url(message: types.Message): buttons = [ types.InlineKeyboardButton(text="GitHub", url="https://github.com"), types.InlineKeyboardButton(text="Оф. канал Telegram", url="tg://resolve?domain=telegram") ] keyboard = types.InlineKeyboardMarkup(row_width=1) keyboard.add(*buttons) await message.answer("Кнопки-ссылки", reply_markup=keyboard)
А если хотите обе кнопки в ряд, то уберите row_width=1 (тогда будет использоваться значение по умолчанию 3).
С URL-кнопками больше обсуждать, по сути, нечего, поэтому перейдём к гвоздю сегодняшней программы — Callback-кнопкам. Это очень мощная штука, которую вы можете встретить практически везде. Кнопки-реакции у постов (лайки), меню у @BotFather и т.д. Суть в чём: у колбэк-кнопок есть специальное значение (data), по которому ваше приложение опознаёт, что нажато и что надо сделать. И выбор правильного data очень важен! Стоит также отметить, что, в отличие от обычных кнопок, нажатие на колбэк-кнопку позволяет сделать практически что угодно, от заказа пиццы до перезагрузки сервера.
Напишем хэндлер, который по команде /random будет отправлять сообщение с колбэк-кнопкой:
@dp.message_handler(commands="random") async def cmd_random(message: types.Message): keyboard = types.InlineKeyboardMarkup() keyboard.add(types.InlineKeyboardButton(text="Нажми меня", callback_data="random_value")) await message.answer("Нажмите на кнопку, чтобы бот отправил число от 1 до 10", reply_markup=keyboard)
Но как же обработать нажатие? Если раньше мы использовали message_handler для обработки входящих сообщений, то теперь будем использовать callback_query_handler для обработки колбэков. Ориентироваться будем на «значение» кнопки, т.е. на её data:
@dp.callback_query_handler(text="random_value") async def send_random_value(call: types.CallbackQuery): await call.message.answer(str(randint(1, 10)))
Несмотря на то, что параметр кнопки callback_data , а значение data лежит в одноимённом поле data объекта CallbackQuery, собственный фильтр aiogram называется text .

Ой, а что это за часики? Оказывается, сервер Telegram ждёт от нас подтверждения о доставке колбэка, иначе в течение 30 секунд будет показывать специальную иконку. Чтобы скрыть часики, нужно вызвать метод answer() у колбэка (или использовать метод API answer_callback_query() ). В общем случае, в метод answer() можно ничего не передавать, но можно вызвать специальное окошко (всплывающее сверху или поверх экрана):
@dp.callback_query_handler(text="random_value") async def send_random_value(call: types.CallbackQuery): await call.message.answer(str(randint(1, 10))) await call.answer(text="Спасибо, что воспользовались ботом!", show_alert=True) # или просто await call.answer()

В функции send_random_value мы вызывали метод answer() не у message , а у call.message . Это связано с тем, что колбэк-хэндлеры работают не с сообщениями (тип Message), а с колбэками (тип CallbackQuery), у которого другие поля, и само сообщение — всего лишь его часть. Учтите также, что message — это сообщение, к которому была прицеплена кнопка (т.е. отправитель такого сообщения — сам бот). Если хотите узнать, кто нажал на кнопку, смотрите поле from (в вашем коде это будет call.from_user , т.к. слово from зарезервировано в Python)
Когда вызывать answer() ?
В общем случае, главное — просто не забыть сообщить Telegram о получении колбэк-запроса, но я рекомендую ставить вызов answer() в самом конце, и вот почему: если вдруг в процессе обработки колбэка случится какая-то ошибка и бот нарвётся на необработанное исключение, пользователь увидит неубирающиеся полминуты часики и поймёт, что что-то не так. В противном случае, часики исчезнут, а пользователь останется в неведении, выполнился его запрос успешно или нет.
Перейдём к примеру посложнее. Пусть пользователю предлагается сообщение с числом 0, а внизу три кнопки: +1, -1 и Подтвердить. Первыми двумя он может редактировать число, а последняя удаляет всю клавиатуру, фиксируя изменения. Хранить значения будем в памяти в словаре (про конечные автоматы поговорим как-нибудь в другой раз).
# Здесь хранятся пользовательские данные. # Т.к. это словарь в памяти, то при перезапуске он очистится user_data = <> def get_keyboard(): # Генерация клавиатуры. buttons = [ types.InlineKeyboardButton(text="-1", callback_data="num_decr"), types.InlineKeyboardButton(text="+1", callback_data="num_incr"), types.InlineKeyboardButton(text="Подтвердить", callback_data="num_finish") ] # Благодаря row_width=2, в первом ряду будет две кнопки, а оставшаяся одна # уйдёт на следующую строку keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard.add(*buttons) return keyboard async def update_num_text(message: types.Message, new_value: int): # Общая функция для обновления текста с отправкой той же клавиатуры await message.edit_text(f"Укажите число: new_value>", reply_markup=get_keyboard()) @dp.message_handler(commands="numbers") async def cmd_numbers(message: types.Message): user_data[message.from_user.id] = 0 await message.answer("Укажите число: 0", reply_markup=get_keyboard()) @dp.callback_query_handler(Text(startswith="num_")) async def callbacks_num(call: types.CallbackQuery): # Получаем текущее значение для пользователя, либо считаем его равным 0 user_value = user_data.get(call.from_user.id, 0) # Парсим строку и извлекаем действие, например `num_incr` -> `incr` action = call.data.split("_")[1] if action == "incr": user_data[call.from_user.id] = user_value+1 await update_num_text(call.message, user_value+1) elif action == "decr": user_data[call.from_user.id] = user_value-1 await update_num_text(call.message, user_value-1) elif action == "finish": # Если бы мы не меняли сообщение, то можно было бы просто удалить клавиатуру # вызовом await call.message.delete_reply_markup(). # Но т.к. мы редактируем сообщение и не отправляем новую клавиатуру, # то она будет удалена и так. await call.message.edit_text(f"Итого: user_value>") # Не забываем отчитаться о получении колбэка await call.answer()
И, казалось бы, всё работает:

Но теперь представим, что ушлый пользователь сделал следующее: вызвал команду /numbers (значение 0), увеличил значение до 1, снова вызвал /numbers (значение сбросилось до 0) и отредактировал нажал кнопку "+1" на первом сообщении. Что произойдёт? Бот по-честному отправит запрос на редактирование текста со значением 1, но т.к. на том сообщении уже стоит цифра 1, то Bot API вернёт ошибку, что старый и новый тексты совпадают, а бот словит исключение: aiogram.utils.exceptions.MessageNotModified: Message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message

С этой ошибкой вы, скорее всего, будете поначалу часто сталкиваться, пытаясь редактировать сообщения. Но, в действительности, решается проблема очень просто: мы проигнорируем исключение MessageNotModified . Из первой главы вы уже знаете о такой прекрасной штуке, как errors_handler , но в этот раз мы поступим чуть иначе и перепишем функцию update_num_text() следующим образом:
# from aiogram.utils.exceptions import MessageNotModified # from contextlib import suppress async def update_num_text(message: types.Message, new_value: int): with suppress(MessageNotModified): await message.edit_text(f"Укажите число: new_value>", reply_markup=get_keyboard())
Если теперь вы попробуете повторить пример выше, то указанное исключение в этом блоке кода бот просто-напросто проигнорирует.
Фабрика колбэков¶
В aiogram существует т.н. фабрика колбэков. Вы создаёте объект CallbackData , указываете ему префикс и произвольное количество доп. аргументов, которые в дальнейшем указываете при создании колбэка для кнопки.
Например, рассмотрим следующий объект:
# from aiogram.utils.callback_data import CallbackData cb= CallbackData("post", "id", "action")
Тогда при создании кнопки вам надо указать её параметры так:
button = types.InlineKeyboardButton( text="Лайкнуть", callback_data=cb.new(id=5, action="like") )
В примере выше в кнопку запишется callback_data , равный post:5:like , а хэндлер на префикс post будет выглядеть так:
@dp.callback_query_handler(cb.filter()) async def callbacks(call: types.CallbackQuery, callback_data: dict): post_id = callback_data["id"] action = callback_data["action"]
В предыдущем примере с числами мы грамотно выбрали callback_data , поэтому смогли легко запихнуть все обработчики в один хэндлер. Но можно логически разнести обработку инкремента и декремента от обработки нажатия на кнопку "Подтвердить". Для этого в фильтре фабрики можно указать желаемые значения какого-либо параметра. Давайте перепишем наш пример с использоваанием фабрики:
# fabnum - префикс, action - название аргумента, которым будем передавать значение callback_numbers = CallbackData("fabnum", "action") def get_keyboard_fab(): buttons = [ types.InlineKeyboardButton(text="-1", callback_data=callback_numbers.new(action="decr")), types.InlineKeyboardButton(text="+1", callback_data=callback_numbers.new(action="incr")), types.InlineKeyboardButton(text="Подтвердить", callback_data=callback_numbers.new(action="finish")) ] keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard.add(*buttons) return keyboard async def update_num_text_fab(message: types.Message, new_value: int): with suppress(MessageNotModified): await message.edit_text(f"Укажите число: new_value>", reply_markup=get_keyboard_fab()) @dp.message_handler(commands="numbers_fab") async def cmd_numbers(message: types.Message): user_data[message.from_user.id] = 0 await message.answer("Укажите число: 0", reply_markup=get_keyboard_fab()) @dp.callback_query_handler(callback_numbers.filter(action=["incr", "decr"])) async def callbacks_num_change_fab(call: types.CallbackQuery, callback_data: dict): user_value = user_data.get(call.from_user.id, 0) action = callback_data["action"] if action == "incr": user_data[call.from_user.id] = user_value + 1 await update_num_text_fab(call.message, user_value + 1) elif action == "decr": user_data[call.from_user.id] = user_value - 1 await update_num_text_fab(call.message, user_value - 1) await call.answer() @dp.callback_query_handler(callback_numbers.filter(action=["finish"])) async def callbacks_num_finish_fab(call: types.CallbackQuery): user_value = user_data.get(call.from_user.id, 0) await call.message.edit_text(f"Итого: user_value>") await call.answer()
На этом глава про кнопки окончена, но про некоторые других их виды мы поговорим в следующих главах.
Как сделать inline-кнопки "Вперед" и "Назад"? (aiogram)
К примеру, у меня есть список товаров, представленный в виде Inline-кнопок, как можно реализовать кнопки "Вперед" и "Назад"? Пример:
Если нужны доп. правки - прокомментируйте, предоставлю
Отслеживать
задан 7 мар 2023 в 20:14
floydpower floydpower
109 9 9 бронзовых знаков
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Создать кнопки "Далее" и столько же "назад", но в каждую из "далее" передавать разную callback_data, при переключении на след.страницу будут выводиться инлайн-кнопки второй страницы.
next_1 = InlineKeyboardMarkup(row_width=1) btn_1 = InlineKeyboardButton(text='Далее', callback_data='next_1') next_1.insert(btn_1) back_1 = InlineKeyboardMarkup(row_width=1) btn_1 = InlineKeyboardButton(text='Назад', callback_data='back_1') back_1.insert(btn_1) @dp.callback_query_handler(text = "next_1") async def ru_menu(callback: types.CallbackQuery): await bot.send_message(callback.from_user.id, 'след.страница', reply_markup=#здесь указываете инлайн_клавиатуру след.страницы) await callback.message.delete() @dp.callback_query_handler(text = "back_1") async def ru_menu(callback: types.CallbackQuery): await bot.send_message(callback.from_user.id, 'пред.страница', reply_markup=#здесь указываете инлайн_клавиатуру пред.страницы) await callback.message.delete()