Create a Standalone Theme#

Using information gained from existing themes, we'll try to use common images as done in the ttktheme Blue, and borrowing heavily from the Ubuntu theme for style. The standalone theme in python can be easily tested, although if you are competent in TCL I'm sure it's just as easy to test with that language.

After the first attempt or so it was found that the widget sizes had to be changed, so common images were not always possible. This theme will use images for most of the varying widgets and their states. Many widgets in ttktheme simply reused the standard theme, sometimes altering the colour of an element or two.

Common Imported Code#

Using one starting image it should be possible to create most, if not all, of the other images for a single widget. Use a class from roundrect that can create a rounded rectangle with a plain or gradient filled inner part. The border can be single or double. Each of these four options can be made with an open side as used in tabs for the notebook. Supply the size, enlargement factor, gap (radius), fill and gradient colours whether it should be treated as a tab.

Options for roundrect.py#

Class

Border

Internal Fill

Base_Rect

single

plain

Bi_Base_Rect

double

plain

Gr_Base_Rect

single

gradient

GR_Bi_Base_Rect

double

gradient

Each option provides a completed image.

Show/Hide Code roundrect.py

"""
provides common methods to generate widgets

Base_Rect single border, plain fill
Bi_Base_Rect double border, plain fill
Gr_Base_Rect single border, gradient fill
Gr_Bi_Base_Rect double border, gradient fill

All methods can make an open side as used in notebook tab
"""

from PIL import Image, ImageDraw

