Python GUIライブラリでポケモン図鑑を作成してみた

# はじめに
どうも、フリーランスエンジニアをしているモブ太郎です。
今回の記事では、PythonのGUIライブラリの「Custom Tkinter」を使用して、ポケモン図鑑を作成したいと思います。

細かい説明は省いていますので、参考程度、こういったものがあるんだな程度に見ていってください。
要望があれば、別の記事で「Custom Tkinter」について細かく書きたいと思います。

## 完成形
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3331610/ba8b6650-2bbc-a8aa-aea0-c506220bec42.png)


# Tkinterとは
Tkinterとは、Pythonに標準で付属しているGUIライブラリです。
Windows、Mac、Linuxといった主要なOSに対応しているクロスプラットフォームなGUIライブラリです。

# Custom Tkinterとは
Custom Tkinterとは、Tkinterに基づくPython UIライブラリであり、カスタマイズすることが可能でモダンなウィジェットを提供してくれます。
Tkinterと違って標準ライブラリではないで、pipよりモジュールをインストールする必要があります。
```
pip install customtkinter
pip3 install customtkinter
```

# PokeAPI とは
PokeAPIとは、ポケモンに関する様々なデータが取得できるAPIです。
図鑑番号、名前、タイプ、画像、高さ、重さ　etc.

https://pokeapi.co/

**使用方法**
以下のURLの最後にポケモンの図鑑No or 名前(英名)を指定することで情報を取得できます。
ピカチュウだとこんな感じです。
```
https://pokeapi.co/api/v2/pokemon/25
https://pokeapi.co/api/v2/pokemon/pikachu
```

# 実装
## Poke API 取得クラス
<details>
<summary>poke_api.py</summary>

```python
import requests

class PokeApi:

    def __init__(self) -> None:
        pass

    def request_pokemon_info(self, number):
        """図鑑No指定取得"""
        url = f"https://pokeapi.co/api/v2/pokemon/{number}"
        return requests.get(url).json()

    def request_pokemon_jp(self, url):
        return requests.get(url).json()
```
</details>

「request_pokemon_info」関数では図鑑Noを渡してAPIリクエストをするようにしています。
PokeAPIは基本的に英語の情報です。
日本語の情報を取得したい場合は、それ用のURLをリクエストすることで取得できます。
「request_pokemon_jp」関数は、PokeAPIで取得した情報に含まれる日本語情報を取得するために使います。

## ポケモンデータクラス
<details>
<summary>pokemon.py</summary>

```python
from dataclasses import dataclass

@dataclass
class Pokemon:
    number: str
    name: str
    imageUrl: str
    typesInfo: list[str]
```
</details>

ポケモンの情報を保持するデータクラスを「dataclasses」モジュールの「dataclass」デコレーターを使用して作成しています。

## ポケモンのタイプの列挙型定義
<details>
<summary>type_enum.py</summary>

```python
from enum import Enum

class TypeEnum(Enum):
    ノーマル = "#A8A878"
    ほのお = "#F08030"
    みず = "#6890F0"
    でんき = "#F8D030"
    くさ = "#78C850"
    こおり = "#98D8D8"
    かくとう = "#C03028"
    どく = "#A040A0"
    じめん = "#E0C068"
    ひこう = "#A890F0"
    エスパー = "#F85888"
    むし = "#A8B820"
    いわ = "#B8A038"
    ゴースト = "#705898"
    ドラゴン = "#7038F8"
    あく = "#705848"
    はがね = "#B8B8D0"
    フェアリー = "#EE99AC"
```
</details>

この列挙型は、ポケモンのタイプの色情報を定義するためのクラスです。
色情報は画面上にタイプを表示する際に使用するためのものです。

## 取得した情報を操作するためのリポジトリクラス
<details>
<summary>pokemon_repository.py</summary>

```python
from api.poke_api import PokeApi
from common.enum.type_enum import TypeEnum
from model.pokemon import Pokemon

class PokemonRepository:
    def __init__(self) -> None:
        pass

    def get_pokemon_info(self, number):
        response = dict(PokeApi().request_pokemon_info(number).items())

        # 日本語の名前情報
        speciesResponse = dict(PokeApi().request_pokemon_jp(response.get("species").get("url")).items())

        # 日本語のタイプ情報リスト
        typesResponse = [dict(PokeApi().request_pokemon_jp(typeData.get("type").get("url")).items()) for typeData in response.get("types")]

        return Pokemon(
            number=response.get("id"),
            name=self.__get_name(speciesResponse),
            imageUrl=response.get("sprites").get("front_default"),
            typesInfo=self.__get_types(typesResponse),
        )

    def __get_jp_info(self, names: list[dict]) -> str:
        """日本語の情報取得"""
        for nameData in names:
            if nameData.get("language").get("name") == "ja-Hrkt":
                return nameData.get("name")

    def __get_name(self, nameResponse: dict) -> str:
        """名前取得"""
        return self.__get_jp_info(nameResponse.get("names"))

    def __get_types(self, typesResponse: list[dict]) -> list[dict]:
        """タイプ情報取得"""
        types: list = []
        for typeResponse in typesResponse:
            types.append(self.__set_type_info(self.__get_jp_info(typeResponse.get("names"))))
        return types

    def __set_type_info(self, typeName: str) -> dict:
        """タイプ情報設定"""
        return {
            "type": TypeEnum[typeName].name,
            "color": TypeEnum[typeName].value
        }
```
</details>

