Wednesday, April 10, 2013

Resistor Calculator wxPython

So recently I decided to take a serious attempt at programming with using a GUI. While it has not been easy, the results are very satisfying. This program was entirely from scratch to get my feet wet, but I think my next project will be converting an already existing program to a GUI.

I chose wx over Tkinter for a couple reasons. The first was wx looks native while Tkinter looks like a Java application. Also, much like a Java application, however the windows are made with Tkinter, they have a bad habit of glitching out and making pieces illegible. The downside to wx is that it is not included with Python, however the wx packages are easy enough to get a hold of (in many different languages) and easy to install.

I'm happy with the end result, both the GUI and the code. I spent some extra time afterwards cleaning up the code, expanding variable names and commenting. This proved useful since I deleted a bunch of code that didn't do anything. Granted it's not perfect, I'm happy that overall it works.

You can check it out on my site here, or just copy/paste the source code.

#!/usr/bin/env python
# Author: Matthew DeSantis
#
# Resistor Calc

import wx

# Colors aranged by value (index=value)
COLOR = [
    'black',
    'brown',
    'red',
    'orange',
    'yellow',
    'green',
    'blue',
    'violet',
    'grey',
    'white']

# Colors because the default ones suck
COLORCODE = {
    "black":    "#000000",
    "brown":    "#a52a2a",
    "red":      "#ff0000",
    "orange":   "#ffa500",
    "yellow":   "#ffff00",
    "green":    "#008000",
    "blue":     "#0000ff",
    "violet":   "#800080",
    "grey":     "#808080",
    "white":    "#ffffff",
    "silver":   "#c0c0c0",
    "gold":     "#d4a017"}

# Multipliers in a dictionary, organized by value for ease of reading
MULTIPLIER = {
    1e-2:   'silver',
    1e-1:   'gold',
    1e0:    'black',
    1e1:    'brown',
    1e2:    'red',
    1e3:    'orange',
    1e4:    'yellow',
    1e5:    'green',
    1e6:    'blue',
    1e7:    'violet',}

# Converts a number to a list of resistor colors of equivalent value
def getColors(num):
    # To-be return value
    returnVals = []
    failReturnVals = [COLOR[0], COLOR[0], MULTIPLIER[1e0]]
    # Process the number
    indexPlaceholder = 0
    numStr = str(num)
    bandValues = ""

    # For anything smaller than 0
    if num < 1 and num >= 0.01:
        while indexPlaceholder < len(numStr):
            # Discard leading 0's and decimal
            if numStr[indexPlaceholder] == "0" or numStr[indexPlaceholder] == ".":
                indexPlaceholder += 1
            else:
                break

        # It's all zeroes, time to leave
        else:
            return failReturnVals

        bandValues = numStr[indexPlaceholder:]

        # Short a value, needs a black band
        if len(bandValues) < 2:
            bandValues = bandValues + "0"

        # Ready return color values
        before = True

        for value in bandValues:
            if len(returnVals) < 2:
                returnVals.append(COLOR[int(value)])
            elif value != "0":
                ret.append(COLOR[int(value)])
            else:
                break

    # And everything else
    elif num >= 1 and num <= 9990000000:
        indexPlaceholder = 2
        bandValues = numStr[:indexPlaceholder]

        # If there's 4 stripes
        if (num / float(bandValues)) % 10:
            indexPlaceholder = 3
            bandValues = numStr[:indexPlaceholder]

        # If there's a third band to add
        if len(numStr) > indexPlaceholder:
            if numStr[indexPlaceholder] != "0":
                bandValues = numStr[:indexPlaceholder + 1]

        # Needs another black band
        if len(bandValues) < 2:
            bandValues = bandValues + "0"

        for value in bandValues:
            if value == ".":
                continue

            returnVals.append(COLOR[int(value)])

    # Outside the limit
    else:
        return failReturnVals

    # Get the multiplier
    returnVals.append(MULTIPLIER[round(num / float(bandValues.replace(".", "")), 2)])

    return returnVals

#List of colors to Ohms
def getOhms(colors):
    bandValues = ""
    # Key storage
    multiply = 0

    # Get the index numbers of the colors
    while len(colors) > 1:
        bandValues = bandValues + str(COLOR.index(colors.pop(0)))

    # Find the key (multiplier) based on color (no easy way to do this)
    for key, value in MULTIPLIER.items():
        if value == colors[0]:
            multiply = key
            break

    # Color value * multiplier
    return float(bandValues) * multiply

def main():
    # Initialize app
    app = wx.App()
    frame = Window(None, "Resistor Calc")
    frame.display()
    app.MainLoop()

