Scaleable Radiobuttons#

Pyscripter unscaled altflex theme

Spyder scaled altflex theme

../_images/pyscripter_testaltflex.png
figures/buttons/Spyder_testaltflex.png

Drawing the Radiobutton#

Almost everything stated about check buttons can be directly related to radiobuttons. How the states interreact is similar except that radiobuttons have usually only one selection in a group at once, all other radiobuttons are not selected. Normally it makes sense to enable/disable all the radiobutton group at once.

Drawing the widget is easier as all the borders are curved and can be made with pieslices, so scaling is easier. The theme background is used as the image background, requiring one more dictionary. Even though the line/pie colours are largely similar to the checkbutton make a separate dictionary.

As with the check buttons test with an added checkbox to disable/enable one of the radiobuttons. When testing without the states (active, selected) and active the radiobuttons refused to change state from unselected to selected, therfore remember to include these states.

Show/Hide Code create_radiobuttons.py

'''
class has image loss due to garbage collection
alt is problem, drawing based on widget images, allow for scaling
'''

from PIL import Image, ImageDraw, ImageTk
from tkinter import Tk
from tkinter.ttk import Style, Frame, Radiobutton, Label, Checkbutton

from RunStatettk import run_state

# createButtons:
switch = 0

radioimg = {}
# colours from alt

states = ['background', 'active',('active', 'selected'), ('disabled', 'selected'),
             'selected','disabled', ('disabled', 'alternate'), 'alternate',
             'pressed']

