Эта статья предназначена для новичков. Я намерено опускаю сложные детали и нюансы, чтобы материал воспринимался легче.

Всем вновь привет! Сегодня мы продолжим цикл статей, посвящённых разработке телеграм бота на PyTelegramBotAPI.

В этом уроке я буду чередовать слова: пользователь и клиент. В контексте нашей задачи эти слова означают одно и тоже - пользователя нашего бота.


<<< Предыдущий урок Навигация по статьям Следующий урок >>>

В этом уроке мы разберём клавиатуру, как одну из важнейших частей бота.

Но перед этим, вспомним что мы сделали ранее.

В первом уроке мы создали токен бота и написали простую функцию приветствия пользователя. Во втором уроке мы написали словарь для хранения пользовательских данных (имя, фамилия) и написали функции для запроса этих данных.

Знания освежили, теперь займёмся кодом. Прежде чем разбирать клавиатуру, немного доработает наш код. А именно: создадим новую функцию, которая будет запрашивать у пользователя имя. Конечно, вы можете сказать что такая функция уже есть это — welcome. Да, согласен, она запрашивает имя, но всё же эта функция запуска нашего бота, функция приветствия. Поэтому логику запроса данных лучше вынести.

@bot.message_handler(func=lambda message: message.text == 'Написать в поддержку')
def write_to_support(message):
    chat_id = message.chat.id
    bot.send_message(chat_id, 'Введите своё имя')
    users[chat_id] = {}
    bot.register_next_step_handler(message, save_username)

Эта функция и будет запрашивать у пользователя имя. Но, вас могла напрячь первая строка кода — строка с лямдой (прочтите про неё, лямбда нам ещё не раз пригодится). На самом деле ничего страшного тут нет. Так как фраза «Написать в поддержку» не является командой (команды со слеша — /), то и использовать аргумент commands мы не можем. Тут то нам на помощь и приходит func, этот аргумент позволяет определить функцию, которая будет вызываться для проверки. Она (функция) должна сообщить библиотеке PyTelegramBotAPI, вызывать эту функцию на текущее сообщение или нет. Наша функция (лямбда) будет возвращать True, только если текст сообщения будет «Написать в поддержку», на другой текст эта функция реагировать не будет. Надеюсь это понятно, если нет — прошу в комментарии, стараюсь отвечать всем.

Теперь поменяем наш welcome. Так как я бы хотел сделать welcome просто функцией приветствия, то уберём весь код отвечающий за запрос имени.

Код обновлённого welcome будет выглядеть так:

@bot.message_handler(commands=['start'])
def welcome(message):
    chat_id = message.chat.id
    bot.send_message(chat_id,
                     'Добро пожаловать в бота сбора обратной связи')

Подготовительная часть завершена, теперь разберёмся с клавиатурой. В начале совсем немного теории из разряда: А зачем это нужно?

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

Для этого и придумали клавиатура, выглядит она так:

Первый тип клавиатуру
Первый тип клавиатуру

Или так:

Второй тип клавиатуру
Второй тип клавиатуру

Изображения взяты с интернета.

Как вы можете заметить — это две абсолютно разные клавиатуры. Первая находится под строкой ввода сообщения (Reply), а вторая в самом сообщении (Inline).

Напишем клавиатуру Reply (та, которая находится под строкой ввода сообщения. Скриншот 1).

@bot.message_handler(commands=['start'])
def welcome(message):
    chat_id = message.chat.id
    keyboard = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True)
    button_support = telebot.types.KeyboardButton(text="Написать в поддержку")
    keyboard.add(button_support)
    bot.send_message(chat_id,
                     'Добро пожаловать в бота сбора обратной связи',
                     reply_markup=keyboard)

Теперь разберём код. На 4 строке я создал объект keyboard, это объект класса ReplyKeyboardMarkup, который и является той самой Reply клавиатурой, в скобках указано resize_keyboard=True, это сделано для нормальной работы кнопок, в некоторых случаях кнопки могут сильно растягиваться, расширяться и будут выглядеть крайне не презентабельно, resize_keyboard решает эту проблему.

Далее создается кнопка button_support, которая в аргументах содержит текст, отображаемый на кнопке.

В конце кода я добавляю кнопку на клавиатуру (делать это обязательно, или кнопка не отобразиться) и отправляю её в сообщении, указав параметр reply_markup.

Вот так выглядит и работает клавиатуры:

Reply клавиатура с одной кнопкой
Reply клавиатура с одной кнопкой

Поиграемся с кнопками, создадим, например, четыре штуки и будем их по разному размещать.

Кнопки в одной строке:

Кнопки в одном keyboard.add()
Кнопки в одном keyboard.add()

