CustomTkinterによる画面作成のポイントをサンプル付きで紹介

本ブログで紹介する自作ツールは、Python標準のGUIライブラリ「 CustomTkinter」を使用しています。

CustomTkinterは非常に機能が豊富であるため、本記事の自作ツールのGUI作成で使っているUIパーツ(テキストボックス、ボタン、コンボボックスなど)の使い方について、サンプルプログラムやスクリーンショットを使って解説しています。

また、記事の最後にはファイルやフォルダ選択のダイアログ、ちょっとだけ便利なUIパーツをクラス(ユーザーコントロール)のソースコードも掲載していますので、是非ご活用ください。

あわせて読みたい
【CustomTkinter】テキスト記述でGUIを簡単作成するライブラリを紹介! 本ブログで紹介しているツールも含め、自作ツールはちょっとした作業を自動化することが目的なので、簡易的な画面で事足りることが大半です。 とはいうものの、CustomTk...
目次

はじめに

引用元:CustomTkinter

CustomTkinterとは?

CustomTkinterは、PythonでGUIを作成するためのライブラリTkinterを拡張したものです。

Tkinterは標準ライブラリとして付属していますが、見た目が古く、カスタマイズ性に欠けるという課題がありました。CustomTkinterは、これらの課題を解決するために開発されました。

あわせて読みたい
Documentation Introduction | CustomTkinter This is the official CustomTkinter documentation, where you can find detailed information about the widgets, windows, customization and scaling.

主な特徴

  • モダンなデザイン
    フラットデザインやマテリアルデザインを取り入れたモダンなデザインのウィジェットを提供します。
  • 高いカスタマイズ性
    ウィジェットの色、フォント、サイズなどを細かく設定することができます。
  • 使いやすさ
    Tkinterとほぼ同じAPIで利用することができます。そのため、Tkinterの経験者であれば、すぐにCustomTkinterを使い始めることができます。

主な機能

Tkinerとほぼ同じですが、TreeViewは用意されていません。TreeViewが必要な場合は Tkinerのものを利用する必要があります。詳細はこちらの記事をご確認ください。
また、リストボックスは用意されていますが、別途モジュール(CTkListBox)のインストールが必要です。

ボタンさまざまなスタイルのボタンを提供します。
ラベルテキスト、アイコン、画像を表示することができます。
入力欄テキストや数値を入力することができます。
チェックボックス選択肢を複数選択することができます。
リストボックス選択肢のリストから1つ、または複数選択することができます。
ラジオボタン複数の選択肢から1つを選択することができます。
ドロップダウンリスト選択肢のリストから1つを選択することができます。
プログレスバー処理の進捗状況を表示することができます。
キャンバス図形を描画することができます。

CustomTkinterを使う利点

  • モダンで洗練されたGUIを作成できる
    CustomTkinterを使えば、古臭い見た目のGUIではなく、モダンで洗練されたGUIを作成することができます。
  • アプリのブランディングに合わせたデザインにできる
    CustomTkinterは、ウィジェットの色やフォントなどを細かく設定することができるので、アプリのブランディングに合わせたデザインにすることができます。
  • 開発時間を短縮できる
    CustomTkinterは、使いやすく、豊富な機能を提供しているので、開発時間を短縮することができます。

ボタン等で使えるアイコンについて

CustomTkinterは、Google が公開しているマテリアルデザインのアイコンと相性が良いので、CustomTkinterでUIを作成するときは、下記のページから必要なアイコンを探してください。

https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368

Tkinter → CustomTkinterに置き換える場合の変更箇所

Tkinterで作られた既存のプログラムを CustomTkinter に置き換える場合、以下の3点について変更が必要です。

  • インポート:tkinter → customtkinter
  • ウィンドウの生成: TK() → CTK()
  • ウィジェット:先頭にCTkを付ける(例 Button() → CTkButton())

しかし、tikinterに存在してcustomtkinterに無いウイジェット(TreeViewなど)や、名前や仕様が微妙に変わっているウィジェットも多数あるため、プログラムによっては単純な置き換えで済まないかもしれません。

Tkinter を使ったUI

import tkinter as tk

# メインウィンドウを作成
root = tk.Tk()

# ウィンドウのタイトルを設定
root.title("Tkinter 基本ウィジェット")

# ウィンドウのサイズを設定
root.geometry("300x200")

# エントリーを作成して配置
entry = tk.Entry(root)
entry.pack()

# ボタンを作成して配置
def on_button_click():
    entry.delete(0, tk.END)  # エントリーのテキストを削除
    entry.insert(0, "ボタンがクリックされました")  # 新しいテキストを挿入

button = tk.Button(root, text="クリック", command=on_button_click)
button.pack()

# ウィンドウを表示
root.mainloop()

CustomTkinterを使ったUI

import customtkinter as tk

# メインウィンドウを作成
root = tk.CTk()

# ウィンドウのタイトルを設定
root.title("Tkinter 基本ウィジェット")

# ウィンドウのサイズを設定
root.geometry("300x200")

# エントリーを作成して配置
entry = tk.CTkEntry(root)
entry.pack()

# ボタンを作成して配置
def on_button_click():
    entry.delete(0, tk.End)  # エントリーのテキストを削除
    entry.insert(0, "ボタンがクリックされました")  # 新しいテキストを挿入

button = tk.CTkButton(root, text="クリック", command=on_button_click)
button.pack()

# ウィンドウを表示
root.mainloop()

これから新しくCustomTkinterで UI を作成する場合、インポート時のエイリアス名は ctk が望ましいです。

import customtkinter as ctk

インストール方法

CustomTkinterを使うには、あらかじめ pip でインストールしておく必要があります。

pip install customtkinter

既にインストール済みのCustomTkinterをアップグレードするには、次のコマンドを実行します。

pip install customtkinter --upgrade

CustomTkinterによる画面の作り方

CustomTkinterはTkinterと同様に機能が豊富であるため、本記事では基本的な機能に限定して画面の作り方を解説致します。CustomTkinterに関する詳細については、「CustomTkinter公式サイト」に掲載されているので、こちらを参考にしてください。

一番簡単な画面(Window)の作成

Tkinter と同じく mainloop() メソッドを呼び出すことで画面の制御が CustomTkinter に渡り、以降は画面に関する全ての操作を CustomTkinter が管理することになります。

Tkinter と異なるのはウィンドウの外観の色を指定する「外観モード」と、ウィジェットの枠や背景色を指定する「テーマ」が用意されていることです。

「外観モード」は "system"、"light"、"dark"の3つの中から、「テーマ」は"blue"、 "green"、 "dark-blue" の3つの中から選択できます。

外観モード:system

※OSのテーマを読み取って設定

外観モードlight

※OSのテーマに関係なくlight固定

外観モードdark

※OSのテーマに関係なくdark固定

import customtkinter as ctk

# 外観モード指定
ctk.set_appearance_mode("dark")  # Modes: system (default), light, dark

# ウィジェットのテーマ指定
ctk.set_default_color_theme("blue")  # Themes: "blue" (standard), "green", "dark-blue"

# メインウィンドウを作成
app = ctk.CTk()

# ウィンドウのタイトルを設定
app.title("CTkinter 基本ウィンドウ")

