[Flet入門] ElevatedButton・Checkbox・SwitchなどでインタラクティブなUI要素を作る方法

## はじめに

前回の記事では、Fletの基本的なテキスト系UI要素（Text, TextField, TextButton）について紹介しました。今回は、**ユーザーとのインタラクションを実現するUI要素**に焦点を当てて解説します。
letは、Pythonだけで美しいクロスプラットフォームUIアプリケーションを構築できるフレームワークです。Flutterをバックエンドに使用しているため、Web、デスクトップ、モバイルで一貫したマテリアルデザインのUIを実現できます。

![flet-ui-diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/f2e50a06-161f-45fe-93f8-bd8ea160af5b.png)

今回紹介するインタラクション系UI要素は以下の通りです：

1. **ボタン**: ElevatedButton, OutlinedButton
2. **選択コントロール**: Checkbox, Radio
3. **値の調整**: Switch, Slider
4. **コントロールのカスタマイズ**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/d4dc1d68-f21d-48d7-9c85-33661b82e392.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/3515b2df-d1b5-4c6c-82f4-b20557940115.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/0168923e-bdb6-43db-b6dc-8545854ff166.png)


## 環境構築

まだFletをインストールしていない場合は、以下のコマンドでインストールしましょう。

```bash
pip install flet
```

## 実装例

まずは全体のコードを見てみましょう。このコードには今回紹介するすべてのインタラクション系UI要素が含まれています。

