Sunday, July 22, 2012

Tkinter Checkbutton Demo

This code is based on the Tcl check.tcl demo, modified to use ttk.Checkbutton. A ttk.Checkbutton does not have the same options as a standard tkinter checkbutton; in particular, the tristatevalue option referenced in the original demo is missing.

checkbutton $w.b0 -text "Safety Check" \
    -variable safety -relief flat \
    -onvalue "all" \
    -offvalue "none" \
    -tristatevalue "partial"

The option determines the value of a checkbutton when it is neither checked (onvalue) nor unchecked (offvalue); it corresponds to the alternate visual state for display purposes. As nothing prevents you from assigning an invalid value to a checkbutton I was able to indirectly mimic the behaviour found in the original demo (see _value_changed and _safety_changed methods in code below). The screenshot on the right shows how the Safety checkbutton's alternate state looks under Windows 7.



The original demo displays the checkbutton control variables in a separate window via a See Variables button. I've chosen to display them in a separate panel along side the checkbuttons themselves.

# File: checkbuttons.py
# References:
#    http://www.tcl.tk/man/tcl8.5/TkCmd/ttk_checkbutton.htm

from tkinter import *
from tkinter import ttk
from demopanels import MsgPanel, SeeDismissPanel

class CheckbuttonDemo(ttk.Frame):
    
    def __init__(self, isapp=True, name='checkbuttondemo'):
        ttk.Frame.__init__(self, name=name)
        self.pack(expand=Y, fill=BOTH)
        self.master.title('Checkbutton Demo')
        self.isapp = isapp
        self._create_widgets()
        
    def _create_widgets(self):
        if self.isapp:
            MsgPanel(self, 
                     ["Four checkbuttons are displayed below.  If you click on a button, ",
                      "it will toggle the button's selection state and set a control variable ",
                      "to a value indicating the state of the checkbutton.\n\n",
                      "The first button follows the state of the other three. ",
                      "If only some of the three are checked, the first button will ",
                      "display the 'alternate' (tri-state) mode."]
                      )
            
            SeeDismissPanel(self)
        
        self._create_demo_panel()
        
    def _create_demo_panel(self):
        demoPanel = ttk.Frame(self)
        demoPanel.pack(side=TOP, fill=BOTH, expand=Y)
        
        left = self._create_btn_panel(demoPanel)    # checkbuttons
        right = self._create_var_panel(demoPanel)   # checkbutton variables
        
        left.pack(side=LEFT, expand=True, padx=10, fill=BOTH)
        right.pack(side=LEFT, expand=True, padx=10, fill=BOTH)

    def _create_var_panel(self, parent):
        # panel to display check button variable values
        right = ttk.LabelFrame(parent, text='Checkbutton Control Variables')
        
        self.vb0 = ttk.Label(right, font=('Courier', 10))
        self.vb1 = ttk.Label(right, font=('Courier', 10))
        self.vb2 = ttk.Label(right, font=('Courier', 10))    
        self.vb3 = ttk.Label(right, font=('Courier', 10))  
        
        self.vb0.pack(anchor=NW, pady=3)
        self.vb1.pack(anchor=NW, pady=3)
        self.vb2.pack(anchor=NW, pady=3)
        self.vb3.pack(anchor=NW, pady=3)  
        
        self._show_vars()   
        
        return right

    def _show_vars(self):
        # set text for labels in var_panel to include the control 
        # variable name and current variable value
        self.vb0['text'] = '{:<8} {:<8}'.format('safety:', self.safety.get())
        self.vb1['text'] = '{:<8} {:<8}'.format('wipers:', self.vars[0].get())
        self.vb2['text'] = '{:<8} {:<8}'.format('brakes:', self.vars[1].get())
        self.vb3['text'] = '{:<8} {:<8}'.format('sober:', self.vars[2].get())
            
    def _create_btn_panel(self, parent):
        # frame to hold the check buttons
        left = ttk.LabelFrame(parent, text='Checkbuttons')
        
        # control variables linked to check buttons
        self.safety = StringVar(value='none')   # group monitor
        
        wipers = IntVar()
        brakes = IntVar()
        sober = IntVar()
        
        self.vars = [wipers, brakes, sober]
        
        # check buttons
        self.b0 = ttk.Checkbutton(left, text='Safety Check', variable=self.safety,
                             onvalue= 'all', offvalue='none', command=self._safety_changed)
        b1 = ttk.Checkbutton(left, text='Wipers OK', variable=wipers, command=self._value_changed)
        b2 = ttk.Checkbutton(left, text='Brakes OK', variable=brakes, command=self._value_changed)
        b3 = ttk.Checkbutton(left, text='Driver Sober', variable=sober, command=self._value_changed)

        # pack the buttons
        self.b0.pack(side=TOP, pady=2, anchor=W)
        
        for b in [b1, b2, b3]:
            b.pack(side=TOP, pady=2, padx=15, anchor=W)
        
        return left
            
    def _safety_changed(self):
        # all or none other check buttons selected
        if self.safety.get() != 'partial':
            value = 0   # deselect all
            
            if self.safety.get() == 'all':
                value = 1   # select all
            
            # select or deselect other buttons
            for i in range(len(self.vars)):
                self.vars[i].set(value)
                
        self._show_vars()
    
    def _value_changed(self):        
        # get check button values
        values = []
        for i in range(len(self.vars)):
            values.append(self.vars[i].get())
            
        if 0 not in values:         # all check buttons selected
            self.safety.set('all')  # set safety check button to 'all'
        else:
            # all not selected, set safety to 'partial'
            self.safety.set(value='partial')
            
            # change safety check button visual state 
            # to 'indefinite', neither checked nor empty
            self.b0.state(['alternate'])  
            
        self._show_vars()
        
    
if __name__ == '__main__':
    CheckbuttonDemo().mainloop(