# メインループ
app.mainloop()

ウィジェットを使った画面の作成

次のサンプルはウインドウにボタンとコンボボックスを張り付けたサンプルです。試しにテーマを変えてみたら、次のようにボタンの表面色が変わりました。

テーマ:blue

テーマ:green

テーマ:dark-blue

Tkinterと同様、CustomTkinter もウィンドウをモニタの中央に表示するオプションが無いので、自力で座標を計算する必要があります。

import customtkinter as ctk

class App(ctk.CTk):
    def __init__(self,width =200,height = 200):
        super().__init__()
        
        # ウィンドウをモニタの中心に表示するための座標計算
        screen_width = self.winfo_screenwidth() # モニタ横方向の表示サイズを取得
        screen_height = self.winfo_screenheight() # モニタ縦報告の表示サイズを取得
        x = (screen_width - width) // 2  # ウィンドウを画面中央に配置するためのX座標
        y = (screen_height - height) // 2  # ウィンドウを画面中央に配置するためのY座標
        self.geometry(f"{width}x{height}+{x}+{y}") # ウィンドウを画面中央に配置

        # 外観モード指定
        ctk.set_appearance_mode("light")  # Modes: system (default), light, dark

        # ウィジェットのテーマ指定
        ctk.set_default_color_theme("blue")  # Themes: "blue" (standard), "green", "dark-blue"
        
        # ボタンの追加
        self.button = ctk.CTkButton(self, command=self.button_click)
        self.button.pack(padx=20, pady=10)

        self.combobox = ctk.CTkComboBox(self, values=["aaa","bbb","ccc"])
        self.combobox.pack(padx=20, pady=10)    
    
    def button_click(self):
        print("button click")

# メインウィンドウを作成
app = App()

# ウィンドウのタイトルを設定
app.title("CTkinter 基本ウィンドウ")

# メインループ
app.mainloop()

基本ウィジェットの紹介

ラベル(CTkLabel)

ラベルは、テキストや画像を表示するために用意されたウィジェットです。

import customtkinter as ctk

app = ctk.CTk()
label = ctk.CTkLabel(app , text="こんにちは、Tkinter!")
label.pack()
app .mainloop()

エントリー(CTkEntry)

エントリーは、ユーザーからのテキスト入力を受け取るために用意されたウィジェットです。

import customtkinter as ctk

app = ctk.CTk()
entry = ctk.CTkEntry(app)
entry.pack()
app .mainloop()

テキストボックス(CTkTextbox)

テキストボックスは、複数行のテキストを表示および編集するために用意されたウィジェットです。

import customtkinter as ctk

app = ctk.CTk()
text = ctk.CTkTextbox(app)
text.pack()
app .mainloop()

テキストボックスに入力された値を取得するには、次の通り記述します。

text.get('1.0', 'end-1c')

コンボボックス(CTkComboBox)

コンボボックスは、ユーザーがリストの選択するか、または直接入力することができるウィジェットです。

import customtkinter as ctk

def on_select(event):
    selected_value = combobox.get()
    print(f"選択された値: {selected_value}")

app = ctk.CTk()

# コンボボックスの作成
combobox = ctk.CTkComboBox(app, values=["オプション1", "オプション2", "オプション3"])
combobox.set("選択してください")
combobox.pack()

# イベントバインディング
combobox.bind("<<ComboboxSelected>>", on_select)

app.mainloop()

リストボックス(CTkListbox)

リストボックスは、複数の項目をリストとして表示し、ユーザーが選択できるウィジェットです。これを使う場合は、あらかじめインストールが必要です。

pip install CTkListbox

import customtkinter as ctk
import CTkListbox as lbox

def on_select(event):
    selection = event.widget.curselection()
    if selection:
        index = selection[0]
        selected_value = event.widget.get(index)
        print(f"選択された値: {selected_value}")

app = ctk.CTk()

# リストボックスの作成
listbox = lbox.CTkListbox(app)
items = ["アイテム1", "アイテム2", "アイテム3"]
for item in items:
    listbox.insert(ctk.END, item)
listbox.pack()

# イベントバインディング
listbox.bind("<<ListboxSelect>>", on_select)

app.mainloop()

ボタン(CTkButton)

ボタンは、クリック可能なボタンを作成するために用意されたウィジェットです。

import customtkinter as ctk

def on_button_click():
    print("ボタンがクリックされました")

app = ctk.CTk()

button = ctk.CTkButton(app, text="クリック", command=on_button_click)
button.pack()
app.mainloop()

チェックボックス(CTkCheckBox)

チェックボックスは、ユーザーがオンまたはオフの状態を選択できるウィジェットです。
この例では、2つのチェックボックスを用意し、一番下のボタンをクリックすると選択状態が表示されるようになっています。
尚、チェックボックスにもcommand が用意されているので、オン/オフのタイミングでイベント処理を行うこともできます。

import customtkinter as ctk

def show_selections():
    print(f"オプション1: {'選択' if var1.get() else '未選択'}")
    print(f"オプション2: {'選択' if var2.get() else '未選択'}")

app = ctk.CTk()

# チェックボックスの状態を保持するための変数
var1 = ctk.IntVar()
var2 = ctk.IntVar()

# チェックボックスの作成
checkbox1 = ctk.CTkCheckBox(app, text="オプション1", variable=var1)
checkbox1.pack()

checkbox2 = ctk.CTkCheckBox(app, text="オプション2", variable=var2)
checkbox2.pack()

# ボタンの作成と配置
button = ctk.CTkButton(app, text="選択を表示", command=show_selections)
button.pack()

app.mainloop()

スイッチ(CTkSwitch)

スイッチは、チェックボックスと同じくユーザーがオンまたはオフの状態を選択できるウィジェットです。チェックボックスとスイッチのどちらを使っても同じことができるので、デザインの好みで使い分けてください。

import customtkinter as ctk

def switch_event():
    print("switch toggled, current value:", switch_var.get())

# メインウィンドウを作成
app = ctk.CTk()

switch_var = ctk.StringVar(value="on")
switch = ctk.CTkSwitch(app, text="CTkSwitch", command=switch_event,
                                 variable=switch_var, onvalue="on", offvalue="off")
switch.pack()

# ウィンドウのタイトルを設定
app.title("CTkinter 基本ウィンドウ")

# メインループ
app.mainloop()

ラジオボタン(CTkRadioButton)

ラジオボタンは、複数のオプションの中から一つを選択できるウィジェットです。ラジオボタンはグループ化されており、一度に一つのみ選択できます。
この例では、3つのラジオボタンが作成され、ボタンクリックで選択されたオプションが表示されるようになっています。

import customtkinter as ctk

def show_selection():
    print(f"選択されたオプション: {var.get()}")

app = ctk.CTk()

# ラジオボタンの状態を保持するための変数
var = ctk.StringVar()
var.set("オプション1")  # 初期選択

# ラジオボタンの作成
radiobutton1 = ctk.CTkRadioButton(app, text="オプション1", variable=var, value="オプション1")
radiobutton1.pack()

radiobutton2 = ctk.CTkRadioButton(app, text="オプション2", variable=var, value="オプション2")
radiobutton2.pack()

