CustomTkinterでMVC！

# はじめに
 CustomTkinterでアプリを自分なりのMVCパターンでのベストプラクティス的なものを考えてみたので、備忘録として残します。ビジネスロジックと見た目を分離できるので保守しやすいコードがかけると思う。。。

※追記
　コメントでご指摘を頂いたので、自分なりにコードの修正をしてみました。
 
<details>
<summary>前回のサンプルコードや解説</summary>


 ファイル構成は下記のような感じです
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627984/ce4f236a-126a-f3b5-2f1e-8d9d3f007028.png)

- controller.py
ページ遷移やビジネスロジックの用のメソッドを記述するクラスを定義します。今回は分けていませんが、役割ごとにクラスを分割すると良いと考えてます。
- main.py
メインモジュールです。起動時はこのファイルを実行します。
- model.py
データ管理用クラスです。何か保持したいデータやDBから値を取得するなどを行います。
- style_manager.py
ここはウィジェットのクラスのスタイルを定義するためのクラスです。複雑なものや、共通したスタイルのものを定義して使います。
- views.py
ページ（見た目）を作るFrameクラスを定義します。

 # サンプルコード
 下記のサンプルコードをコピペして、デバッグで処理の順番を確認してみると何をやっているのかなんとなく分かると思います。
 
```py:main.py
from controller import AppController

if __name__ == "__main__":
    app = AppController()
    app.mainloop()
```

```py:controller.py
import customtkinter as ctk
from tkinter import messagebox
from model import AppModel
from views import Page1, Page2
from style_manager import StyleManager

class AppController(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("MVC with Page Navigation")
        self.geometry("1000x550")
        
        # テーマ設定
        ctk.set_appearance_mode("dark")  # Modes: system (default), light, dark
        ctk.set_default_color_theme("blue")  # Themes: blue (default), dark-blue, green
        
        # モデルの生成　※データはこのself.modelから取得できるようにする。
        self.model = AppModel()
        
        # Gridレイアウトの設定
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

        # 全ページのフレームを格納する辞書
        self.frames = {}

        # 各ページの作成と格納
        for F in (Page1, Page2):
            page_name = F.__name__
            frame = F(master=self, controller=self, **StyleManager.transparent_frame)
            self.frames[page_name] = frame
            frame.grid(row=0, column=0, sticky="nsew")

        # 起動時にPage1Frameを表示する。
        self.show_frame("Page1")

    def show_frame(self, page_name):
        '''ページ切替を行うメソッド'''
        frame = self.frames[page_name]
        frame.tkraise()
        
    def msg_output(self, page_num:int):
        '''メッセージを出力するメソッド'''
        data:str = self.model.data
        messagebox.showinfo("Information", f"ページ{page_num}のメッセージです。\n"
                                            + f"モデルから受け取ったデータ:{data}")

```

```py:model.py
# Modelはこの例ではシンプルにし、ページ遷移には直接関与しません。
class AppModel:
    def __init__(self):
        self.data = "Some important data"
```

```py:style_manager.py
from enum import Enum

FONT_TYPE = "meiryo"
BASE_COLOR_DARK = "#242424"

class FontSettings(Enum):
    HEADER_TITLE = (FONT_TYPE, 20, "bold")
    FRAME_TITLE = (FONT_TYPE, 17, "bold")
    DEFAULT = (FONT_TYPE, 15)
    MENU = (FONT_TYPE, 13)
    
class StyleManager:
    
    transparent_frame = {
        "fg_color": BASE_COLOR_DARK,
    }
    
    inline_btn = {
        "text_color": ("gray10", "#DCE4EE"),
        "fg_color": "transparent",
        "border_width":2,
    }
```

