In this tutorial I will go through the entire length of pagination in discord.py
I kindly ask you to go through the entire thing, as it is not recommended to skip the subtopics because they are interconnected in some way.
Pagination is an extremely common thing to do in discord.py that I decided to create this gist. One of the most common uses of pagination is when you want to show contents that are more than the limit of what discord allows you.
In this tutorial, you would need to install discord-ext-menus
library that was written by Danny. The creator of discord.py. This
library mainly uses reactions as interfaces. Now don't worry, we
will go into discord button once we fully understand this library.
Before diving any further, make sure to install discord-ext-menus.
While the library contains more than just pagination. We will mainly go into pagination because that's the focus.
The library contains a few classes, mainly these ones.
menus.Menu
menus.MenuPages
menus.ListPageSource
menus.GroupByPageSource
menus.AsyncIteratorPageSource
However, we will only look into 3 of them, which is menus.Menu
, menus.MenuPages
and menus.ListPageSource
. The rest are derived from
menus.MenuPages
which you can learn on your own after this tutorial.
This class is responsible for handling the reactions given and the behaviour of what a reaction would do.
Let's use the example given by the library.
from discord.ext import menus
class MyMenu(menus.Menu):
async def send_initial_message(self, ctx, channel):
return await channel.send(f'Hello {ctx.author}')
@menus.button('\N{THUMBS UP SIGN}')
async def on_thumbs_up(self, payload):
await self.message.edit(content=f'Thanks {self.ctx.author}!')
@menus.button('\N{THUMBS DOWN SIGN}')
async def on_thumbs_down(self, payload):
await self.message.edit(content=f"That's not nice {self.ctx.author}...")
@menus.button('\N{BLACK SQUARE FOR STOP}\ufe0f')
async def on_stop(self, payload):
self.stop()
@bot.command()
async def your_command(ctx):
menu = MyMenu()
await menu.start(ctx)
send_initial_message
method is called when you callmenu.start
. You're required to return a message object forMyMenu
to handle them.menus.button
refers to your reaction. They are automatically reacted by your bot as soon asmenu.start
is called. The first argument would be the emoji that you would want to listen to and added to the Message.- The callbacks of each decorator of
menus.button
are called when you reacted to the corresponding reaction in the decorator. self.stop
method stops theMyMenu
class from listening to the reactions. This fully stops the instance from operating.
To simply, this sums up on what menus.Menu
class does.
Of course, it's a lot more complicated than this but this is the simplification i can give you.
As you can see, you can do plenty of things with this class. There's lots of application that can be used from this class alone.
For example, you can do a confirmation button using pure reactions. Have a greater control in controlling reactions without any headache. You can also use this for controls in a game where it is purely based on reactions. While yes, discord button exist. This were made before the existence of discord buttons.
It's great to have a fundamental understanding of how this will help us on creating a pagination based on reactions AND discord buttons.
[Reaction based pagination]
This class is responsible for controlling the reactions for pagination. It is responsible for handling the reactions properly on what to do whether to show the first page, next page and so on.
This class inherits menus.Menu
. Meaning, all functionality coming from menus.Menu
is also available
in this class. This class fully has all the methods to control a pagination.
However, this class alone is entirely useless. To make it useful, we will need
ListPageSource
.
This class is responsible for formatting each page in our paginator. It is also responsible for handling each of our data. Given an iterable into the class would allow it to fully handle the data into each pages. We will see this in action in Combine MenuPages & ListPageSource
With the help of MenuPages
as our reaction manager and ListPageSource
as our data and format
manager. We can fully make an operational paginator with them.
from discord.ext import menus
class MySource(menus.ListPageSource):
async def format_page(self, menu, entries):
return f"This is number {entries}."
@bot.command()
async def your_command(ctx):
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
formatter = MySource(data, per_page=1)
menu = menus.MenuPages(formatter)
await menu.start(ctx)
MySource
acts as a formatter, givendata
, elements will be separated given byper_page
kwargs. In this case, each element is separated as a single page.MenuPages
accepts any class that subclassesmenus.PageSource
. This includesListPageSource
(ourMySource
).MenuPages
adds all the necessary reactions for navigation.- everytime
MenuPages
receives a reaction, it processes them on which way to go. After that, it callsformat_page
after it processed on which page to show. - When
format_page
is called.menu
would beMenuPages
instance andentries
will be the values that were separated given byper_page
fromdata
. - Anything that is returned in
format_page
are displayed onto your Message. The value that can be returned areEmbed
/dict
/str
.
If you want to customize the reaction. You would need to override MenuPages
class. As we know,
MenuPages
is the class that handles the reaction. We can fully change this to fit our needs.
import discord
from discord.ext import menus
from discord.ext.menus import button, First, Last
class MyMenuPages(menus.MenuPages, inherit_buttons=False):
@button('<:before_fast_check:754948796139569224>', position=First(0))
async def go_to_first_page(self, payload):
await self.show_page(0)
@button('<:before_check:754948796487565332>', position=First(1))
async def go_to_previous_page(self, payload):
await self.show_checked_page(self.current_page - 1)
@button('<:next_check:754948796361736213>', position=Last(1))
async def go_to_next_page(self, payload):
await self.show_checked_page(self.current_page + 1)
@button('<:next_fast_check:754948796391227442>', position=Last(2))
async def go_to_last_page(self, payload):
max_pages = self._source.get_max_pages()
last_page = max(max_pages - 1, 0)
await self.show_page(last_page)
@button('<:stop_check:754948796365930517>', position=Last(0))
async def stop_pages(self, payload):
self.stop()
class MySource(menus.ListPageSource):
async def format_page(self, menu, entries):
embed = discord.Embed(
description=f"This is number {entries}.",
color=discord.Colour.random()
)
embed.set_footer(text=f"Requested by {menu.ctx.author}")
return embed
@bot.command()
async def your_command(ctx):
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
formatter = MySource(data, per_page=1)
menu = MyMenuPages(formatter)
await menu.start(ctx)
- Setting
inherit_buttons
kwargs toFalse
removes buttons that came fromMenuPages
. - in each
button
decorator, the first argument would be your emoji. Followed byposition
kwargs.position
kwargs acceptsPosition
,First
,Last
class. First
refers to your position of button. This class inheritsPosition
.First
acts as an anchor, where it will always be added beforeLast
class gets added. In this case, the position of buttons would be;First(0)
,First(1)
,Last(0)
,Last(1)
,Last(2)
show_page
method sets thecurrent_page
to your page. In this case, it is used ingo_to_first_page
where we only wanna show the first page, andgo_to_last_page
where we only want to show the last page.show_checked_page
method usesshow_page
, but checks if it is within the range of your page. This ensure that there isn't an out of bound exception.format_page
this time returns an embed, with a randomized color to make it pretty.- We would use
MyMenuPages
instead ofMenuPages
because we want to try the custom emoji. - The rest of the explanation are available at Menu
As a reference, here are the emojis I'm using currently.
As you can see, it is relatively easy to create your own pagination with the default MenuPages. The complexity increases as you want to start doing customization, however this can be easy once you get the hang of it.
It is recommended for you to explore other classes for other use cases. However, I'm not gonna be focusing them here.
Now that we have the fundamentals of menus. We can also construct them using View on our own ways.
[Button based pagination]
This tutorial is only compatible with discord.py 2.0. Any lower such as discord.py 1.7 will not have this class because it was not built with the newer feature of discord such as buttons and dropdowns.
You would need to install discord.py 2.0 via git instead of pypi. 2.0 were never released thus not available in pypi.
This class is responsible for handling classes that inherits from discord.ui.Item. This includes discord.ui.Button which we will be using in this tutorial as navigation for the pagination.
Here are a list of classes that is related to View.
discord.ui.View
discord.ui.Item
discord.ui.Button
discord.ui.Select
However, we will only talk about 2 here which is discord.ui.View
and discord.ui.Button
. Those are the only thing we
need for the pagination.
To start, discord.ui.View
works similarly like menus.Menu
. To demonstrate, here's an example.
import discord
from discord import ui
class MyView(ui.View):
@ui.button(label="Hello", emoji="\U0001f590", style=discord.ButtonStyle.blurple)
async def on_click_hello(self, button, interaction):
await interaction.response.send_message("Hi")
@bot.command()
async def your_command(ctx):
view = MyView()
await ctx.send("Click button", view=view)
ui.button
is a decorator that will create a Button. The parameter are almost the same as discord.ui.Button.- The said decorator will call the callback when clicked which in this case refers to
on_click_hello
method. It will passbutton
which isdiscord.ui.Button
andinteraction
which isdiscord.Interaction
as the parameter. interaction.response
returnsInteractionResponse
instance, which you can use to respond to the user. We will usesend_message
for now to send a message.MyView
is instantiatedabc.Messageable.send
containsview
kwargs which you can pass yourView
instance into. In our case,view
is the instance ofMyView
.
With this brief knowledge, we can now make pagination with View. As we know, MenuPages
acts as the navigation of
the pagination while ListPageSource
acts as the formatter which format the data and the page. While menus.Menu
handles
reactions and menus.MenuPages
contains everything about pagination handling that we really needed. We only need
the handling part.
Based on this knowledge alone, we can combine MenuPages
with discord.ui.View
to make a fully functioning paginator
with discord.ui.Button
as the navigation. This code will be similar to Custom MenuPages code.
import discord
from discord import ui
from discord.ext import menus
class MyMenuPages(ui.View, menus.MenuPages):
def __init__(self, source):
super().__init__(timeout=60)
self._source = source
self.current_page = 0
self.ctx = None
self.message = None
async def start(self, ctx, *, channel=None, wait=False):
# We wont be using wait/channel, you can implement them yourself. This is to match the MenuPages signature.
await self._source._prepare_once()
self.ctx = ctx
self.message = await self.send_initial_message(ctx, ctx.channel)
async def _get_kwargs_from_page(self, page):
"""This method calls ListPageSource.format_page class"""
value = await super()._get_kwargs_from_page(page)
if 'view' not in value:
value.update({'view': self})
return value
async def interaction_check(self, interaction):
"""Only allow the author that invoke the command to be able to use the interaction"""
return interaction.user == self.ctx.author
# This is extremely similar to Custom MenuPages(I will not explain these)
@ui.button(emoji='<:before_fast_check:754948796139569224>', style=discord.ButtonStyle.blurple)
async def first_page(self, button, interaction):
await self.show_page(0)
@ui.button(emoji='<:before_check:754948796487565332>', style=discord.ButtonStyle.blurple)
async def before_page(self, button, interaction):
await self.show_checked_page(self.current_page - 1)
@ui.button(emoji='<:stop_check:754948796365930517>', style=discord.ButtonStyle.blurple)
async def stop_page(self, button, interaction):
self.stop()
@ui.button(emoji='<:next_check:754948796361736213>', style=discord.ButtonStyle.blurple)
async def next_page(self, button, interaction):
await self.show_checked_page(self.current_page + 1)
@ui.button(emoji='<:next_fast_check:754948796391227442>', style=discord.ButtonStyle.blurple)
async def last_page(self, button, interaction):
await self.show_page(self._source.get_max_pages() - 1)
@bot.command()
async def your_command(ctx):
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
formatter = MySource(data, per_page=1) # MySource came from Custom MenuPages subtopic. [Please refer to that]
menu = MyMenuPages(formatter)
await menu.start(ctx)
Jesus christ that's a long code
- We inherit both
ui.View
andmenus.MenuPages
. Where we will borrow methods frommenus.MenuPages
while usingui.View
as our main navigation for the pagination. - In
__init__
, thesuper().__init__
will refer toui.View
instead. To understand why this is, learn Method Resolution Order(MRO).source
argument must be aPageSource
instance. The rest of the attribute are required to be assign becauseMenuPages
will use them. - In
start
method, it will follow theMenuPages
method signature.self.send_initial_message
is a method fromMenuPages
, it acts as sending a message to the user and returns a Message object. We will store them inself.message
self._source._prepare_once
is a method to declare that thePageSource
object has started._get_kwargs_from_page
method is also fromMenuPages
, it is responsible for callingformat_page
and returns a dictionary which will directly be used indiscord.Message.edit
kwargs. We take advantage of this and putview
as the to put ourView
object into the message for navigation.interaction_check
is a method ofui.View
. It gets called when a button is clicked. ReturnTrue
to allow for callback to be called. We will useinteraction.user
where it's the person who clicked the button to check if they are our author. If it is, it's True else return False. Optionally, you would send an ephemaral message to the user if it is not the author.- The rest of the explanation must refer to Custom MenuPages subtopic. It is exactly the same explanation.
While yes it may seem long. But keep in mind, you would only do MyMenuPages
once, after that, you can create infinite
amount of ListPageSource
that fit your need for all of the pagination you will ever need. Feel free to derive this code
into a much more advance handler. I've only talked briefly on how to use ui.View
. There's plenty more uses you can do
with it and I would recommend exploring more on ui.View
. Danny has created plenty of View examples here: Click Here.
Congrats, you've reached the end of this tutorial. I don't recommend skipping any of the topic here because you will be
left confused on what you've just read and learnt nothing. Each subtopic is linked to each other hence why. Anyways, any
question regarding ui.View
and discord-ext-menus
can be asked in discord.py. I don't check this gist that much. So your
question wont be answered that quickly.
Tbh i wrote this because i need to remind myself about this in about a year. that's all, thanks.
Thank you for the detailed explanation :D