radiobutton3 = ctk.CTkRadioButton(app, text="オプション3", variable=var, value="オプション3")
radiobutton3.pack()

# ボタンの作成と配置
button = ctk.CTkButton(app, text="選択を表示", command=show_selection)
button.pack()

app.mainloop()

スライダー(CTkSlider)

スライダーは、一定範囲内の値を選択できるウィジェットです。水平および垂直方向に配置することが可能です。
この例では、水平および垂直のスライダーが作成され、スライダーの値が変更されると、その値が表示されるようになっています。

import customtkinter as ctk

def show_value(value):
    print(f"選択された値: {value}")

app = ctk.CTk()

# 水平スライダーの作成
horizontal_slider = ctk.CTkSlider(app, from_=0, to=100, orientation=ctk.HORIZONTAL, command=show_value)
horizontal_slider.pack()

# 垂直スライダーの作成
vertical_slider = ctk.CTkSlider(app, from_=0, to=100, orientation=ctk.VERTICAL, command=show_value)
vertical_slider.pack()

app.mainloop()

フレーム(CTkFrame)

フレームは、他のウィジェットをグループ化するために用意されたコンテナウィジェットです。

import customtkinter as ctk

app = ctk.CTk()

frame = ctk.CTkFrame(app, width=200,height=200)
frame.pack(padx=10, pady=10)
label = ctk.CTkLabel(frame, text="これはフレーム内のラベルです")
label.pack()
app.mainloop()

DataFrame


CustomTkinterでpandasのDataFrameが簡単に表示できるウイジェットはありません。Tkinterでは Treeviewウィジェットが使えましたが、それもサポートされていません。
その代わりに、2次元配列のデータをテーブル形式で表示できる CTkTable ウィジェットが公開されています。
スクロールバーには対応していないため、画面サイズより大きなデータは表示できませんが、ちょっとした情報であれば事足りると思います。

pip install CTkTable

import customtkinter
from CTkTable import *

root = customtkinter.CTk()

value = [[1,2,3,4,5],
         [1,2,3,4,5],
         [1,2,3,4,5],
         [1,2,3,4,5],
         [1,2,3,4,5]]

table = CTkTable(master=root, row=5, column=5, values=value)
table.pack(expand=True, fill="both", padx=20, pady=20)

root.mainloop()

メニューバー(CTkOptionMenu)

コンボボックスとよく似たメニューバーです。メニューを選択するとイベントが発生し、何が選択されたかが取得できます。

import customtkinter as ctk

app = ctk.CTk()
app.geometry("200x200")

def optionmenu_callback(choice):
    print("optionmenu dropdown clicked:", choice)

optionmenu = ctk.CTkOptionMenu(app, values=["option 1", "option 2","option 3"],
                                         command=optionmenu_callback)
optionmenu.set("option 2")
optionmenu.pack()

app.mainloop()

ファイルダイアログ

ファイルダイアログを使用すると、ユーザーがファイルを選択または保存できるようになります。以下は、filedialogモジュールを使用してファイルを開くおよび保存するダイアログを表示する例です。

import customtkinter as ctk
from customtkinter import filedialog

# ファイルを開くための関数
def open_file():
    file_path = filedialog.askopenfilename()
    if file_path:
        print(f"開いたファイル: {file_path}")

# ファイルを保存するための関数
def save_file():
    file_path = filedialog.asksaveasfilename()
    if file_path:
        print(f"保存したファイル: {file_path}")

# Tkinterのルートウィンドウの作成
app = ctk.CTk()
app.withdraw()  # ルートウィンドウを非表示にする

# ファイルを開く関数の呼び出し
open_file()

# ファイルを保存する関数の呼び出し
save_file()

# Tkinterのイベントループの開始
app.mainloop()

メッセージボックス

メッセージボックスは、ユーザーに情報を表示したり、確認を求めるために使用します。メッセージボックスを使用するには、CTkMessageboxのインストールが必要です。

pip install CTkMessagebox

from CTkMessagebox import CTkMessagebox
import customtkinter as ctk

def show_info():
    CTkMessagebox(title="情報", message="これは情報メッセージです。")

def show_checkmark():
    CTkMessagebox(title="成功", message="これは成功メッセージです。",icon="check", option_1="Thanks")

def show_warning():
    CTkMessagebox(title="警告", message="これは警告メッセージです。",icon="warning", option_1="Cancel", option_2="Retry")

def show_error():
    CTkMessagebox(title="エラー", message="これはエラーメッセージです。",icon="cancel")

def ask_question():
    msg = CTkMessagebox(title="質問", message="続行しますか?",icon="question", option_1="Cancel", option_2="No", option_3="Yes")
    response = msg.get()
    print(f"ユーザーの回答: {response}")

app = ctk.CTk()

button1 = ctk.CTkButton(app,text="情報メッセージ",command=show_info)
button1.pack(padx=5,pady=5)
button2 = ctk.CTkButton(app,text="成功メッセージ",command=show_checkmark)
button2.pack(padx=5,pady=5)
button3 = ctk.CTkButton(app,text="警告メッセージ",command=show_warning)
button3.pack(padx=5,pady=5)
button4 = ctk.CTkButton(app,text="エラーメッセージ",command=show_error)
button4.pack(padx=5,pady=5)
button5 = ctk.CTkButton(app,text="質問ッセージ",command=ask_question)
button5.pack(padx=5,pady=5)

app.mainloop()

イベント処理

Tkinterでは、特定のイベントが発生したときに関数を呼び出すことができます。イベントと関数を関連づけることを「イベントバインディング」と呼びます。

ボタンのクリックイベント

ボタンがクリックされた時に発生するイベントで、ボタンウィジェット専用です。

import customtkinter as ctk

def on_button_click(event):
    print("ボタンがクリックされました")

app = ctk.CTk()
button = ctk.CTkButton(app, text="クリック")
button.bind("<Button-1>", on_button_click)
button.pack(padx=10,pady=10)
app.mainloop()

キーボードイベント

キーボード入力が行われた時に発生するイベントです。

import customtkinter as ctk

def on_key_press(event):
    print(f"キーが押されました: {event.keysym}")

app = ctk.CTk()
app.bind("<KeyPress>", on_key_press)
app.mainloop()

マウスイベント

マウスがクリックされた時のイベントです。ボタンウィジェット以外のウィジェットでマウス操作された時の処理を書きたい時に使います。

import customtkinter as ctk

def on_mouse_click(event):
    print(f"マウスがクリックされました: ({event.x}, {event.y})")

app = ctk.CTk()
app.bind("<Button-1>", on_mouse_click)
app.mainloop()

レイアウト管理

ウィジェットを画面に表示する場所を pack()、grid()、place() で指定します。ウィジェット1つ1つに対して場所を指定する必要があります。使い方はTkinterと全く同じです。

パック(pack)マネージャー

pack()は、ウィジェットを順番に配置します。

import customtkinter as ctk

app = ctk.CTk()
ctk.CTkLabel(app, text="上").pack(side=ctk.TOP)
ctk.CTkLabel(app, text="下").pack(side=ctk.BOTTOM)
ctk.CTkLabel(app, text="左").pack(side=ctk.LEFT)
ctk.CTkLabel(app, text="右").pack(side=ctk.RIGHT)
app.mainloop()

