【Flet】カラーピッカーの作り方

fletでカラーピッカーが作れたので共有します。
見た目はこんな感じ↓
![スクリーンショット 2023-09-10 223312.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/99e295d2-40c5-3b11-2f93-8f8437ca46a8.png)

>(2023/09/12 追記)
>ドラッグするタイプのカラーピッカーも作れたのでそのうち記事にします

## fletでの色の指定とグラデーションについて
fletでは`ft.colors.RED`など用意されている色のほかに、16進数のカラーコードでの色指定ができます。

https://flet.dev/docs/guides/python/colors

また、fletのグラデーションは三種類あります。
`ft.LinearGradient`と`ft.RadialGradient`では3つ以上の色をグラデーションにできますが、`ft.SweepGradient`だけは2色までしか設定できません。

https://flet.dev/docs/controls/container#gradient

## コントロールの設定
今回のコードでは、スライダーでHSVの各値、もしくはテキスト入力でカラーコードを取得し、プログラムでカラーコードに変換して表示しています。
スライダー自体の色は変更できないので、スライダーをコンテナに格納し、コンテナの背景にグラデーションを設定してカラーそのものを選択しているように見せています。

## コード
今回は、画面に表示するものを書くファイルと色空間の変換を行うファイルに分けて作成しています。

### 色変換
```py:colorpicker_detail_process.py
import math
import colorsys

def hsv_to_hex(h,s,v):
    # hsv to rgb
    r,g,b = colorsys.hsv_to_rgb(h,s,v)
    r,g,b = r*255, g*255, b*255

    # rgb to hex
    return "#%02X%02X%02X" % (int(r), int(g), int(b))

def hex_to_hsv(hex_str):
    try:
        hex_str = hex_str.lstrip("#")
        if len(hex_str) != 6:
            return "error!"
        r,g,b = tuple(int(hex_str[i:i+2], 16) for i in (0, 2, 4))

        h,s,v = colorsys.rgb_to_hsv(r,g,b)
        v = v/255 
        return h,s,v
    except:
        return "error!"
```

