タイトル画像
琴葉葵

おねーちゃん!今回はUIの整理ってことだけど、どんな感じで整理するの?

琴葉茜

そうだねー
じゃあ、まずは検索する機能を別の場所に置こうか!

琴葉葵

別の場所??検索する機能は1番上にあった方が良くない??

琴葉茜

Streamlitは、メインの画面以外に、その画面の左側の「サイドバー」に配置する仕組みもあるんだ

琴葉葵

ほうほう
サイドバーを使うとどんな感じになるの?

琴葉茜

それは「百聞は一見に如かず」っていうし、実際にプログラムを変更して確認してみよう!
「st.input()」を「st.sidebar.input()」って感じで「.sidebar」を追加するだけで変更できるよ

# タイトル・説明
st.sidebar.title("ポケモン図鑑")
st.sidebar.caption("このサイトはポケモン非公式ファン作品です。画像・音声は株式会社ポケモンおよび関連各社の著作物に基づいています。学習目的のみに使用し、無断転載・商用利用・再配布は禁止されています。")

# 入力とボタン
user_input = st.sidebar.text_input("ポケモンの名前またはIDを入力してください")
search_btn = st.sidebar.button("検索")
琴葉葵

サイドバーに変更する時はそれだけで良いんだ!

解説画像1
琴葉葵

サイドバーって開閉できるんだ!結構便利な機能だね

琴葉茜

次は画像を切り替えられるように「タブ切り替え」を出来るようにしようか

琴葉葵

タブ切り替え?

琴葉茜

こんな感じのやつだね

タブ1の内容
タブ2の内容
タブ3の内容
琴葉葵

おお、なるほど!!この切り替える仕組みがStreamlitなら簡単に作れるんだね!!

琴葉茜

Streamlitの場合は「st.tabs(配列)」を使うことで切り替えができるよ
画像の表示部分を次のプログラムに書き換えてみてね!

# タブで画像表示
tab1, tab2, tab3 = st.tabs(["通常", "色違い", "アートワーク"])
with tab1:
    st.image(data["sprites"]["front_default"], caption="通常", width=200)
with tab2:
    st.image(data["sprites"]["front_shiny"], caption="色違い", width=200)
with tab3:
    st.image(data["sprites"]["other"]["official-artwork"]["front_default"], caption="公式アート", width=200)
解説画像2
琴葉葵

おお!ちゃんと切り替わってる!!

琴葉茜

「st.tabs()」を使う場合は、その選択肢と同じ数の「with」で管理する形にすればOKだから簡単でしょ!

琴葉茜

とりあえずはこれで完成だけど、何か他に欲しい機能はある??

琴葉葵

じゃあ、ランダムにポケモンが表示するってしてみたい!

琴葉茜

おおーいいね!そしたら、
1.「ランダム表示」と書かれたボタンをサイドバーに用意
2.押された時に「ID_TO_NAME」のキーからランダムで1つ選択して「user_input」に代入
3.「search_btn」をTrueにする
って流れで作ってみよう

import random # 追加を忘れずに!!

# 途中は省略

# 入力とボタン
user_input = st.sidebar.text_input("ポケモンの名前またはIDを入力してください")
search_btn = st.sidebar.button("検索")
random_btn = st.sidebar.button("ランダム表示")

# ランダムボタンが押された時の処理
if random_btn:
    user_input = random.choice(list(ID_TO_NAME.keys()))
    search_btn = True
琴葉茜

このプログラムを「if search_btn:」の行の上に作る事で、すでに作ってる検索ボタンの処理と連動できるようになるよ!

琴葉葵

ランダムで表示するボタンは「IDをランダムで選ぶ」以外の動きが検索ボタンと同じで良いし、「search_btn」は元々検索ボタンが押されたかどうかでTrueかFalseに切り替わるだけだから、直接変更しても良いんだね!

解説画像3
琴葉葵

おお!押すたびにちゃんといろんなポケモンが出てくる!!ただランダムで出てくるだけでも面白いね

琴葉葵

ちなみにおねーちゃん!Streamlitって1ページしか作れないの??

琴葉茜

いや、そんなことはないよ!もし複数ページ作りたい場合は、今作ってるプログラムが置かれている場所と同じフォルダの中に「pages」っていうフォルダを作って、その中に新しいページのプログラムを作れば勝手に対応してくれるよ!

琴葉葵

え、それだけ!?

琴葉茜

じゃあ、次のプログラムを「About.py」って名前にして「pages」フォルダの中に入れた後、もう一回「streamlit run app.py」で起動し直してみて!

import streamlit as st

st.title("この図鑑について")