グリッド(grid)マネージャー

grid()は、ウィジェットをグリッド状に配置します。グリッドの縦と横は事前に決めておく必要はなく、rowと columnで指定した値によって随時画面が分割されていくイメージです。

import customtkinter as ctk

app = ctk.CTk()
ctk.CTkLabel(app, text="行 0, 列 0").grid(row=0, column=0)
ctk.CTkLabel(app, text="行 0, 列 1").grid(row=0, column=1)
ctk.CTkLabel(app, text="行 1, 列 0").grid(row=1, column=0)
ctk.CTkLabel(app, text="行 1, 列 1").grid(row=1, column=1)
app.mainloop()

プレース(place)マネージャー

place()は、ウィジェットを絶対座標で配置します。

import customtkinter as ctk

app = ctk.CTk()
ctk.CTkLabel(app, text="絶対位置").place(x=50, y=50)
app.mainloop()

レイアウトのコツと注意点

  • ウィジェットの配置順序に注意しましょう
  • グリッドを使うときは、各ウィジェットの位置(行と列)を明確に指定しましょう
  • プレースは細かい位置の調整には有効ですが、動的なレイアウト変更には不向きです

Tkinterでのユーザーコントロールの作り方

ユーザーコントロール(カスタムウィジェット)は、複数のウィジェットを組み合わせて、新しいウィジェットを作成する方法です。
書き方は決まっていて、以下のテンプレート(青文字はそのまま)でユーザーコントロールを定義します。

class クラス名(ctk.CTkFrame):

def __init__(self, parent, label_text, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
ここに必要なウィジェットを配置する
ウィジェットごとに配置メソッド(pack、gridなど)を記述する


def メソッド名(self,引数1,・・・):
~ここに処理を記述する~

この例では、InputPanelというクラス名でカスタムウィジェットを作成しています。このウィジェットは、ラベルとエントリーフィールドを含んでいます。メインウィンドウには 2つのInputPanelウィジェットを配置し、それぞれ「名前」と「年齢」の入力フィールドとして使用しています。

import customtkinter as ctk

class InputPanel(ctk.CTkFrame):
    """
    InputPanelは、ラベルとエントリー(入力フィールド)を含むカスタムウィジェットです。
    """
    def __init__(self, parent, label_text, *args, **kwargs):
        """
        初期化メソッド。InputPanelウィジェットを初期化します。

        Args:
            parent: 親ウィジェット。
            label_text: ラベルに表示するテキスト。
            *args: 可変長引数。
            **kwargs: キーワード引数。
        """
        super().__init__(parent, *args, **kwargs)
        
        # ラベルを作成して配置
        self.label = ctk.CTkLabel(self, text=label_text)
        self.label.pack(side=ctk.LEFT, padx=5, pady=5)
        
        # エントリーフィールドを作成して配置
        self.entry = ctk.CTkEntry(self)
        self.entry.pack(side=ctk.LEFT, padx=5, pady=5)
    
    def get_value(self):
        """
        エントリーフィールドの現在の値を取得します。

        Returns:
            str: エントリーフィールドのテキスト。
        """
        return self.entry.get()

    def set_value(self, value):
        """
        エントリーフィールドに値を設定します。

        Args:
            value (str): エントリーフィールドに設定するテキスト。
        """
        self.entry.delete(0, ctk.END)
        self.entry.insert(0, value)

# メインウィンドウの作成
app = ctk.CTk()
app.title("カスタムウィジェットの例")

# カスタム入力パネルの作成と配置
input_panel1 = InputPanel(app, "名前:")
input_panel1.pack(padx=10, pady=10)

input_panel2 = InputPanel(app, "年齢:")
input_panel2.pack(padx=10, pady=10)

def show_values():
    """
    入力パネルの値を取得してコンソールに表示します。
    """
    print(f"名前: {input_panel1.get_value()}")
    print(f"年齢: {input_panel2.get_value()}")

# ボタンの作成と配置
button = ctk.CTkButton(app, text="表示", command=show_values)
button.pack(padx=10, pady=10)

# メインループの開始
app.mainloop()

本ブログのオリジナルユーザーコントロール

本ブログで紹介する自作ツールでは、次のユーザーコントロールを使っています。Tiknter で作成したものをCustumTkinteに置き換えたものです。TreeView はサポートされていないため、DataFrame表示用のコントロールは省きました。

リファレンス

コントロール名メソッド機能
TextBoxControlinit(master, kwargs)コンストラクタ
get()テキストボックスの値を取得する
set(text)テキストボックスに値を設定する
ComboBoxControlinit(master, kwargs)コンストラクタ
set(text)コンボボックスのテキスト部に値を設定する
set_items(items, text=None)コンボボックスにアイテムを設定する
get()コンボボックスの選択された値を取得する
ListBoxControlinit(master, kwargs)コンストラクタ
set_items(items)リストボックスにアイテムを設定する
get()リストボックスの選択された値を取得する
FileSelectionControlinit(master, kwargs)コンストラクタ
open_file_dialog()ファイル名選択ダイアログを開く
get()テキストボックスの値を取得する
set(text)テキストボックスに値を設定する
FolderSelectionControlinit(master, kwargs)コンストラクタ
open_folder_dialog()フォルダ名選択ダイアログを開く
get()テキストボックスの値を取得する
set(text)テキストボックスに値を設定する
SaveFileDialogControlinit(master, kwargs)コンストラクタ
save_file_dialog()ファイル保存ダイアログを開く
get()テキストボックスの値を取得する
set(text)テキストボックスに値を設定する

ソースコード

import pandas as pd
import customtkinter as ctk
import CTkListbox as ctklbox
from customtkinter import filedialog
from tkinterdnd2 import TkinterDnD, DND_FILES,DND_ALL
from PIL import Image
import os


class App(ctk.CTk, TkinterDnD.DnDWrapper):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.TkdndVersion = TkinterDnD._require(self)
        ctk.set_appearance_mode("light")  # Modes: system (default), light, dark
        ctk.set_default_color_theme("blue")  # Themes: blue (default), dark-blue, green
        

class CenterWindow:
    def __init__(self,root,width=None,height=None):

        # 画面サイズの下限を設定
        root.minsize(width=width, height=height)

        # 画面サイズを取得
        screen_width = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()

        # ウィンドウのサイズと位置を計算
        x = (screen_width - width) // 2  # ウィンドウを画面中央に配置するためのX座標
        y = (screen_height - height) // 2  # ウィンドウを画面中央に配置するためのY座標

        # ウィンドウのサイズと位置を設定
        root.geometry(f"{width}x{height}+{x}+{y}")


class TextBoxControl(ctk.CTkFrame):
    """
    テキストボックスコントロール
    """
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(master, *args, **kwargs)

        self.textbox = ctk.CTkTextbox(self)
        self.textbox.pack(fill="both", expand=True)

        # ドロップを許可するフォーマットを設定
        self.textbox.drop_target_register(DND_FILES)
        self.textbox.dnd_bind('<<Drop>>', self.on_drop)
        
    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.textbox.get('1.0', 'end-1c')

    def set(self,text):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        self.textbox.delete("1.0", ctk.END)
        self.textbox.insert(ctk.END, text)
    

    def on_drop(self, event):
        """
        ドロップされたファイルの中身をテキストボックスに表示するメソッド。

        Args:
            event: ドロップイベント

        Returns:
            None
        """
        # ドロップされたファイルのパスを取得
        file_path = event.data
        if file_path[0] == "{" and file_path[-1] == "}":
            file_path = file_path[1:-1]

        if os.path.isfile(file_path):
            # ファイルの中身を読み取ってテキストボックスに表示
            with open(file_path, "r", encoding="utf-8") as file:
                content = file.read()
                self.set(content)

class ComboBoxControl(ctk.CTkFrame):
    """
    コンボボックスコントロール
    """
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(master, *args, **kwargs)

        self.combobox = ctk.CTkComboBox(self, state="readonly")  # テキストボックス部分を読み取り専用に設定
        self.combobox.pack()
        
    def get(self):
        """
        コンボボックスの選択された値を取得するメソッド。

        Returns:
            str: 選択された値
        """
        return self.combobox.get()

    def set(self,text):
        """
        テキスト部に文字列を設定するメソッド。

        Args:
            text: テキスト部にセットする文字列

        Returns:
            None
        """
        self.combobox.set(text)

    def set_items(self, items,text = None):
        """
        リストボックスにアイテムを設定するメソッド。

        Args:
            items: アイテムのリスト
            text: テキスト部にセットする文字列(省略可能)
        Returns:
            None
        """
        if text is not None:
            self.combobox.set(text)
        self.combobox.configure(values=items)
         
class ListBoxControl(ctk.CTkFrame):
    """
    リストボックスコントロール
    """
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(master, *args, **kwargs)

        self.listbox = ctklbox.CTkListbox(self)  # コンストラクタ内でlistbox属性を初期化
        self.listbox.pack()
        
    def get(self):
        """
        リストボックスの選択された値を取得するメソッド。

        Returns:
            str: 選択された値
        """
        selected_index = self.listbox.curselection()
        if selected_index:
            return self.listbox.get()
        else:
            return None
        
    def set_items(self, items):
        """
        リストボックスにアイテムを設定するメソッド。

        Args:
            items: アイテムのリスト


        Returns:
            None
        """
        self.listbox.delete(0, ctk.END)  # 既存のアイテムを削除
        for item in items:
            self.listbox.insert(ctk.END, item)  # 新しいアイテムを追加


class FileSelectionControl(ctk.CTkFrame):
    """
    ファイル名選択ダイアログ
    """
    def __init__(self, master=None, *args, **kwargs):
        """
        FileSelectionControlクラスのコンストラクタ。

        Args:
            master: 親ウィジェット
            *args: 位置引数
            **kwargs: キーワード引数
        """
        super().__init__(master, *args, **kwargs)
        
        # 先頭のテキスト文字
        self.text = "フォルダ名"
        
        # ファイル拡張子フィルター
        self.filetypes  = [("All FIles", "*.*")]
        
        # ラベルを生成
        self.label = ctk.CTkLabel(self, text=self.text)
        self.label.grid(row=0, column=0, padx=10, pady=10)

        # テキストボックスを生成
        self.entry = ctk.CTkEntry(self)
        # sticky="ew" でテキストボックスが左右に広がるようにする
        self.entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")

        # アイコンのイメージを取得
        #icon_image = Image.open("./images/folder_open_24dp_FILL0_wght400_GRAD0_opsz24.png")
        #icon = ctk.CTkImage(icon_image,size=(24,24))

        # ファイル名選択ボタンを生成
        self.button = ctk.CTkButton(self, text="ファイルを選択", image=None, compound="left", command=self.open_file_dialog)
        self.button.grid(row=0, column=2, padx=10, pady=10)

        # ドロップを許可するフォーマットを設定
        self.entry.drop_target_register(DND_FILES)
        self.entry.dnd_bind('<<Drop>>', self.on_drop)

        # グリッド列の重みを設定してテキストボックスがウィンドウのサイズ変更に追従するようにする
        self.grid_columnconfigure(1, weight=1)

    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.entry.get()
    
    def set(self,text):
        """
        テキストボックスの値を設定するメソッド。

        Args:
            text: 設定したい文字列
        """
        if text is not None:
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, text)     

    def open_file_dialog(self):
        """
        ファイル選択ダイアログを開くメソッド。
        テキストボックスに入力されたパスを取得し、選択されたファイルパスを表示する。

        Returns:
            None
        """
        # テキストボックスに入力されたパスを取得
        path = self.entry.get()
        file_path = None
        if path is None or path == '':
            # パスが未入力の場合、ファイル選択ダイアログを開く
            file_path = filedialog.askopenfilename(filetypes=self.filetypes)
        elif os.path.isfile(path):
            # テキストボックスのパスが有効なファイルの場合、そのディレクトリを初期ディレクトリとしてファイル選択ダイアログを開く
            file_path = filedialog.askopenfilename(initialdir=os.path.dirname(path),filetypes=self.filetypes)

        if file_path:
            # 選択されたファイルパスをテキストボックスに表示
            self.set(file_path)

    def on_drop(self, event):
        """
        ドロップされたファイルのパスを取得し、テキストボックスに表示するメソッド。

        Args:
            event: ドロップイベント

        Returns:
            None
        """
        # ドロップされたファイルのパスを取得
        file_path = event.data
        if file_path[0] == "{" and file_path[-1] == "}" :
            file_path = file_path[1:-1]

        if os.path.isfile(file_path) :
            # テキストボックスにドロップされたファイルパスを表示
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, file_path)
 