```python
import flet as ft

def main(page: ft.Page):
    # ページの設定
    page.title = "Flet インタラクション系UI要素のサンプル"
    page.theme_mode = ft.ThemeMode.LIGHT
    page.padding = 20
    page.scroll = "auto"
    
    # 結果表示用のテキスト
    result_text = ft.Text("ここに操作結果が表示されます", size=18)
    
    # 1. ボタン (ElevatedButton, OutlinedButton)
    def on_button_click(e):
        result_text.value = f"クリックされたボタン: {e.control.text}"
        page.update()
    
    # ElevatedButtonのサンプル
    elevated_button = ft.ElevatedButton(
        "ElevatedButton",
        icon=ft.Icons.PLAY_ARROW,
        on_click=on_button_click,
    )
    
    # スタイル付きElevatedButton
    styled_elevated_button = ft.ElevatedButton(
        "スタイル付きボタン",
        icon=ft.Icons.THUMB_UP,
        on_click=on_button_click,
        style=ft.ButtonStyle(
            color=ft.Colors.WHITE,
            bgcolor=ft.Colors.BLUE,
            padding=15,
            overlay_color=ft.Colors.BLUE_ACCENT,
            elevation=5,
            shape=ft.RoundedRectangleBorder(radius=10),
        ),
    )
    
    # OutlinedButtonのサンプル
    outlined_button = ft.OutlinedButton(
        "OutlinedButton", 
        icon=ft.Icons.BOOKMARK_BORDER,
        on_click=on_button_click,
    )
    
    # 長押し対応のボタン
    long_press_button = ft.ElevatedButton(
        "長押し対応ボタン",
        on_click=on_button_click,
        on_long_press=lambda e: setattr(result_text, 'value', "ボタンが長押しされました！") or page.update(),
    )
    
    # ボタン無効化
    disabled_button = ft.ElevatedButton("無効化ボタン", disabled=True)
    
    buttons_section = ft.Container(
        content=ft.Column(
            [
                ft.Text("1. ボタン (ElevatedButton, OutlinedButton)", size=20, weight=ft.FontWeight.BOLD),
                ft.Row([elevated_button, styled_elevated_button, outlined_button, long_press_button, disabled_button], spacing=10, wrap=True),
            ],
            spacing=20,
        ),
        padding=20,
        bgcolor=ft.Colors.BLUE_50,
        border_radius=10,
        margin=ft.margin.only(bottom=20),
    )
    
    # 2. チェックボックスとラジオボタン (Checkbox, Radio)
    def on_checkbox_change(e):
        result_text.value = f"チェックボックス '{e.control.label}' は {'オン' if e.control.value else 'オフ'} です"
        page.update()
    
    # 基本的なチェックボックス
    basic_checkbox = ft.Checkbox(label="基本的なチェックボックス", on_change=on_checkbox_change)
    
    # 初期値がオンのチェックボックス
    checked_checkbox = ft.Checkbox(
        label="初期値がオンのチェックボックス", 
        value=True,
        on_change=on_checkbox_change
    )
    
    # 無効化されたチェックボックス
    disabled_checkbox = ft.Checkbox(label="無効化されたチェックボックス", disabled=True)
    
    # チェックボックスのラベル位置変更
    label_position_checkbox = ft.Checkbox(
        label="ラベルが左側のチェックボックス", 
        label_position=ft.LabelPosition.LEFT,
        on_change=on_checkbox_change
    )
    
    # ラジオグループの設定
    def on_radio_change(e):
        result_text.value = f"選択されたラジオボタン: {radio_group.value}"
        page.update()
    
    radio_group = ft.RadioGroup(
        value="option2",
        content=ft.Column([
            ft.Radio(value="option1", label="ラジオオプション 1"),
            ft.Radio(value="option2", label="ラジオオプション 2"),
            ft.Radio(value="option3", label="ラジオオプション 3", disabled=True),
        ]),
        on_change=on_radio_change,
    )
    
    # 水平方向のラジオグループ
    horizontal_radio_group = ft.RadioGroup(
        content=ft.Row([
            ft.Radio(value="horizontal1", label="横並び 1"),
            ft.Radio(value="horizontal2", label="横並び 2"),
            ft.Radio(value="horizontal3", label="横並び 3"),
        ]),
        on_change=lambda e: setattr(result_text, 'value', f"水平ラジオ選択: {horizontal_radio_group.value}") or page.update(),
    )
    
    # Textウィジェットにmarginを直接指定できないため、Containerでラップする
    radio_title = ft.Container(
        content=ft.Text("ラジオボタン:", weight=ft.FontWeight.BOLD),
        margin=ft.margin.only(top=20)
    )
    
    horizontal_radio_title = ft.Container(
        content=ft.Text("水平方向のラジオグループ:"),
        margin=ft.margin.only(top=10)
    )
    
    checkbox_radio_section = ft.Container(
        content=ft.Column(
            [
                ft.Text("2. チェックボックスとラジオボタン (Checkbox, Radio)", size=20, weight=ft.FontWeight.BOLD),
                ft.Text("チェックボックス:", weight=ft.FontWeight.BOLD),
                ft.Column([basic_checkbox, checked_checkbox, disabled_checkbox, label_position_checkbox], spacing=10),
                radio_title,
                ft.Text("垂直方向のラジオグループ:"),
                radio_group,
                horizontal_radio_title,
                horizontal_radio_group,
            ],
            spacing=10,
        ),
        padding=20,
        bgcolor=ft.Colors.GREEN_50,
        border_radius=10,
        margin=ft.margin.only(bottom=20),
    )
    
    # 3. スイッチとスライダー (Switch, Slider)
    def on_switch_change(e):
        result_text.value = f"スイッチは {'オン' if e.control.value else 'オフ'} です"
        page.update()
    
    # 基本的なスイッチ
    basic_switch = ft.Switch(label="基本的なスイッチ", on_change=on_switch_change)
    
    # 初期値がオンのスイッチ
    on_switch = ft.Switch(
        label="初期値がオンのスイッチ", 
        value=True,
        on_change=on_switch_change
    )
    
    # カスタムラベル位置のスイッチ
    custom_label_switch = ft.Row(
        [
            ft.Text("カスタムラベル位置: "),
            ft.Switch(on_change=on_switch_change),
        ],
        alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
    )
    
    # スライダーの設定
    def on_slider_change(e):
        result_text.value = f"スライダーの値: {int(e.control.value)}"
        slider_value_text.value = str(int(e.control.value))
        page.update()
    
    slider_value_text = ft.Text("50")
    
    # 基本的なスライダー
    basic_slider = ft.Slider(
        min=0,
        max=100,
        value=50,
        divisions=10,
        on_change=on_slider_change,
    )
    
    # ラベル付きスライダー
    labeled_slider = ft.Slider(
        min=0,
        max=100,
        value=50,
        divisions=10,
        label="{value}%",
        on_change=on_slider_change,
    )
    
    # カスタムスライダー
    custom_slider = ft.Row(
        [
            ft.Text("0"),
            ft.Slider(
                width=300,
                min=0,
                max=100,
                value=50,
                on_change=on_slider_change,
            ),
            ft.Text("100"),
            ft.Text("値:", weight=ft.FontWeight.BOLD),
            slider_value_text,
        ],
    )
    
    # Textウィジェットにmarginを直接指定できないため、Containerでラップする
    slider_title = ft.Container(
        content=ft.Text("スライダー:", weight=ft.FontWeight.BOLD),
        margin=ft.margin.only(top=20)
    )
    
    switch_slider_section = ft.Container(
        content=ft.Column(
            [
                ft.Text("3. スイッチとスライダー (Switch, Slider)", size=20, weight=ft.FontWeight.BOLD),
                ft.Text("スイッチ:", weight=ft.FontWeight.BOLD),
                ft.Column([basic_switch, on_switch, custom_label_switch], spacing=10),
                slider_title,
                ft.Column([
                    ft.Text("基本的なスライダー:"),
                    basic_slider,
                    ft.Text("ラベル付きスライダー:"),
                    labeled_slider,
                    ft.Text("カスタムスライダー:"),
                    custom_slider,
                ], spacing=10),
            ],
            spacing=10,
        ),
        padding=20,
        bgcolor=ft.Colors.PURPLE_50,
        border_radius=10,
        margin=ft.margin.only(bottom=20),
    )
    
    # 4. コントロールのカスタマイズ
    # コントロール有効/無効のトグル
    enabled_state = True
    custom_button = ft.ElevatedButton("カスタムボタン", icon=ft.Icons.STAR)
    custom_checkbox = ft.Checkbox(label="カスタムチェックボックス", value=True)
    custom_slider = ft.Slider(min=0, max=100, value=30)
    
    def toggle_enabled(e):
        nonlocal enabled_state
        enabled_state = not enabled_state
        
        # コントロールの有効/無効状態を切り替え
        custom_button.disabled = not enabled_state
        custom_checkbox.disabled = not enabled_state
        custom_slider.disabled = not enabled_state
        
        # ボタンテキストの更新
        toggle_button.text = "コントロールを無効化" if enabled_state else "コントロールを有効化"
        
        result_text.value = f"コントロールは {'有効' if enabled_state else '無効'} になりました"
        page.update()
    
    toggle_button = ft.ElevatedButton("コントロールを無効化", on_click=toggle_enabled)
    
    # テーマカラーを変更するボタン
    def change_theme_color(e):
        if page.theme_mode == ft.ThemeMode.LIGHT:
            page.theme_mode = ft.ThemeMode.DARK
            theme_button.text = "ライトモードに変更"
            theme_button.icon = ft.Icons.LIGHT_MODE
        else:
            page.theme_mode = ft.ThemeMode.LIGHT
            theme_button.text = "ダークモードに変更"
            theme_button.icon = ft.Icons.DARK_MODE
        
        result_text.value = f"テーマを {page.theme_mode.name} モードに変更しました"
        page.update()
    
    theme_button = ft.ElevatedButton(
        "ダークモードに変更",
        icon=ft.Icons.DARK_MODE,
        on_click=change_theme_color,
        style=ft.ButtonStyle(
            bgcolor={"hovered": ft.Colors.INDIGO_200},
            shape={"hovered": ft.RoundedRectangleBorder(radius=20)},
        ),
    )
    
    # スタイルのホバー効果を持つカスタムボタン
    hover_effect_button = ft.ElevatedButton(
        "ホバーでスタイル変更",
        style=ft.ButtonStyle(
            color={"": ft.Colors.BLACK, "hovered": ft.Colors.WHITE},
            bgcolor={"": ft.Colors.AMBER_100, "hovered": ft.Colors.ORANGE},
            padding=10,
            animation_duration=300,
            shape=ft.RoundedRectangleBorder(radius=10),
        ),
        on_click=lambda e: setattr(result_text, 'value', "ホバー効果付きボタンがクリックされました") or page.update(),
    )
    
    # Textウィジェットにmarginを直接指定できないため、Containerでラップする
    theme_title = ft.Container(
        content=ft.Text("テーマの変更:", weight=ft.FontWeight.BOLD),
        margin=ft.margin.only(top=20)
    )
    
    hover_title = ft.Container(
        content=ft.Text("ホバー効果:", weight=ft.FontWeight.BOLD),
        margin=ft.margin.only(top=20)
    )
    
    customize_section = ft.Container(
        content=ft.Column(
            [
                ft.Text("4. コントロールのカスタマイズ", size=20, weight=ft.FontWeight.BOLD),
                ft.Text("コントロールの有効/無効化:", weight=ft.FontWeight.BOLD),
                ft.Row([toggle_button], spacing=10),
                ft.Row([custom_button, custom_checkbox], spacing=10),
                custom_slider,
                theme_title,
                ft.Row([theme_button], spacing=10),
                hover_title,
                ft.Row([hover_effect_button], spacing=10),
            ],
            spacing=10,
        ),
        padding=20,
        bgcolor=ft.Colors.AMBER_50,
        border_radius=10,
        margin=ft.margin.only(bottom=20),
    )
    
    # ページ全体のコンテンツ
    page_content = ft.Column([
        ft.Text("Fletのインタラクション系UI要素", size=30, weight=ft.FontWeight.BOLD),
        result_text,
        buttons_section,
        checkbox_radio_section,
        switch_slider_section,
        customize_section,
    ])
    
    page.add(page_content)


ft.app(target=main)
```