Код:

@bot.message_handler(commands=['start'])
def welcome(message):
    chat_id = message.chat.id
    keyboard = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True)
    button_support = telebot.types.KeyboardButton(text="Написать в поддержку")
    button1 = telebot.types.KeyboardButton(text="Кнопка 1")
    button2 = telebot.types.KeyboardButton(text="Кнопка 2")
    button3 = telebot.types.KeyboardButton(text="Кнопка 3")
    keyboard.add(button_support, button1, button2, button3)
    bot.send_message(chat_id,
                     'Добро пожаловать в бота сбора обратной связи',
                     reply_markup=keyboard)

Почему я сказал, что кнопки в одной строке, хотя последняя кнопка и находится на новой? Потому что в коде я их добавил на 1 строку, но так как телеграм умный, то он сам их раскидал по строкам чтобы они нормально отображались.

Теперь по две кнопки:

Кнопки в двух keyboard.add()
Кнопки в двух keyboard.add()

Код:

    keyboard = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True)
    button_support = telebot.types.KeyboardButton(text="Написать в поддержку")
    button1 = telebot.types.KeyboardButton(text="Кнопка 1")
    button2 = telebot.types.KeyboardButton(text="Кнопка 2")
    button3 = telebot.types.KeyboardButton(text="Кнопка 3")
    keyboard.add(button_support, button1)
    keyboard.add(button2, button3)

Приведена только середина функции, так как начало и конец не изменились.

Как можно заметить, чтобы располагать кнопки на разных строках, достаточно их добавить в разные keyboard.add(), думаю это вы поняли.

Кстати, чтобы убрать Reply клавиатуру, надо использовать класс ReplyKeyboardRemove. Пример:

@bot.message_handler(commands=['remove_keyboard'])
def remove_keyboard(message):
    chat_id = message.chat.id
    keyboard = telebot.types.ReplyKeyboardRemove()
    bot.send_message(chat_id,
                     'Удаляю клавиатуру',
                     reply_markup=keyboard)

Отлично, это была Reply клавиатура, а теперь поговорим про Inline. Напомню, что при этом типе клавиатуры кнопки будут добавлены не под строчку написания сообщения, а под само сообщение.

Для использования клавиатуры Inline, надо будет использовать класс клавиатуры — InlineKeyboardMarkup, а для кнопок — InlineKeyboardButton. Также в кнопках надо прописать параметр callback_data, зачем он нужен я вам расскажу чуть позже.

Изменим классы и получим такой код:

    keyboard = telebot.types.InlineKeyboardMarkup()
    button_save = telebot.types.InlineKeyboardButton(text="Сохранить",
                                                     callback_data='save_data')
    button_change = telebot.types.InlineKeyboardButton(text="Изменить",
                                                     callback_data='change_data')
    keyboard.add(button_save, button_change)

Запустим, проверим, и правда — кнопки есть. Но есть один минус. Они не работают. Дело в том, что Inline кнопки работают не так, как Reply. При нажатии никакого сообщения не отправляется, а сервер телеграма отправляет запрос к нашему боту, что была нажата кнопка. Появляется вопрос: а как наш бот узнает какая кнопка нажата? Для этого мы и прописывали callback_data, при нажатии бот получит текст, который был указан в callback_data и по этому тексту мы и сможем понять, какая кнопка была нажата.

Добавим обработчик для двух наших кнопок (кнопки сохранения и изменения данных. Под данными я подразумевая имя и фамилию).

@bot.callback_query_handler(func=lambda call: call.data == 'save_data')
def save_btn(call):
    message = call.message
    chat_id = message.chat.id
    bot.send_message(chat_id, f'Данные сохранены')


@bot.callback_query_handler(func=lambda call: call.data == 'change_data')
def save_btn(call):
    message = call.message
    chat_id = message.chat.id
    bot.send_message(chat_id, f'Изменение данных.')
    write_to_support(message)

Тут схожий принцип, что был и ранее. Мы используем лямбду для фильтрации. У нас две функции, реагирующие на разные кнопки, поэтому в первом декораторе мы ожидаем save_data, а во второй change_data.

Теперь разберёмся что же такое call. Как можно заметить, в функциях используется не message, а call. call — это объект, расширяющий стандартный message. Из полезного — в call есть call.data, это название кнопки на которую мы нажали (callback_data). Мы также можем получить и привычный нам message, для этого напишем:

message = call.message

Запустим и протестируем код:

Как можно заметить, всё прекрасно работает.