class Base_Rect():
    """ base class for rounded rectangles, single border, no gradient"""

    def __init__(self,fout,w,h,exp,radius,first,second,tab=0):
        """Creates widget single border, plain fill

        Parameters
        ----------
        fout: str
            output png file
        w,h: int
            width, height output file
        exp: int
            enlargment factor
        radius: int
            corner radius or gap in border
        first: tuple of integers
            outer border
        second: tuple of integers
            internal fill
         tab: int
            0 normal widget - default, 1 open ended tab
        """
        self.fout = fout
        self.w = w
        self.h = h
        self.exp = exp
        self.radius = radius
        self.first = first
        self.second = second
        self.tab = tab

        we = w*exp
        he = h*exp
        re = radius*exp

        img = self.base(we,he,exp,re,first,second,tab)
        self.trans(img,fout,w,h,radius)

    def base(self,we,he,exp,re,first,second,tab):
        """
        Draws base rectangle with rounded corners

        Parameters
        ----------
        we,he: int
            enlarged width, height
        exp: int
            enlargement factor
        re: int
            enlarged corner radius or gap in border
        first: tuple of integers
            outer border
        second: tuple of integers
            internal fill
        tab: int
            0 normal widget - default, 1 open ended tab

        Returns
        -------
        PIL Image handle
        """
        rect = Image.new('RGBA', (we,he), first)
        rdraw = ImageDraw.Draw(rect)

        tex = (0 if tab==1 else exp)
        rdraw.rectangle([exp,exp,we-exp-1,he-tex-1], fill=second)

        corner = self.round_corner(re, exp, first,second)
        if tab == 0:
            rect.paste(corner.rotate(90), (0, he - re))
            rect.paste(corner.rotate(180), (we - re, he - re))
        rect.paste(corner, (0, 0))
        rect.paste(corner.rotate(270), (we - re, 0))
        return rect

    def trans(self,img,fout,w,h,radius):
        """
        Resizes image to final size and makes transparent corners

        Parameters
        ----------
        img: str
            PIL image handle
        fout: str
            output png file
        w,h: int
            width, height output file
        radius: int
            corner radius or gap in border
        """
        img = img.resize((w,h),Image.LANCZOS)

        pixdata = img.load()
        clear = radius//2
        # create transparent pixels at 4 corners
        for x in range(0, img.size[0]):
            for y in range(0, img.size[1]):
                # find near white pixels
                if (x<clear and y<clear) or (x<clear and y>h-clear-1)\
                    or (x>w-clear-1 and y<clear) or (x>w-clear-1 and y>h-clear-1):
                    if sum(pixdata[x,y][:3]) >= 759:
                        pixdata[x,y] = (255,255,255,0)

        img.save(fout)
        return(img)

    def round_corner(self, rad, exp, ofill, ifill):
        """Draw a round corner, single border

        Parameters
        ----------
        rad: int
            corner radius or gap in border
        exp: int
            enlargement factor
        ofill: tuple of integers
            outer border
        ifill: tuple of integers
            internal fill

        Returns
        -------
        corner: str
            handle PIL Image
        """
        corner = Image.new('RGBA', (rad, rad), 'white')
        cdraw = ImageDraw.Draw(corner)
        self.create_pie(cdraw,[rad,rad],rad,fill=ofill)
        self.create_pie(cdraw,[rad,rad],rad-exp,fill=ifill)
        return corner

    def bi_round_corner(self, rad, exp, ofill, mfill, ifill):
        """Draw a round corner, double border

        Parameters
        ----------
        rad: int
            corner radius or gap in border
        exp: int
            enlargement factor
        ofill: tuple of integers
            outer border
        mfill: tuple of integers
            inner border
        ifill: tuple of integers
            internal fill

        Returns
        -------
        corner: str
            handle PIL Image
        """
        corner = Image.new('RGBA', (rad, rad), 'white')
        cdraw = ImageDraw.Draw(corner)
        self.create_pie(cdraw,[rad,rad],rad,fill=ofill)
        self.create_pie(cdraw,[rad,rad],rad-exp,fill=mfill)
        self.create_pie(cdraw,[rad,rad],rad-2*exp,fill=ifill)
        return corner

    def create_pie(self, idraw,c,r,fill='#888888',start=180,end=270):
        """
        create pieslice using centre and radius, outline not used

        defaults to 90° in upper left quadrant
        Parameters
        ----------
        idraw: str
            PIL drawing handle
        c: int
            centre co-ordinates
        r: int
            radius
        fill: str or tuple of int
            fill colour name, hash or tuple
        start, end: int
            start and end angles in degrees
        """
        return idraw.pieslice([c[0]-r,c[1]-r,c[0]+r-1,c[1]+r-1],
                          fill=fill,start=start,end=end)

    def LerpColour(self,c1,c2,t):
        """
        Gives the colour found between 2 colours

        Parameters
        ----------
        c1, c2: tuple of integers
            Colours in rgb
        t: decimal
            Ratio of colour mix 0 <= t <= 1

        Returns
        -------
            colour in rgb
        """
        return (int(c1[0]+(c2[0]-c1[0])*t),int(c1[1]+(c2[1]-c1[1])*t),
            int(c1[2]+(c2[2]-c1[2])*t))

    def icol(self, startc, endc, steps, re, exp):
        """ Find the intermediate colours on the gradient

        Parameters
        ----------
        startc, endc: tuple of integers
            Colours in rgb
        steps: int
            number colour steps
        exp: int
            enlargement factor
        re: int
            enlarged corner radius or gap in border

        Returns
        -------
        startci, endci: tuple of integers
            Colours in rgb
        """
        y = (re - exp)/2
        startci = self.LerpColour(startc, endc, y/steps)
        endci = self.LerpColour(endc, startc, y/steps)
        return startci, endci