# The GUI class setup
class Window(wx.Frame):
    def __init__(self, parent, title):
        # Initialize the frame
        super(Window, self).__init__(
            parent,
            title=title,
            style=wx.DEFAULT_FRAME_STYLE,
            size=(550, 200))

        # UI setup
        self.uiInit()
        self.evtInit()
        self.colorSet()

        # Control variables
        self.lock = False
        self.prvs = self.ohmstxt.GetValue()

    def uiInit(self):
        # Third band list (needs optional added)
        self.r3colors = COLOR[:]
        self.r3colors.insert(0, "-")

        # Fourth band has some extra as well (organize by values)
        self.r4colors = COLOR[:-2]
        self.r4colors.insert(0, "gold")
        self.r4colors.insert(0, "silver")

        # Create panel and sub panels for colored boxes
        self.panel = wx.Panel(self)

        self.pr1 = wx.Panel(self.panel)
        self.pr2 = wx.Panel(self.panel)
        self.pr3 = wx.Panel(self.panel)
        self.pr4 = wx.Panel(self.panel)

        # Initialize font
        font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
        font.SetPointSize(12)

        # Boxes for resizing and aligning
        # 3 tall
        self.vbox = wx.BoxSizer(wx.VERTICAL)
        hbox1 = wx.BoxSizer(wx.HORIZONTAL)
        hbox2 = wx.BoxSizer(wx.HORIZONTAL)
        hbox3 = wx.BoxSizer(wx.HORIZONTAL)

        # First row
        lblohms = wx.StaticText(self.panel, label="Ohms:")
        lblohms.SetFont(font)

        self.ohmstxt = wx.TextCtrl(self.panel, value="0.0")

        # Second row
        lblcolor = wx.StaticText(self.panel, label="Colors:")
        lblcolor.SetFont(font)

        self.r1 = wx.ComboBox(
            self.panel,
            choices=COLOR,
            style=wx.CB_READONLY)

        self.r2 = wx.ComboBox(
            self.panel,
            choices=COLOR,
            style=wx.CB_READONLY)

        self.r3 = wx.ComboBox(
            self.panel,
            choices=self.r3colors,
            style=wx.CB_READONLY)

        self.r4 = wx.ComboBox(
            self.panel,
            choices=self.r4colors,
            style=wx.CB_READONLY)

        # Create first row
        hbox1.Add(lblohms, flag=wx.RIGHT, border=8)
        hbox1.Add(self.ohmstxt, proportion=1)

        # Create second row
        hbox2.AddMany([
            (lblcolor, wx.RIGHT, 8),
            (self.r1,),
            (self.r2,),
            (self.r3,),
            (self.r4,)])

        # Create third row
        hbox3.AddMany([
            (self.pr1, 1, wx.EXPAND),
            (self.pr2, 1, wx.EXPAND),
            (self.pr3, 1, wx.EXPAND),
            (self.pr4, 1, wx.EXPAND)])

        # Insert rows with padding
        self.vbox.Add(hbox1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, border=10)
        self.vbox.Add((-1, 10))
        self.vbox.Add(hbox2, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=10)
        self.vbox.Add((-1, 10))
        self.vbox.Add(hbox3, 1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=10)
        self.vbox.Add((-1, 10))

        # Sets everything in place on the window
        self.panel.SetSizer(self.vbox)

        # Set combobox selections (not intialized by default
        self.r1.SetSelection(0)
        self.r2.SetSelection(0)
        self.r3.SetSelection(0)
        self.r4.SetSelection(2)

    # Initialize events
    def evtInit(self):
        self.Bind(wx.EVT_COMBOBOX, self.onCBChange)
        self.Bind(wx.EVT_TEXT, self.onTextChange)

    # Combobox was changed
    def onCBChange(self, e):
        self.lock = True

        bandColors = []
        thirdBand = self.r3.GetValue()

        bandColors.append(self.r1.GetValue())
        bandColors.append(self.r2.GetValue())

        # Third band is optional, check to see if it's actually used
        if thirdBand != "-":
            bandColors.append(thirdBand)

        bandColors.append(self.r4.GetValue())

        self.ohmstxt.SetValue(str(getOhms(bandColors)))
        self.colorSet()

    # Text change
    def onTextChange(self, e):
        ohms = self.ohmstxt.GetValue()

        # This is because event fires twice for some unknown reason
        if self.lock or ohms == self.prvs:
            self.prvs = ohms
            self.lock = False
            return None

        # If no value given, default to zero
        if not ohms:
            ohms = "0"

        try:
            bandColors = getColors(float(ohms))
            self.r1.SetSelection(COLOR.index(bandColors[0]))
            self.r2.SetSelection(COLOR.index(bandColors[1]))

            # All 4 bands are present
            if len(bandColors) > 3:
                self.r3.SetSelection(self.r3colors.index(bandColors[2]))
                self.r4.SetSelection(self.r4colors.index(bandColors[3]))
            # Third band is not present
            else:
                self.r3.SetSelection(0)
                self.r4.SetSelection(self.r4colors.index(bandColors[2]))

            self.colorSet()

        # Not a valid number or possible value
        except (ValueError, KeyError):
            pass

    # Display the colors for the bands
    def colorSet(self):
        self.pr1.SetBackgroundColour(COLORCODE[self.r1.GetValue()])
        self.pr2.SetBackgroundColour(COLORCODE[self.r2.GetValue()])

        # If band 3 isn't used, don't show it
        if self.r3.GetValue() == "-":
            self.pr3.SetBackgroundColour(wx.NullColour)
        else:
            self.pr3.SetBackgroundColour(COLORCODE[self.r3.GetValue()])

        self.pr4.SetBackgroundColour(COLORCODE[self.r4.GetValue()])

    def display(self):
        self.Centre
        self.Show()

if __name__ == '__main__':
    main()

No comments:

Post a Comment