#### 表示側
```py:main.py
import flet as ft

# 色変換のファイルをインポート
import colorpicker_detail_process as dp

# スライダーとコンテナを組み合わせるクラス
class AreaTemplate(ft.UserControl):
    def __init__(self, value, color_list, change_method):
        super().__init__()
        self.value = value
        self.color_list = color_list
        self.change_method = change_method

    def build(self):
        return ft.Container(
            height=10,
            padding=0,
            content=ft.Slider(
                min=0,
                max=1,
                value=self.value,
                divisions=1000,
                on_change=self.change_method,
                active_color=ft.colors.TRANSPARENT, # スライダーの色を透明にする
                inactive_color=ft.colors.TRANSPARENT, # スライダーの色を透明にする
                thumb_color=ft.colors.WHITE, # スライダーのボタンの色を白にする
            ),
            # 背景グラデーションの設定
            gradient=ft.LinearGradient(
                begin=ft.alignment.center_left,
                end=ft.alignment.center_right,
                colors=self.color_list,
            )
        )

# カラーピッカーの見た目と処理が書いてあるクラス
class ColorPicker(ft.UserControl):
    def __init__(self):
        super().__init__()
        
    def build(self):
        # 色を表示する円の大きさ
        self.color_size = 100

        # グラデーションを構成する色の数
        color_count = 100

        # HSVの初期値
        self.h_value, self.s_value, self.v_value = 0.5, 0.5, 0.5

        # グラデーションにする色のリスト
        self.color_division = [n/color_count for n in range(color_count)] # 等差数列を作成
        self.h_list = [dp.hsv_to_hex(n,1,self.v_value) for n in self.color_division] # 等差数列を利用して、HSVの各値を0から1まで変化させてグラデーションに使う色のリストを作成
        self.s_list = [dp.hsv_to_hex(self.h_value,n,1) for n in self.color_division]
        v_list = [dp.hsv_to_hex(0,0,n) for n in self.color_division]
        
        # HSVのスライダーのインスタンスの作成
        self.h_area = AreaTemplate(self.h_value,self.h_list, self.h_change)
        self.s_area = AreaTemplate(self.s_value,self.s_list, self.s_change)
        self.v_area = AreaTemplate(self.v_value,v_list, self.v_change)

        # カラーコードを表示するTextFieldの作成
        self.display_color_code = ft.TextField(
            value=dp.hsv_to_hex(self.h_value, self.s_value, self.v_value),
            border="underline",
            on_submit=self.color_code_change
        )

        # 色を表示する円の作成
        self.display_color = ft.Container(
            width=self.color_size,
            height=self.color_size,
            border_radius=self.color_size,
            bgcolor=dp.hsv_to_hex(self.h_value, self.s_value, self.v_value)

        # 表示の作成
        return ft.Column(
            alignment="center",
            controls=[
                self.display_color,
                self.display_color_code,
                self.h_area,
                self.s_area,
                self.v_area
            ]
        )

    # h,s,vのスライダーが変更されたときの処理
    def h_change(self, e):
        # スライダーの値を取得
        self.h_value = e.control.value

        # グラデーションにする色のリストを更新
        self.s_list = [dp.hsv_to_hex(self.h_value,n,1) for n in self.color_division]
        self.s_area.controls[0].gradient.colors = self.s_list

        # カラーコードと表示色の更新
        self.display_color_code.value=dp.hsv_to_hex(self.h_value, self.s_value, self.v_value)
        self.display_color.shapes[0].paint.color = dp.hsv_to_hex(self.h_value, self.s_value, self.v_value)

        # 表示の更新
        self.s_area.update()
        self.update()
    
    def s_change(self, e):
        self.s_value = e.control.value
        self.display_color_code.value=dp.hsv_to_hex(self.h_value, self.s_value, self.v_value)
        self.display_color.shapes[0].paint.color = dp.hsv_to_hex(self.h_value, self.s_value, self.v_value)
        self.update()

    def v_change(self, e):
        self.v_value = e.control.value
        self.h_list = [dp.hsv_to_hex(n,1,self.v_value) for n in self.color_division]
        self.h_area.controls[0].gradient.colors = self.h_list
        self.display_color_code.value=dp.hsv_to_hex(self.h_value, self.s_value, self.v_value)
        self.display_color.shapes[0].paint.color = dp.hsv_to_hex(self.h_value, self.s_value, self.v_value)
        self.h_area.update()
        self.update()

    # テキストが入力されたときの処理
    def color_code_change(self, e):
        # 入力された値を取得
        hex_str = e.control.value

        # 入力された値がカラーコードかどうかの判定と処理
        if dp.hex_to_hsv(hex_str) == "error!":
            self.display_color_code.value = dp.hsv_to_hex(self.h_value, self.s_value, self.v_value) # TextFieldの表示を元に戻す
            self.display_color_code.error_text = "16進数のカラーコードを入力してください"
            self.update()
            return "error!"
        else:
            self.display_color_code.error_text = None # エラーテキストをリセット
            self.update()

        # 入力されたカラーコードをHSVに変換して取得
        self.h_value, self.s_value, self.v_value = dp.hex_to_hsv(hex_str)
        
        # 各スライダーの更新
        self.h_area.controls[0].content.value = self.h_value
        self.s_area.controls[0].content.value = self.s_value
        self.v_area.controls[0].content.value = self.v_value

        self.s_list = [dp.hsv_to_hex(self.h_value,n,1) for n in self.color_division]
        self.s_area.controls[0].gradient.colors = self.s_list

        self.h_list = [dp.hsv_to_hex(n,1,self.v_value) for n in self.color_division]
        self.h_area.controls[0].gradient.colors = self.h_list

        self.h_area.update()
        self.s_area.update()
        self.v_area.update()

        # 表示色の更新
        self.display_color.shapes[0].paint.color = dp.hsv_to_hex(self.h_value, self.s_value, self.v_value)
        self.display_color.update()

def main(page: ft.Page):
    page.add(ColorPicker())

if __name__ == "__main__":
    ft.app(target=main)
```