class FolderSelectionControl(ctk.CTkFrame):
    """
    フォルダ名選択ダイアログ
    """
    def __init__(self, master=None, *args, **kwargs):
        """
        FolderSelectionControl

        Args:
            master: 親ウィジェット
            *args: 位置引数
            **kwargs: キーワード引数
        """
        super().__init__(master, *args, **kwargs)
        
        # 先頭のテキスト文字
        self.text = "フォルダ名"
        
        # ラベルを生成
        self.label = ctk.CTkLabel(self, text=self.text)
        self.label.grid(row=0, column=0, padx=10, pady=10)

        # テキストボックスを生成
        self.entry = ctk.CTkEntry(self)
        # sticky="ew" でテキストボックスが左右に広がるようにする
        self.entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")

        # アイコンのイメージを取得
        #icon_image = Image.open("./images/folder_24dp_FILL0_wght400_GRAD0_opsz24.png")
        #icon = ctk.CTkImage(icon_image,size=(24,24))

        # フォルダ名選択ボタンを生成
        self.button = ctk.CTkButton(self, text="フォルダを選択", image=None, compound="left", command=self.open_folder_dialog)
        self.button.grid(row=0, column=2, padx=10, pady=10)

        # ドロップを許可するフォーマットを設定
        self.entry.drop_target_register(DND_ALL)
        self.entry.dnd_bind('<<Drop>>', self.on_drop)

        # グリッド列の重みを設定してテキストボックスがウィンドウのサイズ変更に追従するようにする
        self.grid_columnconfigure(1, weight=1)

    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.entry.get()

    def set(self,text):
        """
        テキストボックスの値を設定するメソッド。

        Args:
            text: 設定したい文字列
        """
        if text is not None:
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, text)

    def open_folder_dialog(self):
        """
        フォルダ選択ダイアログを開くメソッド。
        テキストボックスに入力されたパスを取得し、選択されたファイルパスを表示する。

        Returns:
            None
        """
        # テキストボックスに入力されたパスを取得
        path = self.entry.get()
        folder_path = None
        if path is None or path == '':
            # パスが未入力の場合、ファイル選択ダイアログを開く
            folder_path = filedialog.askdirectory()
        elif os.path.isdir(path):
            # テキストボックスのパスが有効なファイルの場合、そのディレクトリを初期ディレクトリとしてファイル選択ダイアログを開く
            folder_path = filedialog.askdirectory(initialdir=os.path.dirname(path))

        if folder_path:
            # 選択されたファイルパスをテキストボックスに表示
            self.set(folder_path)

    def on_drop(self, event):
        """
        ドロップされたファイルのパスを取得し、テキストボックスに表示するメソッド。

        Args:
            event: ドロップイベント

        Returns:
            None
        """
        # ドロップされたファイルのパスを取得
        # ドロップされたファイルのパスを取得
        folder_path = event.data
        if folder_path[0] == "{" and folder_path[-1] == "}" :
            folder_path = folder_path[1:-1]

        # ファイルがドロップされたらフォルダ名を取得
        if os.path.isfile(folder_path):
            folder_path = os.path.dirname(folder_path)
        # folder_pathがフォルダ名か否かをチェック   
        if os.path.isdir(folder_path) :
            # テキストボックスにドロップされたファイルパスを表示
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, folder_path)