このリポジトリクラスでは、PokeAPIでポケモン情報取得→日本語のポケモン名情報取得→日本語のタイプ情報取得→その情報をもとにポケモンデータを作成まで行っています。
一つのメソッドでいろいろ行っているので分けた方がいいかもですが、今回は細かいことは気にしないことにしますmm

## ポケモン表示 メインクラス
<details>
<summary>main.py</summary>

```python
import customtkinter as ctk
import threading
from PIL import ImageTk
from repository.pokemon_repository import PokemonRepository
from urllib.request import urlopen

class App(ctk.CTk):

    def __init__(self):
        super().__init__()

        self.geometry("1280x720")
        self.title("Pokemon List")
        self.resizable(0, 0)

        self.setup_form()

    def setup_form(self):
        # CustomTkinter のフォームデザイン設定
        ctk.set_appearance_mode("dark")
        ctk.set_default_color_theme("dark-blue")

        self.scroll_frame()

    def scroll_frame(self):
        self.scrollFrame = ctk.CTkScrollableFrame(master=self, height=self.winfo_height())
        self.scrollFrame.pack(side="top", fill="x")
        self.scrollFrame.pack_propagate(False)

        thread1 = threading.Thread(target=self.pokemon_frame)
        thread1.start()

    def pokemon_frame(self):

        row = 0
        column = 0
        for i in range(1, 151 + 1):
            pokemonData = PokemonRepository().get_pokemon_info(f"{i}")
            pokemonFrame = ctk.CTkFrame(master=self.scrollFrame, fg_color="#ffffff")
            pokemonFrame.grid(row=row, column=column, padx=5, pady=5)
            pokemonFrame.pack_propagate(False)

            numberLabel = ctk.CTkLabel(
                master=pokemonFrame,
                text=f"No.{pokemonData.number}",
                fg_color="#fff",
                text_color="#000",
                font=ctk.CTkFont(size=20)
            )
            numberLabel.pack(fill="both")

            image = ImageTk.PhotoImage(data=self.__get_image_data(pokemonData.imageUrl))
            imageLabel = ctk.CTkLabel(master=pokemonFrame, text="", image=image, width=100, height=100)
            imageLabel.pack(padx=5, pady=5, fill="both")

            nameLabel = ctk.CTkLabel(
                master=pokemonFrame,
                text=pokemonData.name,
                fg_color="#fff",
                text_color="#000",
                font=ctk.CTkFont(size=20)
            )
            nameLabel.pack(fill="both")

            for typeInfo in pokemonData.typesInfo:
                typeLabel = ctk.CTkLabel(
                    master=pokemonFrame,
                    text=typeInfo.get("type"),
                    text_color="#000",
                    fg_color=typeInfo.get("color"),
                    width=80,
                    font=ctk.CTkFont(size=15)
                )
                typeLabel.pack(padx=(5, 0), pady=(0, 10), fill="x", side="left")

            if column == 5:
                column = 0
                row += 1
            else:
                column += 1

    def __get_image_data(self, url):
        """URL画像のイメージデータを取得"""
        u = urlopen(url)
        imageData = u.read()
        u.close()
        return imageData

if __name__ == '__main__':
    root = App()
    root.mainloop()

```
</details>

全ポケモンを取得して表示するにはどうしても時間がかかってしまうので、画面表示後にポケモンを表示するように「threading」を使用しています。
ただそれでも時間がかかるので、ポケモンの数は初代の151匹表示するようにfor文でぶん回しています。
他にいい方法があるかも、、、

# さいごに
ここまで見てくだっさてありがとうございます。mm
簡易的ではありますが、Pythonの「Custom tkinter」でポケモンを表示する方法をご紹介しました。
もっと凝りたい方は、検索バーなでを作成しポケモン検索できる機能も作成してみてはいかがでしょうか。

WebAPIは、PokeAPI以外にも沢山ありますので、今回の記事を参考にPythonでデスクトップアプリを作成してみてください。