PythonのTkinterでイベント処理とバインディングをマスターする方法

Tkinterは、PythonでGUIアプリケーションを構築するための標準ライブラリであり、そのシンプルさと柔軟性から、多くの開発者に利用されています。GUIアプリケーションでは、ユーザーがボタンをクリックしたり、キーを押したり、マウスを動かしたりといった操作を行います。これらの操作を「イベント」と呼び、イベントに応じて適切な処理を行うことがアプリケーションの核となります。本記事では、Tkinterにおけるイベント処理とバインディングの基本から応用までを解説し、これらを駆使して実用的なGUIアプリケーションを作成するための知識を提供します。

目次

Tkinterのイベント処理の基本


GUIアプリケーションでの「イベント」とは、ユーザーがアプリケーションに対して行う操作を指します。たとえば、ボタンをクリックしたり、キーを押したり、ウィンドウを閉じたりすることがイベントに該当します。Tkinterでは、こうしたイベントが発生した際に、対応する処理を行う仕組みが組み込まれています。

イベントループとは


Tkinterの基本構造では、mainloop()メソッドがイベントループを実行します。このイベントループが、アプリケーションのウィンドウを維持し、ユーザー操作を検知して対応するイベントハンドラを呼び出します。以下は簡単な例です。

import tkinter as tk

root = tk.Tk()

# ボタンを作成して配置
button = tk.Button(root, text="クリックしてください")
button.pack()

root.mainloop()

上記の例では、ウィンドウが表示され、mainloop()が常にイベントを監視しています。ただし、このコードはイベントを処理する仕組みが未設定のため、ボタンをクリックしても何も起こりません。

イベントハンドラの役割


イベントハンドラとは、特定のイベントが発生したときに実行される関数やメソッドのことです。たとえば、ボタンがクリックされた際に「ボタンがクリックされました」と表示させるには、イベントハンドラを登録する必要があります。この登録を「イベントバインディング」と呼びます。

次節では、イベントバインディングの基本構文を説明し、ハンドラを使ったイベント処理の具体例を見ていきます。

イベントバインディングの基本構文


Tkinterでイベントを処理するためには、特定のイベントに対して関数(イベントハンドラ)をバインディングする必要があります。これにより、ユーザー操作に応じた処理をアプリケーションに組み込むことができます。ここでは、バインディングの基本構文とその使い方について解説します。

bindメソッドの基本構文


イベントをウィジェットにバインディングする際には、bindメソッドを使用します。基本構文は次のとおりです。

widget.bind("<イベントタイプ>", イベントハンドラ)
  • widget: イベントを監視するTkinterウィジェット(例: ボタンやラベル)。
  • <イベントタイプ>: 対応するイベントを指定する文字列(例: <Button-1>はマウス左クリック)。
  • イベントハンドラ: イベントが発生した際に実行される関数。

簡単なバインディングの例


以下のコードは、ボタンをクリックした際にメッセージを表示する例です。

import tkinter as tk

def on_button_click(event):
    print("ボタンがクリックされました!")

root = tk.Tk()

# ボタンを作成
button = tk.Button(root, text="クリックしてください")
button.pack()

# ボタンに左クリックイベントをバインド
button.bind("<Button-1>", on_button_click)

root.mainloop()

このコードでは、bindメソッドを使って<Button-1>(マウス左クリック)イベントをon_button_click関数にバインドしています。ボタンをクリックすると、関数が呼び出され、「ボタンがクリックされました!」というメッセージがコンソールに表示されます。

イベントハンドラの詳細


バインドされた関数(イベントハンドラ)は、必ず1つの引数を取ります。この引数は、イベントオブジェクトを表し、イベントに関する情報(発生場所、ボタン番号など)が含まれています。例を見てみましょう。

def on_event(event):
    print(f"イベント発生!座標: ({event.x}, {event.y})")

この関数をバインドすれば、マウスのクリック位置を取得するなど、より高度な処理が可能です。

次節では、バインディング可能なイベントの種類とその使い方について詳しく解説します。

イベントバインディングの種類


Tkinterでは、さまざまな種類のイベントをウィジェットにバインドできます。これにより、ユーザーの多様な操作(クリック、キー入力、ウィジェットの変更など)に応じた処理を実現できます。この節では、主なイベントの種類とその使い方を詳しく解説します。

