Monday, July 23, 2012

Tkinter Radiobutton Demo

This code is based on the Tcl radio.tcl demo modified to use ttk.Radiobutton and excluding the 'tristate'option (see the Checkbutton Demo if you're interested in knowing how 'tristate' works with ttk widgets.)

Also, I've used a different graphic for the image label. The original demo used a built-in bitmap, questhead. Using a tkinter Label, built-in graphics can be loaded by assigning label['bitmap']='built-in image name'; the bitmap option is not available to ttk.Label.  Neither is the winfo_reqheight option, so, while I've used a different graphic due to the limitations of ttk.Label, in the end I opted to stick with a standard tkinter Label in order to retain full control over the Label resizing properties. When a ttk.Label is used the label grows and shrinks based on the position of the graphic (top, left, etc) which I find distracting.

Similar resize behaviour happens when the Point size is set to one of the larger values; unfortunately, I couldn't find a way to prevent the resizing ... hopefully I'll trip over it one day.



# File: radiobuttons.py
# References:
#    http://www.tcl.tk/man/tcl8.5/TkCmd/ttk_labelframe.htm#M-labelwidget
#    http://wiki.tcl.tk/20054
#    http://www.pythonware.com/library/tkinter/introduction/x444-fonts.htm
#    http://www.tcl.tk/man/tcl8.5/TkCmd/colors.htm
#
# Image source: 
#    http://findicons.com/icon/94113/dialog_question?width=72#

from tkinter import *
from tkinter import ttk
from tkinter.font import *
from PIL import Image, ImageTk
from demopanels import MsgPanel, SeeDismissPanel

class RadiobuttonDemo(ttk.Frame):
    
    def __init__(self, isapp=True, name='radiobuttondemo'):
        ttk.Frame.__init__(self, name=name)
        self.pack(expand=Y, fill=BOTH)
        self.master.title('Radiobutton Demo')
        self.isapp = isapp
        self._create_widgets()
        
    def _create_widgets(self):
        if self.isapp:
            MsgPanel(self, 
                     ["Three groups of radiobuttons are displayed below.  If you click on a button then ",
                      "the button will become selected exclusively among all the buttons in its group. ",
                      "A control variable is associated with each group to indicate which of the group's ",
                      "buttons is selected.\n\n",
                      "Selecting a Point Size or Color changes the associated Labelframe text.\n\n",
                      "Selecting an Alignment repositions the graphic with respect ",
                      "to the label i.e. 'Bottom' puts the graphic 'below' the label."])
            
            SeeDismissPanel(self)
        
        self._create_demo_panel()
        
    def _create_demo_panel(self):
        demoPanel = Frame(self)
        demoPanel.pack(side=TOP, fill=BOTH, expand=Y)
        
        points = self._create_points_panel(demoPanel)   
        color = self._create_color_panel(demoPanel)
        align = self._create_image_panel(demoPanel)
        vars = self._create_var_panel(demoPanel)

        # position the BOTTOM panel first, otherwise it will get
        # tacked onto the end of the LEFT rather than placed at the bottom
        vars.pack(side=BOTTOM, expand=True, padx=10, pady=10, fill=X, anchor=SW)
        
        points.pack(side=LEFT, padx=10, pady=10, fill=BOTH)
        color.pack(side=LEFT, padx=10, pady=10, fill=BOTH)
        align.pack(side=LEFT, expand=True, padx=10, pady=10, fill=BOTH)
        
        
    def _create_points_panel(self, parent):
        # assign a style that applies only to this panel
        f = ttk.Labelframe(parent, text='Point Size', style='Points.TLabelframe', width=100)
        
        font = Font(f)                  # get font used by LabelFrame
        family=font.actual()['family']  # save font family
        weight=font.actual()['weight']  # save font weight
        
        # font sizes are in 'points', to give size in
        # pixels use a negative value e.g. -10, -12, etc.
        # The default font size for ttk.Labelframe is 12 pixels
        points = [10, 12, 14, 18, 24]
        self.size = IntVar()
        rb = []
        for p in points:
            rb.append(ttk.Radiobutton(f, text="Point size " + str(p),
                                         variable = self.size,
                                         value = p,
                                         command=lambda p=p: self._points_changed(family,p,weight)))
        for b in rb:
            b.pack(side=TOP, pady=2, padx=2, anchor=W, fill=X)
                
        return f
    
    def _points_changed(self,family, size, weight):
        ttk.Style().configure('Points.TLabelframe.Label', font=(family,-size,weight))
        self._show_vars()
                    
    def _create_color_panel(self, parent):
        # assign a style that applies only to this panel
        lbl = ttk.Label(text='Color', foreground='blue', style='Color.TLabelframe.Label')
        f = ttk.LabelFrame(parent, labelwidget=lbl)
                
        colors = ['Red', 'Green', 'Blue', 'Yellow', 'Orange', 'Purple']
        self.color = StringVar()
        rb = []
        for c in colors:
            rb.append(ttk.Radiobutton(f, text=c,
                                         variable = self.color,
                                         value = c,
                                         command=lambda c=c: self._color_changed(lbl, c)))
        for b in rb:
            b.pack(side=TOP, pady=2, padx=2, anchor=W, fill=X)
        
        return f
    
    def _color_changed(self,lbl, color):
        lbl.config(foreground=color)
        self._show_vars()
    
    def _create_image_panel(self, parent):
        f = ttk.Labelframe(parent, text='Alignment')
        
        im = Image.open('images/dialog_question.png')
        imh = ImageTk.PhotoImage(im)
        
        lbl = Label(text='Label', compound=LEFT, image=imh, 
                    font=('Helv', 10, 'bold italic'),
                    foreground='blue')  
        lbl.image = imh
        
        # set width and height to prevent label resizing when
        # image alignment changes
        lbl.configure(width=lbl.winfo_reqwidth(), compound=RIGHT)
        lbl.configure(height=lbl.winfo_reqheight(), compound=TOP)
        
        lbl.grid(in_=f, row=1, column=1, padx=5, pady=5)
        
        loc = ['Top', 'Left', 'Bottom', 'Right']
        self.align = StringVar()
        rb = []
        for l in loc:
            rb.append(ttk.Radiobutton(f, text=l,
                                         variable = self.align,
                                         value = l.lower(),
                                         width=7,
                                         command=lambda l=l: self._align_changed(lbl, l.lower())))
            
        
        rb[0].grid(in_=f, row=0, column=1)
        rb[1].grid(in_=f, row=1, column=0)
        rb[2].grid(in_=f, row=2, column=1)
        rb[3].grid(in_=f, row=1, column=2)
        
        return f

    def _align_changed(self, lbl, value):
        lbl.config(compound=value)
        self._show_vars()

    def _create_var_panel(self, parent):
        # panel to display radiobutton variable values
        right = ttk.LabelFrame(parent, text='Radiobutton Control Variables')
        
        self.vb0 = ttk.Label(right, font=('Courier', 10))        
        self.vb0.pack(side=LEFT, anchor=NW, pady=3, padx=15)
        
        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'] = 'size: {} \tcolor: {} \talign: {}'.format(self.size.get(),
                                                                    self.color.get(),
                                                                    self.align.get())
        

if __name__ == '__main__':
    RadiobuttonDemo().mainloop()