Modifying Scale, Spinbox and Entry#

Change the Scale#

At present the scale has that stuck on feeling, it does not sit in with the application. There are two real choices, either go the whole hog and change the scale to a canvas which combines the gradient and cursor, or just change the appearance of the cursor using a theme. The first choice would slow down the whole application, by how much is not really known until tested on the full application when rgb is combined with hsv. The second choice allows a scale to operate just as before. When switching to a themed scale we will need to make provision to display the range values which were included in the tkinter widget.

Changing the Scale Cursor#

Change to Scale Cursor#

tkinter

styled ttk

scale cursor

scale cursor

tkinter cursor on scale

ttk altered cursor on scale

Create a small super class for our scale, purely so that we can use inheritance. First of all create a cursor on an invisible trough, a bit like those in paint.net, but larger so that it is easier to click on the cursor. Using state, cChange its appearance when pressed. Place this in the __init__ of RgbSelect we can change the ttk scale and make changes to entry and spinboxes as required:

img=Image.new("RGBA",(16,10),'#00000000')
trough=ImageTk.PhotoImage(img)
# constants for creating upward pointing arrow
WIDTH = 17*e
HEIGHT = 17*e
OFFSET = 5*e
ST0 = WIDTH // 2, HEIGHT - 1 - OFFSET
LIGHT = 'GreenYellow'
MEDIUM = 'LawnGreen'
DARK = '#5D9B90'

# normal state
im = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
rdraw = ImageDraw.Draw(im)
rdraw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
               WIDTH - 1, HEIGHT - 1], fill=LIGHT)
rdraw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=MEDIUM)
rdraw.polygon([ST0[0], ST0[1], WIDTH - 1,
               HEIGHT - 1, ST0[0], 0], fill=DARK)
slider = ImageTk.PhotoImage(im)

#pressed state
imp = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
draw = ImageDraw.Draw(imp)
draw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
              WIDTH - 1, HEIGHT - 1], fill=LIGHT)
draw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=DARK)
draw.polygon([ST0[0], ST0[1], WIDTH - 1,
              HEIGHT - 1, ST0[0], 0], fill=MEDIUM)
sliderp = ImageTk.PhotoImage(imp)

style = Style()
style.theme_settings('default', {
    'Horizontal.Scale.trough': {"element create":
                                ('image', trough,
                                 {'border': 0, 'sticky': 'wes'})},
    'Horizontal.Scale.slider': {"element create":
                                ('image', slider,
                                 ('pressed', sliderp),
                                 {'border': 3, 'sticky': 'n'})}})

PIL has been loaded for the Scale cursor drawing, this will only affect the start-up time.

Checking out our requirements for the scale, note that the length of the Scale is the movement length of the cursor plus the cursor width, plus any trough borders. The external trough length is the actual Scale length. The centre of the cursor should correspond to the measurement on the range. If the calculations are correct then when the cursor is at its minimum from_ it should show 0 and the corresponding to value is at its maximum.

Adding Range Values#

As usual I'm indebted to some clever programmer for the method in stackoverflow that can be used in our application. We only require a horizontal scale where the range is in positive integers, so after simplification it becomes more manageable. It would be nice to pass the tick_interval to the widget since we require an easy way to change the range appearance whenever different ranges are being used. Possibly digits may be useful (the number of digits passed to the tk variable).

We are taking over the following Scale options:-
  • parent

  • from_

  • to

  • command

  • length

  • orient

  • variable

We are using the following from the old style scale:-
  • digits

  • tickinterval

  • sliderlength

This should give us:

class TtkScale(Scale):
    def __init__(self, parent, from_=0, to=255, length=300, orient='horizontal',
             variable=0, digits=None, tickinterval=None, sliderlength=16,
             command=None):
        self.from_=from_
        self.to=to
        self.variable=variable

        super().__init__(parent, length=length + sliderlength,
                     variable=variable, from_=from_, to=to, command=command)

        self.digits=digits
        self.length=length

        self.build(parent,from_,to,sliderlength,tickinterval, length)