```py:views.py
import customtkinter as ctk

class Page1(ctk.CTkFrame):
    def __init__(self, master, controller, **kwargs):
        super().__init__(master, **kwargs)
        self.controller = controller

        # Gridレイアウト設定
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure((0,1), weight=1)

        self.label = ctk.CTkLabel(self, text="ページ１です。")
        self.label.grid(row=0, column=0, columnspan=2, pady=20)

        self.page_btn = ctk.CTkButton(self, text="ページ２へ", command=lambda: controller.show_frame("Page2"))
        self.page_btn.grid(row=1, column=0, padx=20)
        
        self.msg_btn = ctk.CTkButton(self, text="メッセージ表示", command=lambda: controller.msg_output(1))
        self.msg_btn.grid(row=1, column=1, padx=(0,20))

class Page2(ctk.CTkFrame):
    def __init__(self, master, controller, **kwargs):
        super().__init__(master, **kwargs)
        self.controller = controller

        # Gridレイアウト設定
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure((0,1), weight=1)
        
        self.label = ctk.CTkLabel(self, text="ページ２です。")
        self.label.grid(row=0, column=0, columnspan=2, pady=20)

        self.page_btn = ctk.CTkButton(self, text="ページ１へ", command=lambda: controller.show_frame("Page1"))
        self.page_btn.grid(row=1, column=0, padx=20)
        
        self.msg_btn = ctk.CTkButton(self, text="メッセージ表示", command=lambda: controller.msg_output(2))
        self.msg_btn.grid(row=1, column=1, padx=(0,20))

```


# 解説
こまかいcustomtkinterの設定部分については、割愛します。

- ページの表示
AppControllerクラスの下記の部分で行っています。
```(Page1, Page2)```のタプルにviews.pyに定義したクラスを格納すると、そのFrameをGridレイアウトで配置してくれます。

```py:controller.py
# 各ページの作成と格納
for F in (Page1, Page2):
    page_name = F.__name__
    frame = F(master=self, controller=self, **StyleManager.transparent_frame)
    self.frames[page_name] = frame
    frame.grid(row=0, column=0, sticky="nsew")

# 起動時にPage1Frameを表示する。
self.show_frame("Page1")
```
どのページを表示するかは```self.show_frame("Page1")```メソッドにクラス名を渡すとページ遷移するように作ってあります。ページ遷移も下記メソッドを呼び出して行います。
```py:controller.py
def show_frame(self, page_name):
'''ページ切替を行うメソッド'''
frame = self.frames[page_name]
frame.tkraise()
```

- StyleManagerクラスについて
定義はクラス変数に辞書型を渡して定義しています。CSSのような感覚で書けるのが良いですね。
```py:style_manager.py
class StyleManager:
    
    transparent_frame = {
        "fg_color": BASE_COLOR_DARK,
    }
    
    inline_btn = {
        "text_color": ("gray10", "#DCE4EE"),
        "fg_color": "transparent",
        "border_width":2,
    }
```
使い方はスタイルマネージャークラスは上記解説（ページの表示）のループ内で記述の第３引数に渡している
```**StyleManager.transparent_frame```のように使います。

```py:controller.py
frame = F(master=self, controller=self, **StyleManager.transparent_frame)
```

引数の受け取りはviews.pyのPageクラスで**kwargsです。

```py:views.py
class Page1(ctk.CTkFrame):
    def __init__(self, master, controller, **kwargs):　#←ココ
        super().__init__(master, **kwargs) #←ココ
        self.controller = controller
```

ただし、全部のスタイルをここで作るのはコード量がとても多くなってしまうので、あまりお勧めではないかもしれません。デザインのために引数に多くの値を渡す場合や共通で使用するものに関してここに定義しておくと管理しやすいと思ってます。

すみません。FontSettingsについては今回は利用していません。やっていることは挙型を使って一か所でフォントとサイズを管理しているだけです。全体のフォントやサイズを変えたいときにここを変えるだけで済みます。

- viewsでのビジネスロジックの呼び出し
command部にラムダ式を使って他クラスのメソッドを渡すだけでOK
メソッドの処理が動きます。
```py:views.py
self.page_btn = ctk.CTkButton(self, text="ページ２へ", command=lambda: controller.show_frame("Page2"))
self.page_btn.grid(row=1, column=0, padx=20)

self.msg_btn = ctk.CTkButton(self, text="メッセージ表示", command=lambda: controller.msg_output(1))
self.msg_btn.grid(row=1, column=1, padx=(0,20))
```
</details>

# ファイル構成

 ファイル構成は下記のような感じです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627984/2c4b41b6-c3e7-39f7-77ee-adc81dc8f1cf.png)

※sample.dbはサンプルのデータベース（sqlite3）でです。事前準備の手順でcreate_DB.pyのスクリプトを実行するとサンプルDBが作成されます。

## 見た目
- ページ１
ボタンを押すとメッセージでDBから取得した一覧データが表示されます。
![ページ１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627984/b1f0f3c1-4106-0528-4aa6-834a0ea1a527.png)