class Bi_Base_Rect(Base_Rect):
    # base class for rounded rectangles, double border, no gradient
    def __init__(self,fout,w,h,exp,radius,first,second,third,tab=0):
        '''Creates widget double border, plain fill

        Parameters
        ----------
        fout: str
            output png file
        w,h: int
            width, height output file
        exp: int
            enlargment factor
        radius: int
            corner radius or gap in border
        first: tuple of integers
            outer border
        second: tuple of integers
            inner border
        third: tuple of integers
            internal fill
         tab: int
            0 normal widget - default, 1 open ended tab
        '''
        self.fout = fout
        self.w = w
        self.h = h
        self.exp = exp
        self.radius = radius
        self.first = first
        self.second = second
        self.tab = tab
        Base_Rect.__init__(self,fout,w,h,exp,radius,first,second,tab)
        self.third = third

        we = w*exp
        he = h*exp
        re = radius*exp

        img = self.bi_base(we,he,exp,re,first,second,third,tab)
        self.trans(img,fout,w,h,radius)

    def bi_base(self,we,he,exp,re,first,second,third,tab):
        """
        Draws base rectangle with rounded corners

        Parameters
        ----------
        we,he: int
            enlarged width, height
        exp: int
            enlargement factor
        re: int
            enlarged corner radius or gap in border
        first: tuple of integers
            outer border
        second: tuple of integers
            inner border
        third: tuple of integers
            internal fill
        tab: int
            0 normal widget - default, 1 open ended tab

        Returns
        -------
            PIL Image handle
        """
        rect = Image.new('RGBA', (we,he), first)
        rdraw = ImageDraw.Draw(rect)

        tex = (0 if tab==1 else exp)
        rdraw.rectangle([exp,exp,we-exp-1,he-tex-1], fill=second)
        rdraw.rectangle([2*exp,2*exp,we-2*exp-1,he-2*tex-1], fill=third)

        corner = self.bi_round_corner(re, exp, first,second,third)
        if tab == 0:
            rect.paste(corner.rotate(90), (0, he - re))
            rect.paste(corner.rotate(180), (we - re, he - re))
        rect.paste(corner, (0, 0))
        rect.paste(corner.rotate(270), (we - re, 0))
        return rect

class Gr_Base_Rect(Base_Rect):
    # class for rounded rectangles, single border, with gradient
    def __init__(self,fout,w,h,exp,radius,first,second,startc,stopc,tab=0):
        '''Creates widget single border, gradient fill

        Parameters
        ----------
        fout: str
            output png file
        w,h: int
            width, height output file
        exp: int
            enlargment factor
        radius: int
            corner radius or gap in border
        first: tuple of integers
            outer border
        second: tuple of integers
            internal fill
        startc: tuple of integers
            gradient start colour - must be RGB
        stopc: tuple of integers
            gradient finish colour - must be RGB
         tab: int
            0 normal widget - default, 1 open ended tab

        '''
        self.fout = fout
        self.w = w
        self.h = h
        self.exp = exp
        self.radius = radius
        self.first = first
        self.second = second
        self.tab = tab
        Base_Rect.__init__(self,fout,w,h,exp,radius,first,second,tab)
        self.startc = startc
        self.stopc = stopc

        we = w*exp
        he = h*exp
        re = radius*exp

        img = self.gr_base(we,he,exp,re,first,second,startc,stopc,tab)
        self.trans(img,fout,w,h,radius)

    def gr_base(self,we,he,exp,re,first,second,startc,stopc,tab):
        """Draws gradient filled rectangle with rounded corners

        Parameters
        ----------
        we,he: int
            enlarged width, height
        exp: int
            enlargement factor
        re: int
            enlarged corner radius or gap in border
        first: tuple of integers
            outer border
        second: tuple of integers
            internal fill
        startc: tuple of integers
            gradient start colour - must be RGB
        stopc: tuple of integers
            gradient finish colour - must be RGB
        tab: int
            0 normal widget - default, 1 open ended tab

        Returns
        -------
            PIL Image handle
        """
        rect = Image.new('RGBA', (we,he), first)
        rdraw = ImageDraw.Draw(rect)

        tex = (0 if tab==1 else exp)
        steps = he - 2*tex

        for j in range(steps):
            cr,cg,cb = self.LerpColour(startc,stopc,j/(steps))
            rdraw.line([exp,j+exp,we-exp-1,j+exp],fill=(cr,cg,cb))

        inter = self.icol(startc, stopc, steps, re, exp)
        ufill = inter[0]
        lfill = inter[1]
        ucorner = self.round_corner(re, exp, first,ufill)
        lcorner = self.round_corner(re, exp, first,lfill)
        if tab == 0:
            rect.paste(lcorner.rotate(90), (0, he - re))
            rect.paste(lcorner.rotate(180), (we - re, he - re))
        rect.paste(ucorner, (0, 0))
        rect.paste(ucorner.rotate(270), (we - re, 0))
        return rect