Кстати, чтобы удалить Inline клавиатуру, надо отредактировать сообщение. Зачем удалять клавиатуру? Чтобы после нажатия на кнопку, они (кнопки) пропадали и пользователь не спамил боту нажимая на кнопки. Пример редактирования:

@bot.callback_query_handler(func=lambda call: call.data == 'save_data')
def save_btn(call):
  message = call.message
  chat_id = message.chat.id  
  message_id = message.message_id  
  bot.edit_message_text(chat_id=chat_id, message_id=message_id, 
                         text='Данные сохранены!')   

Получившийся код:

import telebot
from config import TOKEN

bot = telebot.TeleBot(TOKEN)

users = {}


@bot.message_handler(commands=['start'])
def welcome(message):
    chat_id = message.chat.id
    keyboard = telebot.types.ReplyKeyboardMarkup()
    button_save = telebot.types.InlineKeyboardButton(
        text="Написать в поддержку")
    keyboard.add(button_save)
    bot.send_message(chat_id,
                     'Добро пожаловать в бота сбора обратной связи',
                     reply_markup=keyboard)


@bot.message_handler(
    func=lambda message: message.text == 'Написать в поддержку')
def write_to_support(message):
    chat_id = message.chat.id
    bot.send_message(chat_id, 'Введите своё имя')
    users[chat_id] = {}
    bot.register_next_step_handler(message, save_username)


def save_username(message):
    chat_id = message.chat.id
    name = message.text
    users[chat_id]['name'] = name
    bot.send_message(chat_id, f'Отлично, {name}. Теперь укажи свою фамилию')
    bot.register_next_step_handler(message, save_surname)


def save_surname(message):
    chat_id = message.chat.id
    surname = message.text
    users[chat_id]['surname'] = surname
    keyboard = telebot.types.InlineKeyboardMarkup()
    button_save = telebot.types.InlineKeyboardButton(text="Сохранить",
                                                     callback_data='save_data')
    button_change = telebot.types.InlineKeyboardButton(text="Изменить",
                                                       callback_data='change_data')
    keyboard.add(button_save, button_change)

    bot.send_message(chat_id, f'Сохранить данные?', reply_markup=keyboard)


@bot.message_handler(commands=['who_i'])
def who_i(message):
    chat_id = message.chat.id
    name = users[chat_id]['name']
    surname = users[chat_id]['surname']
    bot.send_message(chat_id, f'Вы: {name} {surname}')


@bot.callback_query_handler(func=lambda call: call.data == 'save_data')
def save_btn(call):
    message = call.message
    chat_id = message.chat.id
    message_id = message.message_id  
    bot.edit_message_text(chat_id=chat_id, message_id=message_id, 
                         text='Данные сохранены!')  


@bot.callback_query_handler(func=lambda call: call.data == 'change_data')
def save_btn(call):
    message = call.message
    chat_id = message.chat.id
    message_id = message.message_id  
    bot.edit_message_text(chat_id=chat_id, message_id=message_id, 
                         text='Изменение данных!')  
    write_to_support(message)


if __name__ == '__main__':
    print('Бот запущен!')
    bot.infinity_polling()

Отлично, всё работает!

Подведём итого. Сегодня мы изучили:

  1. Изучили фильтрацию запросов через лямбду

  2. Написали Reply клавиатуру

  3. Написали Inline клавиатуру

Отличный результат, практически все основные возможности изучены, осталось совсем чуть-чуть.

<<< Предыдущий урок Навигация по статьям Следующий урок >>>


Ну вот, третья часть закончена! Можно считать, что все основные возможности клавиатуры изучены. Конечно, это не весь возможный функционал, но основная его часть.

Увидимся в следующей статье, в который мы полностью доделаем нашего бота и приготовим его к деплою на сервер.

Код с этого урока на GitHub.

Комментарии (2)


  1. DonAlPAtino
    23.11.2023 11:10
    +1

    А inline клавиатуру удалить можно?

        keyboard = telebot.types.InlineKeyboardMarkup()
        bot.send_message(chat_id,
                         'Удаляю клавиатуру',
                         reply_markup=keyboard)

    не срабатывает. Или я что-то не так делаю?


    1. Ryize Автор
      23.11.2023 11:10

      Чтобы удалить клавиатуру, надо изменить сообщение (отредактировать его). Вот пример кода:

      @bot.callback_query_handler(func=lambda call: call.data == 'save_data')
      def save_btn(call):
        message = call.message
        chat_id = message.chat.id  
        message_id = message.message_id  
        bot.edit_message_text(chat_id=chat_id, message_id=message_id, 
                               text='Данные сохранены!')   

      При редактировании сообщения Inline клавиатура сбрасывается.

      P. S. Обновил статью, внизу написан пример и итоговый код