st.markdown("""
### このアプリは何?
このWebアプリは、**PokeAPI** を使ってポケモンの情報を検索できる、  
**Streamlit製の学習用アプリ**です。

- 使用技術:Python / Streamlit / REST API
- 想定対象:中学生〜高校生
- 学習内容:APIの使い方・辞書の扱い・UI設計

---

### 著作権について

- このアプリは **ポケモン非公式のファン作品**です。
- キャラクターや名称は **株式会社ポケモン** および **関連各社の著作物**です。
- 学習目的以外での使用、特に **商用利用・転載・画像配布は禁止**されています。
""")
解説画像4
琴葉葵

pagesフォルダの中にある「〇〇.py」の「〇〇」部分がページの名前になってサイドバーに追加されてる...!!

琴葉茜

サイドバーに出てきた項目をクリックすると、ちゃんとページが切り替わってるでしょ!

琴葉葵

すごく簡単にページが増やせるのは良いね!!

琴葉茜

さて、無事良い感じにページが完成したことだし、次回は完成したページを誰でも見れるように公開してみよう!!

この時点での「app.py」のプログラム

import streamlit as st
import json
import requests
import random

# タイプと能力値の日本語辞書
TYPES = {
    "normal": "ノーマル", "fire": "ほのお", "water": "みず", "electric": "でんき",
    "grass": "くさ", "ice": "こおり", "fighting": "かくとう", "poison": "どく",
    "ground": "じめん", "flying": "ひこう", "psychic": "エスパー", "bug": "むし",
    "rock": "いわ", "ghost": "ゴースト", "dragon": "ドラゴン", "dark": "あく",
    "steel": "はがね", "fairy": "フェアリー"
}

STATS = {
    "hp": "HP", "attack": "こうげき", "defense": "ぼうぎょ",
    "special-attack": "とくこう", "special-defense": "とくぼう",
    "speed": "すばやさ"
}

# JSONファイルを読み込み(日本語名 → ID)
with open("poke_dict.json", "r", encoding="utf-8") as f:
    NAME_TO_ID = json.load(f)

# ID → 日本語名 の逆引き辞書も作成
ID_TO_NAME = {str(v): k for k, v in NAME_TO_ID.items()}

# タイトル・説明
st.sidebar.title("ポケモン図鑑")
st.sidebar.caption("このサイトはポケモン非公式ファン作品です。画像・音声は株式会社ポケモンおよび関連各社の著作物に基づいています。学習目的のみに使用し、無断転載・商用利用・再配布は禁止されています。")

# 入力とボタン
user_input = st.sidebar.text_input("ポケモンの名前またはIDを入力してください")
search_btn = st.sidebar.button("検索")
random_btn = st.sidebar.button("ランダム表示")

# ランダムボタンが押された時の処理
if random_btn:
    user_input = random.choice(list(ID_TO_NAME.keys()))
    search_btn = True

# 検索処理
if search_btn:
    poke_id = None
    poke_name = None

    if user_input in NAME_TO_ID:
        poke_id = NAME_TO_ID[user_input]
        poke_name = user_input
    elif user_input in ID_TO_NAME:
        poke_id = int(user_input)
        poke_name = ID_TO_NAME[user_input]

    if poke_id is not None:
        api_url = f"https://pokeapi.co/api/v2/pokemon/{poke_id}"
        res = requests.get(api_url)
        
        if res.status_code == 200:
            data = res.json()
            height = data["height"] / 10
            weight = data["weight"] / 10

            # タイプを日本語に変換
            types = [TYPES[t["type"]["name"]] for t in data["types"]]

            # 種族値を日本語に変換
            stats = {STATS[s["stat"]["name"]]: s["base_stat"] for s in data["stats"]}


            st.header(f"{poke_name}")
            st.write(f"ID: {poke_id}")
            st.write(f"タイプ: {', '.join(types)}")
            st.write(f"たかさ: {height} m")
            st.write(f"おもさ: {weight} kg")
            st.subheader("種族値")
            for stat_name, stat_val in stats.items():
                st.write(f"- {stat_name}: {stat_val}")

            # タブで画像表示
            tab1, tab2, tab3 = st.tabs(["通常", "色違い", "アートワーク"])
            with tab1:
                st.image(data["sprites"]["front_default"], caption="通常", width=200)
            with tab2:
                st.image(data["sprites"]["front_shiny"], caption="色違い", width=200)
            with tab3:
                st.image(data["sprites"]["other"]["official-artwork"]["front_default"], caption="公式アート", width=200)

            # 鳴き声の再生
            cry_url = data.get("cries", {}).get("latest")
            if cry_url:
                st.subheader("鳴き声")
                st.audio(cry_url)
            else:
                st.info("このポケモンには鳴き声が登録されていません。")
    else:
        st.error("その名前やIDのポケモンは見つかりませんでした。")