- ページ２
ボタンを押すとメッセージボックスに入力したIDのレコードデータが表示されます。
![ページ２.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627984/3d345b3e-d7ff-440b-bc39-63c4e37e996d.png)

## アーキテクチャ
 - ビュー：windows.py & pages.py(Presentation Layer)
 - コントローラ：controllers.py(Business Logic Layer)
 - モデル：models.py(DataAccess Layer)

![アーキテクチャ図_architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627984/2218615b-64bc-c7e7-eb1b-2c105b5d125f.png)


## クラス構成
主なクラス構成は下記のようになっております。

| クラス名 | 種類 | 説明 | 備考 |
| -- | -- | -- | -- |
| MainWindowクラス | ビュー | メインウィンドウ表示 & Pageクラスを管理 | ページの表示・遷移など |
| Pageクラス | ビュー | 画面UIの生成 & UI更新担当 | 抽象クラス有 |
| Contorllerクラス | コントローラ | ビジネスロジックを担当 |  |
| Modelクラス | モデル | DB連携を担当 | 抽象クラス有 |
| CommonStyeleクラス | ビュー | ウィジェットのスタイル保持する(共通化したいものなど) ||
| Appクラス | ビュー | 各クラスの連携やアプリ起動を担当 |下記のクラス図には載せていません。|

![クラス図.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627984/cfd6ab6c-6495-fd15-e23d-70046eef6a29.png)


処理の流れ
　MainWindowクラスでPageクラスの管理を行います。ビジネスロジックはContorllerの役割としてそれをViewを通してPageが受け取り使用するという処理の流れになってます。ControllerはModelとセットで作って、データをモデルから受け取る感じです。

# 事前準備
今回のサンプルコードは事前にSqlite3で事前にDBを準備する必要があります。
下記のスクリプトを実行してsample.dbを作成し、ルートディレクトリに配置してください。
    
```python:create_DB.py
import sqlite3

# データベースに接続（ファイルが存在しない場合は自動で作成される）
conn = sqlite3.connect('sample.db')

# カーソルオブジェクトを作成
cur = conn.cursor()

# SQLクエリを実行
sql_query = '''
-- table
DROP TABLE IF EXISTS Diary;

CREATE TABLE IF NOT EXISTS Diary (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    create_at TEXT
);

-- data
INSERT INTO Diary (title, content, create_at)
VALUES ('日記1', '内容１', '2024-8-31 16:34'),
       ('日記2', '内容２', '2024-8-31 16:34'),
       ('日記3', '内容３', '2024-8-31 16:34');
'''

# SQLクエリの実行
try:
    cur.executescript(sql_query)
    print("テーブルの作成およびデータの挿入が成功しました。")
except sqlite3.Error as e:
    print(f"エラーが発生しました: {e}")
finally:
    # コミットしてデータを保存
    conn.commit()

    # 接続を閉じる
    conn.close()

```

# サンプルコード
 下記のサンプルコードをコピペして、デバッグで処理の順番を確認してみると何をやっているのかなんとなく分かると思います。

```py:models.py
import sqlite3
from abc import ABC
from typing import Optional

# 接続DB名
DB_NAME = 'sample.db'

class BaseModel(ABC):
    def __init__(self) -> None:
        # 接続のためのオブジェクト変数用意
        self.conn:Optional[sqlite3.Connection] = None
        self.cursor:Optional[sqlite3.Cursor] = None
    
    def connect(self) -> None:
        '''DBへの接続を開くメソッド'''
        try:
            self.conn = sqlite3.connect(DB_NAME)
            self.cursor = self.conn.cursor()
            # ↓辞書型でフィールドデータを取得するための指定
            self.cursor.row_factory = sqlite3.Row
        except sqlite3.Error as e:
            print(f"Connection error: {e}")
            raise
        
    def close(self) -> None:
        '''DBへの接続を閉じるメソッド'''
        self.conn.close()
        self.conn = None
        self.cursor = None

class DiaryModel(BaseModel):
    def __init__(self):
        super().__init__()
        self.t_diary = 'Diary'
    
    def get_all(self) -> list[sqlite3.Row]:
        '''Diaryテーブルから全てのデータを取得するメソッド'''
        data = []
        try :
            # 接続を確立
            self.connect()
            # データ取得の例
            self.cursor.execute(f'SELECT * FROM {self.t_diary}')
            data = self.cursor.fetchall()
        except Exception as e:
            print(f'Error{e}')
        finally:
            if self.conn:
                # 接続を閉じる
                self.close()
        return data
    
    def get_one(self, id:int) -> list[sqlite3.Row]:
        '''Diaryテーブルから全てのデータを取得するメソッド'''
        data = []
        try :
            # 接続を確立
            self.connect()
            # データ取得の例
            self.cursor.execute(f'SELECT * FROM {self.t_diary} WHERE id = ?', (id,))
            data = self.cursor.fetchone()
        except Exception as e:
            print(f'Error{e}')
        finally:
            if self.conn:
                # 接続を閉じる
                self.close()
        return data
```