Position the range values just below the trough, build them as part of the scale widget and use the place layout manager, so these act as a built-in feature of the widget:

def build(self, parent, from_, to, sliderlength, tickinterval, length):
    # create ticks
    scRange = to-from_

    if tickinterval:
        for i in range(from_, to + 2, tickinterval):
            item = Label(parent, text=i, bg='#EFFEFF')
            item.place(in_=self, bordermode='outside',
                       relx=sliderlength / length / 2 + i /
                       sc_range * (1 - sliderlength / length),
                       rely=1, anchor='n')

The first tick value is positioned just below the centre of the slider when it is hard up against the left hand border, whilst the final tick is below the centre of the slider when it is hard up against the right hand border. This means that the first tick is half a slider length inside the border, and the last tick is half a slider length inside the opposite border. Other ticks are equally spaced between these two extremes.

Now change all the Scales to TtkScale, altering any attributes as necessary.

After the scale range values are made, notice that they are mostly hidden by the canvas. Place empty labels one row below the Spinboxes to give the values enough space to show - do not use pady in the grid as we want the scale and canvas to abut. Lower widgets have to be shifted down to accommodate these labels.

As the cursor is not showing try placing the style information in the main part. That's better, but it is obvious that the gradients are too short. Reinstate the 30 pixels subtracted in __init__ of RgbSelect. Better, now remove the border and highlightthickness from the alpha canvas. Wait until after hsv has been installed before changing the labels to ttk type, so we can easily see the empty labels.

Show/Hide Code 06scalemod.py

""" Construction four gradients in rgba using PPM image
    added final colour
    working with modified Scale
"""

from tkinter import Tk, Canvas, Label, IntVar, Frame, StringVar
from tkinter.ttk import LabelFrame, Scale, Style, Entry, Spinbox
from PIL import Image, ImageDraw, ImageTk
from colourTools import rgb2hash, draw_gradient, \
    draw_agradient, vdraw_gradient


class TtkScale(Scale):
    """Class to draw themed Scale widget

    Parameters
    ----------
    parent : str
        parent widget
    enlargement : int
        dpi enlargement factor
    from_ : int
        start of scale
    to : int
        end of scale
    length : int
        length in pixels
    orient : str
        orientation
    variable : str
        tk variable
    digits : int
        length variable when converted to string
    tickinterval : float or int
        how many digits show up in tick interval
    sliderlength : int
        what it says
    command : str
        procedure called when slider moves
    """

    def __init__(self, parent, from_=0, to=255, length=300, orient='horizontal',
                 variable=0, digits=None, tickinterval=None, sliderlength=16,
                 command=None, enlargement=1):

        self.from_ = from_
        self.to = to
        self.variable = variable

        super().__init__(parent, length=length + sliderlength,
                         variable=variable, from_=from_, to=to, command=command)
        self.e = enlargement
        self.digits = digits
        self.length = length

        self.build(parent, from_, to, sliderlength, tickinterval, length)

    def build(self, parent, from_, to, sliderlength, tickinterval, length):
        """Create ticks

        Parameters
        ----------
        parent : str
            parent widget
        from_ : int
            start of scale
        to : int
            end of scale
        length : int
            length in pixels
        tickinterval : float or int

        """

        sc_range = to - from_

        if tickinterval:
            for i in range(from_, to + 2, tickinterval):
                item = Label(parent, text=i, bg='#EFFEFF')
                item.place(in_=self, bordermode='outside',
                           relx=sliderlength*self.e / length*self.e / 2 + i /
                           sc_range * (1 - sliderlength*self.e / length*self.e),
                           rely=1, anchor='n')