キーボードイベント


キーボードのキー操作に関するイベントは、<Key>を使用します。特定のキーに反応させたい場合は、<Key-文字>の形式を指定します。

import tkinter as tk

def on_key_press(event):
    print(f"{event.char}キーが押されました")

root = tk.Tk()

# 全体ウィンドウにキー押下イベントをバインド
root.bind("<Key>", on_key_press)

root.mainloop()


上記の例では、どのキーが押されたかがコンソールに表示されます。特定のキーだけを検知する場合は、次のようにします。

root.bind("<Key-a>", lambda event: print("Aキーが押されました"))

マウスイベント


マウスの操作に関するイベントは、<Button>を使います。

  • <Button-1>: 左クリック
  • <Button-2>: 中クリック
  • <Button-3>: 右クリック
def on_left_click(event):
    print("左クリックされました!")

root.bind("<Button-1>", on_left_click)

また、マウスの移動やドラッグにも対応可能です。

  • <Motion>: マウス移動
  • <B1-Motion>: 左クリックしたまま移動
def on_mouse_drag(event):
    print(f"ドラッグ中: ({event.x}, {event.y})")

root.bind("<B1-Motion>", on_mouse_drag)

ウィジェットイベント


ウィジェット自体に関連するイベントもあります。

  • <Enter>: マウスがウィジェットに入る
  • <Leave>: マウスがウィジェットから離れる
  • <FocusIn>: ウィジェットがフォーカスを受け取る
  • <FocusOut>: ウィジェットからフォーカスが外れる
def on_enter(event):
    print("マウスがウィジェットに入りました")

button = tk.Button(root, text="ホバーしてみてください")
button.bind("<Enter>", on_enter)

特殊イベント

  • <Configure>: ウィンドウサイズ変更
  • <Destroy>: ウィジェット破棄
  • <MouseWheel>: マウスホイールの操作

以下は、ウィンドウサイズ変更時の例です。

def on_resize(event):
    print(f"新しいサイズ: 幅={event.width}, 高さ={event.height}")

root.bind("<Configure>", on_resize)

イベントバインディングの工夫


複数のイベントを同じ関数で処理したい場合や、デフォルトの動作をカスタマイズしたい場合があります。その際は、以下のように応用できます。

def on_event(event):
    print(f"イベント発生: {event.type}, 座標: ({event.x}, {event.y})")

button.bind("<Button-1>", on_event)
button.bind("<Button-3>", on_event)

次節では、独自のカスタムイベントを作成し、さらに高度なイベント処理を学びます。

カスタムイベントの作成方法


Tkinterでは、標準的なイベントに加えて、独自のカスタムイベントを作成してバインドすることが可能です。これにより、アプリケーション特有の動作を柔軟に実装できます。この節では、カスタムイベントの基本的な作成方法と、実用的な応用例を紹介します。

カスタムイベントの基本


カスタムイベントは、event_generateメソッドを使ってトリガー(発火)します。このイベントは、Tkinterの通常のイベントと同じようにバインドできます。以下に基本構文を示します。

widget.event_generate("<イベント名>")

イベント名は、標準イベントと区別するために「<<>>」の形式で記述するのが一般的です。たとえば、<<MyCustomEvent>>のようにします。

カスタムイベントの例


以下は、ボタンをクリックするとカスタムイベントをトリガーする例です。

import tkinter as tk

def on_custom_event(event):
    print("カスタムイベントが発生しました!")

root = tk.Tk()

# ボタンを作成
button = tk.Button(root, text="クリックでカスタムイベント発生")
button.pack()

# カスタムイベントをバインド
root.bind("<<CustomEvent>>", on_custom_event)

# ボタンにクリックでカスタムイベントをトリガーする処理を追加
button.config(command=lambda: root.event_generate("<<CustomEvent>>"))

root.mainloop()

このコードでは、ボタンをクリックすると<<CustomEvent>>というカスタムイベントが発生し、on_custom_event関数が呼び出されます。

イベントにデータを渡す


カスタムイベントにデータを含めることもできます。event_generateメソッドのxyパラメータを使って、情報をイベントハンドラに渡せます。

def on_custom_event(event):
    print(f"カスタムイベント発生!座標: ({event.x}, {event.y})")

# カスタムイベント発生時にデータを設定
button.config(command=lambda: root.event_generate("<<CustomEvent>>", x=100, y=200))