```py:controllers.py
from models import BaseModel
from typing import Any

class DiaryController:
    def __init__(self, diary_model:BaseModel) -> None:
        # モデル変数用意
        self.diary_model = diary_model

    def get_list_data(self) -> str:
        '''Diaryデータを取得するメソッド'''
        # モデルからデータ取得
        data_list:list[Any] = self.diary_model.get_all()
        # データを文字列へ成形する
        data_str_list:list[str] = [
            f"id: {data['id']} title: {data['title']} content: {data['content']} create_at: {data['create_at']}"
            for data in data_list
        ]
        # リストを改行で結合して、一つの文字列にする
        data_str:str = "\n".join(data_str_list)
        
        return data_str

    def get_one(self, id:int) -> str:
        '''idでレコ―ドデータを取得'''
        # 初期化
        data_str = ""
        # モデルからデータ取得
        data:list[Any] = self.diary_model.get_one(id)
        # データを文字列へ成形する
        if data:
            data_str:str = f"id: {data['id']} title: {data['title']} content: {data['content']} create_at: {data['create_at']}"
        
        return data_str
```

```py:style_manager.py
from enum import Enum

FONT_TYPE = "meiryo"
BASE_COLOR_DARK = "#242424"
BASE_COLOR_LIGHT = "#DBDBDB"

class CommonStyle:
    ''' 共通のスタイルを定義するクラス '''
    def __init__(self) -> None:
        # 文字フォント設定
        self.HEADER_TITLE = (FONT_TYPE, 20, "bold")
        self.FRAME_TITLE = (FONT_TYPE, 17, "bold")
        self.DEFAULT = (FONT_TYPE, 15)
        
        # Windowの背景色
        self.transparent_frame = {
            "fg_color": BASE_COLOR_DARK,
        }
        
        # インラインボタン
        self.inline_btn = {
            "text_color": ("gray10", "#DCE4EE"),
            "fg_color": "transparent",
            "border_width":2,
        }
        
    def change_transparent_frame(self, theme:str):
        ''' windowsクラスの背景色テーマに合わせてデザイン変更'''
        if theme == 'dark':
            self.transparent_frame["fg_color"] = BASE_COLOR_DARK
        elif theme == 'light':
            self.transparent_frame["fg_color"] = BASE_COLOR_LIGHT
        elif theme == 'system':
            self.transparent_frame["fg_color"] = BASE_COLOR_LIGHT
```

```py:windows.py
import customtkinter as ctk
from typing import Any, Tuple
from abc import ABC

class BaseWindow(ctk.CTk, ABC):
    def __init__(self, controllers: dict[str, Any], style: Any, **kwargs):
        super().__init__(**kwargs)
        
        # 初期化
        self.theme = 'dark'                 # テーマカラー
        self.style = style                  # スタイルクラス
        self.controllers = controllers      # コントローラ群
        self.pages:dict[str] = {}           # ページのフレームを格納する辞書
        
        # テーマ設定
        self.bg_set(self.theme)
        ctk.set_default_color_theme("blue")  # Themes: blue (default), dark-blue, green
        
        # Gridレイアウトの設定
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
        
    def show_page(self, page_name:str) -> None:
        '''ページ切替を行うメソッド'''
        page = self.pages[page_name]
        page.tkraise()
        
    def page_set(self, pages:Any):
        ''' Pageクラスの配置を行うメソッド '''
        # ページクラス配置
        for PageClass in pages:
            page_name = PageClass.__name__
            page = PageClass(master=self, **self.style.transparent_frame)
            self.pages[page_name] = page
            page.grid(row=0, column=0, sticky="nsew")
        
    def bg_set(self, theme:str):
        ''' 背景テーマを設定するメソッド '''
        ctk.set_appearance_mode(theme)  # Modes: system (default), light, dark
        self.style.change_transparent_frame(theme)

class MainWindow(BaseWindow):
    def __init__(self, controllers: dict[str, Any], style: Any, **kwargs) -> None:
        super().__init__(controllers, style, **kwargs)

        self.title("MVC with Page Navigation")
        self.geometry("500x400")```