class RgbSelect:
    """Class to construct rgba gradients and final colour

    Parameters
    ----------
    fr0 : str
        parent widget

    Returns
    -------
    None
    """

    def __init__(self, fr0, enlargement):
        self.fr0 = fr0
        self.e = enlargement

        self.cursor_w = 16 * self.e

        self.rvar = IntVar()
        self.gvar = IntVar()
        self.bvar = IntVar()
        self.avar = IntVar()
        self.evar = StringVar()

        self.scale_l = 300 * self.e
        self.canvas_w = self.scale_l
        self.canvas_h = 26 * self.e
        self.canvas_b = 30 * self.e
        self.build()

        self.rvar.set(255)
        self.gvar.set(0)
        self.bvar.set(0)
        self.avar.set(255)
        self.evar.set('#ff0000')

    def rhandle(self, *args):
        """command callback for red

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        alpha = self.avar.get()
        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       self.e, width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=self.canvas_b, height=self.canvas_b)
        self.evar.set(rgb2hash(red, green, blue))

    def ghandle(self, *args):
        """command callback for green

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        alpha = self.avar.get()
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue), self.e,
                       width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=self.canvas_b, height=self.canvas_b)
        self.evar.set(rgb2hash(red, green, blue))

    def bhandle(self, *args):
        """command callback for blue

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        red = self.rvar.get() #round(self.rvar.get(),0)
        green = self.gvar.get()
        blue = self.bvar.get()
        alpha = self.avar.get()
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       self.e, width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=self.canvas_b, height=self.canvas_b)
        self.evar.set(rgb2hash(red, green, blue))

    def ahandle(self, *args):
        """command callback for alpha

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        alpha = self.avar.get()
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=self.canvas_b, height=self.canvas_b)

    def build(self):
        """widget construction

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        fr1 = LabelFrame(self.fr0, text='rgb')
        fr1.grid(column=0, row=0, sticky='news')

        rl0 = Label(fr1, text='red  ')
        rl0.grid(column=0, row=0, sticky='s')

        self.rcan = Canvas(fr1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.rcan.grid(column=1, row=0, sticky='s')

        rsc = TtkScale(fr1, from_=0, to=255, variable=self.rvar, orient='horizontal',
                       length=self.scale_l, command=self.rhandle, tickinterval=20,
                       enlargement=self.e)
        rsc.grid(column=1, row=1, sticky='news')

        rsb = Spinbox(fr1, from_=0, to=255, textvariable=self.rvar,
                      command=self.rhandle, width=5)
        rsb.grid(column=2, row=1, sticky='nw')

        rel = Label(fr1, height=1)
        rel.grid(column=2, row=2)

        gl0 = Label(fr1, text='green')
        gl0.grid(column=0, row=3)

        self.gcan = Canvas(fr1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.gcan.grid(column=1, row=3, sticky='s')

        gsc = TtkScale(fr1, from_=0, to=255, variable=self.gvar, orient='horizontal',
                       length=self.scale_l, command=self.ghandle, tickinterval=20,
                       enlargement=self.e)
        gsc.grid(column=1, row=4, sticky='news')

        gsb = Spinbox(fr1, from_=0, to=255, textvariable=self.gvar,
                      command=self.ghandle, width=5)
        gsb.grid(column=2, row=4, sticky='nw')

        gel = Label(fr1, height=1)
        gel.grid(column=2, row=5)

        bl0 = Label(fr1, text='blue ')
        bl0.grid(column=0, row=6, sticky='s')

        self.bcan = Canvas(fr1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.bcan.grid(column=1, row=6, sticky='n')

        bsc = TtkScale(fr1, from_=0, to=255, variable=self.bvar, orient='horizontal',
                       length=self.scale_l, command=self.bhandle, tickinterval=20,
                       enlargement=self.e)
        bsc.grid(column=1, row=7, sticky='news')

        bsb = Spinbox(fr1, from_=0, to=255, textvariable=self.bvar,
                      command=self.bhandle, width=5)
        bsb.grid(column=2, row=7, sticky='nw')

        bel = Label(fr1, height=1)
        bel.grid(column=2, row=8)

        fr3 = LabelFrame(self.fr0, text='colour mix')
        fr3.grid(column=1, row=0, sticky='nw')

        self.cmcan = cmcan = Canvas(fr3, width=30*self.e, height=30*self.e, bd=0,
                                    highlightthickness=0)
        cmcan.grid(column=0, row=0, sticky='n', columnspan=2)
        cmcan.grid_propagate(0)
        vdraw_gradient(self.cmcan, (255, 0, 0), self.e, alpha=255)

        cml = Label(fr3, text='hash\nvalue')
        cml.grid(column=0, row=1)

        ent0 = Entry(fr3, width=8, textvariable=self.evar)
        ent0.grid(column=1, row=1)

        fr2 = LabelFrame(self.fr0, text='opacity')
        fr2.grid(column=0, row=1, sticky='news')

        al0 = Label(fr2, text='alpha')
        al0.grid(column=0, row=0, sticky='s')

        self.acan = Canvas(fr2, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.acan.grid(column=1, row=0, sticky='n')

        asc = TtkScale(fr2, from_=0, to=255, variable=self.avar, orient='horizontal',
                       length=self.scale_l, command=self.ahandle, tickinterval=20,
                       enlargement=self.e)
        asc.grid(column=1, row=1, sticky='news')

        asb = Spinbox(fr2, from_=0, to=255, textvariable=self.avar,
                      command=self.ahandle, width=5)
        asb.grid(column=2, row=1, sticky='nw')

        ael = Label(fr2, text=' ', height=1)
        ael.grid(column=2, row=2, sticky='s')

        draw_gradient(self.rcan, (0, 0, 0), (255, 0, 0),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.gcan, (255, 0, 0), (255, 255, 0),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (255, 0, 0), (255, 0, 255),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (255, 0, 0),
                       self.e, width=self.canvas_w, height=self.canvas_h)


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")
    enlargement = e = int(scaling / BASELINE + 0.5)

    img = Image.new("RGBA", (16*e, 10*e), '#00000000')
    trough = ImageTk.PhotoImage(img)

    # constants for creating upward pointing arrow
    WIDTH = 17*e
    HEIGHT = 17*e
    OFFSET = 5*e
    ST0 = WIDTH // 2, HEIGHT - 1 - OFFSET
    LIGHT = 'GreenYellow'
    MEDIUM = 'LawnGreen'
    DARK = '#5D9B90'

    # normal state
    im = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
    rdraw = ImageDraw.Draw(im)
    rdraw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
                   WIDTH - 1, HEIGHT - 1], fill=LIGHT)
    rdraw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=MEDIUM)
    rdraw.polygon([ST0[0], ST0[1], WIDTH - 1,
                   HEIGHT - 1, ST0[0], 0], fill=DARK)
    slider = ImageTk.PhotoImage(im)

    # pressed state
    imp = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
    draw = ImageDraw.Draw(imp)
    draw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
                  WIDTH - 1, HEIGHT - 1], fill=LIGHT)
    draw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=DARK)
    draw.polygon([ST0[0], ST0[1], WIDTH - 1,
                  HEIGHT - 1, ST0[0], 0], fill=MEDIUM)
    sliderp = ImageTk.PhotoImage(imp)

    style = Style()
    style.theme_settings('default', {
        'Horizontal.Scale.trough': {"element create":
                                    ('image', trough,
                                     {'border': 0, 'sticky': 'wes'})},
        'Horizontal.Scale.slider': {"element create":
                                    ('image', slider,
                                     ('pressed', sliderp),
                                     {'border': 3, 'sticky': 'n'})}})

    style.theme_use('default')
    style.configure('TSpinbox', arrowsize=10*e)
    fra0 = Frame(root)
    root.columnconfigure(0, weight=1)
    fra0.grid(row=0, column=0, sticky='nsew')
    fra0.columnconfigure(0, weight=1)
    RgbSelect(fra0, enlargement)
    root.mainloop()

This should produce:-

changing scale add tick scale

The layout after modifying scale#

Move the cursors, and we see that the output in all the spinboxes is a float, no longer an integer.



User Input in Entry and Spinboxes#

User validation on both entry and spinboxes is necessary to ensure that input is correct, spinboxes allow integer input up to 3 figures with an upper and lower limit, whereas entry deals with a hash and hexadecimal input. When adding validation to both the entry and spinboxes some of our automatic adjustment is lost, therefore add a bind to each of these widgets, so that any changes in values are reflected in our gradients and shown value in entry:

def sb_okay(action, text, input):  # '%d', '%P','%S'
    if action == "1":
        if input.isdigit():
            return bool(0 <= int(text) <= 255)
        return False
    return True

Each spinbox requires to register the above function, which is the same for each colour component RGBA. Validate on keystroke, action, current input and text. The bind handler is common for the colour components RGB, and slightly less complicated for the alpha component, since we are only updating the final colour, whereas a standard component must update all the other component gradients, the alpha component and the final colour and its hash value. As these changes occur during user input to the entry or spinbox, speed is not so critical, whenever possible use common validation and handler functions.

The validation for the entry is a bit more complicated. First check that the first character is a hash, then all the subsequent input is checked as hexadecimal. Finally limit it to 6 hexadecimal units using a bool function in the return clause of the try clause, which is equivalent to range checking:

def isOkay(index, text, input_):  # '%i','%P','%S'
    # hash cannot be removed, hex check on input after hash
    index = int(index)  # index is string!
    if index == 0 and text == '#':
        return True
    try:
        int(input_, 16)
        return bool(0 < index < 7)
    except ValueError:  # not a hex
        return False

The validation otherwise is similar to the spinboxes, but also use the index, that must be called as well. When initialising the entry the tk variable is set to the default value, otherwise validation prevents an update.

The bind handler gets the new hash value from which it determines each colour component that in turn sets the colour component of the tk variable. Each of the colour gradients then is drawn.

After all that you should see something like the following:-

Show/Hide Code 07entryscalemod.py

""" Construction four gradients in rgba using PPM image
    added final colour, added entry and spinbox validation
    working with modified Scale
