LoginSignup
1
1

More than 1 year has passed since last update.

py-cordのUI KITを使ってボタンやドロップダウンメニューを実装する方法

Last updated at Posted at 2022-09-08

はじめに

この記事ではPythonのDiscordでBOTを作成する為のライブラリのpy-cordを利用したUIの作成について説明させていただいています。

前提知識

主にpy-cordでコマンドを実装したことがある方であればおおよそわかる内容かと思います。
もし、一度も実装した事がない場合はこちらの記事などを参考にして基本的なコマンドを実装してみてください。
python自体のクラス継承なども登場しますが、ある程度解説するので必須ではありません。

環境

  • python 3.10.6
  • py-cord 2.1.3

もっとも簡単な方法

py-cordでは簡潔な記述でボタンを実装するデコレータが用意されているため、最も簡単な実装では以下のようにボタンを実装することができます。

この情報はほとんどPycord公式のガイドとなりますので、そちらで参考してもいいと思います。

ボタンの場合

from discord.ext import commands
import discord

intent = discord.Intents.default()
intent.messages = True

bot = commands.Bot(
    debug_guilds=[000000000],  # Guild id
    intent=intent
)


# Viewクラスを継承してButtonを持ったViewを作成します。
class TestView(discord.ui.View):
    # デコレータを使ってButtonオブジェクトをViewのitemsに格納する
    @discord.ui.button(label="ボタン1", style=discord.ButtonStyle.green)
    async def first_button(self, button: discord.ui.Button, interaction: discord.Interaction):
        button.disabled = True  # ボタンの無効化
        await interaction.response.edit_message(content="ボタンが押されました", view=self)


# Viewを呼び出すためのコマンドを定義
@bot.slash_command(name="view")
async def view_test(ctx: discord.ApplicationContext):
    view = TestView()
    await ctx.interaction.response.send_message(content="テストのボタンです", view=view)


bot.run("TOKEN")

簡単に解説するとデコレータのlabelがボタンの表示名で、first_button関数の引数になっているbuttonself(Viewのこと)に存在するbuttonオブジェクトになります。
つまり、この動作ではボタン1のラベルのボタンが押された時そのボタンを無効化(グレーアウト)し、ボタンがおされた事を通知する動作となります。

この記述方法であれば

  1. Viewを継承クラスを用意する
  2. デコレータを使ってボタンの動作を定義する
  3. コマンドからViewを呼び出す

の3ステップで記述することが可能でとても扱いやすいです。

ドロップダウンメニューの場合

# Viewクラスを継承してButtonを持ったViewを
class TestView(discord.ui.View):
    # デコレータを使ってSelectオブジェクトをViewのitemsに格納する
    @discord.ui.select(placeholder="選択", options=[
        discord.SelectOption(label="選択肢1", value="1"),
        discord.SelectOption(label="選択肢2", value="2"),
        discord.SelectOption(label="選択肢3", value="3"),
    ])
    # ユーザーがドロップダウンメニューから選択したときの動作
    async def select_menu(self, select: discord.ui.Select, interaction: discord.Interaction):
        await interaction.response.edit_message(content=f"あなたが選択したのは {select.values[0]} です", view=self)


@bot.slash_command(name="view")
async def view_test(ctx: discord.ApplicationContext):
    view = TestView()
    await ctx.interaction.response.send_message(content="テストのドロップダウンメニューです", view=view)

こちらも簡単に解説するとデコレータのplaceholderがドロップダウンメニューを選択していない時に表示されるラベルで、メニューを押した時にoptionsの配列から各labelを表示します。
select_menu関数の引数になっているselectself(Viewのこと)に存在するSelectオブジェクトになります。
この動作ではselectのラベルのメニューが選択された時、そのvalueの値を取得して選択したものを通知しています。

こちらも同様に以下の3ステップで実装可能です。

  1. Viewを継承クラスを用意する
  2. デコレータを使ってドロップダウンメニューの動作を定義する
  3. コマンドからViewを呼び出す

必ず任意のボタンを持たせた状態での実装

上記のオブジェクトはどちらのViewを継承しており、自作のViewクラスを更に継承することで任意のボタンを必ず持たせたViewを作成することが可能になります。
例えば、かならず終了するボタンがあるViewを送信したいときなどに便利です。