```py:pages.py
import customtkinter as ctk
import tkinter as tk
from tkinter import messagebox
from abc import ABC, abstractmethod

class BasePage(ctk.CTkFrame, ABC):
    def __init__(self, master:ctk.CTk|tk.Tk, **kwargs) -> None:
        super().__init__(master, **kwargs)
        self.style = self.master.style
    
    @abstractmethod
    def build_ui(self) -> None:
        """UIを構築するための抽象メソッド"""
        ...
    
    def show_page(self, page_name:str) -> None:
        '''ページ遷移するメソッド'''
        self.master.show_page(page_name)
        
class Page1(BasePage):
    def __init__(self, master:ctk.CTk, **kwargs) -> None:
        super().__init__(master, **kwargs)
        # コントローラ設定
        self.diary_controller = self.master.controllers['diary']
        # UI生成
        self.build_ui()
        
    def build_ui(self) -> None:
        '''UI生成するメソッド'''
        # Gridレイアウト設定
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure((0,1), weight=1)

        self.label = ctk.CTkLabel(self, text="ページ１です。")
        self.label.grid(row=0, column=0, columnspan=2, pady=20)

        self.page_btn = ctk.CTkButton(self, text="ページ２へ", command=lambda: self.show_page("Page2"))
        self.page_btn.grid(row=1, column=0, padx=20, pady=40)
        
        self.msg_btn = ctk.CTkButton(self, text="メッセージ表示", command=lambda: self.msg_output(1), **self.style.inline_btn)
        self.msg_btn.grid(row=1, column=1, padx=(0,20), pady=40)
        
    def msg_output(self, page_num:int) -> None:
        '''メッセージを出力するメソッド'''
        data:str = self.diary_controller.get_list_data()
        messagebox.showinfo("Information", f"ページ{page_num}のメッセージです。\n\n"
                            + "Diaryデータ：\n"
                            + f"{data}")

class Page2(BasePage):
    def __init__(self, master:ctk.CTk, **kwargs) -> None:
        super().__init__(master, **kwargs)
        # コントローラ設定
        self.diary_controller = master.controllers['diary']
        # 入力検証用のコマンドを登録　※バリデーションはself.registerを使用して関数登録して使用する必要がある
        self.validate_cmd = self.register(self.validate_numeric_input)
        # UI生成
        self.build_ui()
        
    def build_ui(self) -> None:
        '''UI生成するメソッド'''
        # Gridレイアウト設定
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure((0,1), weight=1)
        
        self.label = ctk.CTkLabel(self, text="ページ２です。")
        self.label.grid(row=0, column=0, columnspan=2, pady=20)
        
        # idでレコードデータを取得するためのもの
        self.get_label = ctk.CTkLabel(self, text="idを入力してください")
        self.get_label.grid(row=1, column=0, columnspan=2)
        self.get_input = ctk.CTkEntry(self)
        self.get_input.configure(validate="key", validatecommand=(self.validate_cmd, "%S"))
        self.get_input.grid(row=2, column=0, columnspan=2)

        self.page_btn = ctk.CTkButton(self, text="ページ１へ", command=lambda: self.show_page("Page1"))
        self.page_btn.grid(row=4, column=0, padx=20, pady=40)
        
        self.msg_btn = ctk.CTkButton(self, text="idでデータ取得", command=lambda: self.msg_output(2), **self.style.inline_btn)
        self.msg_btn.grid(row=4, column=1, padx=(0,20), pady=40)
        
    def msg_output(self, page_num:int) -> None:
        '''メッセージを出力するメソッド'''
        id = self.get_input.get()
        if not id:
            self.error_label = ctk.CTkLabel(self, text="※idを入力してください。", text_color="red")
            self.error_label.grid(row=3, column=0, columnspan=2)
        else:
            data:str = self.diary_controller.get_one(id)
            if not data:
                data = '指定idのデータはありません。'
                
            messagebox.showinfo("Information", f"ページ{page_num}のメッセージです。\n\n"
                                + "Diaryデータ：\n"
                                + f"{data}")
        
        
    def validate_numeric_input(self, char: str) -> str:
        """入力を数字のみに制限するメソッド"""
        return "true" if char.isdigit() or char == "" else "false"
```