class SaveFileDialogControl(ctk.CTkFrame):
    """
    ファイル保存用のファイル名選択ダイアログ
    """
    def __init__(self, master=None, *args, **kwargs):
        """
        SaveFileDialogControlクラスのコンストラクタ。

        Args:
            master: 親ウィジェット
            *args: 位置引数
            **kwargs: キーワード引数
        """
        super().__init__(master, *args, **kwargs)
        
        # 先頭のテキスト文字
        self.text = "ファイル名"
        
        # ラベルを生成
        self.label = ctk.CTkLabel(self, text=self.text)
        self.label.grid(row=0, column=0, padx=10, pady=10)

        # テキストボックスを生成
        self.entry = ctk.CTkEntry(self)
        # sticky="ew" でテキストボックスが左右に広がるようにする
        self.entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")

        # アイコンのイメージを取得
        #icon_image = Image.open("./images/save_as_24dp_FILL0_wght400_GRAD0_opsz24.png")
        #icon = ctk.CTkImage(icon_image,size=(24,24))

        # ファイル保存ボタンを生成
        self.button = ctk.CTkButton(self, text="ファイルを保存", image=None, compound="left", command=self.save_file_dialog)
        self.button.grid(row=0, column=2, padx=10, pady=10)

        # グリッド列の重みを設定してテキストボックスがウィンドウのサイズ変更に追従するようにする
        self.grid_columnconfigure(1, weight=1)

    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.entry.get()

    def set(self,text):
        """
        テキストボックスの値を設定するメソッド。

        Args:
            text: 設定したい文字列
        """
        if text is not None:
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, text)   

    def save_file_dialog(self):
        """
        ファイル保存ダイアログを開くメソッド。
        テキストボックスに入力されたファイル名を取得し、保存先ファイルパスを表示する。

        Returns:
            None
        """
        # テキストボックスに入力されたファイル名を取得
        file_name = self.entry.get()
            # ファイル保存ダイアログを開く
        file_path = filedialog.asksaveasfilename(defaultextension=".txt", initialfile=file_name)
        if file_path:
            # 選択された保存先ファイルパスをテキストボックスに表示
            self.set(file_path)

下記はオリジナルユーザーコントロールを張り付けたデモ画面です。

画面の初期状態

ウィンドウサイズに合わせてユーザーコントロールの幅が追従

使い方のサンプルソース

import pandas as pd
import customtkinter as ctk
import CTkListbox as ctklbox
from customtkinter import filedialog
from tkinterdnd2 import TkinterDnD, DND_FILES,DND_ALL
from PIL import Image, ImageTk
import os
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

class App(ctk.CTk, TkinterDnD.DnDWrapper):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.TkdndVersion = TkinterDnD._require(self)
        ctk.set_appearance_mode("light")  # Modes: system (default), light, dark
        ctk.set_default_color_theme("blue")  # Themes: blue (default), dark-blue, green
        

class CenterWindow:
    def __init__(self,root,width=None,height=None):

        # 画面サイズの下限を設定
        root.minsize(width=width, height=height)

        # 画面サイズを取得
        screen_width = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()

        # ウィンドウのサイズと位置を計算
        x = (screen_width - width) // 2  # ウィンドウを画面中央に配置するためのX座標
        y = (screen_height - height) // 2  # ウィンドウを画面中央に配置するためのY座標

        # ウィンドウのサイズと位置を設定
        root.geometry(f"{width}x{height}+{x}+{y}")

class EntryControl(ctk.CTkFrame):
    """
    テキストボックスコントロール
    """
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(master, *args, **kwargs)

        self.entry = ctk.CTkEntry(self)
        self.entry.pack(fill="both", expand=True)

       
    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.entry.get()

    def set(self,text):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        self.entry.delete(0, ctk.END)
        self.entry.insert(ctk.END, text)

class TextBoxControl(ctk.CTkFrame):
    """
    テキストボックスコントロール
    """
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(master, *args, **kwargs)
        
        self.textbox = ctk.CTkTextbox(self)
        self.textbox.pack(fill="both", expand=True)

        # ドロップを許可するフォーマットを設定
        self.textbox.drop_target_register(DND_FILES)
        self.textbox.dnd_bind('<<Drop>>', self.on_drop)
        
    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.textbox.get('1.0', 'end-1c')

    def set(self,text):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        self.textbox.delete("1.0", ctk.END)
        self.textbox.insert(ctk.END, text)
    

    def on_drop(self, event):
        """
        ドロップされたファイルの中身をテキストボックスに表示するメソッド。

        Args:
            event: ドロップイベント

        Returns:
            None
        """
        # ドロップされたファイルのパスを取得
        file_path = event.data
        if file_path[0] == "{" and file_path[-1] == "}":
            file_path = file_path[1:-1]

        if os.path.isfile(file_path):
            # ファイルの中身を読み取ってテキストボックスに表示
            with open(file_path, "r", encoding="utf-8") as file:
                content = file.read()
                self.set(content)

class ComboBoxControl(ctk.CTkFrame):
    """
    コンボボックスコントロール
    """
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(master, *args, **kwargs)

        self.combobox = ctk.CTkComboBox(self, state="readonly")  # テキストボックス部分を読み取り専用に設定
        self.combobox.pack()
        
    def get(self):
        """
        コンボボックスの選択された値を取得するメソッド。

        Returns:
            str: 選択された値
        """
        return self.combobox.get()

    def set(self,text):
        """
        テキスト部に文字列を設定するメソッド。

        Args:
            text: テキスト部にセットする文字列

        Returns:
            None
        """
        self.combobox.set(text)

    def set_items(self, items,text = None):
        """
        リストボックスにアイテムを設定するメソッド。

        Args:
            items: アイテムのリスト
            text: テキスト部にセットする文字列(省略可能)
        Returns:
            None
        """
        if text is not None:
            self.combobox.set(text)
        self.combobox.configure(values=items)
         