class Gr_Bi_Base_Rect(Gr_Base_Rect):
    # class for rounded rectangles, double border, with gradient
    def __init__(self,fout,w,h,exp,radius,first,second,third,startc,stopc,tab=0):
        '''Creates widget double border, gradient fill

        Parameters
        ----------
        fout: str
            output png file
        w,h: int
            width, height output file
        exp: int
            enlargment factor
        radius: int
            corner radius or gap in border
        first: tuple of integers
            outer border
        second: tuple of integers
            inner border
        third: tuple of integers
            internal fill
        startc: tuple of integers
            gradient start colour - must be RGB
        stopc: tuple of integers
            gradient finish colour - must be RGB
         tab: int
            0 normal widget - default, 1 open ended tab
        '''
        self.fout = fout
        self.w = w
        self.h = h
        self.exp = exp
        self.radius = radius
        self.first = first
        self.second = second
        self.tab = tab
        self.startc = startc
        self.stopc = stopc
        #Bi_Base_Rect.__init__(self,fout,w,h,exp,radius,first,second,third,tab)


        we = w*exp
        he = h*exp
        re = radius*exp

        img = self.gr_bi_base(we,he,exp,re,first,second,third,startc,stopc,tab)
        self.trans(img,fout,w,h,radius)

    def gr_bi_base(self,we,he,exp,re,first,second,third,startc,stopc,tab):
        """Draws gradient filled rectangle, double border with rounded corners

        Parameters
        ----------
        we,he: int
            enlarged width, height
        exp: int
            enlargement factor
        re: int
            enlarged corner radius or gap in border
        first: tuple of integers
            outer border
        second: tuple of integers
            inner border
        third: tuple of integers
            internal fill
        startc: tuple of integers
            gradient start colour - must be RGB
        stopc: tuple of integers
            gradient finish colour - must be RGB
        tab: int
            0 normal widget - default, 1 open ended tab

        Returns
        -------
            PIL Image handle
        """
        rect = Image.new('RGBA', (we,he), first)
        rdraw = ImageDraw.Draw(rect)
        tex = (0 if tab==1 else exp)
        steps = he - 2*exp  - 2*tex

        rdraw.rectangle([exp,exp,we-exp-1,he-tex-1], fill=second)
        for j in range(steps):
            cr,cg,cb = self.LerpColour(startc,stopc,j/(steps))
            rdraw.line([2*exp,j+2*exp,we-2*exp-1,j+2*exp],fill=(cr,cg,cb))

        inter = self.icol(startc, stopc, steps, re, exp)
        ufill = inter[0]
        lfill = inter[1]

        ucorner = self.bi_round_corner(re, exp, first,second,ufill)
        lcorner = self.bi_round_corner(re, exp, first,second,lfill)
        if tab == 0:
            rect.paste(lcorner.rotate(90), (0, he - re))
            rect.paste(lcorner.rotate(180), (we - re, he - re))
        rect.paste(ucorner, (0, 0))
        rect.paste(ucorner.rotate(270), (we - re, 0))
        return rect

if __name__ == "__main__":

    Fout = 'test.png'
    W=25
    H=25
    Exp = 9 # enlargement, also thickness between radii
    Radius = 4 # gap
    '''
    First = '#5D9B90'
    Second = 'white'
    Third = None
    '''
    First = '#A3CCC4'
    Second = '#5D9B90'
    Third = 'white'

    Tab = 0
    Startc = (222,247,222)
    Stopc = (143,188,143)

    #Base_Rect(Fout, W, H, Exp, Radius, First, Second, Tab)
    #Bi_Base_Rect(Fout, W, H, Exp, Radius, First, Second, Third, Tab)
    #Gr_Base_Rect(Fout,W,H,Exp,Radius,First,Second,Startc,Stopc,Tab)
    Gr_Bi_Base_Rect(Fout,W,H,Exp,Radius,First,Second,Third,Startc,Stopc,Tab)


