Fletで簡単なお絵かきアプリを作ってみた

Fletを使用してPythonで簡単なお絵かきアプリを作成しました。
完成イメージはこんな感じです。

![Videotogif (9).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/68b7c666-b664-16d1-7019-78afdd8e3d05.gif)

この記事を読む前に[公式チュートリアル](https://flet.dev/docs/tutorials "Tutorials | Flet")を読んだりやったりしておくと理解がはかどります。

# Hello world
動作確認としてハローワールドをやってみました。

適当な名前で`.py`ファイルを作成したら以下のコードを書いて実行しました。
```py
# Fletのインポート
import flet as ft

# ページに表示する内容を定義
def main(page: ft.Page): #←必ずpageを受け取る引数を持たせる
    
    #ページにコントロールを追加
    page.add(
        # "Hello, world!"というテキストを表示
        ft.Text(value = "Hello, world!")
    )


# 実行する
ft.app(target=main)
```
Fletではテキストやボタンなど、ページに配置するパーツをコントロールと呼びます。
上のコードでは`ft.Text`というテキストを表示するコントロールをページに追加しています。

下の画面が表示されました。ハロワ成功です。
![todo-app-hello-world.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/802f7bc1-7e6b-dd99-05a9-062cd183cc8f.png)



# キャンバスとペンを設定する
絵を描くためのキャンバスとペンを設定します。

## キャンバスの準備
Fletには図形を書くための[canvas](https://flet.dev/docs/controls/canvas/ "Canvas|Flet")というコントロールがあるのでこれを使います。
canvasの使い方はこんな感じです。
```py
# キャンバスの設定
paint_area = cv.Canvas(
    [
        # キャンバスの背景色の設定
        cv.Color(color = ft.colors.WHITE)
    ],
    # カーソルをドラッグしたときの設定
    content = ft.GestureDetector(
        on_pan_start = pan_start,  # ドラッグを開始したときの処理
        on_pan_update = pan_update,  # カーソルが動いた時の処理
        drag_interval = 10,  # ドラッグを認識する間隔
    ),
    expand=True,  # ウィンドウの大きさに合わせる
)
```
* `ft.colors.WHITE`でキャンバスの背景色を白に指定します。`WHITE`の部分を変えると他の色に変えられます。Fletで用意されている色の一覧は[こちら](https://qiita.com/obukhov/items/9419105c0b810fccad09 "[Python] flet.colorsで簡便に利用できる色の一覧 - Qiita")で確認できます。

* `ft.GestureDetector`はドラッグなどの動きを処理するコントロールです。多分。

* `on_pan_start`にドラッグを開始したときの処理、`on_pan_update`にドラッグ中の処理を渡します。`pan_start`と`pan_update`はこの後作成する関数です。

* `expand`はコントロールの大きさを決めるのに設定します。

## ドラッグしたときの処理
キャンバスが準備できたので、線を描画するために`pan_start`と`pan_update`を作成しました。
それぞれの中身はこちら。

```py
# カーソルの位置を記憶するクラス
class State:
    x: float
    y: float

state = State()

# ドラッグ開始時の処理
def pan_start(e: ft.DragStartEvent):  # eはイベントハンドラ
    #カーソルの位置を更新
    state.x = e.local_x
    state.y = e.local_y

# ドラッグ中の処理
def pan_update(e:ft.DragUpdateEvent):
    
    # キャンバスに線を追加
    paint_area.shapes.append(
        cv.Line(
            state.x, state.y, e.local_x, e.local_y,  # 線の開始位置と終了位置
            # 描画線を指定
            paint = line
        )
    )
    # 表示を更新
    paint_area.update() 
    
    # 位置を更新
    state.x = e.local_x
    state.y = e.local_y

# 描画線の設定
line=ft.Paint( 
    stroke_widt h= 3,  # 線の幅
    color = "black",  # 線の色
    style = "stroke",  # 線のスタイル
    stroke_cap = ft.StrokeCap.ROUND  # 線の始点と終点の形
)
```
* 色は名前だけでも指定できます。その場合は全部小文字でアンダーバーは省略します。
* `style=`と`stroke_cap=`は書かなくても動きますが、なめらかな線にするために設定しています。

<details><summary>ここまでのコード全体はこちら</summary>

``` py
import flet as ft
import flet.canvas as cv


# カーソルの位置を記憶するクラス
class State:
    x: float
    y: float

state = State()

def main(page: ft.Page):

    # ドラッグ開始時の処理
    def pan_start(e: ft.DragStartEvent):  # eはイベントハンドラ
        # カーソルの位置を更新
        state.x = e.local_x
        state.y = e.local_y
    
    # ドラッグ中の処理
    def pan_update(e: ft.DragUpdateEvent):
        
        # キャンバスに線を追加
        paint_area.shapes.append(
            cv.Line(
                state.x, state.y, e.local_x, e.local_y,  # 線の開始位置と終了位置
                # 描画線の設定
                paint = line
            )
        )
        # 表示を更新
        paint_area.update() 
        
        # 位置を更新
        state.x = e.local_x
        state.y = e.local_y
    
    # 描画線の設定
    line=ft.Paint( 
        stroke_width = 3,  # 線の幅
        color = "black",  # 線の色
        style = "stroke",  # 線のスタイル
        stroke_cap = ft.StrokeCap.ROUND  # 線の始点と終点の形
    )

    # キャンバスの設定
    paint_area = cv.Canvas(
        [
            # キャンバスの背景色の設定
            cv.Color(color = ft.colors.WHITE)
        ],
        # カーソルをドラッグしたときの設定
        content = ft.GestureDetector(
            on_pan_start = pan_start,  # ドラッグを開始したときの処理
            on_pan_update = pan_update,  # カーソルが動いた時の処理
            drag_interval = 10,  # ドラッグを認識する間隔
        ),
        expand = True,  # ウィンドウの大きさに合わせる
    )

    page.add(paint_area)

ft.app(target=main)
```

</details>

このコードを動かすと、ドラッグして黒い線が引けるようになりました。

![Videotogif (5).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/0c18b011-21a3-69d7-5fee-3656c75bdcd4.gif)

# ペンの設定を変えてみる
ドラッグして線を引く基本的な部分はできたので、次に線の色やサイズを変えるボタンを作りました。

## 線の色を変えるボタン
ボタンを作成するときは次のように書きます。

```py
#テキストボタンの作成
button = ft.TextButton(
    text = "change color",  # 表示するテキスト
    on_click = button_clicked  # クリックしたときの処理
)
```

クリックしたときの処理なんですが、下のように書くと描画済みの全ての線の色が変わってしまいました。
```py
def button_clicked(e):
    line.color = "amber"
```
ボタンをクリックした後の線だけ色が変わるようにするには、描画線の情報を新しいものに置き換える必要があるようです。
```py
def button_clicked(e):
    line = ft.Paint( 
        stroke_width = 3, #線の幅
        color = "black", #線の色
        style = "stroke", #線のスタイル
        stroke_cap = ft.StrokeCap.ROUND #線の始点と終点の形
    )
```
しかし、`def main`にべた書きだと更新することができなかったので、(出来る書き方があったら教えてください。)ここまで書いたコードをキャンバスのコードを[コンポーネント化](https://qiita.com/donraq/items/043bde2e480d5359261b "Fletを試す(2) - 再利用できるようにコンポーネント化 - Qiita")してボタンを追加しました。

<details><summary>コードはこちら</summary>

```py
import flet as ft
import flet.canvas as cv

# カーソルの位置を記憶するクラス
class State:
    x: float
    y: float

state = State()

#コ ンポーネントの作成
class Paint_area(ft.UserControl):
    def __init__(self):
        # 継承元クラスの__init__の読み込み(必須)
        super().__init__()

        # 描画線の初期設定
        self.line = ft.Paint( 
            stroke_width = 3,
            color = "black",
            style = "stroke",
            stroke_cap = "round"
        )

    #ドラッグ開始時の処理
    def pan_start(self, e:ft.DragStartEvent):  # eはイベントハンドラ
        # カーソルの位置を更新
        state.x = e.local_x
        state.y = e.local_y


    #ドラッグ中の処理
    def pan_update(self, e:ft.DragUpdateEvent):
        
        #キャンバスに線を追加
        self.paint_area.shapes.append(
            cv.Line(
                state.x, state.y, e.local_x, e.local_y,  # 線の開始位置と終了位置
                # 描画線の指定
                paint=self.line
            )
        )
        # 表示を更新
        self.paint_area.update()

        # 位置を更新
        state.x = e.local_x
        state.y = e.local_y

    # テキストボタンをクリックしたときの処理
    def button_clicked(self, e):
        self.line = ft.Paint(
            stroke_width = 3,  # 線の幅
            color = "amber",  # 線の色
            style = "stroke",  # 線のスタイル
            stroke_cap = ft.StrokeCap.ROUND  # 線の始点と終点の形
        )
    
    #クラスの返り値の設定
    def build(self):
        # キャンバスの設定
        self.paint_area = cv.Canvas(
            [
                # キャンバスの背景色の設定
                cv.Color(color = ft.colors.WHITE)
            ],
            # カーソルをドラッグしたときの設定
            content = ft.GestureDetector(
                on_pan_start = self.pan_start,  # ドラッグを開始したときの処理
                on_pan_update = self.pan_update,  # カーソルが動いた時の処理
                drag_interval = 10,  # ドラッグを認識する間隔
            ),
            expand=True, #ウィンドウの大きさに合わせる
        )

        # テキストボタンの作成
        self.button = ft.TextButton(
            text="change color",  # 表示するテキスト
            on_click=self.button_clicked  # クリックしたときの処理
        )

        # コンポーネントの返り値はコントロールかコントロールのリスト
        return [self.paint_area, self.button]

def main(page: ft.Page):

    view = Paint_area()  # インスタンス化

    # ページに追加
    page.add(
        ft.Container(
            content = view,
            expand=True  # コントロールの大きさをウィンドウに合わせる
            )
        )

ft.app(target=main)
```
`ft.Container`はコントロールを入れる枠を作るメソッドです。`ft.Container`で`expand=True`にすることで、全画面が描画範囲になります。

</details>

実行すると、最初は黒い線、ボタンを押すとオレンジの線が描けました。

![Videotogif (6).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/d58775ba-1876-ffdd-53dd-76a2e50e5434.gif)

## ペンのサイズを変えるスライダー
スライダーとはドラッグして値を変えられるコントロールです。

↓[公式リファレンスより](https://flet.dev/docs/controls/slider "Slider | Flet")
![slider-with-custom-content.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/d843dc5e-1dc2-5721-2385-e4e410dfa25e.gif)

これを利用してペンのサイズを変えてみましょう。
スライダーの書き方はこちら
```py
# スライダーの作成
slider = ft.Slider(
    min  =0,  # 最小値
    max = 100,  # 最大値
    divisions = 100,  # 最小値から最大値の間の分割数
    label = "{value}",  # スライダーを変更したときに表示されるテキスト
    value = 3,  # 初期値
    expand = True,
    on_change = slider_change  # スライダーが変更されたときの処理
)
```

スライダーが変更されたときの処理はこちら
```py
#スライダーが変更されたときの処理
def slider_change(e):
    line = ft.Paint(
        # 線の太さにスライダーの値を代入
        stroke_width = int(e.control.value),
        color = "amber",
        style = "stroke",
        stroke_cap = "round"
    )
```

<details><summary>スライダーの処理を加えたコード全体がこちら</summary>

```py
import flet as ft
import flet.canvas as cv

# カーソルの位置を記憶するクラス
class State:
    x: float
    y: float

state = State()

# コンポーネントの作成
class Paint_area(ft.UserControl):
    def __init__(self):
        super().__init__()

        #描画線の初期設定
        self.line = ft.Paint( 
            stroke_width = 3,
            color = "black",
            style = "stroke",
            stroke_cap = "round"
        )

    # ドラッグ開始時の処理
    def pan_start(self, e: ft.DragStartEvent):  # eはイベントハンドラ
        # カーソルの位置を更新
        state.x = e.local_x
        state.y = e.local_y


    #ドラッグ中の処理
    def pan_update(self, e:ft.DragUpdateEvent):
        
        # キャンバスに線を追加
        self.paint_area.shapes.append(
            cv.Line(
                state.x, state.y, e.local_x, e.local_y,  # 線の開始位置と終了位置
                # 描画線の指定
                paint=self.line
            )
        )
        # 表示を更新
        self.paint_area.update()

        # 位置を更新
        state.x = e.local_x
        state.y = e.local_y

    # テキストボタンをクリックしたときの処理
    def button_clicked(self, e):
        self.line = ft.Paint(
            stroke_width = 3,  # 線の幅
            color = "amber",  # 線の色
            style = "stroke",  # 線のスタイル
            stroke_cap = ft.StrokeCap.ROUND  # 線の始点と終点の形
        )

    # スライダーが変更されたときの処理
    def slider_change(self, e):
        self.line = ft.Paint(
            # 線の太さにスライダーの値を代入
            stroke_width = int(e.control.value),
            color = "black",
            style = "stroke",
            stroke_cap = "round"
        )
    
    # クラスの返り値の設定
    def build(self):
        # キャンバスの設定
        self.paint_area = cv.Canvas(
            [
                # キャンバスの背景色の設定
                cv.Color(color = ft.colors.WHITE)
            ],
            # カーソルをドラッグしたときの設定
            content = ft.GestureDetector(
                on_pan_start = self.pan_start,  # ドラッグを開始したときの処理
                on_pan_update =  self.pan_update,  # カーソルが動いた時の処理
                drag_interval = 10,  # ドラッグを認識する間隔
            ),
            expand = True,  # ウィンドウの大きさに合わせる
        )

        # テキストボタンの作成
        self.button = ft.TextButton(
            text = "change color",  # 表示するテキスト
            on_click = self.button_clicked  # クリックしたときの処理
        )

        # スライダーの作成
        self.slider = ft.Slider(
            min = 0,  # 最小値
            max = 100,  # 最大値
            divisions = 100,  # 最小値から最大値の間の分割数
            label = "{value}",  # スライダーを変更したときに表示されるテキスト
            value = 3,  # 初期値
            expand = True,
            on_change = self.slider_change  #ス ライダーが変更されたときの処理
        )

        return ft.Column(  # リストの要素を行として並べる
            [
                self.paint_area,
                self.button,
                self.slider
            ]
        )

def main(page: ft.Page):

    view = Paint_area()  # インスタンス化

    # ページに追加
    page.add(
        ft.Container(
            content = view,
            expand = True  # コントロールの大きさをウィンドウに合わせる
            )
        )


ft.app(target=main)
```
</details>

ここまでのコードを実行するとこんな感じです。

![Videotogif (7).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/02bdb11b-12d9-4be9-f429-273dc0d735bf.gif)

ペンの色と太さを変えることはできましたが、それぞれの変更が連動してないのでこれを修正していきます。

## ペンの色と太さの変更を連動させる
コードを次のように変更しました。

```py
# ~~省略~~
    def __init__(self):
        super().__init__()

        # 描画線の初期設定
        self.size = 3  # 初期サイズ
        self.color = "black"  # 初期色
        self.line = ft.Paint( 
            stroke_width = self.size,
            color = self.color,
            style = "stroke",
            stroke_cap = "round"
        )
# ~~省略~~
    # テキストボタンをクリックしたときの処理
    def button_clicked(self, e):
        color = "amber"
        self.color = color
        self.line=ft.Paint(
            stroke_width =s elf.size,  # 線の幅
            color = self.color,  # 線の色
            style = "stroke",  # 線のスタイル
            stroke_cap = ft.StrokeCap.ROUND  # 線の始点と終点の形
        )

    # スライダーが変更されたときの処理
    def slider_change(self, e):
        size = int(e.control.value)
        self.size = size
        self.line = ft.Paint(
            # 線の太さにスライダーの値を代入
            stroke_width = self.size,
            color = self.color,
            style = "stroke",
            stroke_cap = "round"
        )
# ~~省略~~
```

修正したコードを実行すると、色の変更とサイズの変更が連動できました。

![Videotogif (8).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/573e56fa-f412-8683-6f0e-38bc7cdad5be.gif)

# レイアウトを整える
基本的な機能はできたので、レイアウトを整えていきます。
具体的な変更点は以下です。
* ページ下部に色を変更するタブとサイズを変更するタブを追加
* 色を変更するボタンを増やす
* スライダーの横に現在のスライダーの値を表示する

<details><summary>タブの作成</summary>

```py
class Tool_panel(ft.UserControl):
    def __init__(self, color_change, size_change):
        super().__init__()
        self.color_change = color_change #色を変える関数を受け取る変数
        self.size_change = size_change #サイズを変える関数を受け取る変数
    
    def build(self):

        # タブのコントロールを作成
        t = ft.Tabs(
            height = 110,  # タブの高さ
            expand = True,
            selected_index = 0,  # 最初に選択されているタブの指定
            animation_duration = 10,  # タブ変更時のアニメーションの速度
            # タブの中身のリスト
            tabs=[
                # タブを作成
                ft.Tab(
                    text = "brush color",  # 表示するテキスト
                    icon = "color_lens",  # 表示するアイコン
                    content = ft.Container(
                        padding = 10,
                        content = Color_buttons(self.color_change),  # タブの中身
                        bgcolor = "grey100"  # 背景色
                    )
                ),
                ft.Tab(
                    text = "brush size",
                    icon = "brush",
                    content = ft.Container(
                        padding = 10,
                        content = Size_slider(self.size_change),
                        bgcolor = "grey100"
                    )
                )
            ]
        )

        return t
```
fletで使えるアイコンは[こちら](https://gallery.flet.dev/icons-browser/ "Flet icons browser")で検索できます。

</details>

<details><summary>色を変えるボタンの作成</summary>

```py
class Color_buttons(ft.UserControl):
    def __init__(self, change):
        super().__init__()
        self.change = change  # 色を変更する関数の受け取り

    def build(self):


        return self.color_series(
            [
                "red",
                "orange",
                "amber",
                "yellow",
                "lightgreen",
                "cyan",
                "blue",
                "purple",
                "white",
                "black"
            ]
        )

    def color_series(self, colors):
        col = ft.Row(spacing = 5, wrap = True)  # コントロールを行として並べる
        for c in colors:
            col.controls.append(  # コントロールをRowに追加
                self.color_button(c)
            )
        return col

    def color_button(self, color):
        # ボタンの作成
        return ft.IconButton(
                icon = "brush",
                tooltip = color,
                icon_color = color,
                icon_size = 10,
                bgcolor = color,
                on_click = lambda c: self.change(color)
                )
```
色を変える関数はこのコンポーネントを呼び出すときに引数として渡します。
</details>

<details><summary>サイズを変えるスライダーの作成</summary>

```py
class Size_slider(ft.UserControl):
    def __init__(self, size_change):
        super().__init__()
        self.size_change = size_change


    def build(self):
        # スライダーの作成
        self.slider = ft.Slider(
            min = 0,
            max = 100,
            divisions = 100,
            label = "{value}",
            value = 3,
            expand = True,
            on_change = self.slider_change
        )    

        # ライダーの値を表示するテキストの作成
        self.value = ft.Text(value = int(self.slider.value))

        # テキストとスライダーを並べて表示
        return ft.Row(
            [
                self.value,
                self.slider
            ]
        )

    def slider_change(self, e):
        # ペンのサイズを変更
        self.size_change(int(e.control.value))

        # テキストの値を変更
        self.value.value = int(e.control.value)

        # 表示の更新
        self.update()
```
fletには表示を更新するメソッドが二つあります。
ひとつはページ全体の表示を更新する`page.update()`
もう一つはコンポーネントの表示を更新する`self.update()`
ここではコンポーネント内のテキストの表示を更新したいので`self.update()`を使っています。
</details>

これらの機能を一つのファイルにまとめると長くなるので別のファイルに分けました。

<details><summary>タブとボタンとスライダーのコード</summary>

```py
import flet as ft

class Tool_panel(ft.UserControl):
    def __init__(self, color_change, size_change):
        super().__init__()
        self.color_change = color_change  # 色を変える関数を受け取る変数
        self.size_change = size_change  # サイズを変える関数を受け取る変数
    
    def build(self):

        # タブのコントロールを作成
        t = ft.Tabs(
            height=110,  # タブの高さ
            expand=True,
            selected_index=0,  # 最初に選択されているタブの指定
            animation_duration=10,  # タブ変更時のアニメーションの速度
            # タブの中身のリスト
            tabs = [
                # タブを作成
                ft.Tab(
                    text ="brush color",  # 表示するテキスト
                    icon = "color_lens",  # 表示するアイコン
                    content = ft.Container(
                        padding = 10,
                        content = Color_buttons(self.color_change),  # タブの中身
                        bgcolor = "grey100"  # 背景色
                    )
                ),
                ft.Tab(
                    text = "brush size",
                    icon = "brush",
                    content = ft.Container(
                        padding = 10,
                        content = Size_slider(self.size_change),
                        bgcolor = "grey100"
                    )
                )
            ]
        )

        return t


class Color_buttons(ft.UserControl):
    def __init__(self, change):
        super().__init__()
        self.change = change  # 色を変更する関数の受け取り

    def build(self):


        return self.color_series(
            [
                "red",
                "orange",
                "amber",
                "yellow",
                "lightgreen",
                "cyan",
                "blue",
                "purple",
                "white",
                "black"
            ]
        )

    def color_series(self, colors):
        col = ft.Row(spacing=5, wrap=True) #コントロールを行として並べる
        for c in colors:
            col.controls.append(  # コントロールをRowに追加
                self.color_button(c)
            )
        return col

    def color_button(self, color):
        # ボタンの作成
        return ft.IconButton(
                icon = "brush",
                tooltip = color,
                icon_color = color,
                icon_size = 10,
                bgcolor = color,
                on_click = lambda c: self.change(color)
                )


class Size_slider(ft.UserControl):
    def __init__(self, size_change):
        super().__init__()
        self.size_change = size_change


    def build(self):
        # スライダーの作成
        self.slider = ft.Slider(
            min = 0,
            max = 100,
            divisions = 100,
            label = "{value}",
            value = 3,
            expand = True,
            on_change = self.slider_change
        )    

        # スライダーの値を表示するテキストの作成
        self.value = ft.Text(value = int(self.slider.value))

        # テキストとスライダーを並べて表示
        return ft.Row(
            [
                self.value,
                self.slider
            ]
        )

    def slider_change(self, e):
        # ペンのサイズを変更
        self.size_change(int(e.control.value))

        # テキストの値を変更
        self.value.value = int(e.control.value)

        # 表示の更新
        self.update()



def color_test(color):
    print(color)

def slider_test(test):
    print(test)

def main(page: ft.Page):
    page.add(ft.Container(expand = True, content = Tool_panel(color_test,  slider_test)))

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

</details>

このファイルを読み込むためにキャンバス側のコードも少々変更します。
```py
import flet as ft
import flet.canvas as cv

# タブとボタンとスライダーが書いてあるファイルのインポート
from test2 import Tool_panel

# ~~省略~~

    # 色を変えるボタンをクリックしたときの処理
    def button_clicked(self, color):
        self.color = color
        self.line = ft.Paint(
            stroke_width = self.size,  # 線の幅
            color = self.color,  # 線の色
            style = "stroke",  # 線のスタイル
            stroke_cap = ft.StrokeCap.ROUND  # 線の始点と終点の形
        )

    #スライダーが変更されたときの処理
    def slider_change(self, size):
        self.size=size
        self.line = ft.Paint(
            #線の太さにスライダーの値を代入
            stroke_width=self.size,
            color=self.color,
            style="stroke",
            stroke_cap="round"
        )

# ~~省略~~

    # クラスの返り値の設定
    def build(self):
        # キャンバスの設定
        self.paint_area = cv.Canvas(
            [
                # キャンバスの背景色の設定
                cv.Color(color = ft.colors.WHITE)
            ],
            # カーソルをドラッグしたときの設定
            content = ft.GestureDetector(
                on_pan_start = self.pan_start,  # ドラッグを開始したときの処理
                on_pan_update = self.pan_update,  # カーソルが動いた時の処理
                drag_interval = 10,  # ドラッグを認識する間隔
            ),
            expand = True,  # ウィンドウの大きさに合わせる
        )
        
        # ボタンとスライダーは削除

        return ft.Column(  # リストの要素を行として並べる
            [
                self.paint_area,
                # 色とサイズの変更処理を引数としてタブを呼び出す
                Tool_panel(self.button_clicked, self.slider_change)
            ]
        )

```

実行するとこのようになりました。
![Videotogif (9).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/68b7c666-b664-16d1-7019-78afdd8e3d05.gif)

満足したのでここで終わりにしています。
気が向いたら消しゴムボタンや塗りつぶし、Undo,Redoも実装するかもしれません。
描画線が多くなると動作が重くなるのでそこも改善できればなと思います。

最後に最終的なコードを載せておきます。

<details><summary>test.py</summary>

```py
import flet as ft
import flet.canvas as cv

from test2 import Tool_panel

# カーソルの位置を記憶するクラス
class State:
    x: float
    y: float

state = State()

# コンポーネントの作成
class Paint_area(ft.UserControl):
    def __init__(self):
        super().__init__()

        # 描画線の初期設定
        self.size = 3  # 初期サイズ
        self.color = "black"  # 初期色
        self.line = ft.Paint( 
            stroke_width = self.size,
            color = self.color,
            style = "stroke",
            stroke_cap = "round"
        )

    # ドラッグ開始時の処理
    def pan_start(self, e:ft.DragStartEvent):  # eはイベントハンドラ
        # カーソルの位置を更新
        state.x = e.local_x
        state.y = e.local_y


    # ドラッグ中の処理
    def pan_update(self, e:ft.DragUpdateEvent):
        
        # キャンバスに線を追加
        self.paint_area.shapes.append(
            cv.Line(
                state.x, state.y, e.local_x, e.local_y,  # 線の開始位置と終了位置
                # 描画線の指定
                paint=self.line
            )
        )
        # 表示を更新
        self.paint_area.update()

        # 位置を更新
        state.x = e.local_x
        state.y = e.local_y

    # 色を変えるボタンをクリックしたときの処理
    def button_clicked(self, color):
        self.color = color
        self.line = ft.Paint(
            stroke_width = self.size,  # 線の幅
            color = self.color,  # 線の色
            style = "stroke",  # 線のスタイル
            stroke_cap = ft.StrokeCap.ROUND  # 線の始点と終点の形
        )

    # スライダーが変更されたときの処理
    def slider_change(self, size):
        self.size = size
        self.line = ft.Paint(
            # 線の太さにスライダーの値を代入
            stroke_width = self.size,
            color = self.color,
            style = "stroke",
            stroke_cap = "round"
        )
    
    # クラスの返り値の設定
    def build(self):
        # キャンバスの設定
        self.paint_area = cv.Canvas(
            [
                # キャンバスの背景色の設定
                cv.Color(color = ft.colors.WHITE)
            ],
            # カーソルをドラッグしたときの設定
            content = ft.GestureDetector(
                on_pan_start = self.pan_start,  # ドラッグを開始したときの処理
                on_pan_update = self.pan_update,  # カーソルが動いた時の処理
                drag_interval = 10,  # ドラッグを認識する間隔
            ),
            expand = True,  # ウィンドウの大きさに合わせる
        )

        return ft.Column(  # リストの要素を行として並べる
            [
                self.paint_area,
                Tool_panel(self.button_clicked, self.slider_change)
            ]
        )

def main(page: ft.Page):

    view = Paint_area()  # インスタンス化

    # ページに追加
    page.add(
        ft.Container(
            content = view,
            expand = True #コントロールの大きさをウィンドウに合わせる
            )
        )


ft.app(target = main)
```
</details>

<details><summary>test2.py</summary>

```py
import flet as ft

class Tool_panel(ft.UserControl):
    def __init__(self, color_change, size_change):
        super().__init__()
        self.color_change = color_change  # 色を変える関数を受け取る変数
        self.size_change = size_change  # サイズを変える関数を受け取る変数
    
    def build(self):

        # タブのコントロールを作成
        t = ft.Tabs(
            height = 110,  # タブの高さ
            expand = True,
            selected_index = 0,  # 最初に選択されているタブの指定
            animation_duration = 10,  # タブ変更時のアニメーションの速度
            #タ ブの中身のリスト
            tabs = [
                # タブを作成
                ft.Tab(
                    text = "brush color",  # 表示するテキスト
                    icon = "color_lens",  # 表示するアイコン
                    content = ft.Container(
                        padding = 10,
                        content = Color_buttons(self.color_change),  # タブの中身
                        bgcolor = "grey100"  # 背景色
                    )
                ),
                ft.Tab(
                    text = "brush size",
                    icon = "brush",
                    content = ft.Container(
                        padding = 10,
                        content = Size_slider(self.size_change),
                        bgcolor = "grey100"
                    )
                )
            ]
        )

        return t


class Color_buttons(ft.UserControl):
    def __init__(self, change):
        super().__init__()
        self.change = change  # 色を変更する関数の受け取り

    def build(self):


        return self.color_series(
            [
                "red",
                "orange",
                "amber",
                "yellow",
                "lightgreen",
                "cyan",
                "blue",
                "purple",
                "white",
                "black"
            ]
        )

    def color_series(self, colors):
        col = ft.Row(spacing=5, wrap=True)  # コントロールを行として並べる
        for c in colors:
            col.controls.append(  # コントロールをRowに追加
                self.color_button(c)
            )
        return col

    def color_button(self, color):
        # ボタンの作成
        return ft.IconButton(
                icon = "brush",
                tooltip = color,
                icon_color = color,
                icon_size = 10,
                bgcolor = color,
                on_click = ambda c:self.change(color)
                )


class Size_slider(ft.UserControl):
    def __init__(self, size_change):
        super().__init__()
        self.size_change = size_change


    def build(self):
        # スライダーの作成
        self.slider = ft.Slider(
            min = 0,
            max = 100,
            divisions = 100,
            label = "{value}",
            value = 3,
            expand = True,
            on_change = self.slider_change
        )    

        # スライダーの値を表示するテキストの作成
        self.value = ft.Text(value = int(self.slider.value))

        # テキストとスライダーを並べて表示
        return ft.Row(
            [
                self.value,
                self.slider
            ]
        )

    def slider_change(self, e):
        # ペンのサイズを変更
        self.size_change(int(e.control.value))

        # テキストの値を変更
        self.value.value=int(e.control.value)

        # 表示の更新
        self.update()
```
</details>