Spinbox in Tk (tkinter)

The ttk.Spinbox widget is similar to a textbox, but it also incorporates two buttons to increase or decrease its numerical content. Although this is its main use (as a container for numeric data), it can also display non-numeric values, like a dropdown list. We'll see their differences in the last section.

In order to present all the functionalities of a spinbox, we will develop a virtual thermostat like the following:

/images/spinbox-in-tk-tkinter/thermostat-tkinter.gif

This thermostat, built from a ttk.Spinbox instance, lets the user select a temperature between 10 and 30 ºC, while indicating whether the energy consumption is low, medium or high.

So let's start by creating a window with the necessary widgets.

from tkinter import ttk
import tkinter as tk
root = tk.Tk()
root.config(width=300, height=200)
root.title("Virtual Thermostat")
temp_label = ttk.Label(text="Temperature:")
temp_label.place(x=20, y=30, width=100)
spin_temp = ttk.Spinbox()
spin_temp.place(x=105, y=30, width=70)
root.mainloop()
/images/spinbox-in-tk-tkinter/tkinter-spinbox.png

If we try to press the buttons to increase and decrease the temperature, we will see that the text always stays at zero. This happens because we have not yet specified the range of numbers that our spinbox can display. We need to pass the from_ and to arguments when creating an instance of ttk.Spinbox.

from tkinter import ttk
import tkinter as tk
root = tk.Tk()
root.config(width=300, height=200)
root.title("Virtual Thermostat")
temp_label = ttk.Label(text="Temperature:")
temp_label.place(x=20, y=30, width=100)
# The thermostat will support values between 10 and 30.
spin_temp = ttk.Spinbox(from_=10, to=30)
spin_temp.place(x=105, y=30, width=70)
root.mainloop()

Now we'll see that the spinbox lets us increase or decrease the temperature by 1. If the temperature is 10, when pressing the increase button, it will change to 11, then to 12, and so on. Since we want to implement a thermostat, we are going to change the interval to 0.5 via the increment argument:

from tkinter import ttk
import tkinter as tk
root = tk.Tk()
root.config(width=300, height=200)
root.title("Virtual Thermostat")
temp_label = ttk.Label(text="Temperature:")
temp_label.place(x=20, y=30, width=100)
# Temperature must increase and decrease by 0.5.
spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5)
spin_temp.place(x=105, y=30, width=70)
root.mainloop()

Perfect! But you may have noticed that there is a problem: the user is able to write any other value in the thermostat (since ttk.Spinbox inherits from ttk.Entry), even non-numeric values! We could fix this by creating a validation rule, as we explained in our Textbox (Entry) Validation in Tk (tkinter) post. However, in this case it seems simpler to just make the spinbox read-only by passing the state="readonly" argument.

from tkinter import ttk
import tkinter as tk
root = tk.Tk()
root.config(width=300, height=200)
root.title("Virtual Thermostat")
temp_label = ttk.Label(text="Temperature:")
temp_label.place(x=20, y=30, width=100)
# Disable writing.
spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, state="readonly")
spin_temp.place(x=105, y=30, width=70)
root.mainloop()

So far so good. But we want the temperature to be measured in degrees Celsius (º C), so we are going to apply a display format by passing the format argument, which uses C-like format codes (%f, %d, %s, etc.) Since the temperature is a floating-point number, we will use the %.1fºC format, which means: limit the decimal portion to one digit and add the “ºC” suffix.

from tkinter import ttk
import tkinter as tk
root = tk.Tk()
root.config(width=300, height=200)
root.title("Virtual Thermostat")
temp_label = ttk.Label(text="Temperature:")
temp_label.place(x=20, y=30, width=100)
spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, state="readonly",
                        format="%.1fºC")
spin_temp.place(x=105, y=30, width=70)
root.mainloop()

Great, the UI looks good. Now let's add some functionality. We want to know when the user changes the temperature so that we can execute code in response to this event. Just like buttons, we will use the command argument to pass the name of a callback function.

from tkinter import ttk
import tkinter as tk
root = tk.Tk()
root.config(width=300, height=200)
root.title("Virtual Thermostat")
temp_label = ttk.Label(text="Temperature:")
temp_label.place(x=20, y=30, width=100)
spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, state="readonly",
                        format="%.1fºC", command=temp_changed)
spin_temp.place(x=105, y=30, width=70)
root.mainloop()

