5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Streamlitの@st.dialogデコレータでモーダルダイアログを実装する

5
Posted at

はじめに

Streamlit v1.37から追加された@st.dialogデコレータを使うと、これまで実装が難しかったモーダルダイアログを簡単に作成できるようになりました。本記事では、実際のユーザー登録ウィザードを例に、ステップ形式のフォームをダイアログ内に実装する方法を解説します。

@st.dialogデコレータとは

image.png

@st.dialogは、関数をモーダルダイアログとして表示できるデコレータです。背景をグレーアウトしてユーザーの注意を集中させたり、メインアプリとは独立した領域で内容を描画したりできます。また、st.session_stateと連携した状態管理が可能で、width引数を使えば「small」「large」などサイズ調整も柔軟に行えます。

実装例:ステップ形式のユーザー登録ウィザード

全体構成

今回実装するウィザードUIの主な機能:

  • 2ステップの登録フロー(基本情報入力 → 確認)
  • プログレスバーによる進捗表示
  • 戻る/次へナビゲーション
  • 完了後の結果表示と継続登録オプション

完成イメージ

image.png

image.png

image.png

image.png

image.png
登録後の一覧の画面

コード解説

1. ダイアログの基本定義

@st.dialog("アカウント作成", width="large")
def signup_wizard():
    # ダイアログ内のコンテンツ
    pass

@st.dialogデコレータにタイトルとwidthを指定します。widthは"small"(デフォルト)、"large"が選択可能です。

2. 状態管理の仕組み

ダイアログの表示/非表示を制御するためにst.session_stateを活用:

# メインアプリ側
if st.button("新規ユーザー登録", type="primary"):
    st.session_state.show_signup_dialog = True
    st.rerun()

# フラグがTrueの時だけダイアログを表示
if st.session_state.get("show_signup_dialog"):
    signup_wizard()

重要なポイント

  • ダイアログ表示用のフラグをsession_stateで管理
  • ボタンクリック時にフラグをTrueにしてst.rerun()
  • 毎回の実行でフラグをチェックしてダイアログを再表示

3. ステップ管理とプログレス表示

# ステップ初期化
if "signup_step" not in st.session_state:
    st.session_state.signup_step = 1

step = st.session_state.signup_step

# プログレスバー表示(2ステップ想定)
st.progress((step - 1) / 1)
st.write(f"ステップ {step} / 2")

session_stateでステップ番号を管理し、st.progress()で視覚的に進捗を表示します。

4. フォーム入力と検証

if step == 1:
    st.subheader("ステップ 1: 基本情報")
    with st.form("step1_form"):
        name = st.text_input("ユーザー名", 
                           value=st.session_state.get("signup_name", ""))
        email = st.text_input("メールアドレス", 
                            value=st.session_state.get("signup_email", ""))
        
        submitted = st.form_submit_button("次へ", type="primary")
        if submitted:
            if name and email:
                # 入力値を保存して次のステップへ
                st.session_state.signup_name = name
                st.session_state.signup_email = email
                st.session_state.signup_step = 2
                st.rerun()
            else:
                st.error("すべての項目を入力してください")

ポイント

  • st.formを使用して送信ボタンクリック時のみ処理を実行
  • 入力値はsession_stateに保存し、ステップ間で保持
  • st.rerun()で画面を更新し、次のステップを表示

5. 完了処理とクリーンアップ

if st.session_state.get("signup_completed", False):
    st.success("✅ 登録が完了しました!")
    st.write(f"ユーザー名: {st.session_state.get('last_registered_user', '')}")
    
    col1, col2 = st.columns(2)
    with col1:
        if st.button("別のユーザーを登録", type="primary", use_container_width=True):
            st.session_state.signup_completed = False
            st.session_state.signup_step = 1
            st.rerun()
    with col2:
        if st.button("閉じる", use_container_width=True):
            # 状態をクリーンアップしてダイアログを閉じる
            st.session_state.signup_completed = False
            st.session_state.pop("last_registered_user", None)
            st.session_state.show_signup_dialog = False  # ★重要
            st.rerun()

完了後の処理では、状態のクリーンアップと次のアクション(継続登録 or 終了)を提供します。

実装時の注意点

@st.dialogを使う場合は、表示状態をsession_stateで管理する必要があります。状態を変更した後はst.rerun()で再描画し、新しい状態に基づいた画面を反映させます。また、ダイアログを閉じるときには一時的に使った状態変数をクリアしておくことで、次回の表示が正しく初期化されます。

# 一時変数のクリーンアップ例
for k in ["signup_step", "signup_name", "signup_email"]:
    st.session_state.pop(k, None)

応用例