```py:main.py
from controllers import DiaryController
from windows import MainWindow
from models import DiaryModel
from pages import Page1, Page2
from style_manager import CommonStyle

def main() -> None:
    # 各クラスのインスタンス生成
    app = App()
    # アプリ起動
    app.run()
    
class App:
    def __init__(self) -> None:
        ''' 各クラスの連携(インスタンス生成) '''
        # モデルクラス群を生成
        self.diary_model = DiaryModel()
        
        # コントローラクラス群の辞書型定義
        self.main_controllers={
            "diary": DiaryController(self.diary_model),
        }
        # スタイルクラス群を生成
        self.main_style=CommonStyle()     # MainWindowクラスで使用
        
        # ウィンドウクラスにコントローラークラス群を渡す
        self.main_window = MainWindow(self.main_controllers, self.main_style)
        
        # ページクラス配置
        self.main_window.page_set([Page1, Page2])      # ← 配置したいPageクラスを配列で渡す
        
    def run(self) -> None:
        ''' アプリ起動処理 '''
        self.main_window.show_page("Page1") # 最初に表示したいページクラス名を渡す
        self.main_window.mainloop()         # 起動

if __name__ == "__main__":
    main()
```

# 解説
こまかいcustomtkinterの設定部分については、割愛します。

## 使い方
1. モデルとコントローラはアプリDBの使用するテーブルごとに作成する

1. 作成したコントローラはmain()関数でAppクラスをインスタンス化し、run()メソッドでアプリ起動する仕組みになっています
    ```python:main.py
    def main() -> None:
        # 各クラスのインスタンス生成
        app = App()
        # アプリ起動
        app.run()
    ```
1. 各クラス間の連携はAppクラスのコンストラクタで行う。
    ```python:main.py
    class App:
        def __init__(self) -> None:
            ''' 各クラスの連携(インスタンス生成) '''
            # モデルクラス群を生成
            self.diary_model = DiaryModel()
            
            # コントローラクラス群の辞書型定義
            self.main_controllers={
                "diary": DiaryController(self.diary_model),
            }
            # スタイルクラス群を生成
            self.main_style=CommonStyle()     # 共通スタイル定義クラス
            
            # ウィンドウクラスにコントローラークラス群とスタイルクラスを渡す
            self.main_window = MainWindow(self.main_controllers, self.main_style)
            
            # ページクラス配置
            self.main_window.page_set([Page1, Page2])      # ← 配置したいPageクラスを配列で渡す
    ```
1. BaseWindowクラス(抽象クラス)のpage_setメソッドにページクラスを配置します。
（Appクラスのコンストラクタで呼び出しています。）
    ```python:windows.py
    class BaseWindow(ctk.CTk, ABC):
        # ・・省略・・
        def page_set(self, pages:Any):
            ''' Pageクラスの配置を行うメソッド '''
            # ページクラス配置
            for PageClass in pages:
                page_name = PageClass.__name__
                page = PageClass(master=self, **self.style.transparent_frame)
                self.pages[page_name] = page
                page.grid(row=0, column=0, sticky="nsew")
    ```

    配置したページクラスは下記のBaseWindowクラスのインスタンス変数に辞書型で保持して管理し、ShowPageメソッドなどで使用したいときに、クラス名をキーにして取り出します。

    ```python:windows.py
    # ・・省略・・
    self.pages:dict[str] = {}           # ページのフレームを格納する辞書
    ```
    
1. 最初に表示したいページはAppクラスのrunメソッドで設定します。
    ```python:main.py
    class App:
        # ・・省略・・
        def run(self) -> None:
            ''' アプリ起動処理 '''
            self.main_window.show_page("Page1") # 最初に表示したいページクラス名を渡す
            self.main_window.mainloop()         # 起動
    ```

    
## Modelクラスについて
今回作ったモデルクラスはDBに作成したテーブルごとModelを作ります

ポイント
基本的なデータの取得方法と変わらないですが、辞書型で取り出すために下記を指定しています。
```py:models.py
class BaseModel(ABC):
    # ・・省略・・
    def connect(self) -> None:
        '''DBへの接続を開くメソッド'''
        try:
            self.conn = sqlite3.connect('sample.db')
            self.cursor = self.conn.cursor()
            # ↓辞書型でフィールドデータを取得するための指定
            self.cursor.row_factory = sqlite3.Row # ←コレ
        except sqlite3.Error as e:
            print(f"Connection error: {e}")
            raise
```