Where a non-standard widget is created use tools.py, which can be supplemented as necessary. The toolbox only uses functions, so it is possible to work with an image that is already opened in the calling program, make the change then return back to the original image. When creating a border with its own gradient there are actions that need to be made before the image is resized, so the complete widget would not be suitable. Examples are given in the alternative button, scale trough and progressbar.

Only gr_2d_rect creates an almost complete widget, used to make a simple border with 2d gradient based on rectangles, otherwise the other functions are called individually.

Show/Hide Code tools.py

"""
Toolbox of functions used while creating theme
"""

from PIL import Image, ImageDraw
from math import sqrt

def LerpColour(c1,c2,t):
    """
    Gives the colour found between 2 colours

    Parameters
    ----------
    c1, c2: tuple of integers
        Colours in rgb
    t: decimal
        Ratio of colour mix 0 <= t <= 1

    Returns
    -------
        colour in rgb
    """
    return (int(c1[0]+(c2[0]-c1[0])*t),int(c1[1]+(c2[1]-c1[1])*t),
            int(c1[2]+(c2[2]-c1[2])*t))

def create_circle(idraw,c,r,fill):
    """
    create circle using centre and radius

    Parameters
    ----------
        idraw: str
            PIL drawing handle
        c: int
            centre co-ordinates
        r: int
            radius
        fill: str or tuple of int
            fill colour name, hash or tuple

    Returns
    -------
        Circle
    """
    return idraw.ellipse([c[0]-r,c[1]-r,c[0]+r-1,c[1]+r-1],
            fill=fill)

def create_pie(idraw,c,r,fill='#888888',start=180,end=270):
    """
    create pieslice using centre and radius, outline not used

    defaults to 90° in upper left quadrant
    Parameters
    ----------
        idraw: str
            PIL drawing handle
        c: int
            centre co-ordinates
        r: int
            radius
        fill: str or tuple of int
            fill colour name, hash or tuple
        start, end: int
            start and end angles in degrees
    """
    return idraw.pieslice([c[0]-r,c[1]-r,c[0]+r-1,c[1]+r-1],
                          fill=fill,start=start,end=end)

def icol(startc, stopc, steps, re, exp):
    """ find the intermediate colours on the gradient

    Parameters
    ----------
        startc: tuple of int
            start rgb colour
        stopc: tuple of int
            finish rgb colour
        steps: int
            number steps in gradient
        re: int
            enlarged radius
        exp: int
            enlargement factor
    """
    y = (re - exp)/2
    startci = LerpColour(startc, stopc, y/steps)
    stopci = LerpColour(stopc, startc, y/steps)
    return startci, stopci

def trans(img,w,h,radius,cmax=759):
    """
    create transparent pixels at 4 corners

    Parameters
    ----------
        img: str
            PIL drawing handle
        w,h: int
            image size width height
        radius: int
            corner radius/ gap size
        cmax: int
            near white colour sum
    """
    pixdata = img.load()
    clear = radius//2
    for x in range(0, img.size[0]):
        for y in range(0, img.size[1]):
            # find near white pixels
            if (x<clear and y<clear) or (x<clear and y>h-clear-1)\
                or (x>w-clear-1 and y<clear) or (x>w-clear-1 and y>h-clear-1):
                if sum(pixdata[x,y][:3]) >= cmax:
                    pixdata[x,y] = (255,255,255,0)

    return(img)