We are telling Tk that it must invoke the temp_changed() function every time the user presses one of the two buttons on the thermostat. Within the function body we will get the temperature from the thermostat and calculate the level of energy usage in order to display it in a new label:

from tkinter import ttk
import tkinter as tk
def temp_changed():
    temp = float(spin_temp.get()[:2])
    if temp <= 17:
        usage = "Low"
    elif temp <= 24:
        usage = "Medium"
    elif temp <= 30:
        usage = "High"
    usage_label["text"] = f"Energy usage: {usage}."
root = tk.Tk()
root.config(width=300, height=200)
root.title("Virtual Thermostat")
temp_label = ttk.Label(text="Temperature:")
temp_label.place(x=20, y=30, width=100)
spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, state="readonly",
                        format="%.1fºC", command=temp_changed)
spin_temp.place(x=105, y=30, width=70)
usage_label = ttk.Label()
usage_label.place(x=20, y=80)
root.mainloop()

Let's take a moment to explain this function:

def temp_changed():
    temp = float(spin_temp.get()[:2])
    if temp <= 17:
        usage = "Low"
    elif temp <= 24:
        usage = "Medium"
    elif temp <= 30:
        usage = "High"
    usage_label["text"] = f"Energy usage: {usage}."

We first get the temperature of the thermostat (spin_temp) via the get() method, like any other textbox. Since in the we had specified that every value must have a ºC suffix (via format), we use slicing ([:2]) to drop those last two characters. Finally, we convert the remaining text to a floating point number via the float() built-in. Then, based on the temperature obtained, we figure out if the energy usage is low, medium or high, and we show it in the usage_label.

It only remains to establish a default temperature when the program starts. To insert text into a spinbox we use insert(), just like in textboxes. However, it is not possible to execute this function if we said that the thermostat is read-only via state="readonly". To get around this, we'll create the thermostat without that argument, set the initial value, and then disable writing.

from tkinter import ttk
import tkinter as tk
def temp_changed():
    temp = float(spin_temp.get()[:2])
    if temp <= 17:
        usage = "Low"
    elif temp <= 24:
        usage = "Medium"
    elif temp <= 30:
        usage = "High"
    usage_label["text"] = f"Energy usage: {usage}."
root = tk.Tk()
root.config(width=300, height=200)
root.title("Virtual Thermostat")
temp_label = ttk.Label(text="Temperature:")
temp_label.place(x=20, y=30, width=100)
spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, format="%.1fºC",
                        command=temp_changed)
spin_temp.place(x=105, y=30, width=70)
usage_label = ttk.Label()
usage_label.place(x=20, y=80)
# Set the initial value.
spin_temp.insert(0, "10ºC")
# Disable writing.
spin_temp["state"] = "readonly"
# Update the energy usage label.
temp_changed()
root.mainloop()

Voilá! Our virtual thermostat is ready.

Events

While the command argument let us pass a callback function to be invoked when the user changes the value of the thermostat, in some circumstances it is useful to discriminate between the increase and decrease buttons. For this purpose, Tk provides the more specific <<Increment>> and <<Decrement>> signals. For example:

spin_temp.bind("<<Increment>>", temp_increased)
spin_temp.bind("<<Decrement>>", temp_decreased)

Functions associated with these two events must receive an argument, which will be an instance of tk.Event, unlike the function associated with command. For example:

def temp_increased(event):
    ...

def temp_decreased(event):
    ...

Other options

Since we have restricted the range of numbers allowed by our thermostat, Tk will not let the user decrease the temperature below 10ºC or increase it above 30ºC. But if we pass wrap=True, when the user tries to reduce the temperature below 10ºC, Tk will take the thermostat to the upper limit (30ºC) and, inversely, when trying to increase the value above 30ºC, it will return to 10ºC.

spin_temp = ttk.Spinbox(from_=10, to=30, increment=0.5, format="%.1fºC",
                        command=temp_changed, wrap=True)
/images/spinbox-in-tk-tkinter/spinbox-wrap.gif

Finally, instead of using a spinbox to allow the user to select a numeric value, we can provide a list of valid (even non-numeric) options. For example:

months = ("January", "February", "March", "April", "May", "June", "July",
          "August", "September", "October", "November", "December")
months_spin = ttk.Spinbox(values=months)

In this case, the spinbox behaviour is almost the same as that of a ttk.Combobox widget, although without a drop-down.

Comments