tkinterで表形式GUIを作る

tk.Frameを親クラスとして、汎用的に利用できるtkinterのクラスを作成する（試作中）
Pythonコードは以下のように作成中


```python:excel_like_frame.py
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
import pandas as pd
import numpy as np
from datetime import datetime, date
import inspect
from typing import Literal, Optional, Union, Tuple, Generator, Callable, List, Dict, Any
from abc import ABC, abstractmethod, abstractproperty, abstractclassmethod
from dataclasses import dataclass, asdict

_DTYPES = Literal['int', 'float', 'str', 'date', 'bool']


class DateVar(tk.StringVar):
    _default = ""

    def __init__(self, master=None, value: date=None, name=None):
        date_val = value.strftime('%Y/%m/%d') if value else ""
        tk.StringVar.__init__(self, master, date_val, name)

    def get(self):
        """Return value of variable as string."""
        value = self._tk.globalgetvar(self._name)
        try:
            return datetime.strptime(value, '%Y/%m/%d').date()
        except:
            return str(value)



@dataclass
class CellId:
    table_name: str
    row: int
    col: int
    @property
    def name(self):
        return f'{self.table_name}:{self.row}-{self.col}'
    def __hash__(self) -> int:
        return hash(self.name)
    def __eq__(self, other: CellId) -> bool:
        return self.name == other.name

@dataclass
class CellConfig:
    width: float = 10
    # height: float = 2
    bg: str = 'white'
    fg: str = 'black'
    
    def as_dict(self, unusable: list = [], rename: Dict[str, str] = {}) -> dict:
        return {rename.get(k, k): v for k, v in asdict(self).items() if k not in unusable}


class Cell(ABC):
    _id: CellId
    _dtype: _DTYPES
    _variable: tk.Variable
    _config: CellConfig
    _commandas: list[Callable]

    @abstractproperty
    def value(self):
        pass
    
    @abstractmethod
    @value.setter
    def value(self, new_value):
        pass

    def initilize_attr(self, master, cell_id: CellId, config: Optional[CellConfig], value: Any, data_type: Optional[_DTYPES]):
        # set id
        self._id = cell_id
        # set dtype
        self._dtype, self._variable = get_variable(master, cell_id, value, data_type)
        # set config
        self._config = config if config is not None else CellConfig()

    def arrange_grid(self):
        self.grid(row=self._id.row, column=self._id.col)
        self.reflect_config(self._config)
        print()

    def reflect_config(self, config: CellConfig):
        self.config(**config.as_dict())


def get_variable(master, cell_id: CellId, value: Any, data_type: Optional[_DTYPES]) -> Tuple[_DTYPES, tk.Variable]:
        # set dtype
        if data_type is None:
            if isinstance(value, int):
                _dtype = 'int'
            elif isinstance(value, float):
                _dtype = 'float'
            elif isinstance(value, date) or isinstance(value, datetime):
                _dtype = 'date'
            elif isinstance(value, bool):
                _dtype = 'bool'
            else:
                _dtype = 'str'
        else:
            _dtype = data_type
        # set variable
        if _dtype == 'int':
            _variable = tk.IntVar(master, value, cell_id.name)
        elif _dtype == 'float':
            _variable = tk.DoubleVar(master, value, cell_id.name)
        elif _dtype == 'date':
            _variable = DateVar(master, value, cell_id.name)
        elif _dtype == 'bool':
            _variable = tk.BooleanVar(master, value, cell_id.name)
        elif _dtype == 'str':
            _variable = tk.StringVar(master, value, cell_id.name)
        # return
        return _dtype, _variable



class EntryCell(Cell, tk.Entry):
    def __init__(self, master, cell_id: CellId, config: Optional[CellConfig] = None, value: Any = None, data_type: Optional[_DTYPES] = None) -> None:
        # set attr
        self.initilize_attr(master, cell_id, config, value, data_type)
        # draw
        tk.Entry.__init__(self, master, textvariable=self._variable)
        self.arrange_grid()

    @property
    def value(self):
        return self._variable.get()
    @value.setter
    def value(self, new_value):
        self._variable.set(new_value)


class TextCell(Cell, tk.Label):
    def __init__(self, master, cell_id: CellId, config: Optional[CellConfig] = None, value: Any = None, relief='groove'):
        # set attr
        self.initilize_attr(master, cell_id, config, value, None)
        # draw
        tk.Label.__init__(self, master, text=value, relief=relief)
        self.arrange_grid()

    @property
    def value(self):
        return self._variable.get()
    @value.setter
    def value(self, new_value):
        self._variable.set(new_value)

class ButtonCell(Cell, tk.Button):
    def __init__(self, master, cell_id: CellId, config: Optional[CellConfig] = None, text: str = 'button', command: Callable = None):
        # set attr
        self.initilize_attr(master, cell_id, config, f'ButtonCell:{str(text)}', None)
        # draw
        tk.Button.__init__(self, master, text=text)
        self.arrange_grid()

    @property
    def value(self):
        return self._variable.get()
    @value.setter
    def value(self, new_value):
        self._variable.set(new_value)

class ComboboxCell(Cell, ttk.Combobox):
    def __init__(self, master, cell_id: CellId, config: Optional[CellConfig] = None, value: str = None, item_list: list = []):
        # set attr
        self.initilize_attr(master, cell_id, config, value, 'str')
        # draw
        ttk.Combobox.__init__(self, master, values=item_list, textvariable=self._variable)
        self.arrange_grid()

    @property
    def value(self):
        return self._variable.get()
    @value.setter
    def value(self, new_value):
        self._variable.set(new_value)

    # override
    def reflect_config(self, config: CellConfig):
        self.config(**config.as_dict(unusable=['bg', 'fg']))

class CellsManager:
    def __init__(self, master, table_name: str, rows: int, cols: int):
        self._cell_dict = {}
        for i in range(rows):
            for j in range(cols):
                cell_id = CellId(table_name, i, j)
                self._cell_dict[cell_id] = EntryCell(master, cell_id, value=i*j)

class ExcelFrame(tk.LabelFrame):
    def __init__(self, master, table_name: str, rows: int, cols: int, relief='sunken'):
        self.table_name: str = table_name
        self.shape: Tuple[int, int] = (rows, cols)
        super().__init__(master, relief=relief, text=table_name)
        self.cell_manager: CellsManager = CellsManager(self, table_name, rows, cols)

class Range:
    def __init__(self, excel_frame: ExcelFrame, row_range: Optional[Tuple[int, int]] = None, col_range: Optional[Tuple[int, int]] = None) -> None:
        parent_cell_ids = list(excel_frame.cell_manager._cell_dict.keys())
        self.cells: Dict[CellId, Cell] = {}
        i_range = range(row_range[0], row_range[1]+1) if row_range is not None else range(excel_frame.shape[0])
        j_range = range(col_range[0], col_range[1]+1) if col_range is not None else range(excel_frame.shape[1])
        for i in i_range:
            for j in j_range:
                cell_id = CellId(excel_frame.table_name, i, j)
                if cell_id in parent_cell_ids:
                    self.cells[cell_id] = excel_frame.cell_manager._cell_dict[cell_id]
    
    def generate_cells(self):
        for cell in self.cells.values():
            yield cell

    def apply_config(self, config: CellConfig):
        for cell in self.generate_cells():
            cell.reflect_config(config)

class AppWidgets(tk.Frame):
    def __init__(self, master) -> None:
        super().__init__(master)
        # excel-like frame
        self.frame = ExcelFrame(master, 'sample', 5, 5)
        # button1
        self.button1 = tk.Button(master, text='button')
        # dist
        self.frame.pack(anchor=tk.W)
        self.button1.pack(anchor=tk.W)
        self.pack()

class AppActions:
    frame: ExcelFrame

    def fillcolor(self):
        Range(self.frame).apply_config(CellConfig(bg='red'))

class App(AppWidgets, AppActions):
    def __init__(self, master) -> None:
        AppWidgets.__init__(self, master)
        #
        self.button1.config(command=self.fillcolor)
```