def transx(img,w,h,cmax=759):
    """
    find all near white pixels and change into transparent pixels

    Parameters
    ----------
        img: str
            PIL drawing handle
        w,h: int
            image size width height
        cmax: int
            near white colour sum
    """
    pixdata = img.load()
    for x in range(0, img.size[0]):
        for y in range(0, img.size[1]):
            # find near white pixels
            rgba = pixdata[x,y]
            if sum(pixdata[x,y][:3]) >= cmax:
                pixdata[x,y] = (255,255,255,0)
    return(img)

def round_corner(rad, exp, ofill, ifill):
    """Draw a round corner, single border

    Parameters
    ----------
    rad: int
        corner radius
    exp: int
        enlargement factor, normally 9
    ofill: str or int
        outer fill
    ifill: str or int
        inner fill

    """
    corner = Image.new('RGBA', (rad, rad), 'white')
    cdraw = ImageDraw.Draw(corner)
    create_pie(cdraw,[rad,rad],rad,fill=ofill)
    create_pie(cdraw,[rad,rad],rad-exp,fill=ifill)
    return corner

def gr_base(we,he,exp,re,first,second,startc,stopc):
    """Single border rounded rectangle with gradient

    Parameters
    ----------
    we, he: int
        width, height of enlarged image
    exp: int
        enlargement factor
    re: int
        enlarged corner radius/ gap size
    first, second: str or int
        colours of corners
    startc: tuple of int
        start rgb colour
    stopc: tuple of int
        finish rgb colour

    """
    rect = Image.new('RGBA', (we,he), first)
    rdraw = ImageDraw.Draw(rect)

    #tex = (0 if tab==1 else exp)
    steps = he - 2*exp

    for j in range(steps):
        cr,cg,cb = LerpColour(startc,stopc,j/(steps))
        rdraw.line([exp,j+exp,we-exp-1,j+exp],fill=(cr,cg,cb))

    inter = icol(startc, stopc, steps, re, exp)
    ufill = inter[0]
    lfill = inter[1]
    ucorner = round_corner(re, exp, first,ufill)
    lcorner = round_corner(re, exp, second,lfill)
    rect.paste(lcorner.rotate(90), (0, he - re))
    rect.paste(lcorner.rotate(180), (we - re, he - re))
    rect.paste(ucorner, (0, 0))
    rect.paste(ucorner.rotate(270), (we - re, 0))
    return rect

def gr_2d_rect(we,he,exp,re,first,second,startc,stopc):
    """Creates most widget single border, gradient fill

    Need to reduce in size and add transparent pixels

    Parameters
    ----------
        we, he: int
            width, height enlarged sizes
        exp: int
            enlargment factor
        re: int
            enlarged radius
        first: tuple of int
            border rgb colour
        second: tuple of int
            fill rgb colour
        startc: tuple of int
            rgb gradient colour
        stopc: tuple of int
            rgb gradient colour
    """
    rect = Image.new('RGBA', (we,he), first)
    rdraw = ImageDraw.Draw(rect)

    steps = we//2+1-exp

    for j in range(steps):
        #j = j//2
        cr,cg,cb = LerpColour(startc,stopc,j/(steps-1))
        rdraw.rectangle([j+exp,j+exp,we-exp-1-j,he-exp-1-j],fill=(cr,cg,cb))

    inter = icol(startc, stopc, steps, re, exp)
    ufill = inter[0]
    lfill = inter[1]
    ucorner = round_corner(re, exp, first, ufill)
    lcorner = round_corner(re, exp, second, ufill)
    rect.paste(lcorner.rotate(90), (0, he - re))
    rect.paste(lcorner.rotate(180), (we - re, he - re))
    rect.paste(ucorner, (0, 0))
    rect.paste(ucorner.rotate(270), (we - re, 0))
    return rect

if __name__ == "__main__":
    fout = '../images/lime/test2.png'
    w=26
    h=24
    radius = 5
    img = Image.open(fout)

    trans(w,h,radius,img)
    transx(w,h,img)

Each widget has its own generation and separate test script. The latter are similar to the 07pirate scripts.

Note

roundrect.py and tools.py

both these scripts are the subjects of the Documentation.