この実装パターンは、さまざまな場面で応用できます。たとえば複数タブを切り替えられる設定画面や、段階的に入力を進めるデータ入力フォーム。さらに、削除や更新の前に表示する確認ダイアログや、リスト項目をクリックした際に詳細をモーダルで見せるケースなどにも有効です。

まとめ

image.png

@st.dialogデコレータを使うことで、これまでStreamlitでは実装が困難だったモーダルダイアログUIを簡単に作成できるようになりました。st.session_stateによる状態管理と組み合わせることで、複雑なステップ形式のフォームも実装可能です。

本記事のコードを参考に、ぜひ自分のアプリケーションにもモーダルダイアログを試してみてください。
Streamlitアプリのユーザビリティが大きく向上するはずです。

実装例

import streamlit as st

# 進捗バーの計算(進行中ベース:ステップ1=50%、ステップ2=100%)
def calc_progress(step: int, total: int, completed: bool) -> int:
    if completed:
        return 100
    ratio = max(0.0, min(1.0, step / float(total)))
    return int(round(ratio * 100))


TOTAL_STEPS = 2  # 将来拡張しやすいようにハードコード回避

@st.dialog("アカウント作成", width="large")
def signup_wizard():
    # 完了表示
    if st.session_state.get("signup_completed", False):
        st.success("✅ 登録が完了しました!")
        st.write(f"ユーザー名: {st.session_state.get('last_registered_user', '')}")

        col1, col2 = st.columns(2)
        with col1:
            if st.button("別のユーザーを登録", type="primary", use_container_width=True):
                # 再開時はステップ1から
                st.session_state.signup_completed = False
                st.session_state.signup_step = 1
                st.rerun()
        with col2:
            if st.button("閉じる", use_container_width=True):
                # 完了状態の掃除とダイアログ閉じ
                st.session_state.signup_completed = False
                st.session_state.pop("last_registered_user", None)
                st.session_state.show_signup_dialog = False
                st.rerun()
        return

    # ステップ初期化
    if "signup_step" not in st.session_state:
        st.session_state.signup_step = 1
    step = st.session_state.signup_step

    # 進捗バー(進行中ベース)
    completed = st.session_state.get("signup_completed", False)
    progress_val = calc_progress(step, TOTAL_STEPS, completed)
    st.progress(progress_val, text=f"ステップ {step} / {TOTAL_STEPS}")

    # ステップ1:入力
    if step == 1:
        st.subheader("ステップ 1: 基本情報")
        with st.form("step1_form"):
            name = st.text_input("ユーザー名", value=st.session_state.get("signup_name", ""))
            email = st.text_input("メールアドレス", value=st.session_state.get("signup_email", ""))
            submitted = st.form_submit_button("次へ", type="primary")
            if submitted:
                if name and email:
                    st.session_state.signup_name = name
                    st.session_state.signup_email = email
                    st.session_state.signup_step = 2
                    st.rerun()
                else:
                    st.error("すべての項目を入力してください")

    # ステップ2:確認
    elif step == 2:
        st.subheader("ステップ 2: 確認")
        st.write(f"ユーザー名: {st.session_state.get('signup_name', '')}")
        st.write(f"メールアドレス: {st.session_state.get('signup_email', '')}")

        col1, col2 = st.columns(2)
        with col1:
            if st.button("戻る", use_container_width=True):
                st.session_state.signup_step = 1
                st.rerun()
        with col2:
            if st.button("登録", type="primary", use_container_width=True):
                users = st.session_state.setdefault("users", [])
                users.append({
                    "name": st.session_state.get("signup_name", ""),
                    "email": st.session_state.get("signup_email", "")
                })
                st.session_state.signup_completed = True
                st.session_state.last_registered_user = st.session_state.get("signup_name", "")
                # 入力系の一時状態は掃除
                for k in ["signup_step", "signup_name", "signup_email"]:
                    st.session_state.pop(k, None)
                st.rerun()


# ===== メイン画面 =====
st.title("ユーザー管理システム")

# ダイアログ表示トグル(状態で開閉を管理)
if st.button("新規ユーザー登録", type="primary"):
    st.session_state.show_signup_dialog = True
    # 初回オープン時はステップ1から
    st.session_state.signup_step = 1
    st.rerun()

# 毎実行でフラグを見てダイアログ再表示
if st.session_state.get("show_signup_dialog"):
    signup_wizard()

# 登録済みユーザー表示
if st.session_state.get("users"):
    st.subheader("登録済みユーザー")
    for i, user in enumerate(st.session_state.users, start=1):
        st.write(f"{i}. {user.get('name', '')} ({user.get('email', '')})")

注意事項:
入力値チェックやエラー処理については十分ではありません。
そのため、ダイアログ表示の処理部分のみを参考にしてください。

参考リンク

5
5
0

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
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?