class ListBoxControl(ctk.CTkFrame):
    """
    リストボックスコントロール
    """
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(master, *args, **kwargs)

        self.listbox = ctklbox.CTkListbox(self)  # コンストラクタ内でlistbox属性を初期化
        self.listbox.pack()
        
    def get(self):
        """
        リストボックスの選択された値を取得するメソッド。

        Returns:
            str: 選択された値
        """
        selected_index = self.listbox.curselection()
        if selected_index:
            return self.listbox.get()
        else:
            return None
        
    def set_items(self, items):
        """
        リストボックスにアイテムを設定するメソッド。

        Args:
            items: アイテムのリスト


        Returns:
            None
        """
        self.listbox.delete(0, ctk.END)  # 既存のアイテムを削除
        for item in items:
            self.listbox.insert(ctk.END, item)  # 新しいアイテムを追加


class FileSelectionControl(ctk.CTkFrame):
    """
    ファイル名選択ダイアログ
    """
    def __init__(self, master=None,*args, **kwargs):
        """
        FileSelectionControlクラスのコンストラクタ。

        Args:
            master: 親ウィジェット
            *args: 位置引数
            **kwargs: キーワード引数
        """
        # 引数から command を取得する
        self.command = kwargs.pop('command', None)
 
        # 親のコンストラクタを呼び出す
        super().__init__(master, *args, **kwargs)
              
        # 先頭のテキスト文字
        self.text = "フォルダ名"
        
        # ファイル拡張子フィルター
        self.filetypes  = [("All FIles", "*.*")]
        
        # ラベルを生成
        self.label = ctk.CTkLabel(self, text=self.text)
        self.label.grid(row=0, column=0, padx=10, pady=10)

        # テキストボックスを生成
        self.inpstr = ctk.StringVar()
        self.inpstr.trace('w',self.entry_changed)
        self.entry = ctk.CTkEntry(self,textvariable= self.inpstr)
        
        # sticky="ew" でテキストボックスが左右に広がるようにする
        self.entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")

        # アイコンのイメージを取得
        #icon_image = Image.open("./images/folder_open_24dp_FILL0_wght400_GRAD0_opsz24.png")
        #icon = ctk.CTkImage(icon_image,size=(24,24))

        # ファイル名選択ボタンを生成
        self.button = ctk.CTkButton(self, text="ファイルを選択", image=None, compound="left", command=self.open_file_dialog)
        self.button.grid(row=0, column=2, padx=10, pady=10)

        # ドロップを許可するフォーマットを設定
        self.entry.drop_target_register(DND_FILES)
        self.entry.dnd_bind('<<Drop>>', self.on_drop)

        # グリッド列の重みを設定してテキストボックスがウィンドウのサイズ変更に追従するようにする
        self.grid_columnconfigure(1, weight=1)

    def entry_changed(self, *args):
        """
        テキストボックスが変更されたらコマンドを呼び出す。

        Returns:
            None
        """
        if args[0] == self.inpstr._name and self.command is not None:
            self.command()

    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.entry.get()
    
    def set(self,text):
        """
        テキストボックスの値を設定するメソッド。

        Args:
            text: 設定したい文字列
        """
        if text is not None:
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, text)     

    def open_file_dialog(self):
        """
        ファイル選択ダイアログを開くメソッド。
        テキストボックスに入力されたパスを取得し、選択されたファイルパスを表示する。

        Returns:
            None
        """
        # テキストボックスに入力されたパスを取得
        path = self.entry.get()
        file_path = None
        if path is None or path == '':
            # パスが未入力の場合、ファイル選択ダイアログを開く
            file_path = filedialog.askopenfilename(filetypes=self.filetypes)
        elif os.path.isfile(path):
            # テキストボックスのパスが有効なファイルの場合、そのディレクトリを初期ディレクトリとしてファイル選択ダイアログを開く
            file_path = filedialog.askopenfilename(initialdir=os.path.dirname(path),filetypes=self.filetypes)

        if file_path:
            # 選択されたファイルパスをテキストボックスに表示
            self.set(file_path)

    def on_drop(self, event):
        """
        ドロップされたファイルのパスを取得し、テキストボックスに表示するメソッド。

        Args:
            event: ドロップイベント

        Returns:
            None
        """
        # ドロップされたファイルのパスを取得
        file_path = event.data
        if file_path[0] == "{" and file_path[-1] == "}" :
            file_path = file_path[1:-1]

        if os.path.isfile(file_path) :
            # テキストボックスにドロップされたファイルパスを表示
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, file_path)
 

class FolderSelectionControl(ctk.CTkFrame):
    """
    フォルダ名選択ダイアログ
    """
    def __init__(self, master=None, *args, **kwargs):
        """
        FolderSelectionControl

        Args:
            master: 親ウィジェット
            *args: 位置引数
            **kwargs: キーワード引数
        """
        # 引数から command を取得する
        self.command = kwargs.pop('command', None)
        
        # 親のコンストラクタを呼び出す
        super().__init__(master, *args, **kwargs)
        
        # 先頭のテキスト文字
        self.text = "フォルダ名"
        
        # ラベルを生成
        self.label = ctk.CTkLabel(self, text=self.text)
        self.label.grid(row=0, column=0, padx=10, pady=10)

        # テキストボックスを生成
        self.inpstr = ctk.StringVar()
        self.inpstr.trace('w',self.entry_changed)
        self.entry = ctk.CTkEntry(self,textvariable= self.inpstr)
        
        # sticky="ew" でテキストボックスが左右に広がるようにする
        self.entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")

        # アイコンのイメージを取得
        #icon_image = Image.open("./images/folder_24dp_FILL0_wght400_GRAD0_opsz24.png")
        #icon = ctk.CTkImage(icon_image,size=(24,24))

        # フォルダ名選択ボタンを生成
        self.button = ctk.CTkButton(self, text="フォルダを選択", image=None, compound="left", command=self.open_folder_dialog)
        self.button.grid(row=0, column=2, padx=10, pady=10)

        # ドロップを許可するフォーマットを設定
        self.entry.drop_target_register(DND_ALL)
        self.entry.dnd_bind('<<Drop>>', self.on_drop)

        # グリッド列の重みを設定してテキストボックスがウィンドウのサイズ変更に追従するようにする
        self.grid_columnconfigure(1, weight=1)

    def entry_changed(self, *args):
        """
        テキストボックスが変更されたらコマンドを呼び出す。

        Returns:
            None
        """
        if args[0] == self.inpstr._name and self.command is not None:
            self.command()

    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.entry.get()

    def set(self,text):
        """
        テキストボックスの値を設定するメソッド。

        Args:
            text: 設定したい文字列
        """
        if text is not None:
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, text)

    def open_folder_dialog(self):
        """
        フォルダ選択ダイアログを開くメソッド。
        テキストボックスに入力されたパスを取得し、選択されたファイルパスを表示する。

        Returns:
            None
        """
        # テキストボックスに入力されたパスを取得
        path = self.entry.get()
        folder_path = None
        if path is None or path == '':
            # パスが未入力の場合、ファイル選択ダイアログを開く
            folder_path = filedialog.askdirectory()
        elif os.path.isdir(path):
            # テキストボックスのパスが有効なファイルの場合、そのディレクトリを初期ディレクトリとしてファイル選択ダイアログを開く
            folder_path = filedialog.askdirectory(initialdir=os.path.dirname(path))

        if folder_path:
            # 選択されたファイルパスをテキストボックスに表示
            self.set(folder_path)

    def on_drop(self, event):
        """
        ドロップされたファイルのパスを取得し、テキストボックスに表示するメソッド。

        Args:
            event: ドロップイベント

        Returns:
            None
        """
        # ドロップされたファイルのパスを取得
        # ドロップされたファイルのパスを取得
        folder_path = event.data
        if folder_path[0] == "{" and folder_path[-1] == "}" :
            folder_path = folder_path[1:-1]

        # ファイルがドロップされたらフォルダ名を取得
        if os.path.isfile(folder_path):
            folder_path = os.path.dirname(folder_path)
        # folder_pathがフォルダ名か否かをチェック   
        if os.path.isdir(folder_path) :
            # テキストボックスにドロップされたファイルパスを表示
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, folder_path)