実用例: 状態変更イベント


カスタムイベントは、アプリケーションの状態変更を通知するのに便利です。以下は、カウンターの状態が変化したときにカスタムイベントを使用する例です。

def on_counter_change(event):
    print(f"カウンターが{event.widget['text']}に変更されました")

counter = 0

def increment():
    global counter
    counter += 1
    label.config(text=str(counter))
    label.event_generate("<<CounterChanged>>")

root = tk.Tk()

# ラベルを作成
label = tk.Label(root, text="0", font=("Arial", 24))
label.pack()

# カスタムイベントをバインド
label.bind("<<CounterChanged>>", on_counter_change)

# ボタンを作成
button = tk.Button(root, text="カウントアップ", command=increment)
button.pack()

root.mainloop()

この例では、ラベルのテキストが更新されるたびに<<CounterChanged>>というカスタムイベントが発生し、その新しい値が表示されます。

カスタムイベントの利点

  • モジュール性: 特定の状態変更や動作を分離して管理できます。
  • 再利用性: 他のウィジェットやモジュールで同じイベントを利用可能です。
  • 柔軟性: 標準イベントでは対応できない複雑な処理をカプセル化できます。

次節では、イベントオブジェクトを活用して、イベント間でのデータのやり取りを詳しく解説します。

イベントとデータのやり取り


Tkinterでは、イベントハンドラにイベントオブジェクトが渡されます。このイベントオブジェクトには、発生したイベントに関する詳細な情報が含まれており、これを活用することで、イベント間のデータのやり取りや高度な処理を実現できます。この節では、イベントオブジェクトの基本構造と、データを活用する方法について解説します。

イベントオブジェクトの基本


イベントオブジェクトは、イベントが発生した際に自動的にハンドラ関数に渡されます。以下のように定義されたイベントハンドラでは、eventオブジェクトを通じてイベントに関する情報を取得できます。

def on_event(event):
    print(f"イベント発生: {event.type}")

主な属性は次のとおりです。