# 必ず表示させたいボタンを持ったViewを定義します
class BaseView(discord.ui.View):
    # rowを4に設定し一番下に表示されるようにする
    @discord.ui.button(label="終了する", style=discord.ButtonStyle.red, row=4)
    # もし、コールバック関数の中で使用しない引数が存在してしまう場合は _ にすると親切です
    async def exit_button(self, _: discord.ui.Button, interaction: discord.Interaction):
        await interaction.response.edit_message(content="お疲れ様でした", view=None)
        await interaction.delete_original_message(delay=10)


# BaseViewを継承したクラスを定義します
class ButtonTest(BaseView):
    @discord.ui.button(label="ボタン1", style=discord.ButtonStyle.green)
    async def first_button(self, button: discord.ui.Button, interaction: discord.Interaction):
        button.disabled = True
        await interaction.response.edit_message(content="ボタンが押されました", view=self)


class SelectTest(BaseView):
    @discord.ui.select(placeholder="選択", options=[
        discord.SelectOption(label="選択肢1", value="1"),
        discord.SelectOption(label="選択肢2", value="2"),
        discord.SelectOption(label="選択肢3", value="3"),
    ])
    async def select_callback(self, select: discord.ui.Select, interaction: discord.Interaction):
        await interaction.response.edit_message(content=f"あなたが選択したのは {select.values[0]} です", view=self)


@bot.slash_command(name="testbutton")
async def view_test(ctx: discord.ApplicationContext):
    view = ButtonTest()
    await ctx.interaction.response.send_message(content="テストのボタンです", view=view)


@bot.slash_command(name="testselect")
async def view_test(ctx: discord.ApplicationContext):
    view = SelectTest()
    await ctx.interaction.response.send_message(content="テストのドロップダウンメニューです", view=view)

これによりどちらのコマンドを呼び出した場合にも終了するボタンが表示され動作します。

rowで指定できる数は0~4で各ボタンなどの順番を決めるのに役立ちます。

動的な実装を行う場合

先程は定数的なボタンやドロップダウンメニューを実装する方法を解説しました。
例えば「ユーザーがコマンドにて値を入力してそれを何かしらの方法で値を保持し、それをドロップダウンメニューで選択できるようにする。」といった動作をさせる場合はデコレータを利用した方法だと行うことができません。
このような動作を実装する場合はButtonクラスやSelectクラスを継承する必要が出てきます。

ここではSelectクラスを継承して動的なメニュー表示を実装する方法を解説します。

# ユーザー入力を保存するためのlistを定義
options: list[discord.SelectOption] = []


# Selectクラスを継承し、処理の実装などを行います。
class UserInputSelect(discord.ui.Select):
    def __init__(self, options: list[discord.SelectOption] = ...) -> None:
        # placeholderは定数で良いのでここでは選択としています。
        super().__init__(placeholder="選択", options=options)

    # callbackメソッドをオーバーライドして処理を実装します。
    async def callback(self, interaction: discord.Interaction):
        value = self.values[0]
        await interaction.response.edit_message(content=f"あなたが選択したのは{value}です")


class TestView(discord.ui.View):
    def __init__(self, options: list[discord.SelectOption]):
        # 5分後にボタンなどを無効にする様に設定
        super().__init__(timeout=300, disable_on_timeout=True)
        # optionsを使ってオブジェクトを作成しitemsに格納する
        self.add_item(UserInputSelect(options=options))


# ドロップダウンメニューにデータを追加するためのコマンドを実装
@bot.slash_command(name="add_option")
async def add_option(ctx: discord.ApplicationContext, label: str, value: str):
    # 受け取った値からoptionsに値を追加していきます。
    options.append(discord.SelectOption(label=label, value=value))
    await ctx.interaction.response.send_message(content=f"ラベル:{label}, 値:{value} で登録しました")


@bot.slash_command(name="select_menu")
async def select_menu(ctx: discord.ApplicationContext):
    if len(options) > 0:
        view = TestView(options=options)
        await ctx.interaction.response.send_message(content="ドロップダウンメニュー", view=view)
    else:
        await ctx.interaction.response.send_message(content="まだ値が入力されていません。")

Selectクラスを継承することで任意の処理をオーバーライドしたcallbackに記述することが可能です。
optionsを受け取りオブジェクトを作成し、add_itemメソッドにてitemsに格納しています。
デコレータを使った場合内部的に同じような事が行われているため、とても簡潔に記述することが可能となっています。

disable_on_timeoutはpy-cord 2.1からの機能みたいなので実装する場合は注意してください。
このオプションはtimeoutで設定された秒数が経過後にボタンなどをすべて無効にする処理を行うかの設定です。
不要な場合はFalseにするかこの引数自体を消してしまっても問題ありません。

参考にさせて頂いたもの

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1