tk.Frameを親クラスとして、汎用的に利用できるtkinterのクラスを作成する(試作中)
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)