0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CustomTkinterでSpinBoxを作成する

Posted at

まえがき

業務でCustomTkinterを使って簡単なデモアプリを作っているのですが、SpinBoxが用意されていないことが分かりました。
通常のTkinterでは用意されているのですが見た目が古臭い…。
ということでCustomTkinterを使ってイケてる(?)SpinBoxのWidgetを作りました。

本記事では

  • CustomTkinter版SpinBoxの実装
  • 実装の解説

の順で紹介します。

CustomTkinter版SpinBoxの実装

動作仕様

  • 任意の数値を入力できる
    • 設定された上限/下限の範囲外の数値は入力できず、上限/下限にクリップされる
  • 上ボタンをクリックすると入力した数値を一定の数値で加算する
    • 設定された上限を超える場合は上限にクリップされる
  • 下ボタンをクリックすると入力した数値を一定の数値で減算する
    • 設定された下限を下回る場合は下限にクリップされる
  • 数値入力時、上ボタンクリック時、下ボタンクリック時に設定されたコールバック関数が呼び出される
    • 引数として更新後の数値が与えらえる

実装

SpinBox.py
class SpinBox(ctk.CTkFrame):
    def __init__(self, master, width=100, increment=5, from_=0, to=100, initial_value=0, command=lambda v: None,font=None):
        super().__init__(master)

        self.current_value = initial_value
        self.from_ = float(from_)
        self.to = float(to)
        self.command = command

        self.entry = ctk.CTkEntry(self, width=width, font=font)
        self.entry.bind('<FocusOut>', self.update_value)
        self.entry.bind('<Return>', self.update_value)
        self.entry.insert(0, float(self.current_value))

        font = self.entry.cget("font")
        font_size = font[1] if type(font)==tuple else font.cget("size")
        self.up_button = ctk.CTkButton(self, text="", width=5, height=5, font=("Helvetica", int(font_size/2)), command=lambda: self.push_button(increment))
        self.down_button = ctk.CTkButton(self, text="", width=5, height=5, font=("Helvetica", int(font_size/2)), command=lambda: self.push_button(-increment))

        self.entry.grid(row=0, column=0, rowspan=2, sticky="nsew")
        self.up_button.grid(row=0, column=1, sticky="nsew")
        self.down_button.grid(row=1, column=1, sticky="nsew")

    def push_button(self, increment):
        current_value = self.current_value
        current_value += increment
        self.entry.delete(0, "end")
        self.entry.insert(0, current_value)

        self.update_value()

    def update_value(self, event=None):
        if self.validate_input():
            value = min(max(float(self.entry.get()), self.from_), self.to)
            self.current_value = value
            self.entry.delete(0, "end")
            self.entry.insert(0, self.current_value)
            self.command(self.current_value)
        else:
            self.entry.delete(0, 'end')
            self.entry.insert(0, self.current_value)

    def validate_input(self):
        value = self.entry.get()
        try:
            num = float(value)
            return True
        except ValueError:
            return False

動作確認用のコード

main.py
import customtkinter as ctk
from SpinBox import SpinBox

main = ctk.CTk()
main.bind_all("<Button-1>", lambda e: e.widget.focus_set())

value = ctk.StringVar(main, value=10.0)
label = ctk.CTkLabel(main, textvariable=value)
label.pack()

spin_box = SpinBox(main, initial_value=10.0, command=lambda v: value.set(v))
spin_box.pack()

main.mainloop()

動作イメージ
Videotogif.gif

実装の解説

EntryWidgetの作成

SpinBox.py
        self.entry = ctk.CTkEntry(self, width=width, font=font)
        self.entry.bind('<FocusOut>', self.update_value)
        self.entry.bind('<Return>', self.update_value)
        self.entry.insert(0, float(initial_value))

<FocusOut><Return>のイベントをバインドすることで、数値を入力した後にSpinBoxからフォーカスを外した時、もしくはReturn押下時に入力されている数値を確定しコールバック関数を呼ぶようにしています。
ただし、これだけでは他のWidgetをクリックしたときにフォーカスが外れずコールバック関数が呼び出されません。それを解消するためにmain.pyで

main.py
main.bind_all("<Button-1>", lambda e: e.widget.focus_set())

と設定することで、クリックしたときにクリックしたWidgetへフォーカスが移るようになりコールバック関数が呼ばれるようになります。

この時呼ばれる関数は以下です。

python SpinBox.py
    def update_value(self, event=None):
        if self.validate_input():
            value = min(max(float(self.entry.get()), self.from_), self.to)
            self.current_value = value
            self.entry.delete(0, "end")
            self.entry.insert(0, self.current_value)
            self.command(self.current_value)
        else:
            self.entry.delete(0, 'end')
            self.entry.insert(0, self.current_value)

validate_input()でEntryWidgetの中身が数値であるかを確認し、数値であれば数値を上限と下限にクリップしてからself.current_valueに数値を設定し指定されたコールバック関数を呼び出します。数値以外だった場合、コールバック関数を呼び出さず変更前の数値に戻します。
validate_input()については後で説明します。

インクリメント/デクリメントボタンの作成

SpinBox.py
        font = self.entry.cget("font")
        font_size = font[1] if type(font)==tuple else font.cget("size")
        self.up_button = ctk.CTkButton(self, text="", width=5, height=5, font=("Helvetica", int(font_size/2)), command=lambda: self.push_button(increment))
        self.down_button = ctk.CTkButton(self, text="", width=5, height=5, font=("Helvetica", int(font_size/2)), command=lambda: self.push_button(-increment))

インクリメント用とデクリメント用のボタンを作成しています。
フォントサイズを計算しているのは、EntryWidgetのフォントサイズに応じてボタンのフォントサイズも変更しデザインが崩れないようにするためです。
フォントの形式はTupleかCTkFontがあるため、型を確認してフォントサイズを取得する処理を変えています。

ボタン押下時に呼び出される関数は以下です。

SpinBox.py
    def push_button(self, increment):
        current_value = float(self.entry.get())
        current_value += increment
        self.entry.delete(0, "end")
        self.entry.insert(0, current_value)

        self.update_value()

EntryWidgetの内容を取り出し、そこにincrementを足し合わせてからEntryWidgetを書き直しています。その後、前述のupdate_value()を呼び出して値の確認とクリップ、コールバック関数の呼び出しをしています。

EntryWidgetの内容のチェック

EntryWidgetは数値に限らず任意の文字列を入力出来てしまいます。今回は数値だけに限定したいので、入力内容が数値かどうかを判断する必要があります。
以下のvalidate_input()によって入力内容が数値であるかを判断しています。

SpinBox.py
    def validate_input(self):
        value = self.entry.get()
        try:
            num = float(value)
            return True
        except ValueError:
            return False

始めにEntryWidgetから内容を取り出し、その内容をfloatに型変換します。この時正常に型変換が出来ればTrueを返します。正常に型変換が出来ずValueErrorの例外が発生したらFalseを返します。

おわりに

本記事ではCustomTkinter版SpinBoxの実装とその解説をしました。

CustomTkinterはPythonでそれなりに見た目の良いGUIアプリがサクッと作れるので便利なのですが、Tkinterで出ていたことが出来なくなっているものもあるのでそこがちょっと残念ですね。
ただ、既存のWidgetを組み合わせて自分で作るのもそれはそれで楽しいなと感じています。

参考文献

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?