属性説明
typeイベントの種類(例: ButtonPress
widgetイベントが発生したウィジェット
x, yイベント発生時のウィジェット内の座標
x_root, y_root画面全体での座標
charキーボード入力の文字(文字イベントのみ)
keysym入力されたキーの名前(例: “a”, “Enter”)
state修飾キーの状態(Shift, Ctrlなど)

イベントオブジェクトを使ったデータの活用例

以下の例では、クリック位置を表示します。

def on_click(event):
    print(f"クリックされた位置: ({event.x}, {event.y})")

button = tk.Button(root, text="クリックして座標を表示")
button.bind("<Button-1>", on_click)

このコードを実行すると、ボタンをクリックした際に、そのクリック位置(ボタン内の相対座標)がコンソールに表示されます。

キーボードイベントでのデータ取得

キーボードイベントの場合、押されたキーの情報がcharkeysymとして取得できます。

def on_key_press(event):
    print(f"押されたキー: {event.char} ({event.keysym})")

root.bind("<Key>", on_key_press)

たとえば、「A」キーを押すと、charには"a"が、keysymには"A"が格納されます。

データをイベント間で渡す方法


データを持たせてイベントをトリガーしたい場合、event_generateメソッドにカスタムパラメータを渡すことができます。

def on_custom_event(event):
    print(f"カスタムイベントデータ: {event.data}")

button.config(command=lambda: root.event_generate("<<CustomEvent>>", data="Hello!"))
root.bind("<<CustomEvent>>", on_custom_event)

このコードでは、dataパラメータを使ってイベントオブジェクトにデータを追加しています。

イベントハンドラ間のデータ共有


複数のイベントハンドラ間でデータをやり取りする際、イベントオブジェクトを共通のデータ源として使用することができます。以下の例では、マウスドラッグ時に開始位置と終了位置を取得します。

start_x, start_y = 0, 0

def on_drag_start(event):
    global start_x, start_y
    start_x, start_y = event.x, event.y
    print(f"ドラッグ開始位置: ({start_x}, {start_y})")

def on_drag_end(event):
    print(f"ドラッグ終了位置: ({event.x}, {event.y})")
    print(f"ドラッグ距離: ({event.x - start_x}, {event.y - start_y})")

root.bind("<ButtonPress-1>", on_drag_start)
root.bind("<ButtonRelease-1>", on_drag_end)

このコードでは、マウスドラッグの開始位置と終了位置を記録し、ドラッグの距離を計算しています。

実用例: カラー選択ツール


以下は、クリックした位置の色を取得して表示する例です。

from tkinter import Canvas

def on_canvas_click(event):
    canvas = event.widget
    color = canvas.itemcget(canvas.find_closest(event.x, event.y), "fill")
    print(f"クリックした色: {color}")

root = tk.Tk()
canvas = Canvas(root, width=300, height=200)
canvas.pack()

canvas.create_rectangle(50, 50, 150, 150, fill="blue")
canvas.create_rectangle(150, 50, 250, 150, fill="red")

canvas.bind("<Button-1>", on_canvas_click)
root.mainloop()

このように、イベントオブジェクトを活用することで、インタラクティブなアプリケーションを構築できます。次節では、複数イベントを同時に処理する方法について解説します。

複数イベントの同時処理


GUIアプリケーションでは、同じウィジェットで複数のイベントを扱うケースがよくあります。また、イベントが発生した際に、その影響が親ウィジェットや他のハンドラに伝播する場合があります。この節では、複数のイベントを効果的に処理する方法と、イベントの伝播について解説します。

同じウィジェットに複数のイベントをバインド


Tkinterでは、1つのウィジェットに複数のイベントをバインドできます。たとえば、ボタンにクリックとホバーイベントをバインドする例を見てみましょう。

def on_click(event):
    print("ボタンがクリックされました")

def on_hover(event):
    print("マウスがボタンに入りました")

button = tk.Button(root, text="複数イベントの例")
button.pack()

button.bind("<Button-1>", on_click)
button.bind("<Enter>", on_hover)

このコードでは、ボタンを左クリックした際と、マウスカーソルがボタンに入った際に、それぞれ異なるハンドラが実行されます。

1つのイベントで複数のハンドラを呼び出す


1つのイベントに対して複数のハンドラを登録すると、順番にすべてのハンドラが実行されます。

def handler_one(event):
    print("ハンドラ1が実行されました")

def handler_two(event):
    print("ハンドラ2が実行されました")

button = tk.Button(root, text="複数ハンドラ")
button.pack()

button.bind("<Button-1>", handler_one)
button.bind("<Button-1>", handler_two)

この例では、ボタンが左クリックされると「ハンドラ1が実行されました」と「ハンドラ2が実行されました」が順に表示されます。

イベントの伝播とキャンセル


イベントは、バインドされたウィジェットから親ウィジェットに伝播します(バブルアップ)。これを制御することで、特定のイベントの影響範囲を限定できます。

イベント伝播の仕組み

  1. イベントがウィジェットで発生します。
  2. バインドされているハンドラが実行されます。
  3. 伝播が許可されている場合、親ウィジェットに伝播します。

以下のコードは、イベント伝播を示した例です。

def child_handler(event):
    print("子ウィジェットのイベントが発生しました")

def parent_handler(event):
    print("親ウィジェットのイベントが発生しました")

frame = tk.Frame(root, width=200, height=100, bg="lightblue")
frame.pack()

button = tk.Button(frame, text="クリックしてください")
button.pack()

button.bind("<Button-1>", child_handler)
frame.bind("<Button-1>", parent_handler)

この例では、ボタンをクリックすると、「子ウィジェットのイベントが発生しました」と「親ウィジェットのイベントが発生しました」が順に表示されます。

イベント伝播の停止


イベントの伝播を停止するには、ハンドラでevent.stop_propagation()を呼び出します。

def child_handler(event):
    print("子ウィジェットのイベントが発生しました")
    return "break"  # 伝播を停止

button.bind("<Button-1>", child_handler)

この例では、ボタンをクリックしても親ウィジェットにはイベントが伝播しません。

修飾キーによる複数イベントの管理


修飾キー(Shift, Ctrl, Altなど)を使ってイベントを区別することも可能です。修飾キーの状態はevent.stateで確認できます。

def on_key_with_modifier(event):
    if event.state & 0x0001:  # Shiftキー
        print("Shift + キーが押されました")
    elif event.state & 0x0004:  # Controlキー
        print("Ctrl + キーが押されました")
    else:
        print("通常のキーが押されました")

root.bind("<Key>", on_key_with_modifier)

実用例: マルチイベント対応ボタン


以下は、右クリックでメニューを表示し、左クリックでアクションを実行するボタンの例です。

def on_left_click(event):
    print("左クリックでアクションを実行しました")

def on_right_click(event):
    print("右クリックでメニューを表示しました")

button = tk.Button(root, text="マルチイベント対応")
button.pack()

button.bind("<Button-1>", on_left_click)
button.bind("<Button-3>", on_right_click)

このように、Tkinterのイベント処理を駆使すれば、複雑なユーザー操作にも対応できる柔軟なアプリケーションを作成できます。

次節では、イベント処理を活用したシンプルなTo-Doリストアプリの実装例を紹介します。

実用例: シンプルなTo-Doリストアプリ


イベント処理とバインディングの知識を活用して、実際に動作するTo-Doリストアプリを作成します。このアプリでは、以下の機能を実装します:

  1. タスクを追加する
  2. タスクを選択して削除する
  3. タスクをクリックして状態を切り替える

この例を通じて、イベント処理の応用方法を学びましょう。

アプリの全体コード

以下にTo-Doリストアプリのコードを示します。

import tkinter as tk

def add_task():
    """入力されたタスクをリストに追加"""
    task = task_entry.get()
    if task:  # 入力が空でない場合のみ追加
        task_listbox.insert(tk.END, task)
        task_entry.delete(0, tk.END)  # 入力欄をクリア

def delete_task(event):
    """選択されたタスクを削除"""
    selected_task_index = task_listbox.curselection()
    if selected_task_index:  # タスクが選択されている場合
        task_listbox.delete(selected_task_index)

def toggle_task_state(event):
    """タスクの状態を切り替え(完了/未完了)"""
    selected_task_index = task_listbox.curselection()
    if selected_task_index:
        task = task_listbox.get(selected_task_index)
        # 未完了と完了の切り替え
        if task.startswith("[完了] "):
            task_listbox.delete(selected_task_index)
            task_listbox.insert(selected_task_index, task[5:])
        else:
            task_listbox.delete(selected_task_index)
            task_listbox.insert(selected_task_index, "[完了] " + task)

# メインウィンドウの作成
root = tk.Tk()
root.title("シンプルなTo-Doリスト")

# タスク入力エリア
task_entry = tk.Entry(root, width=30)
task_entry.pack(pady=5)

# タスク追加ボタン
add_button = tk.Button(root, text="タスクを追加", command=add_task)
add_button.pack(pady=5)

# タスクリスト表示エリア
task_listbox = tk.Listbox(root, width=50, height=10)
task_listbox.pack(pady=10)

# イベントバインディング
task_listbox.bind("<Delete>", delete_task)  # Deleteキーでタスクを削除
task_listbox.bind("<Double-1>", toggle_task_state)  # ダブルクリックで状態切り替え

# メインループ開始
root.mainloop()

アプリの機能解説

1. タスクを追加する


add_task関数は、テキストエントリに入力された文字列をリストボックスに追加します。Entryウィジェットで入力を受け取り、Listboxに反映しています。

2. タスクを削除する


delete_task関数は、<Delete>キーが押された際に選択されたタスクを削除します。Listboxcurselectionメソッドで選択されたインデックスを取得し、削除処理を行います。

3. タスクの状態を切り替える


toggle_task_state関数は、ダブルクリックでタスクの状態を「完了」と「未完了」に切り替えます。[完了]というプレフィックスを用いることで、タスクの状態を示しています。

使い方

  1. タスク名を入力欄に入力し、「タスクを追加」ボタンをクリックすると、タスクがリストに追加されます。
  2. リスト内のタスクを選択し、Deleteキーを押すとタスクが削除されます。
  3. タスクをダブルクリックすると、「完了」と「未完了」の状態が切り替わります。

ポイント

  • イベントバインディングを使うことで、操作を直感的に実装できる。
  • curselectionメソッドで選択状態を管理し、動的に変更を加える。
  • シンプルなGUI構築ながら、実用的なアプリケーションを実現できる。

次節では、イベント処理でよくある問題とその解決方法について解説します。

よくある問題とその解決方法


Tkinterのイベント処理を使用する際、初心者が陥りやすいエラーや問題がいくつかあります。この節では、よくある問題とその解決策を紹介し、イベント処理を安定して利用するためのヒントを提供します。

問題1: イベントが反応しない


イベントバインディングが正しく設定されていない場合、期待した反応が得られないことがあります。

原因と解決策

  1. バインディング先が間違っている
    イベントを適切なウィジェットにバインドしていない場合、イベントは検知されません。 例:
   # rootにバインドすべきところを間違えてbuttonにバインド
   root.bind("<Button-1>", handler)  # 正しい
   button.bind("<Button-1>", handler)  # 意図的でない場合はエラー
  1. イベントタイプの記述ミス
    イベントタイプの文字列が間違っていると無効になります。 解決策:
    イベントタイプが正しいか確認します。たとえば、<button-1>ではなく<Button-1>が正しい記述です。
  2. ウィジェットがフォーカスされていない
    特定のイベント(例: <Key>)は、ウィジェットがフォーカスを持っていないと発生しません。 解決策:
    focus_setメソッドを使用して、ウィジェットにフォーカスを与えます。
   entry.focus_set()

問題2: イベントが複数回発生する


イベントが意図せず複数回トリガーされることがあります。たとえば、ボタンのクリックイベントが複数回処理される場合です。

原因と解決策

  1. 複数のバインディングが重複している
    同じウィジェットに同じイベントを複数回バインドしている可能性があります。 解決策:
    バインディングを設定するコードを見直し、不要な重複を排除します。
  2. デフォルト動作の干渉
    一部のウィジェット(例: エントリフィールドやボタン)は、独自のイベント処理を持ちます。その結果、独自の処理とユーザー定義のハンドラが両方実行される場合があります。 解決策:
    return "break"を使用してイベントの伝播を防ぎます。
   def handler(event):
       print("カスタム処理")
       return "break"  # デフォルトの動作をキャンセル

問題3: イベントが誤動作する


イベントの優先順位や状態管理が不十分だと、意図しない挙動が発生することがあります。

原因と解決策

  1. イベントの順序管理が不十分
    同じウィジェットに異なるイベントをバインドしている場合、順序が問題となることがあります。 解決策:
    bindの代わりにbind_allbind_classを使用して、スコープを明確にします。
   root.bind_all("<Key>", handler)  # アプリ全体でキーイベントを処理
  1. 状態管理が不足している
    複数のイベントで共有する状態を適切に管理していないと、意図しない挙動が発生します。 解決策:
    グローバル変数やクラスを使用して、イベント間の状態を明確に管理します。
   drag_start = None

   def on_drag_start(event):
       global drag_start
       drag_start = (event.x, event.y)

   def on_drag_end(event):
       global drag_start
       if drag_start:
           print(f"ドラッグ距離: ({event.x - drag_start[0]}, {event.y - drag_start[1]})")

問題4: 特定の環境で動作しない


一部のイベント(例: <MouseWheel>)は、環境(Windows, Mac, Linux)によって動作が異なる場合があります。

解決策


イベントタイプを環境ごとに適切に切り替えます。

import platform

if platform.system() == "Windows":
    root.bind("<MouseWheel>", handler)
elif platform.system() == "Darwin":  # macOS
    root.bind("<Button-4>", handler)

まとめ


Tkinterのイベント処理では、正確なバインディング設定と伝播管理が重要です。また、デバッグ時には、イベント発生のタイミングや順序を慎重に確認しましょう。これらのポイントを抑えることで、安定したイベント駆動型アプリケーションを構築できます。

次節では、今回学んだ内容をまとめます。

まとめ


本記事では、PythonのTkinterを用いたイベント処理とバインディングについて、基本から応用までを詳しく解説しました。Tkinterのイベントバインディングを理解することで、ユーザー操作に応じた柔軟でインタラクティブなGUIアプリケーションを構築できるようになります。

特に、イベントの種類やカスタムイベントの作成、データのやり取り、複数イベントの同時処理といった実践的な内容を学ぶことで、Tkinterの強力な機能を最大限に活用できるでしょう。また、To-Doリストアプリの作成例を通じて、実際にイベント処理をどのように組み込むかを体験できたはずです。

これらの知識を活用し、より複雑で実用的なアプリケーションの構築に挑戦してください。イベント処理の理解を深めることで、Tkinterでの開発がさらに楽しくなるはずです!

コメント

コメントする

目次