class SaveFileDialogControl(ctk.CTkFrame):
    """
    ファイル保存用のファイル名選択ダイアログ
    """
    def __init__(self, master=None, *args, **kwargs):
        """
        SaveFileDialogControlクラスのコンストラクタ。

        Args:
            master: 親ウィジェット
            *args: 位置引数
            **kwargs: キーワード引数
        """
        super().__init__(master, *args, **kwargs)
        
        # 先頭のテキスト文字
        self.text = "ファイル名"
        
        # ラベルを生成
        self.label = ctk.CTkLabel(self, text=self.text)
        self.label.grid(row=0, column=0, padx=10, pady=10)

        # テキストボックスを生成
        self.entry = ctk.CTkEntry(self)
        # sticky="ew" でテキストボックスが左右に広がるようにする
        self.entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")

        # アイコンのイメージを取得
        #icon_image = Image.open("./images/save_as_24dp_FILL0_wght400_GRAD0_opsz24.png")
        #icon = ctk.CTkImage(icon_image,size=(24,24))

        # ファイル保存ボタンを生成
        self.button = ctk.CTkButton(self, text="ファイルを保存", image=None, compound="left", command=self.save_file_dialog)
        self.button.grid(row=0, column=2, padx=10, pady=10)

        # グリッド列の重みを設定してテキストボックスがウィンドウのサイズ変更に追従するようにする
        self.grid_columnconfigure(1, weight=1)

    def get(self):
        """
        テキストボックスの値を取得するメソッド。

        Returns:
            str: テキストボックスの値
        """
        return self.entry.get()

    def set(self,text):
        """
        テキストボックスの値を設定するメソッド。

        Args:
            text: 設定したい文字列
        """
        if text is not None:
            self.entry.delete(0, ctk.END)
            self.entry.insert(0, text)   

    def save_file_dialog(self):
        """
        ファイル保存ダイアログを開くメソッド。
        テキストボックスに入力されたファイル名を取得し、保存先ファイルパスを表示する。

        Returns:
            None
        """
        # テキストボックスに入力されたファイル名を取得
        file_name = self.entry.get()
            # ファイル保存ダイアログを開く
        file_path = filedialog.asksaveasfilename(defaultextension=".txt", initialfile=file_name)
        if file_path:
            # 選択された保存先ファイルパスをテキストボックスに表示
            self.set(file_path)
            
class FigureCanvas(FigureCanvasTkAgg):
    """
    グラフ表示
    """
    def __init__(self, master=None, **kwargs):
        """
        FigureCanvasクラスのコンストラクタ。

        Args:
            master: 親ウィジェット
            **kwargs: キーワード引数
                figsize: 図のサイズ (デフォルト値: (4, 3))
                dpi: 解像度 (デフォルト値: 100)
        """
        self.figsize = kwargs.pop('figsize',(4,3))
        self.dpi = kwargs.pop('dpi',100)
        self.fig = plt.Figure(figsize=self.figsize, dpi=self.dpi)  # Figureオブジェクトを属性として保持
        super().__init__(self.fig, master, **kwargs)
        self.get_tk_widget().pack(padx=10,pady=10,fill="both", expand=True)

        
class ImageControl(ctk.CTkLabel):
    """
    画像表示
    """
    def __init__(self, master, width, height, *args, **kwargs):
        """
        ImageControlクラスのコンストラクタ。

        Args:
            master: 親ウィジェット
            width: 画像の幅
            height: 画像の高さ
            *args: 位置引数
            **kwargs: キーワード引数
        """
        super().__init__(master, *args, **kwargs)
        self.width = width
        self.height = height
        self.image = None
        self.image_path = None
        self.configure(width=width, height=height, text="")  # 初期化時にテキストを空に設定

        # ドロップイベントをバインド
        self.drop_target_register(DND_FILES)
        self.dnd_bind('<<Drop>>', self.on_drop)

    def on_drop(self, event):
        """
        ドロップイベントハンドラー。
        ドロップされた画像ファイルのパスを取得し、画像を表示する。

        Args:
            event: ドロップイベント
        """
        self.image_path = event.data.strip('{}')  # ファイルパスを取得
        self.set(self.image_path)

    def set(self, image_path):
        """
        画像を表示するメソッド。

        Args:
            image_path: 画像ファイルのパス
        """
        if os.path.isfile(image_path):
            img = Image.open(image_path)
            img.thumbnail((self.width, self.height))
        else:
           img = Image.new('RGB', (self.width, self.height), color="gray")        
        self.image = ImageTk.PhotoImage(img)
        self.configure(image=self.image, text="")
        self.image_path = image_path  # 画像パスを保存

    def get(self):
        """
        画像ファイルのパスを取得するメソッド。

        Returns:
            str: 画像ファイルのパス
        """
        return self.image_path

    @property
    def image_data(self):
        """
        表示中の画像データを取得するプロパティ。
        """
        return self.image

    @image_data.setter
    def image_data(self, new_image_data):
        """
        表示中の画像データを設定するプロパティ。
        """
        self.image = new_image_data
        self.configure(image=self.image)

まとめ

本記事では、CustomTkinterで用意されているウィジェットについて、具体的なサンプルソースを紹介しながら解説しました。

そして記事の最後には、本ブログの自作ツールで使っているオリジナルのユーザーコントロールのソースコードも公開しました。

CustomTkinter は、Python標準ライブラリである Tkinter のデザインを今風にするためのライブラリです。TKinterとは互換性は高いものの、TkinterにあってCustomTkinterで用意されていないウィジェットや、名前や仕様が微妙に変っているウィジェットも結構あるので、置き換えには注意が必要です。

CustomTkinterは、ちょっとした画面を今風のデザインでサクッと作るには便利なライブラリです。マテリアルデザインと相性が良いので、機会があれば使ってみてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次