## 各UI要素の詳細説明

### 1. ボタン (ElevatedButton, OutlinedButton)

前回紹介した`TextButton`の他にも、Fletには様々なスタイルのボタンがあります。

#### ElevatedButton

`ElevatedButton`は影付きの浮き上がったスタイルのボタンです。

```python
# 基本的な使い方
elevated_button = ft.ElevatedButton(
    "ElevatedButton",  # ボタンテキスト
    icon=ft.Icons.PLAY_ARROW,  # アイコン (更新: icons → Icons)
    on_click=on_button_click,  # クリック時のコールバック
)

# スタイル付きElevatedButton
styled_elevated_button = ft.ElevatedButton(
    "スタイル付きボタン",
    icon=ft.Icons.THUMB_UP,  # 更新: icons → Icons
    on_click=on_button_click,
    style=ft.ButtonStyle(
        color=ft.Colors.WHITE,  # テキスト色 (更新: colors → Colors)
        bgcolor=ft.Colors.BLUE,  # 背景色 (更新: colors → Colors)
        padding=15,  # パディング
        overlay_color=ft.Colors.BLUE_ACCENT,  # ホバー時の色 (更新: colors → Colors)
        elevation=5,  # 影の強さ
        shape=ft.RoundedRectangleBorder(radius=10),  # 形状
    ),
)
```

