1. はじめに
tkinterはPythonでGUI化させる場合にはまず検討されると思いますが、いかんせん見た目がよくありません。GUIライブラリは他にもいろいろあるようですが、tkinterベースで手軽におしゃれGUIが作れる CustomTkinter というライブラリがあります。
簡単におしゃれにできるので素晴らしいのですが、日本語での情報が全然ないこともあり、一度Qiitaにまとめてみようと思っていたら、公式Wikiのチュートリアル+αをわかりやすくした素晴らしい記事を他の方が書かれていました。
そんなわけで二番煎じは意味がないので、
公式Wiki に記載の主に見た目の設定についてまとめつつ、ウィジェットの設定変更を簡単にプレビューできるツールを作ったので、紹介したいと思います。
作ったツールは以下のような動作をするものです。
ツールだけサクッと見たい方は他は飛ばして、3.4. ソースコード をご覧ください。
[追記]
本ツール以外に簡易カスタムテーマ作成ツールも作成してみました。
1.1. 開発環境
Windows 10 Pro 64bit
Python 3.9.13
CustomTkinter 5.1.2
1.2. この記事が対象とする方
-
tkinterでGUIアプリを作ったことがあり、新たにCustomTkinterを使ってみようと思っている方
-
CustomTkinterで見た目にこだわったPython GUIを作りたいけど、いまいち細かい設定がわからない方
あたりを対象として意識した記事になります。
2. CustomTkinterの見た目の基本的な設定
2.1. CustomTkinterとtkinterで異なるオプション引数
CustomTkinterの最大の特徴は 角が丸いウィジェットの見た目 です。
そのためtkinterとは設定する項目数がそもそも違うため、オプション引数の数や名称が異なり、tkinterからの移行時に困る点かと思います。
オプションの概要としてわかりやすいのは公式Wikiにある以下の図になります。
tkinterで通常 bg
で指定している領域は fg_color
となり、bg_color
はボタン自体の後ろ側になります。またtkinterで fg
は文字色を設定するオプション引数ですが、こちらは text_color
となり、比較的直感的な名称となっています。
そのほかに個人的に設定することが多いオプション引数の違いを以下に示しておきます。
設定項目 | tkinter | CustomTkinter |
---|---|---|
テキスト色 | fg | text_color |
ウィジェット色 | bg | fg_color |
スケールの方向 | orient | orientation |
枠の浮き彫り | relief | - |
枠幅 | borderwidth | border_width |
配置 | anchor (tk.CENTER, tk.N, tk.S, tk.NW, tk.SW, tk.NE, tk.SE) |
anchor ('center', 'n', 's', 'nw', 'sw', 'ne', 'se') |
配置 | anchor (tk.CENTER, tk.N, tk.S, tk.NW, tk.SW, tk.NE, tk.SE) |
anchor ('center', 'n', 's', 'nw', 'sw', 'ne', 'se') |
角のr | - | corner_radius |
マウスホバー | - | hover_color |
2.2. Appearance Modeの設定(Dark/Lightモード)
CustomTkinterの特徴の二つ目として、Appearance ModeというDarkモードとLightモードを切り替えて表示する機能があり、デフォルト値の設定は以下のように記述します。
customtkinter.set_appearance_mode("Dark") # "System"(default) or "Light" or "dark"
ウィジェットのオプション引数でもDarkモードとLightモードでの色をタプルで両方設定しておくことが可能で、fg_color=("#ff0000", "#0000ff")
という形式で、(Lightモード, Darkモード)
の順に設定します。
後述するjsonでの設定では["#ff0000", "#0000ff"]
というようにリスト形式で指定されており、上記のオプション引数でもリストを渡しても設定されます。
2.3. Themes(テーマ)の設定
CustomTkinterには、特に何もしなくてもおしゃれGUIを実現するために、ウィジェットの色の設定に、blue
、green
、dark-blue
の3種類のデフォルトテーマが用意されており、以下の記述でデフォルトテーマを設定できます。
customtkinter.set_default_color_theme("dark-blue") # Themes: "blue" (standard), "green", "dark-blue"
基本的な外観はDark/Lightモードの設定でベースとなる白背景か黒背景を選んで、ボタンなどのアクセントとなる色をThemeで決めるという流れになります。
3. ウィジェットプレビューツール
ここまでで説明してきた流れで、ざっくりとした外観の設定はわかるのですが、いざ各ウィジェットを設定してみようとなると、どのオプション引数がどう見た目に影響するのかわかりにくい状態でした。
そこでせっかくのGUIライブラリなので、CustomTkinterを使って、CustomTkinterの設定をGUI上ですぐに確認できるウィジェットプレビューツールを作ってみることにしました。
3.1. ツールの概要
名前のとおり、各ウィジェットの設定項目を変更して、アプリ上ですぐにプレビューができるツールです。
ツールの画面は公式のimage_example.pyをベースに作成しています。
左側に各ウィジェットを選択するナビゲーションバー、右側に各ウィジェットのオプション引数を並べた設定項目変更画面とプレビュー画面が存在します。
オプション引数を変更して、「Apply to settings」ボタンを押すと、右側のウィジェットに対して、設定したオプション引数が反映されます。
3.2. ウィジェットプレビュー機能について
3.2.1. 初期値
- ウィジェットの設定項目の画面生成と初期値をjsonファイルから読み込んでいるので、初期値用jsonファイル の
initial_theme.json
をpyファイルと同じフォルダに入れて起動する必要があります。 -
initial_theme.json
は公式のWikiのarguments
を参考にデフォルトのテーマであるblue.json
に追記しています。
3.2.2. 使い方
-
各オプション引数の設定を入力して「Apply to settings」ボタンを押すだけです。
-
例外処理があまいので、想定していないデータを入力するとエラーを吐きます。一度エラーが出ると「Apply to settings」ボタンを押しても適用されなくなるので、「Reset preview」をする必要があります。
-
色の設定は一度入力・適用してから削除・適用をしても、デフォルト状態に戻りません。「Reset preview」で戻してください。
-
font
は('Meiryo', 20)
でもMeiryo, 20, bold, italic
でもカンマ区切りで引数の順番と型が合っていれば、適用されます。 -
色選択の参考用にtkinterのColorChooserからカラーコードを出せるようにしています。「color chooser」ボタンを押して、OKを押した後にEntryに出てくるカラーコードをコピーして使えます。
** configure()で設定変更できない要素 **
以下の引数はconfigureメソッドでの変更ができないため、設定してもプレビューできませんでした。
- CTkCheckBox text_color_disabled
- CTkSwitch text_color
- CTkSwitch hover_color
- CTkProgressBar orientation
- CTkSlider orientation
- CTkScrollbar orientation
- CTkScrollableFrame orientation
実行時のエラーはValueError: ['text_color_disabled'] are not supported arguments.
になりますが、ツール上ではexcept ValueError
をpassさせているので無反応な引数となっています。
各ウィジェットを生成時に上記の引数を設定すると反映されるので、あくまでconfigureメソッドでの変更時のバグだと思われます。
3.3. ソースコードについての説明
やっていることは CTkEntry
に入力された設定値を.configure()
メソッドで適用するだけですが、ウィジェットの数が多いので、ウィジェット生成部分について説明しておきます。
ウィジェットは同じ要素であれば、リストとしてオブジェクトをまとめることが可能なので、基本的には要素を内包表記で複数個同時に生成して、for文でgrid配置させています(grid配置も内包表記で動きますが、内包表記の意味合い的に微妙な気がするのと、grid_rowconfigureやgrid_columnconfigureとまとめられるので、forで良いと思っています)。
# navigation menu
self.navi_button = [ctk.CTkButton(self.navi_frame, corner_radius=0, height=38, border_spacing=6, text=f" {self.navi_list[i]} >",
fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
anchor="w", font=self.navi2_font, command=partial(self.select_frame_by_name, f'{self.navi_list[i]}'))
for i in range(len(self.navi_list))]
for i in range(len(self.navi_list)):
self.navi_button[i].grid(row=i+1, column=0, sticky="ew")
またオプション引数の設定項目入力欄となるCTkEntry
は 各ウィジェット(16個) × オプション引数(6~15個) × Light/Darkモード(色なら2個) という数だけ生成する必要があります。
全部内包表記だとさすがにわかりにくいので、 各ウィジェット名のリスト をfor文で回す -> [jsonファイル内のキー]/[設定項目が色なら2つ生成]させる内包表記 といった挙動を以下のコードで書いています。
# create setting widgets
def create_setting_widget(self):
# create widget list
self.example_frame = []
self.example_title = []
self.example_label = []
self.example_entry = []
self.example_codes = []
self.example_apply = []
self.example_reset = []
self.color_hexnum = []
self.color_button = []
# create setting
for i, item in enumerate(self.navi_list):
# list keys in dict
keys = list(self.color.get(item).keys())
# create setting widget frame
self.example_frame.append(ctk.CTkFrame(self.sub_frame[i][0], fg_color="transparent"))
self.example_frame[i].grid(row=0, column=0, padx=20, pady=(0,20), sticky="new")
self.example_frame[i].grid_columnconfigure(0, weight=1)
# create setting widget
self.example_title.append(ctk.CTkLabel(self.example_frame[i], text=f"{item} Settings",
corner_radius=10, height=60, width=400, font=self.title_font,
fg_color=["#3a7ebf", "#1f538d"], text_color=["#DCE4EE", "#DCE4EE"]))
self.example_label.append([ctk.CTkLabel(self.example_frame[i], text=f"{key}", width=150,
wraplength=150, anchor='e', justify='right', font=self.normal_font)
for key in keys])
self.example_entry.append([[ctk.CTkEntry(self.example_frame[i], font=self.normal_font) for _ in range(2)]
if 'color' in key else [ctk.CTkEntry(self.example_frame[i], font=self.normal_font)]
for key in keys])
self.example_codes.append(ctk.CTkEntry(self.example_frame[i], font=self.normal_font))
self.example_apply.append(ctk.CTkButton(self.example_frame[i], text="Apply to settings", font=self.normal_font,
command=partial(self.apply_buttuon_click, i)))
self.example_reset.append(ctk.CTkButton(self.example_frame[i], text="Reset preview", font=self.normal_font,
command=partial(self.preview_reset, i)))
self.color_hexnum.append(ctk.CTkEntry(self.example_frame[i], font=self.normal_font))
self.color_button.append(ctk.CTkButton(self.example_frame[i], text="color chooser", font=self.normal_font,
command=partial(self.color_choose_tool, i)))
# widgets put in frame
self.example_title[i].grid(row=0, column=0, columnspan=3, padx=20, pady=20, sticky="nsew")
for j, key in enumerate(keys):
self.example_label[i][j].grid(row=j+1, column=0, padx=10, pady=3, sticky="nsew")
self.example_entry[i][j][0].grid(row=j+1, column=1, padx=10, pady=3, sticky="nsew")
if 'color' in key:
self.example_entry[i][j][1].grid(row=j+1, column=2, padx=10, pady=3, sticky="nsew")
self.example_codes[i].grid(row=j+2, column=0, columnspan=3, padx=10, pady=(10, 2), sticky="nsew")
self.example_apply[i].grid(row=j+3, column=0, columnspan=2, padx=(10, 2), pady=(2, 10), sticky="nsew")
self.example_reset[i].grid(row=j+3, column=2, padx=(2, 10), pady=(2, 10), sticky="nsew")
self.color_hexnum[i].grid(row=j+4, column=1, padx=(10, 2), pady=4, sticky="nsew")
self.color_button[i].grid(row=j+4, column=2, padx=(2, 10), pady=4, sticky="nsew")
さすがに可読性が低いような気もしますが、生成している数からすれば、これくらいの量で書けるならまとめてしまった方がわかりやすいと思っています。
また個人的には比較的短いコードで配置できるので、1人で作るプログラムならこれでいいんじゃないかなーとは思いますが、ソフトウェア開発が本業の方からするとよろしくないんでしょうね…
本ツールは公式WikiのApp structure and layoutに記載されているようにフレームをcustomtkinter.CTkFrameから継承したクラスで構造化することはしていません。
これは基本的には私のプログラミングに関する知識不足のためで、製作していくうちにWikiに記載の実装をするべきだと知った次第です。なので、今後がんばります。
3.4. ソースコードと初期値用jsonファイル
ソースコード
import json
import tkinter as tk
from tkinter import messagebox
from tkinter import colorchooser
import customtkinter as ctk
from functools import partial
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("CTk widget preview")
self.geometry("1200x780")
self.minsize(1000,740)
# default color settings
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
# set grid layout 1x2
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
# navigation menu lists
self.navi_list = ["CTkFrame", "CTkButton", "CTkEntry", "CTkLabel", "CTkComboBox", "CTkCheckBox", "CTkRadioButton", "CTkProgressBar",
"CTkOptionMenu", "CTkSwitch", "CTkSlider", "CTkScrollbar", "CTkScrollableFrame", "CTkSegmentedButton", "CTkTextbox", "CTkTabview"]
# font settings
self.title_font = ctk.CTkFont(family='Yu Gothic', size=26, weight='bold')
self.navi1_font = ctk.CTkFont(family='Yu Gothic', size=16, weight='bold')
self.navi2_font = ctk.CTkFont(family='Yu Gothic', size=14, weight='bold')
self.normal_font = ctk.CTkFont(family='Yu Gothic', size=13)
# create navigation frame
self.create_navigation_frame()
# create main frame
self.create_mainframe()
# Create sample widget
self.create_example_widget()
try:
# json file loading
self.color = json.load(open('./initial_theme.json', 'r'))
except FileNotFoundError:
messagebox.showinfo('error', 'initial_theme.json does not exist.\nPlease prepare the json file for the theme needed to display the configuration items.')
# create setting widget
self.create_setting_widget()
# Insert Entry Values
self.insert_entry_values('all')
# select default frame
self.select_frame_by_name(f"{self.navi_list[0]}")
# create navigation frame
def create_navigation_frame(self):
self.navi_frame = ctk.CTkFrame(self, corner_radius=0)
self.navi_frame.grid(row=0, column=0, sticky="nsew")
self.navi_frame.grid_rowconfigure(len(self.navi_list)+1, weight=1)
# Application Title
self.navi_label = ctk.CTkLabel(self.navi_frame, text="■ Widget Preview", font=self.navi1_font)
self.navi_label.grid(row=0, column=0, padx=(10,20), pady=20)
# navigation menu
self.navi_button = [ctk.CTkButton(self.navi_frame, corner_radius=0, height=38, border_spacing=6, text=f" {self.navi_list[i]} >",
fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
anchor="w", font=self.navi2_font, command=partial(self.select_frame_by_name, f'{self.navi_list[i]}'))
for i in range(len(self.navi_list))]
for i in range(len(self.navi_list)):
self.navi_button[i].grid(row=i+1, column=0, sticky="ew")
# Appearance mode selection
self.appearance_mode_menu = ctk.CTkOptionMenu(self.navi_frame, values=["Light", "Dark", "System"],
command=self.change_appearance_mode_event)
self.appearance_mode_menu.grid(row=len(self.navi_list)+1, column=0, padx=20, pady=20, sticky="s")
def change_appearance_mode_event(self, new_appearance_mode):
ctk.set_appearance_mode(new_appearance_mode)
# select navi frame
def select_frame_by_name(self, name):
# set button color for selected button
for i in range(len(self.navi_list)):
self.navi_button[i].configure(fg_color=("gray75", "gray25") if name == f"{self.navi_list[i]}" else "transparent")
# show selected frame
for i in range(len(self.navi_list)):
if name == f"{self.navi_list[i]}":
self.main_frame[i].grid(row=0, column=1, sticky="nsew")
else:
self.main_frame[i].grid_forget()
# create main frame
def create_mainframe(self):
self.main_frame = [ctk.CTkFrame(self, corner_radius=0, fg_color="transparent") for _ in range(len(self.navi_list))]
for i in range(len(self.navi_list)):
self.main_frame[i].grid(row=0, column=0, sticky="nsew")
self.main_frame[i].grid_rowconfigure(0, weight=1)
self.main_frame[i].grid_columnconfigure(1, weight=1)
# Create sub frame
self.sub_frame = [[ctk.CTkFrame(self.main_frame[i], corner_radius=0, fg_color="transparent") for _ in range(2)] for i in range(len(self.navi_list))]
for i in range(len(self.navi_list)):
self.sub_frame[i][0].grid(row=0, column=0, padx=20, pady=(10, 20), sticky="nsew")
self.sub_frame[i][1].grid(row=0, column=1, padx=20, pady=30, sticky="nsew")
self.sub_frame[i][1].configure(border_width=2, border_color="gray50", corner_radius=10)
for j in range(2):
self.sub_frame[i][j].grid_rowconfigure(0, weight=1)
self.sub_frame[i][j].grid_columnconfigure(0, weight=1)
# create sample widget in right frame
def create_example_widget(self):
self.example = []
self.example.append(ctk.CTkFrame(self.sub_frame[0][1]))
self.example.append(ctk.CTkButton(self.sub_frame[1][1]))
self.example.append(ctk.CTkEntry(self.sub_frame[2][1]))
self.example.append(ctk.CTkLabel(self.sub_frame[3][1]))
self.example.append(ctk.CTkComboBox(self.sub_frame[4][1]))
self.example.append(ctk.CTkCheckBox(self.sub_frame[5][1]))
self.example.append(ctk.CTkRadioButton(self.sub_frame[6][1]))
self.example.append(ctk.CTkProgressBar(self.sub_frame[7][1]))
self.example.append(ctk.CTkOptionMenu(self.sub_frame[8][1]))
self.example.append(ctk.CTkSwitch(self.sub_frame[9][1]))
self.example.append(ctk.CTkSlider(self.sub_frame[10][1]))
self.example.append(ctk.CTkScrollbar(self.sub_frame[11][1]))
self.example.append(ctk.CTkScrollableFrame(self.sub_frame[12][1]))
self.example.append(ctk.CTkSegmentedButton(self.sub_frame[13][1]))
self.example.append(ctk.CTkTextbox(self.sub_frame[14][1]))
self.example.append(ctk.CTkTabview(self.sub_frame[15][1]))
for i, item in enumerate(self.navi_list):
self.example[i].grid(row=0, column=0, padx=20, pady=10)
if item == 'CTkTabview':
self.example[i].add("Tab 1")
self.example[i].add("Tab 2")
self.example[14].insert('0.0', 'sample_text, '*50)
# create setting widgets
def create_setting_widget(self):
# create widget list
self.example_frame = []
self.example_title = []
self.example_label = []
self.example_entry = []
self.example_codes = []
self.example_apply = []
self.example_reset = []
self.color_hexnum = []
self.color_button = []
# create setting
for i, item in enumerate(self.navi_list):
# list keys in dict
keys = list(self.color.get(item).keys())
# create setting widget frame
self.example_frame.append(ctk.CTkFrame(self.sub_frame[i][0], fg_color="transparent"))
self.example_frame[i].grid(row=0, column=0, padx=20, pady=(0,20), sticky="new")
self.example_frame[i].grid_columnconfigure(0, weight=1)
# create setting widget
self.example_title.append(ctk.CTkLabel(self.example_frame[i], text=f"{item} Settings",
corner_radius=10, height=60, width=400, font=self.title_font,
fg_color=["#3a7ebf", "#1f538d"], text_color=["#DCE4EE", "#DCE4EE"]))
self.example_label.append([ctk.CTkLabel(self.example_frame[i], text=f"{key}", width=150,
wraplength=150, anchor='e', justify='right', font=self.normal_font)
for key in keys])
self.example_entry.append([[ctk.CTkEntry(self.example_frame[i], font=self.normal_font) for _ in range(2)]
if 'color' in key else [ctk.CTkEntry(self.example_frame[i], font=self.normal_font)]
for key in keys])
self.example_codes.append(ctk.CTkEntry(self.example_frame[i], font=self.normal_font))
self.example_apply.append(ctk.CTkButton(self.example_frame[i], text="Apply to settings", font=self.normal_font,
command=partial(self.apply_buttuon_click, i)))
self.example_reset.append(ctk.CTkButton(self.example_frame[i], text="Reset preview", font=self.normal_font,
command=partial(self.preview_reset, i)))
self.color_hexnum.append(ctk.CTkEntry(self.example_frame[i], font=self.normal_font))
self.color_button.append(ctk.CTkButton(self.example_frame[i], text="color chooser", font=self.normal_font,
command=partial(self.color_choose_tool, i)))
# widgets put in frame
self.example_title[i].grid(row=0, column=0, columnspan=3, padx=20, pady=20, sticky="nsew")
for j, key in enumerate(keys):
self.example_label[i][j].grid(row=j+1, column=0, padx=10, pady=3, sticky="nsew")
self.example_entry[i][j][0].grid(row=j+1, column=1, padx=10, pady=3, sticky="nsew")
if 'color' in key:
self.example_entry[i][j][1].grid(row=j+1, column=2, padx=10, pady=3, sticky="nsew")
self.example_codes[i].grid(row=j+2, column=0, columnspan=3, padx=10, pady=(10, 2), sticky="nsew")
self.example_apply[i].grid(row=j+3, column=0, columnspan=2, padx=(10, 2), pady=(2, 10), sticky="nsew")
self.example_reset[i].grid(row=j+3, column=2, padx=(2, 10), pady=(2, 10), sticky="nsew")
self.color_hexnum[i].grid(row=j+4, column=1, padx=(10, 2), pady=4, sticky="nsew")
self.color_button[i].grid(row=j+4, column=2, padx=(2, 10), pady=4, sticky="nsew")
def color_choose_tool(self, i):
selected_color = colorchooser.askcolor()
self.color_hexnum[i].insert(0, selected_color[1])
def insert_entry_values(self, i):
def insert_value():
keys = list(self.color.get(self.navi_list[i]).keys())
values = list([self.color[self.navi_list[i]][key] for key in keys])
for j, key in enumerate(keys):
self.example_entry[i][j][0].delete(0, tk.END)
if values[j] != None:
if 'color' in key:
if values[j] == 'transparent':
self.example_entry[i][j][0].insert(0, 'transparent')
else:
self.example_entry[i][j][1].delete(0, tk.END)
[self.example_entry[i][j][k].insert(0, f"{values[j][k]}") for k in range(2)]
else:
self.example_entry[i][j][0].insert(0, f"{values[j]}")
if i == 'all':
for i in range(len(self.navi_list)):
insert_value()
else:
insert_value()
def preview_reset(self, i):
self.example[i].grid_forget()
self.example_codes[i].delete(0, tk.END)
self.create_example_widget()
self.insert_entry_values(i)
# Apply to preview widget & create example code
def apply_buttuon_click(self, i):
keys, values = self.get_widgets_data(i)
self.create_example_code(i, keys, values)
self.apply_example_configure(i, keys, values)
# Collect configuration items entered in setting widgets
def get_widgets_data(self, i):
keys = list(self.color.get(self.navi_list[i]).keys())
values = []
for j, key in enumerate(keys):
value = self.example_entry[i][j][0].get()
if value == '':
values.append(None)
else:
# color setting('transparent' or color list)
if 'color' in key:
if value == 'transparent':
values.append('transparent')
elif self.example_entry[i][j][1].get() == '':
values.append(self.example_entry[i][j][0].get())
else:
values.append([self.example_entry[i][j][k].get() for k in range(2)])
# font setting(tuple)
elif key == 'font' or key == 'dropdown_font' or key == 'label_font':
for replace_word in ['(', ')', '"', "'", ' ']:
value = value.replace(replace_word, '')
values.append(tuple([int(ele) if k == 1 else ele for k, ele in enumerate(value.split(','))]))
# values setting(list)
elif key == 'values':
for replace_word in ['"', "'"]:
value = value.replace(replace_word, '')
values.append([ele for ele in value.split(',')])
# int or str
else:
try:
values.append(int(value))
except ValueError:
values.append(value)
return keys, values
def create_example_code(self, i, keys, values):
excample_code = f'customtkinter.{self.navi_list[i]}(self, '
for j, key in enumerate(keys):
if values[j] != None:
if isinstance(values[j], str):
excample_code += f'{key}="{values[j]}", '
else:
excample_code += f'{key}={values[j]}, '
excample_code = excample_code[:-2] + ')'
self.example_codes[i].delete(0, tk.END)
self.example_codes[i].insert(0, excample_code)
# Apply to preview widget
def apply_example_configure(self, i, keys, values):
for j, key in enumerate(keys):
if 'color' in key:
if values[j] != None:
if key == 'fg_color':
self.example[i].configure(fg_color=values[j])
elif key == 'top_fg_color':
self.example[i].configure(top_fg_color=values[j])
elif key == 'border_color':
self.example[i].configure(border_color=values[j])
elif key == 'hover_color':
self.example[i].configure(hover_color=values[j])
elif key == 'progress_color':
self.example[i].configure(progress_color=values[j])
elif key == 'button_color':
self.example[i].configure(button_color=values[j])
elif key == 'button_hover_color':
self.example[i].configure(button_hover_color=values[j])
elif key == 'selected_color':
self.example[i].configure(selected_color=values[j])
elif key == 'selected_hover_color':
self.example[i].configure(selected_hover_color=values[j])
elif key == 'unselected_color':
self.example[i].configure(unselected_color=values[j])
elif key == 'unselected_hover_color':
self.example[i].configure(unselected_hover_color=values[j])
elif key == 'scrollbar_button_color':
self.example[i].configure(scrollbar_button_color=values[j])
elif key == 'scrollbar_button_hover_color':
self.example[i].configure(scrollbar_button_hover_color=values[j])
elif key == 'label_text_color':
self.example[i].configure(label_text_color=values[j])
elif key == 'label_fg_color':
self.example[i].configure(label_fg_color=values[j])
elif key == 'segmented_button_fg_color':
self.example[i].configure(segmented_button_fg_color=values[j])
elif key == 'segmented_button_selected_color':
self.example[i].configure(segmented_button_selected_color=values[j])
elif key == 'segmented_button_selected_hover_color':
self.example[i].configure(segmented_button_selected_hover_color=values[j])
elif key == 'segmented_button_unselected_color':
self.example[i].configure(segmented_button_unselected_color=values[j])
elif key == 'segmented_button_unselected_hover_color':
self.example[i].configure(segmented_button_unselected_hover_color=values[j])
elif key == 'dropdown_fg_color':
self.example[i].configure(dropdown_fg_color=values[j])
elif key == 'dropdown_hover_color':
self.example[i].configure(dropdown_hover_color=values[j])
elif key == 'dropdown_text_color':
self.example[i].configure(dropdown_text_color=values[j])
elif key == 'placeholder_text_color':
self.example[i].configure(placeholder_text_color=values[j])
elif key == 'text_color':
try:
self.example[i].configure(text_color=values[j])
except ValueError:
pass
elif key == 'text_color_disabled':
try:
self.example[i].configure(text_color_disabled=values[j])
except ValueError:
pass
elif key == 'text':
self.example[i].configure(text=values[j])
elif key == 'label_text':
self.example[i].configure(label_text=values[j])
elif key == 'placeholder_text':
self.example[i].configure(placeholder_text=values[j])
elif key == 'number_of_steps':
self.example[i].configure(number_of_steps=values[j])
elif key == 'values':
if values[j] != None:
self.example[i].configure(values=values[j])
elif key == 'font':
if values[j] != None:
self.example[i].configure(font=values[j])
elif key == 'dropdown_font':
if values[j] != None:
self.example[i].configure(dropdown_font=values[j])
elif key == 'label_font':
if values[j] != None:
self.example[i].configure(label_font=values[j])
elif key == 'from_':
if values[j] != None:
self.example[i].configure(from_=values[j])
elif key == 'to':
if values[j] != None:
self.example[i].configure(to=values[j])
elif key == 'width':
default_value = [200, 140, 140, 0, 140, 100, 100, 200, 140, 100, 200, 16, 200, 140, 200, 300]
self.example[i].configure(width=default_value[i] if values[j] == None else values[j])
elif key == 'height':
default_value = [200, 28, 28, 28, 28, 24, 22, 8, 28, 24, 16, 200, 200, 28, 200, 250]
self.example[i].configure(height=default_value[i] if values[j] == None else values[j])
elif key == 'corner_radius':
default_value = [6, 6, 6, 0, 6, 6, 1000, 1000, 6, 1000, 1000, 1000, 6, 6, 6, 6]
self.example[i].configure(corner_radius=default_value[i] if values[j] == None else values[j])
elif key == 'wraplength':
self.example[i].configure(wraplength=0 if values[j] == None else values[j])
elif key == 'border_width':
default_value = [0, 0, 2, '', 2, 3, '', 0, '', 3, 6, '', 0, 3, 0, 0]
self.example[i].configure(border_width=default_value[i] if values[j] == None else values[j])
elif key == 'border_spacing':
default_value = ['', 2, '', '', '', '', '', '', '', '', '', 4, '', '', 3, '']
self.example[i].configure(border_spacing=default_value[i] if values[j] == None else values[j])
elif key == 'hover':
self.example[i].configure(hover=True if values[j] == None else values[j])
elif key == 'radiobutton_width':
self.example[i].configure(radiobutton_width=22 if values[j] == None else values[j])
elif key == 'radiobutton_height':
self.example[i].configure(radiobutton_height=22 if values[j] == None else values[j])
elif key == 'checkbox_width':
self.example[i].configure(checkbox_width=24 if values[j] == None else values[j])
elif key == 'checkbox_height':
self.example[i].configure(checkbox_height=24 if values[j] == None else values[j])
elif key == 'switch_width':
self.example[i].configure(switch_width=36 if values[j] == None else values[j])
elif key == 'switch_height':
self.example[i].configure(switch_height=18 if values[j] == None else values[j])
elif key == 'button_length':
self.example[i].configure(button_length=0 if values[j] == None else values[j])
elif key == 'border_width_checked':
self.example[i].configure(border_width_checked=6 if values[j] == None else values[j])
elif key == 'border_width_unchecked':
self.example[i].configure(border_width_unchecked=3 if values[j] == None else values[j])
elif key == 'button_corner_radius':
self.example[i].configure(button_corner_radius=1000 if values[j] == None else values[j])
elif key == 'padx':
self.example[i].configure(padx=0 if values[j] == None else values[j])
elif key == 'pady':
self.example[i].configure(pady=0 if values[j] == None else values[j])
elif key == 'anchor':
if values[j] in ['center','n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']:
self.example[i].configure(anchor=values[j])
elif values[j] == None:
self.example[i].configure(anchor='center')
elif key == 'label_anchor':
if values[j] in ['center','n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']:
self.example[i].configure(label_anchor=values[j])
elif values[j] == None:
self.example[i].configure(label_anchor='center')
elif key == 'justify':
if values[j] in ['center','right', 'left']:
self.example[i].configure(justify=values[j])
elif values[j] == None:
self.example[i].configure(justify='left')
elif key == 'mode':
if values[j] in ['determinate', 'indeterminate']:
self.example[i].configure(mode=values[j])
elif values[j] == None:
self.example[i].configure(mode='determinate')
elif key == 'wrap':
if values[j] in ['char','word', 'none']:
self.example[i].configure(wrap=values[j])
elif values[j] == None:
self.example[i].configure(wrap='char')
elif key == 'orientation':
try:
if values[j] in ['horizontal', 'vertical']:
self.example[i].configure(orientation=values[j])
elif values[j] == None:
self.example[i].configure(orientation='horizontal')
except ValueError:
pass
def main():
app = App()
app.mainloop()
if __name__ == "__main__":
main()
初期値用jsonファイル
{
"CTk": {
"fg_color": ["gray95", "gray10"]
},
"CTkToplevel": {
"fg_color": ["gray95", "gray10"]
},
"CTkFrame": {
"width": null,
"height": null,
"corner_radius": 6,
"border_width": 0,
"fg_color": ["gray90", "gray13"],
"border_color": ["gray65", "gray28"]
},
"CTkButton": {
"text": null,
"font": null,
"width": null,
"height": null,
"anchor": "center",
"corner_radius": 6,
"border_width": 0,
"border_spacing": 2,
"fg_color": ["#3a7ebf", "#1f538d"],
"hover_color": ["#325882", "#14375e"],
"border_color": ["#3E454A", "#949A9F"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkLabel": {
"text": null,
"font": null,
"width": null,
"height": null,
"wraplength": null,
"anchor": "center",
"justify": "center",
"compound": "center",
"padx": 1,
"pady": 1,
"corner_radius": 0,
"fg_color": "transparent",
"text_color": ["gray14", "gray84"]
},
"CTkEntry": {
"font": null,
"width": null,
"height": null,
"corner_radius": 6,
"border_width": 2,
"justify": "left",
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
"text_color": ["gray14", "gray84"],
"placeholder_text_color": ["gray52", "gray62"],
"placeholder_text": null
},
"CTkCheckBox": {
"text": null,
"font": null,
"width": null,
"height": null,
"checkbox_width": null,
"checkbox_height": null,
"corner_radius": 6,
"border_width": 3,
"fg_color": ["#3a7ebf", "#1f538d"],
"border_color": ["#3E454A", "#949A9F"],
"hover_color": ["#325882", "#14375e"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkComboBox": {
"font": null,
"dropdown_font": null,
"width": null,
"height": null,
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
"button_color": ["#979DA2", "#565B5E"],
"button_hover_color": ["#6E7174", "#7A848D"],
"dropdown_fg_color": ["#979DA2", "#565B5E"],
"dropdown_hover_color": ["#979DA2", "#565B5E"],
"dropdown_text_color": ["#6E7174", "#7A848D"],
"text_color": ["gray14", "gray84"],
"text_color_disabled": ["gray50", "gray45"]
},
"CTkRadioButton": {
"text": null,
"font": null,
"width": null,
"height": null,
"radiobutton_width": null,
"radiobutton_height": null,
"corner_radius": 1000,
"border_width_checked": 6,
"border_width_unchecked": 3,
"fg_color": ["#3a7ebf", "#1f538d"],
"border_color": ["#3E454A", "#949A9F"],
"hover_color": ["#325882", "#14375e"],
"text_color": ["gray14", "gray84"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkProgressBar": {
"width": null,
"height": null,
"corner_radius": 1000,
"border_width": 0,
"orientation": "horizontal",
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["#3a7ebf", "#1f538d"],
"border_color": ["gray", "gray"],
"mode": "determinate"
},
"CTkOptionMenu": {
"values": null,
"font": null,
"dropdown_font": null,
"width": null,
"height": null,
"anchor": "w",
"corner_radius": 6,
"fg_color": ["#3a7ebf", "#1f538d"],
"button_color": ["#325882", "#14375e"],
"button_hover_color": ["#234567", "#1e2c40"],
"dropdown_fg_color": ["gray90", "gray20"],
"dropdown_hover_color": ["gray75", "gray28"],
"dropdown_text_color": ["gray10", "gray90"],
"text_color": ["#DCE4EE", "#DCE4EE"]
},
"CTkSwitch": {
"text": null,
"font": null,
"width": null,
"height": null,
"switch_width": null,
"switch_height": null,
"corner_radius": 1000,
"border_width": 3,
"button_length": 0,
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["#3a7ebf", "#1f538d"],
"button_color": ["gray36", "#D5D9DE"],
"button_hover_color": ["gray20", "gray100"],
"text_color": ["gray10", "#DCE4EE"]
},
"CTkSlider": {
"from_": 0,
"to": 1,
"number_of_steps": null,
"width": null,
"height": null,
"border_width": 6,
"orientation": "horizontal",
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["gray40", "#AAB0B5"],
"border_color": "transparent",
"button_color": ["#3a7ebf", "#1f538d"],
"button_hover_color": ["#325882", "#14375e"]
},
"CTkScrollbar": {
"width": null,
"height": null,
"corner_radius": 1000,
"border_spacing": 4,
"orientation": "horizontal",
"fg_color": "transparent",
"button_color": ["gray55", "gray41"],
"button_hover_color": ["gray40", "gray53"]
},
"CTkScrollableFrame": {
"label_text": null,
"label_font": null,
"label_anchor": "center",
"width": null,
"height": null,
"corner_radius": 6,
"border_width": 0,
"orientation": "vertical",
"fg_color": ["gray86", "gray17"],
"border_color": ["gray65", "gray28"],
"scrollbar_fg_color": "transparent",
"scrollbar_button_color": "transparent",
"scrollbar_button_hover_color": "transparent",
"label_fg_color": ["gray78", "gray23"],
"label_text_color": ["gray10", "#DCE4EE"]
},
"CTkSegmentedButton": {
"values": "Button1, Button2, Button3",
"font": null,
"width": null,
"height": null,
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#979DA2", "gray29"],
"selected_color": ["#3a7ebf", "#1f538d"],
"selected_hover_color": ["#325882", "#14375e"],
"unselected_color": ["#979DA2", "gray29"],
"unselected_hover_color": ["gray70", "gray41"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkTextbox": {
"font": null,
"width": null,
"height": null,
"corner_radius": 6,
"border_width": 0,
"border_spacing": 3,
"wrap": "char",
"fg_color": ["gray100", "gray20"],
"border_color": ["#979DA2", "#565B5E"],
"text_color": ["gray14", "gray84"],
"scrollbar_button_color": ["gray55", "gray41"],
"scrollbar_button_hover_color": ["gray40", "gray53"]
},
"CTkTabview": {
"width": null,
"height": null,
"corner_radius": 6,
"border_width": 0,
"fg_color": null,
"border_color": null,
"segmented_button_fg_color": null,
"segmented_button_selected_color": ["#3a7ebf", "#1f538d"],
"segmented_button_selected_hover_color": ["#325882", "#14375e"],
"segmented_button_unselected_color": null,
"segmented_button_unselected_hover_color": null,
"text_color": null,
"text_color_disabled": null
},
"DropdownMenu": {
"fg_color": ["gray90", "gray20"],
"hover_color": ["gray75", "gray28"],
"text_color": ["gray14", "gray84"]
},
"CTkFont": {
"macOS": {
"family": "SF Display", "size": 13, "weight": "normal"
},
"Windows": {
"family": "Roboto", "size": 13, "weight": "normal"
},
"Linux": {
"family": "Roboto", "size": 13, "weight": "normal"
}
}
}
4. さいごに
ただ単にCustomTkinterを使うだけでは調べないようなことまで、今回のウィジェットプレビューツールを作ることで確認しましたし、Qiitaに投稿するために調べるほど、自身のスキルの至らなさに気付く状態で、なかなか投稿に時間がかかりました。
クラスによる構造化やら、テストしやすいかとか、拡張性とか、デザインパターンとか、今後勉強が必要なようです。
まあでも自身の本業からすれば動けばいいという面はあるので、今後も自分にとって使いやすいものができたらいいかなとも思います。