"""

from tkinter import Tk, Canvas, Label, IntVar, Frame, StringVar
from tkinter.ttk import LabelFrame, Scale, Style, Entry, Spinbox
from PIL import Image, ImageDraw, ImageTk
from colourTools import rgb2hash, draw_gradient, \
    draw_agradient, vdraw_gradient, hash2rgb


def is_okay(index, text, input_):  # '%i','%P','%S'
    """Validation for hash, which cannot be removed,
        hex check on input after hash

    Parameters
    ----------
    index : str
        index
    text : str
        text if accepted
    input_ : str
        current input

    Returns
    -------
    boolean
    """
    index = int(index)  # index is string!
    if index == 0 and text == '#':
        return True
    try:
        int(input_, 16)
        return bool(0 < index < 7)
    except ValueError:  # not a hex
        return False

    '''
    if index == 1:
        try:
            int(input, 16)  # checks text being inserted or deleted
            return True
        except ValueError:
            return False

    elif index > 1 and index < 7:
        try:
            int(text[1:], 16)
            return True     # accept hexadecimal
        except ValueError:  # not a hex
            return False
    else:
        return False
    '''

def sb_okay(action, text, input_):  # '%i', '%P','%S'
    """Validation for colour components

    Parameters
    ----------
    text : str
        text if accepted
    input : str
        current input

    Returns
    -------
    boolean
    """

    if action == "1":
        if input.isdigit():
            return bool(0 <= int(text) <= 255)
        return False
    return True



class TtkScale(Scale):
    """Class to draw themed Scale widget

    Parameters
    ----------
    parent : str
        parent widget
    from_ : int
        start of scale
    to : int
        end of scale
    length : int
        length in pixels
    orient : str
        orientation
    variable : str
        tk variable
    digits : int
        length variable when converted to string
    tickinterval : float or int
        how many digits show up in tick interval
    sliderlength : int
        what it says
    command : str
        procedure called when slider moves
    """

    def __init__(self, parent, from_=0, to=255, length=300, orient='horizontal',
                 variable=0, digits=None, tickinterval=None, sliderlength=16,
                 command=None, enlargement=1):
        self.from_ = from_
        self.to = to
        self.variable = variable

        super().__init__(parent, length=length + sliderlength,
                         variable=variable, from_=from_, to=to, command=command)

        self.digits = digits
        self.length = length
        self.e = enlargement

        self.build(parent, from_, to, sliderlength, tickinterval, length)

    def build(self, parent, from_, to, sliderlength, tickinterval, length):
        """Create ticks

        Parameters
        ----------
        parent : str
            parent widget
        from_ : int
            start of scale
        to : int
            end of scale
        length : int
            length in pixels
        tickinterval : float or int

        """

        # create ticks
        sc_range = to - from_

        if tickinterval:
            for i in range(from_, to + 2, tickinterval):
                item = Label(parent, text=i, bg='#EFFEFF')
                item.place(in_=self, bordermode='outside',
                           relx=sliderlength*self.e / length*self.e / 2 + i /
                           sc_range * (1 - sliderlength*self.e / length*self.e),
                           rely=1, anchor='n')


class RgbSelect:
    """Class to construct rgba gradients and final colour

    Parameters
    ----------
    fr : str
        parent widget

    Returns
    -------
    None
    """

    def __init__(self, parent, enlargement):
        self.parent = parent
        self.e = enlargement

        self.cursor_w = 16 * self.e

        self.rvar = IntVar()
        self.gvar = IntVar()
        self.bvar = IntVar()
        self.avar = IntVar()
        self.evar = StringVar()

        self.scale_l = 300 * self.e
        self.canvas_w = self.scale_l
        self.canvas_h = 26 * self.e
        self.build()

        self.rvar.set(255)
        self.gvar.set(0)
        self.bvar.set(0)
        self.avar.set(255)
        self.evar.set('#ff0000')

    def rhandle(self, *args):
        """command callback for red

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        red = self.rvar.get()
        self.rvar.set(red)
        green = self.gvar.get()
        blue = self.bvar.get()
        alpha = self.avar.get()
        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       self.e, width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=30*self.e, height=30*self.e)
        self.evar.set(rgb2hash(red, green, blue))

    def ghandle(self, *args):
        """command callback for green

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        red = self.rvar.get()
        green = self.gvar.get()
        self.gvar.set(green)
        blue = self.bvar.get()
        alpha = self.avar.get()
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue), self.e,
                       width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=30*self.e, height=30*self.e)
        self.evar.set(rgb2hash(red, green, blue))

    def bhandle(self, *args):
        """command callback for blue

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        self.bvar.set(blue)
        alpha = self.avar.get()
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       self.e, width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=30*self.e, height=30*self.e)
        self.evar.set(rgb2hash(red, green, blue))

    def ahandle(self, *args):
        """command callback for opacity

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        alpha = self.avar.get()
        self.avar.set(alpha)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=30*self.e, height=30*self.e)

    def build(self):
        """widget construction

        Parameters
        ----------
        None

        Results
        -------
        None
        """

        fr1 = LabelFrame(self.parent, text='rgb')
        fr1.grid(column=0, row=0)

        rl0 = Label(fr1, text='red  ')
        rl0.grid(column=0, row=0, sticky='s')

        self.rcan = Canvas(fr1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.rcan.grid(column=1, row=0, sticky='s')

        rsc = TtkScale(fr1, from_=0, to=255, variable=self.rvar, orient='horizontal',
                       length=self.scale_l, command=self.rhandle, tickinterval=20,
                       enlargement=self.e)
        rsc.grid(column=1, row=1, sticky='nw')

        vcmdsb = root.register(sb_okay)

        rsb = Spinbox(fr1, from_=0, to=255, textvariable=self.rvar, validate='key',
                      validatecommand=(vcmdsb, '%i', '%P', '%S'),
                      command=self.rhandle, width=5)
        rsb.grid(column=2, row=1, sticky='nw')
        rsb.bind('<KeyRelease>', self.checksb)

        rel = Label(fr1, height=1)
        rel.grid(column=2, row=2)

        gl0 = Label(fr1, text='green')
        gl0.grid(column=0, row=3)

        self.gcan = Canvas(fr1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.gcan.grid(column=1, row=3, sticky='s')

        gsc = TtkScale(fr1, from_=0, to=255, variable=self.gvar, orient='horizontal',
                       length=self.scale_l, command=self.ghandle, tickinterval=20,
                       enlargement=self.e)
        gsc.grid(column=1, row=4, sticky='nw')

        gsb = Spinbox(fr1, from_=0, to=255, textvariable=self.gvar, validate='key',
                      validatecommand=(vcmdsb, '%i', '%P', '%S'),
                      command=self.ghandle, width=5)
        gsb.grid(column=2, row=4, sticky='nw')
        gsb.bind('<KeyRelease>', self.checksb)

        gel = Label(fr1, height=1)
        gel.grid(column=2, row=5)

        bl0 = Label(fr1, text='blue ')
        bl0.grid(column=0, row=6, sticky='s')

        self.bcan = Canvas(fr1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.bcan.grid(column=1, row=6, sticky='n')

        bsc = TtkScale(fr1, from_=0, to=255, variable=self.bvar, orient='horizontal',
                       length=self.scale_l, command=self.bhandle, tickinterval=20,
                       enlargement=self.e)
        bsc.grid(column=1, row=7, sticky='nw')

        bsb = Spinbox(fr1, from_=0, to=255, textvariable=self.bvar, validate='key',
                      validatecommand=(vcmdsb, '%i', '%P', '%S'),
                      command=self.bhandle, width=5)
        bsb.grid(column=2, row=7, sticky='nw')
        bsb.bind('<KeyRelease>', self.checksb)

        bel = Label(fr1, height=1)
        bel.grid(column=2, row=8)

        fr3 = LabelFrame(self.parent, text='colour mix')
        fr3.grid(column=1, row=0, sticky='nw')

        self.cmcan = cmcan = Canvas(fr3, width=30*self.e, height=30*self.e, bd=0,
                                    highlightthickness=0)
        cmcan.grid(column=0, row=0, sticky='n', columnspan=2)
        cmcan.grid_propagate(0)
        vdraw_gradient(self.cmcan, (255, 0, 0), self.e, alpha=255)

        cml = Label(fr3, text='hash\nvalue')
        cml.grid(column=0, row=1)

        vcmd = root.register(is_okay)
        self.ent0 = ent0 = Entry(fr3, width=8, validate='key',
                                 validatecommand=(vcmd, '%i', '%P', '%S'), textvariable=self.evar)
        ent0.grid(column=1, row=1)
        ent0.bind('<KeyRelease>', self.checkhash)

        fr2 = LabelFrame(self.parent, text='opacity')
        fr2.grid(column=0, row=1, sticky='nsw')

        al0 = Label(fr2, text='alpha')
        al0.grid(column=0, row=0, sticky='s')

        self.acan = Canvas(fr2, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.acan.grid(column=1, row=0, sticky='n')

        asc = TtkScale(fr2, from_=0, to=255, variable=self.avar, orient='horizontal',
                       length=self.scale_l, command=self.ahandle, tickinterval=20,
                       enlargement=self.e)
        asc.grid(column=1, row=1, sticky='nw')

        asb = Spinbox(fr2, from_=0, to=255, textvariable=self.avar, validate='key',
                      validatecommand=(vcmdsb, '%i', '%P', '%S'),
                      command=self.ahandle, width=5)
        asb.grid(column=2, row=1, sticky='nw')
        asb.bind('<KeyRelease>', self.checksba)

        ael = Label(fr2, text=' ', height=1)
        ael.grid(column=2, row=2, sticky='s')

        draw_gradient(self.rcan, (0, 0, 0), (255, 0, 0),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.gcan, (255, 0, 0), (255, 255, 0),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (255, 0, 0), (255, 0, 255),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (255, 0, 0),
                       self.e, width=self.canvas_w, height=self.canvas_h)

    def checkhash(self, evt):
        """Procedure called by entry for hash

        Parameters
        ----------
        evt : str
            bind handles

        Results
        -------
        None
        """

        hash0 = self.ent0.get()
        if len(hash0) == 7:
            red, green, blue = hash2rgb(hash0)
            alpha = self.avar.get()
            self.rvar.set(red)
            self.gvar.set(green)
            self.bvar.set(blue)
            draw_agradient(self.acan, (127, 127, 127), (red, green, blue), self.e,
                           width=self.canvas_w, height=self.canvas_h)
            draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                          width=self.canvas_w, height=self.canvas_h)
            draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                          width=self.canvas_w, height=self.canvas_h)
            draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                          width=self.canvas_w, height=self.canvas_h)
            vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=30*self.e, height=30*self.e)

    def checksba(self, evt):
        """Procedure called by alpha spinbox

        Parameters
        ----------
        evt : str
            bind handles

        Results
        -------
        None
        """

        alpha = self.avar.get()
        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=30*self.e, height=30*self.e)

    def checksb(self, evt):
        """Procedure called by colour spinboxes

        Parameters
        ----------
        evt : str
            bind handles

        Results
        -------
        None
        """

        alpha = self.avar.get()
        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=30*self.e, height=30*self.e)
        self.evar.set(rgb2hash(red, green, blue))


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")
    enlargement = e = int(scaling / BASELINE + 0.5)

    img = Image.new("RGBA", (16*e, 10*e), '#00000000')
    trough = ImageTk.PhotoImage(img)

    # constants for creating upward pointing arrow
    WIDTH = 17*e
    HEIGHT = 17*e
    OFFSET = 5*e
    ST0 = WIDTH // 2, HEIGHT - 1 - OFFSET
    LIGHT = 'GreenYellow'
    MEDIUM = 'LawnGreen'
    DARK = '#5D9B90'

    # normal state
    im = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
    rdraw = ImageDraw.Draw(im)
    rdraw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
                   WIDTH - 1, HEIGHT - 1], fill=LIGHT)
    rdraw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=MEDIUM)
    rdraw.polygon([ST0[0], ST0[1], WIDTH - 1,
                   HEIGHT - 1, ST0[0], 0], fill=DARK)
    slider = ImageTk.PhotoImage(im)

    # pressed state
    imp = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
    draw = ImageDraw.Draw(imp)
    draw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
                  WIDTH - 1, HEIGHT - 1], fill=LIGHT)
    draw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=DARK)
    draw.polygon([ST0[0], ST0[1], WIDTH - 1,
                  HEIGHT - 1, ST0[0], 0], fill=MEDIUM)
    sliderp = ImageTk.PhotoImage(imp)

    style = Style()
    style.theme_settings('default', {
        'Horizontal.Scale.trough': {"element create":
                                    ('image', trough,
                                     {'border': 0, 'sticky': 'wes'})},
        'Horizontal.Scale.slider': {"element create":
                                    ('image', slider,
                                     ('pressed', sliderp),
                                     {'border': 3*e, 'sticky': 'n'})}})

    style.theme_use('default')
    style.configure('TSpinbox', arrowsize=10*e)
    fr = Frame(root)
    fr.grid(row=0, column=0, sticky='nsew')
    RgbSelect(fr, enlargement)
    root.mainloop()

Now that the rgba has been almost finalised develop the hsv along generally similar lines. Many of the calling functions should stay similar, so it is relatively straightforward to import and leave the application uncluttered. That's the theory at least.