#### OutlinedButton

`OutlinedButton`は枠線のみのボタンです。

```python
# 基本的な使い方
outlined_button = ft.OutlinedButton(
    "OutlinedButton", 
    icon=ft.Icons.BOOKMARK_BORDER,  # 更新: icons → Icons
    on_click=on_button_click,
)
```

#### ボタンのイベント処理

ボタンには様々なイベントを設定できます。

```python
# クリック時のイベント
on_click=lambda e: print("ボタンがクリックされました")

# 長押し時のイベント
on_long_press=lambda e: print("ボタンが長押しされました")

# 無効化されたボタン
disabled_button = ft.ElevatedButton("無効化ボタン", disabled=True)
```

### 2. チェックボックスとラジオボタン (Checkbox, Radio)

#### Checkbox

`Checkbox`は、オン/オフを切り替えるコントロールです。

```python
# 基本的な使い方
basic_checkbox = ft.Checkbox(
    label="基本的なチェックボックス",  # ラベル
    on_change=on_checkbox_change  # 変更時のコールバック
)

# 初期値がオンのチェックボックス
checked_checkbox = ft.Checkbox(
    label="初期値がオンのチェックボックス", 
    value=True,  # 初期値
    on_change=on_checkbox_change
)

# ラベル位置の変更
label_position_checkbox = ft.Checkbox(
    label="ラベルが左側のチェックボックス", 
    label_position=ft.LabelPosition.LEFT,
    on_change=on_checkbox_change
)
```