rline_colours =  {'selected': {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
                'alternate': {'topleft': "#888888",
                            'botright': "#888888",
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
                'active': {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#ececec"},
    ('active', 'selected'): {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#ececec"},
                'disabled': {'topleft': "#888888",
                            'botright': None,
                            'intopleft': "#aaaaaa",
                            'inbotright': None},
                'pressed': {'topleft': "#888888",
                            'botright': None,
                            'intopleft': "#414141",
                            'inbotright': None},
                'background': {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
   ('disabled', 'alternate'): {'topleft': "#888888",
                            'botright': "#aaaaaa",
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
   ('disabled', 'selected'): {'topleft': "#888888",
                            'botright': "#aaaaaa",
                            'intopleft': "#414141",
                            'inbotright': None}
                            }

imagebg = {'selected': 'white',
            'alternate': "#aaaaaa", 'active': 'white',
            ('active', 'selected'): 'white',
            'disabled': "#d9d9d9", 'pressed': "#d9d9d9",
            'background': 'white', ('disabled', 'alternate'): "#d9d9d9",
            ('disabled', 'selected'): "#d9d9d9"}

outerbg = {'selected': "#d9d9d9",
            'alternate': "#d9d9d9", 'active': "#ececec",
            ('active', 'selected'): "#ececec",
            'disabled': "#d9d9d9", 'pressed': "#d9d9d9",
            'background': "#d9d9d9", ('disabled', 'alternate'): "#d9d9d9",
            ('disabled', 'selected'): "#d9d9d9"}

def circle(dr, center, radius, fill):
    dr.ellipse((center[0] - radius, center[1] - radius,
                center[0] + radius - 1, center[1] + radius - 1),
               fill=fill, outline=None)

# create pieslice with centre and radius, assume only fill used
def pie(idraw,c,r,fill='#888888',start=180,end=270):
    return idraw.pieslice([c[0]-r,c[1]-r,c[0]+r-1,c[1]+r-1],
                          fill=fill,start=start,end=end)


def rdraw_widgets(scaling):
    width, height = int(12 * scaling), int(12 * scaling)
    b = int(1 * scaling)

    for ix, state in enumerate(states):
        image = Image.new('RGB', (width,height), outerbg[state])
        idraw = ImageDraw.Draw(image)

        pie(idraw, [width//2, height//2], width//2,
            fill=rline_colours[state]['topleft'], start=135, end=315)

        if rline_colours[state]['botright']:
            pie(idraw, [width//2, height//2], width//2,
            fill=rline_colours[state]['botright'], start=315, end=135)

        pie(idraw, [width//2, height//2], width//2-b,
            fill=rline_colours[state]['intopleft'], start=135, end=315)

        if rline_colours[state]['inbotright']:
            pie(idraw, [width//2, height//2], width//2-b,
            fill=rline_colours[state]['inbotright'], start=315, end=135)

        circle(idraw, (width//2, height//2), width//2-2*b,
            fill=imagebg[state])

        if state in ('selected',('disabled','selected'),
                    ('active', 'selected')):
            circle(idraw, (width//2, height//2), 2*b, fill='#a3a3a3' \
                if state == ('disabled','selected') else 'black')

        radioimg[state] = ImageTk.PhotoImage(image)

def show_widgets(fr, scaling):
    st0 = Style()

    st0.theme_create( "altflex", parent="alt", settings={

        'Radiobutton.indicator': {"element create":
                ('image', radioimg['background'],
                ('disabled', 'selected', radioimg[('disabled', 'selected')]),
                ('disabled', radioimg['disabled']),
                ('disabled', 'alternate', radioimg[('disabled', 'alternate')]),
                ('alternate', radioimg['alternate']),
                ('pressed', radioimg['pressed']),
                ('active', 'selected', radioimg[('active', 'selected')]),
                ('selected', radioimg['selected']),
                ('active', radioimg['active']),
                { 'sticky': "w", 'padding':3*scaling})
                }})

    st0.theme_use('altflex')
    st0.map('TRadiobutton', background=[('active',"#ececec")])

    def change():
        global switch
        if switch == 0:
            widg.state(['disabled']) # active
            print(widg.state())
        else:
            widg.state(['!disabled']) # !active
        switch = 1 if switch == 0 else 0

    widg = Radiobutton(fr, text='Cheese' ,width=-8, value=1)
    widg1 = Radiobutton(fr, text='Tomato' ,width=-8, value=2)
    widg.grid(column=0,row=15,sticky='nsew', padx=5, pady=5)
    widg1.grid(column=0,row=16,sticky='nsew', padx=5, pady=5)

    label = Label(root, text='selected', \
            image=radioimg[('selected')], compound='left')
    label.image = radioimg['selected']
    label.grid(column=0,row=17,sticky='nsew', padx=5, pady=5)

    c = Checkbutton(root, text='disabled', command=change)
    c.grid(column=0, row=18, pady=5)
    run_state(fr,widg,widg1)

if __name__ == "__main__":
    root = Tk()
    winsys = root.tk.call("tk", "windowingsystem")
    BASELINE = 1.33398982438864281 if winsys != 'aqua' else 1.000492368291482
    scaling = root.tk.call("tk", "scaling")
    dpi_scaling = int(scaling / BASELINE + 0.5)
    fr0 = Frame(root)
    fr0.grid(column=0,row=0,sticky='nsew')
    rdraw_widgets(dpi_scaling)
    show_widgets(fr0, dpi_scaling)

    root.mainloop()

Show/Hide Code RunStatettk.py

from tkinter import Tk, StringVar
from tkinter.ttk import Frame, Radiobutton, Checkbutton, Separator, Style
        
class run_state():
    def __init__(self, fr, widg, widg1=None):
        ''' Used to enable state change

        Creates radio buttons showing states
        Creates check button with "Enabled", useful for testing
            check and radio buttons

        Args:
            fr: frame reference in calling program
            widg: widget reference
            widg1: optional widget
        '''
        self.fr = fr
        self.widg = widg
        self.widg1 = widg1

        # Create radio buttons which will display widget states
        states = ['active', ('active', 'selected'),'alternate', 'background',
                  'disabled', ('disabled', 'alternate'), 'focus', 'invalid',
                  'pressed', 'readonly', ('disabled', 'selected'), 'selected']

        self.rb = []
        self.state_val = StringVar()
        for iy, state in enumerate(states):
            st_rb = Radiobutton(fr, value=state, text=state,
                variable=self.state_val, command=self.change_state)
            st_rb.grid(column=0,row=iy+2,padx=5,pady=5, sticky='nw')
            self.rb.append(st_rb)
        sep = Separator(fr, orient='h')
        sep.grid(column=0,row=14,sticky='ew')

    def change_state(self):
        ''' used to enable state change'''
        newstate = self.state_val.get()
        oldstate = self.widg.state()
        # Check and Radio buttons start with alternate state
        # prefix oldstate with !
        oldst = [f"!{s}" for s in oldstate]
        # convert tuple to string
        oldst = " ".join(oldst)
        self.widg.state([oldst])
        # if newstate is compound run each part separately
        if ' ' in newstate:
            newstate, nstate = newstate.split()
            self.widg.state([newstate])
            self.widg.state([nstate])
        else:
            self.widg.state([newstate])

if __name__ == '__main__':
    root = Tk()
    st0 = Style()
    st0.theme_use('alt')
    fr1 = Frame()
    fr1.grid(column=0,row=0,sticky='nsew')

    Widg = Checkbutton(fr1,text='Checkbutton')
    Widg.grid(column=0,row=15)
    Widg1 = Checkbutton(fr1,text='Another one')
    Widg1.grid(column=0,row=16)
    run_state(fr1,Widg,Widg1)
    root.mainloop()

Prove the radiobuttons with a similar script to that with checkbuttons. When everything works as required almalgamate the check and radiobutton scripts. So far we have proved the concept, now to change the almalgamated script into a standalone module. Use one main function install, with a subsidiary function _load_images, place the dictionaries checkimg, radioimg in the global space before the two functions, add two more dictionaries checkimage and radioimage into the global space. Bring the scaling part from the main to the install function. The drawings will now store the PIL information into checkimage and radioimage dictionaries, then just before the altflex theme is created, call up _load_images, this will convert the PIL information to tkinter readable images and store in checkimg and radioimg.

Show/Hide Code altflex.py

from PIL import Image, ImageDraw, ImageTk
from tkinter import Tk
from tkinter.ttk import Style, Frame, Checkbutton

checkimg = {}
radioimg = {}
checkimage = {}
radioimage = {}

states = ['background', 'active', ('disabled', 'selected'), ('active', 'selected'),
             'selected','disabled', ('disabled', 'alternate'), 'alternate',
             'pressed']

def _load_images():
    for iz, state in enumerate(states):
        checkimg[state] = ImageTk.PhotoImage(checkimage[state])
        radioimg[state] = ImageTk.PhotoImage(radioimage[state])

def install():

    root = Tk()

    winsys = root.tk.call("tk", "windowingsystem")
    BASELINE = 1.33398982438864281 if winsys != 'aqua' else 1.000492368291482
    scaling = root.tk.call("tk", "scaling")
    dpi_scaling = int(scaling / BASELINE + 0.5)
    root.destroy()

    chline_colours = {'selected': {'topleft': "#888888",
                            'botright': None,
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
                'alternate': {'topleft': "#888888",
                            'botright': "#888888",
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
                'active': {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#ececec"},
        ('active', 'selected'): {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#ececec"},
                'disabled': {'topleft': "#888888",
                            'botright': None,
                            'intopleft': "#aaaaaa",
                            'inbotright': None},
                'pressed': {'topleft': "#888888",
                            'botright': None,
                            'intopleft': "#414141",
                            'inbotright': None},
                'background': {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
        ('disabled', 'alternate'): {'topleft': "#888888",
                            'botright': "#aaaaaa",
                            'intopleft': "#414141",
                            'inbotright': None},
        ('disabled', 'selected'): {'topleft': "#888888",
                            'botright': None,
                            'intopleft': "#aaaaaa",
                            'inbotright': None}
                            }

    rline_colours =  {'selected': {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
                'alternate': {'topleft': "#888888",
                            'botright': "#888888",
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
                'active': {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#ececec"},
        ('active', 'selected'): {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#ececec"},
                'disabled': {'topleft': "#888888",
                            'botright': None,
                            'intopleft': "#aaaaaa",
                            'inbotright': None},
                'pressed': {'topleft': "#888888",
                            'botright': None,
                            'intopleft': "#414141",
                            'inbotright': None},
                'background': {'topleft': "#888888",
                            'botright': 'white',
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
        ('disabled', 'alternate'): {'topleft': "#888888",
                            'botright': "#aaaaaa",
                            'intopleft': "#414141",
                            'inbotright': "#d9d9d9"},
        ('disabled', 'selected'): {'topleft': "#888888",
                            'botright': "#aaaaaa",
                            'intopleft': "#414141",
                            'inbotright': None}
                            }

    imagebg = {'selected': 'white', 'alternate': "#aaaaaa",
        'active': 'white', 'disabled': "#d9d9d9", 'pressed': "#d9d9d9",
        'background': 'white', ('disabled', 'alternate'): "#d9d9d9",
        ('disabled', 'selected'): "#d9d9d9", ('active', 'selected'): 'white'}

    outerbg = {'selected': "#d9d9d9",
            'alternate': "#d9d9d9", 'active': "#ececec",
            ('active', 'selected'): "#ececec",
            'disabled': "#d9d9d9", 'pressed': "#d9d9d9",
            'background': "#d9d9d9", ('disabled', 'alternate'): "#d9d9d9",
            ('disabled', 'selected'): "#d9d9d9"}

    def circle(dr, center, radius, fill):
        dr.ellipse((center[0] - radius, center[1] - radius,
                center[0] + radius - 1, center[1] + radius - 1),
               fill=fill, outline=None)

    # create pieslice with centre and radius, assume only fill used
    def pie(idraw,c,r,fill='#888888',start=180,end=270):
        return idraw.pieslice([c[0]-r,c[1]-r,c[0]+r-1,c[1]+r-1],
                          fill=fill,start=start,end=end)


    width, height = int(15*dpi_scaling), int(15*dpi_scaling)
    b = int(1*dpi_scaling)

    for ix, state in enumerate(states):
        image = Image.new('RGB', (width,height), imagebg[state])
        idraw = ImageDraw.Draw(image)

        idraw.line([(0,(b-1)//2), (width-b-1,(b-1)//2)], width=b,
            fill=chline_colours[state]['topleft'])
        idraw.line([((b-1)//2,0), ((b-1)//2,height-b-1)], width=b,
            fill=chline_colours[state]['topleft'])

        if chline_colours[state]['botright']:
            idraw.line([(0,height-1-b//2), (width-1,height-1-b//2)], width=b,
                fill=chline_colours[state]['botright'])
            idraw.line([(width-1-b//2,0), (width-1-b//2,height-1)], width=b,
                fill=chline_colours[state]['botright'])

        idraw.line([(b,b+(b-1)//2), (width-2*b-1,b+(b-1)//2)], width=b,
            fill=chline_colours[state]['intopleft'])
        idraw.line([(b+(b-1)//2,b), (b+(b-1)//2,height-2*b-1)], width=b,
            fill=chline_colours[state]['intopleft'])

        if chline_colours[state]['inbotright']:
            idraw.line([(b,height-1-b//2-b), (width-b-1,height-1-b//2-b)], width=b,
                fill=chline_colours[state]['inbotright'])
            idraw.line([(width-1-b//2-b,b), (width-1-b//2-b,height-b-1)], width=b,
                fill=chline_colours[state]['inbotright'])

        if state in ('selected',('disabled','selected')):
            # tick
            idraw.line([4*b, 6*b, 4*b, 9*b],
                fill='black' if state == 'selected' else '#a3a3a3', width=b)
            idraw.line([5*b, 7*b, 5*b, 10*b],
                fill='black' if state == 'selected' else '#a3a3a3', width=b)
            idraw.line([6*b, 8*b, 6*b, 11*b],
                fill='black' if state == 'selected' else '#a3a3a3', width=b)
            idraw.line([7*b, 7*b, 7*b, 10*b],
                fill='black' if state == 'selected' else '#a3a3a3', width=b)
            idraw.line([8*b, 6*b, 8*b, 9*b],
                fill='black' if state == 'selected' else '#a3a3a3', width=b)
            idraw.line([9*b, 5*b, 9*b, 8*b],
                fill='black' if state == 'selected' else '#a3a3a3', width=b)
            idraw.line([10*b, 4*b, 10*b, 7*b],
                fill='black' if state == 'selected' else '#a3a3a3', width=b)

        checkimage[state] = image

    width, height = int(12 * dpi_scaling), int(12 * dpi_scaling)
    b = int(1 * dpi_scaling)

    for ix, state in enumerate(states):
        image = Image.new('RGB', (width,height), outerbg[state])
        idraw = ImageDraw.Draw(image)

        pie(idraw, [width//2, height//2], width//2,
            fill=rline_colours[state]['topleft'], start=135, end=315)

        if rline_colours[state]['botright']:
            pie(idraw, [width//2, height//2], width//2,
                fill=rline_colours[state]['botright'], start=315, end=135)

        pie(idraw, [width//2, height//2], width//2-b,
            fill=rline_colours[state]['intopleft'], start=135, end=315)

        if rline_colours[state]['inbotright']:
            pie(idraw, [width//2, height//2], width//2-b,
                fill=rline_colours[state]['inbotright'], start=315, end=135)

        circle(idraw, (width//2, height//2), width//2-2*b,
            fill=imagebg[state])

        if state in ('selected',('disabled','selected'), ('active', 'selected')):
            circle(idraw, (width//2, height//2), 2*b, fill='#a3a3a3' \
                if state == ('disabled','selected') else 'black')

        radioimage[state] = image

    _load_images()
    st0 = Style()
    st0.theme_create( "altflex", parent="alt", settings={

        'Checkbutton.indicator': {"element create":
                ('image', checkimg['background'],
                ('disabled', 'selected', checkimg[('disabled', 'selected')]),
                ('disabled', checkimg['disabled']),
                ('pressed', checkimg['pressed']),
                ('disabled', 'alternate', checkimg[('disabled', 'alternate')]),
                ('alternate', checkimg['alternate']),
                ('selected', checkimg['selected']),
                { 'sticky': "w", 'padding':3*dpi_scaling}),
            },

        'Radiobutton.indicator': {"element create":
                ('image', radioimg['background'],
                ('disabled', 'selected', radioimg[('disabled', 'selected')]),
                ('disabled', radioimg['disabled']),
                ('disabled', 'alternate', radioimg[('disabled', 'alternate')]),
                ('alternate', radioimg['alternate']),
                ('pressed', radioimg['pressed']),
                ('active', 'selected', radioimg[('active', 'selected')]),
                ('selected', radioimg['selected']),
                ('active', radioimg['active']),
                { 'sticky': "w", 'padding':3*dpi_scaling})
            },
        'TCheckbutton': {'map': {'background':[('active',"#ececec")]}},
        'TRadiobutton': {'map': {'background':[('active',"#ececec")]}}

                })


if __name__ == "__main__":
    root = Tk()
    fr0 = Frame(root)
    fr0.grid(column=0,row=0,sticky='nsew')
    st = Style()
    install()
    st.theme_use('altflex')
    widg = Checkbutton(fr0, text='Cheese' ,width=-8)
    widg1 = Checkbutton(fr0, text='Tomato' ,width=-8)
    widg.grid(column=0,row=15,sticky='nsew', padx=5, pady=5)
    widg1.grid(column=0,row=16,sticky='nsew', padx=5, pady=5)

    root.mainloop()

To test the altflex theme one needs to import altflex, run altflex.install() then run Style.theme_use('altflex'). The altered widgets can then be tested. Additional alt widgets are automatically included when using the altflex theme and are sized separately as required for dpi awareness.

Show/Hide Code test_altflex.py

from tkinter import Tk
from tkinter.ttk import Style, Frame, Checkbutton, Radiobutton

import altflex

switch = 0

def change():
    global switch
    if switch == 0:
        widg.state(['disabled']) # active
        widg2.state(['disabled'])
        #print(widg.state())
    else:
        widg.state(['!disabled']) # !active
        widg2.state(['!disabled'])
    switch = 1 if switch == 0 else 0

root = Tk()

st = Style()
altflex.install()
st.theme_use('altflex')

fr0 = Frame(root)
fr0.grid(column=0,row=0,sticky='nsew')

widg = Checkbutton(fr0, text='Cheese' ,width=-8)
widg1 = Checkbutton(fr0, text='Tomato' ,width=-8)
widg.grid(column=0,row=15,sticky='nsew', padx=5, pady=5)
widg1.grid(column=0,row=16,sticky='nsew', padx=5, pady=5)

c = Checkbutton(fr0, text='disable/enable\n top checkbutton\n top radiobutton',
    command=change, width=-8)
c.grid(column=0, row=17, pady=5, sticky='nsew')

widg2 = Radiobutton(fr0, text='Ketchup' ,width=-8, value=1)
widg3 = Radiobutton(fr0, text='OK Sauce' ,width=-8, value=2)
widg2.grid(column=0,row=18,sticky='nsew', padx=5, pady=5)
widg3.grid(column=0,row=19,sticky='nsew', padx=5, pady=5)

root.mainloop()

As can be seen from the test script results (figures at the top of the page) Scaleable Radiobuttons the check and radiobuttons match in size just by scaling.