上記を指定することにより、カラム名をキーとして値を取得できます。

``` py:controllers.py
def get_diary_data(self) -> str:
    '''Diaryデータを取得するメソッド'''
    # モデルからデータ取得
    data_list:list[Any] = self.diary_model.get_all()
    # データを文字列へ成形する
    data_str_list:list[str] = [
        f"title: {data['title']} content: {data['content']} create_at: {data['create_at']}"
        for data in data_list
    ]
    # リストを改行で結合して、一つの文字列にする
    data_str:str = "\n".join(data_str_list)
    return data_str
```

## Pageクラスの作り方
下記のクラスを抽象クラスを継承して作る。

```python:pages.py
class BasePage(ctk.CTkFrame, ABC):
    def __init__(self, master:ctk.CTk|tk.Tk, **kwargs) -> None:
        super().__init__(master, **kwargs)

    @abstractmethod
    def build_ui(self):
        """UIを構築するための抽象メソッド"""
        ...
        
    def show_page(self, page_name:str) -> None:
        '''ページ遷移するメソッド'''
        self.master.show_page(page_name)
```

build_ui()メソッドを使ってuiを作る。
ページ更新用のメソッドもここに関数を使ってボタンなどに設定する。
データを取得したいコントローラはviewで生成時に渡したcontrollersからページで使用するコントローラをインスタンスメソッドに出す。

```python:pages.py
class Page1(BasePage):
    def __init__(self, master:ctk.CTk, controllers: dict[str, Any], **kwargs) -> None:
        super().__init__(master, **kwargs)
        # コントローラ設定
        self.diary_controller = controllers['diary']
        # UI生成
        self.build_ui()
        
    def build_ui(self):
        '''UI生成するメソッド'''
        # ここでこのページで表示したいUIを作る
        ...
```

ビューを更新するメソッドはこのPageクラスに定義します。必要なビジネスロジックがあればコントローラクラスのメソッドを使用し、データを取得したりします。

```python:pages.py
class Page1(BasePage):
    # ...省略...
    def msg_output(self, page_num:int) -> None:
        '''メッセージを出力するメソッド'''
        data:str = self.diary_controller.get_list_data()
        messagebox.showinfo("Information", f"ページ{page_num}のメッセージです。\n\n"
                            + "Diaryデータ：\n"
                            + f"{data}")
```

## ページ遷移について
ページ遷移メソッドは上記のようにMainWindowに定義しています。
なので、Page生成の時に渡したmasterプロパティからshow_page()呼び出してページ遷移を行います。

```python:pages.py
class BasePage(ctk.CTkFrame, ABC):        
    # ・・省略・・
    
    def show_page(self, page_name:str) -> None:
        '''ページ遷移するメソッド'''
        self.master.show_page(page_name)
```


## StyleManagerクラスについて
定義はクラス変数に辞書型を渡して定義しています。CSSのような感覚で書けるのが良いですね。
```py:style_manager.py
class StyleManager:
    
    transparent_frame = {
        "fg_color": BASE_COLOR_DARK,
    }
    
    inline_btn = {
        "text_color": ("gray10", "#DCE4EE"),
        "fg_color": "transparent",
        "border_width":2,
    }
```
使い方はスタイルマネージャークラスは上記解説（ページの表示）のループ内で記述の第３引数に渡している
```**StyleManager.transparent_frame```のように使います。

```py:windows.py
PageClass(master=self, controllers=controllers, **StyleManager.transparent_frame)
```

引数の受け取りはpages.pyのPageクラスで**kwargsです。

```py:pages.py
class Page1(ctk.CTkFrame):
    def __init__(self, master, controller, **kwargs):　#←ココ
        super().__init__(master, **kwargs) #←ココ
        self.controller = controller
```

ただし、全部のスタイルをここで作るのはコード量がとても多くなってしまうので、あまりお勧めではないかもしれません。デザインのために引数を多くの値を渡す場合や共通で使用するものに関してここに定義しておくと管理しやすいと思ってます。

# 最後に
単純なひな形としても使えると思います。良かったら使ってください。

※追記
コメントで頂いた、クラスの相互参照や役割の明確化についてを自分なりに考えてみました。
また改善点や問題点などありましたら、教えて頂けると嬉しいです。ありがとうございました！

## Github

https://github.com/homan05872/Template_customtkinter_mvc