#### RadioGroup と Radio

`RadioGroup`と`Radio`を使用して、排他的な選択肢を作成できます。

```python
# ラジオグループの基本的な使い方
radio_group = ft.RadioGroup(
    value="option2",  # 初期選択値
    content=ft.Column([  # 縦方向に配置
        ft.Radio(value="option1", label="ラジオオプション 1"),
        ft.Radio(value="option2", label="ラジオオプション 2"),
        ft.Radio(value="option3", label="ラジオオプション 3", disabled=True),
    ]),
    on_change=on_radio_change,  # 変更時のコールバック
)

# 水平方向のラジオグループ
horizontal_radio_group = ft.RadioGroup(
    content=ft.Row([  # 横方向に配置
        ft.Radio(value="horizontal1", label="横並び 1"),
        ft.Radio(value="horizontal2", label="横並び 2"),
        ft.Radio(value="horizontal3", label="横並び 3"),
    ]),
    on_change=on_radio_change,
)
```

### 3. スイッチとスライダー (Switch, Slider)

#### Switch

`Switch`は、オン/オフを切り替えるトグルコントロールです。

```python
# 基本的な使い方
basic_switch = ft.Switch(
    label="基本的なスイッチ",  # ラベル
    on_change=on_switch_change  # 変更時のコールバック
)

# 初期値がオンのスイッチ
on_switch = ft.Switch(
    label="初期値がオンのスイッチ", 
    value=True,  # 初期値
    on_change=on_switch_change
)

# カスタムラベル位置
# Rowを使って自由にレイアウト
custom_label_switch = ft.Row(
    [
        ft.Text("カスタムラベル位置: "),
        ft.Switch(on_change=on_switch_change),
    ],
    alignment=ft.MainAxisAlignment.SPACE_BETWEEN,  # 両端揃え
)
```

#### Slider

`Slider`は、範囲内の値を選択するコントロールです。

```python
# 基本的な使い方
basic_slider = ft.Slider(
    min=0,  # 最小値
    max=100,  # 最大値
    value=50,  # 初期値
    divisions=10,  # 目盛りの数
    on_change=on_slider_change,  # 変更時のコールバック
)

# ラベル付きスライダー
labeled_slider = ft.Slider(
    min=0,
    max=100,
    value=50,
    divisions=10,
    label="{value}%",  # スライダー操作時に表示されるラベル
    on_change=on_slider_change,
)

# カスタムスライダー
# 最小値、最大値、現在値を表示
custom_slider = ft.Row(
    [
        ft.Text("0"),  # 最小値表示
        ft.Slider(
            width=300,
            min=0,
            max=100,
            value=50,
            on_change=on_slider_change,
        ),
        ft.Text("100"),  # 最大値表示
        ft.Text("値:", weight=ft.FontWeight.BOLD),
        slider_value_text,  # 現在値表示
    ],
)
```

### 4. コントロールのカスタマイズ

#### コントロールの有効/無効の切り替え

全てのコントロールは`disabled`プロパティを変更することで有効/無効を切り替えることができます。

```python
# ボタンクリックで複数のコントロールの有効/無効を切り替える
def toggle_enabled(e):
    nonlocal enabled_state
    enabled_state = not enabled_state
    
    # コントロールの有効/無効状態を切り替え
    custom_button.disabled = not enabled_state
    custom_checkbox.disabled = not enabled_state
    custom_slider.disabled = not enabled_state
    
    # ボタンテキストの更新
    toggle_button.text = "コントロールを無効化" if enabled_state else "コントロールを有効化"
    
    result_text.value = f"コントロールは {'有効' if enabled_state else '無効'} になりました"
    page.update()
```

#### テーマの変更

Fletではアプリケーション全体のテーマを変更することができます。

