LoginSignup
14

[Python] CustomTkinter ウィジェットプレビューツール

Last updated at Posted at 2023-03-03

1. はじめに

tkinterはPythonでGUI化させる場合にはまず検討されると思いますが、いかんせん見た目がよくありません。GUIライブラリは他にもいろいろあるようですが、tkinterベースで手軽におしゃれGUIが作れる CustomTkinter というライブラリがあります。

簡単におしゃれにできるので素晴らしいのですが、日本語での情報が全然ないこともあり、一度Qiitaにまとめてみようと思っていたら、公式Wikiのチュートリアル+αをわかりやすくした素晴らしい記事を他の方が書かれていました。

そんなわけで二番煎じは意味がないので、
公式Wiki に記載の主に見た目の設定についてまとめつつ、ウィジェットの設定変更を簡単にプレビューできるツールを作ったので、紹介したいと思います。

作ったツールは以下のような動作をするものです。

preview_800_5fps.gif

ツールだけサクッと見たい方は他は飛ばして、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にある以下の図になります。

130222A7-CEAF-46BF-85E7-F7E6A139269C.png

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"]というようにリスト形式で指定されており、上記のオプション引数でもリストを渡しても設定されます。

Lightモード
77769478-9F4C-46D3-ADB8-402136AD4EE6.png

Darkモード
310EE5EF-090C-4333-A54C-F642A4FCBFD3.png

2.3. Themes(テーマ)の設定

CustomTkinterには、特に何もしなくてもおしゃれGUIを実現するために、ウィジェットの色の設定に、bluegreendark-blue の3種類のデフォルトテーマが用意されており、以下の記述でデフォルトテーマを設定できます。

customtkinter.set_default_color_theme("dark-blue")  # Themes: "blue" (standard), "green", "dark-blue"

基本的な外観はDark/Lightモードの設定でベースとなる白背景か黒背景を選んで、ボタンなどのアクセントとなる色をThemeで決めるという流れになります。

blue
7A987E07-919A-4C7D-B71C-3765CE98DE1A.png
dark-blue
459E5CEF-A144-44B9-9F7D-69AEF4A64B38.png
green
98161BF4-B7C2-4073-812A-CFE132D7E70A.png

3. ウィジェットプレビューツール

ここまでで説明してきた流れで、ざっくりとした外観の設定はわかるのですが、いざ各ウィジェットを設定してみようとなると、どのオプション引数がどう見た目に影響するのかわかりにくい状態でした。

そこでせっかくのGUIライブラリなので、CustomTkinterを使って、CustomTkinterの設定をGUI上ですぐに確認できるウィジェットプレビューツールを作ってみることにしました。

3.1. ツールの概要

名前のとおり、各ウィジェットの設定項目を変更して、アプリ上ですぐにプレビューができるツールです。

ツールの画面は公式のimage_example.pyをベースに作成しています。

左側に各ウィジェットを選択するナビゲーションバー、右側に各ウィジェットのオプション引数を並べた設定項目変更画面とプレビュー画面が存在します。

2674780A-DB9A-4F41-B10A-A07C32BF152B.png

オプション引数を変更して、「Apply to settings」ボタンを押すと、右側のウィジェットに対して、設定したオプション引数が反映されます。

preview_800_5fps.gif

3.2. ウィジェットプレビュー機能について

3.2.1. 初期値

  • ウィジェットの設定項目の画面生成と初期値をjsonファイルから読み込んでいるので、初期値用jsonファイルinitial_theme.jsonをpyファイルと同じフォルダに入れて起動する必要があります。
  • initial_theme.json公式のWikiargumentsを参考にデフォルトのテーマである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ファイル

ソースコード
widgets_preview.py
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ファイル
initial_theme.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に投稿するために調べるほど、自身のスキルの至らなさに気付く状態で、なかなか投稿に時間がかかりました。

クラスによる構造化やら、テストしやすいかとか、拡張性とか、デザインパターンとか、今後勉強が必要なようです。

まあでも自身の本業からすれば動けばいいという面はあるので、今後も自分にとって使いやすいものができたらいいかなとも思います。

5. 参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14