```python
def change_theme_color(e):
    if page.theme_mode == ft.ThemeMode.LIGHT:
        page.theme_mode = ft.ThemeMode.DARK
        theme_button.text = "ライトモードに変更"
        theme_button.icon = ft.Icons.LIGHT_MODE  # 更新: icons → Icons
    else:
        page.theme_mode = ft.ThemeMode.LIGHT
        theme_button.text = "ダークモードに変更"
        theme_button.icon = ft.Icons.DARK_MODE  # 更新: icons → Icons
    
    result_text.value = f"テーマを {page.theme_mode.name} モードに変更しました"
    page.update()
```

#### ホバー効果の追加

ButtonStyleを使用して、ホバー時の効果を設定できます。

```python
hover_effect_button = ft.ElevatedButton(
    "ホバーでスタイル変更",
    style=ft.ButtonStyle(
        color={"": ft.Colors.BLACK, "hovered": ft.Colors.WHITE},  # 通常時と、ホバー時の色 (更新: colors → Colors)
        bgcolor={"": ft.Colors.AMBER_100, "hovered": ft.Colors.ORANGE},  # 通常時と、ホバー時の背景色 (更新: colors → Colors)
        padding=10,
        animation_duration=300,  # アニメーション時間（ミリ秒）
        shape=ft.RoundedRectangleBorder(radius=10),
    ),
    on_click=lambda e: setattr(result_text, 'value', "ホバー効果付きボタンがクリックされました") or page.update(),
)
```

## Fletの状態管理

Fletではコントロールの状態を管理する際に、以下の点に注意する必要があります：

1. **状態の変更後は`page.update()`を呼び出す**
   - UIに変更を反映させるために必要です
   - イベントハンドラ内で状態を変更した後に必ず呼び出します

2. **コールバック関数の活用**
   - `on_click`, `on_change`などのイベントで状態を更新します
   - コールバック関数の引数`e`からは操作されたコントロールにアクセスできます

3. **ラムダ式の使用**
   - 簡単な処理は`lambda e: setattr(obj, 'prop', value) or page.update()`のように記述できます
   - `or page.update()`を追加することで、状態更新と画面更新を1行で書けます

## 発展的なUI構築のヒント

Fletでより高度なUIを作成するためのヒントをいくつか紹介します：

1. **コンポーネント化**
   - 再利用可能なUIパーツはクラスや関数として実装すると管理しやすくなります
   - 例: `def create_settings_panel(page): ...`

2. **レスポンシブデザイン**
   - `Row`の`wrap=True`属性を使用すると、画面サイズに応じて要素が折り返されます
   - `ResponsiveRow`を使用すると、グリッドレイアウトが実現できます

3. **アニメーション**
   - `animation_duration`属性でアニメーション時間を指定できます
   - ボタンやその他のコントロールの状態遷移をスムーズにします

4. **ビジュアル統一**
   - 一貫したカラースキームとスタイルを使用しましょう
   - `ThemeMode`を活用して、ライト/ダークモードの対応も簡単に行えます

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/73154180-912f-4221-a7ef-903d8c7529b0.png)

今回はFletのインタラクション系UI要素について解説しました。

- **ボタン**: ElevatedButton, OutlinedButtonは様々な用途に応じたスタイリングが可能
- **選択コントロール**: CheckboxとRadioは排他的でない選択肢と排他的な選択肢を実現
- **値の調整**: SwitchとSliderは異なる形式の値入力を可能に
- **カスタマイズ**: テーマ変更やホバー効果など、きめ細かな調整が可能

これらの要素を組み合わせることで、直感的で使いやすいインターフェースを構築できます。Fletの強みは、シンプルなPythonコードでクロスプラットフォームのUIを構築できる点にあります。

## Flet 0.25.0以降の重要な変更点

Flet 0.25.0からいくつかの重要な変更があります：

1. `ft.icons` → `ft.Icons` に名前が変更されました
2. `ft.colors` → `ft.Colors` に名前が変更されました

これらの変更は、将来的に（0.28.0で）完全に非推奨となり削除される予定です。最新のFletを使用する場合は、必ずこの新しい命名規則に従ってください。


## 免責事項
本記事の作成にあたり、文章や図解の生成にClaude Sonnetを、ファクトチェックにGensparkを活用しました。最終的な編集と確認は筆者が行っています。