A Quest Begins

I recently stumbled across two little applications that, along with my recent advances in figuring out Tkinter, have finally given a much needed boost of inspiration — and a goal to strive for.

First there was this:

phoneapp

This is a step-by-step tutorial for a simple phone list using Tkinter and SQLite.  The user selects a name from the list and presses the “Load” button to load it into the form at the top.  The form is used to make changes to the data; the list is just for viewing and selecting.  This is what I have wanted to accomplish, boiled down to its absolute simplest level.  This little app also answered a big question for me; how do I get data from an SQLite database table into a Tkinter form for viewing and editing?  The short answer: the Tkinter Listbox widget, which has an INSERT method.

Even better, I found this:

inventory

Now we’re talking! Even though it’s a bit buggy, it’s the closest thing I have yet seen to what I have wanted to accomplish. It’s got a system of relational tables: customers, invoices and items. It has forms that are called when the user wants to add or change data. But its most intriguing item is a custom widget called a MultiListbox:
multilist

I found this object fascinating, because it opened my eyes to the power of Tkinter. It’s actually possible to make custom widgets out of the basic ones provided. The MultiListbox is a set of regular Listboxes squashed together in a frame and synchronized. It lines up each data field in its own column. It accepts data input in the form of tuples, for example: (‘Date’,’Item Number’,’Quantity’,’Price’). Each tuple is a row of data in the MultiListbox.

So, now, at last, I’ve found my quest!

holygrail

I plan to build an app around two basic objects: what I call a “TableForm”, for filtering, viewing and selecting data….
TableForm

..and what I have named a “RecordForm”, which will be used to edit, add, delete or update a record…
RecordForm

These two base objects could be duplicated for each table in the underlying SQLite database.

So here’s the short version of my plan. I’m starting with the coding of the TableForm, since it’s really the heart of my app. I have broken the Table Form down into component classes, starting with the MultiListbox (the most complex part). I plan to code and test each object separately, then piece them all together to make the TableForm class. I’ll then repeat this process with the RecordForm. This rudimentary data management setup could be applied to an endless array of applications, from personal finance to getting data from the Web to tracking my running

In true Python spirit, I wanted to name my app “DataQuest”, but found out there is a company called Dataquest. I didn’t want to get in trouble with them or cause confusion, so I’ve decided to call my app “DataQ.” I can already see the most time-consuming part of development is just figuring out how stuff works. When I first saw the code for the MultiListbox, it might as well have been in Chinese. It has taken some in-depth study of my Tkinter links and a lot of trial-and-error experiments with the code to get it deciphered. I feel like one of the great seafaring explorers of the 15th and 16th centuries, crossing a turbulent, uncharted ocean to a new world, or a biologist trying to decode DNA sequences.

See below for what I have so far in my version of the MultiListbox base class, along with comments I wrote in the code that explain the various parts as I was able to figure out what they did. For all the related files needed to make it work, see my DataQ repository on Github. The repository’s Wiki has all the details, including the latest news on my progress with DataQ, a complete set of GUI mockups and the overall development plan.

# A custom widget consisting of multiple Listbox widgets, with each listbox containing a column of data.
# Based on MultiListBox code used by suhailvs@gmail.com (https://sourceforge.net/projects/pyinvoice/)
# The code I was able to figure out is explained in the comments.
# Functions with comment ???? are ones I haven't figured out yet.

from tkinter import *
class MultiListbox(Frame):
    '''MultiListbox made by Labels as table header and Listbox as table columns'''
    def __init__(self, master, lists):
        Frame.__init__(self, master)
        self.lists = [] # A list of listboxes
        self.footers = [] # List of listbox footers
        for l,w in lists: # l=label, w=width for each listbox
            # Each listbox gets its own frame
            frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH)

            # Label = listbox name
            Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X)

            # Create each listbox
            lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0,
                 relief=FLAT, exportselection=FALSE)
            lb.pack(expand=YES, fill=BOTH)

            # Add a footer for each listbox. Available for calculated totals.
            footer = Label(frame)
            footer.pack(fill=X)

            # Lists = list of numerical references to each listbox (ex .43982632.44021632.44021744.44022080)
            # Keeps track of listboxes after they are created.
            self.lists.append(lb)
            self.footers.append(footer)

            # Each listbox gets these bindings

            # left mouse button pressed. Required so left click selects entire line.
            lb.bind('', lambda e, s=self: s._select(e.y))

            # double click will eventually be what brings up the transaction edit form.
            lb.bind('', lambda e, s=self: s.double_click())

            # These came with the original MLB code but don't seem to do anything.
                #lb.bind('', lambda e, s=self: s._select(e.y)) # left mouse button pressed while moving
                #lb.bind('', lambda e: 'break') # mouse pointer leaves the widget
                #lb.bind('', lambda e, s=self: s._b2motion(e.x, e.y))
                #lb.bind('', lambda e, s=self: s._button2(e.x, e.y)) #right mouse button pressed

        # The frame that will contain the individual list boxes, each with its own frame (see above).
        frame = Frame(self); frame.pack(side=LEFT, fill=Y)
        Label(frame, borderwidth=1, relief=RAISED).pack(fill=X)
        sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll)
        sb.pack(expand=YES, fill=Y)

        # lists[0] = the first (left most) list box.
        self.lists[0]['yscrollcommand']=sb.set

    def double_click(self):
        print(self.item_selected)

    def _select(self, y):
        row = self.lists[0].nearest(y) # integer index of selected row

        # Calls the selection_clear function to un-select whatever row was selected before left button was clicked.
        self.selection_clear(0, END)

        # Calls the selection_set function to select the line clicked on.
        self.selection_set(row)
        return 'break'

    def _button2(self, x, y): #????
        for l in self.lists: l.scan_mark(x, y)
        return 'break'

    def _b2motion(self, x, y): #????
        for l in self.lists: l.scan_dragto(x, y)
        return 'break'

    def _scroll(self, *args): #????
        for l in self.lists:
            l.yview(*args)

    def curselection(self): #????
        return self.lists[0].curselection()

    def delete(self, first, last=None):
        for l in self.lists:
            l.delete(first, last)

    def get(self, first, last=None):
        result = []
        for l in self.lists:
            result.append(l.get(first,last))
        if last: return list(map(*[None] + result))
        return result

    def index(self, index):
        self.lists[0].index(index)

    def insert(self, index, *elements): #Elements = the fields in a row.

        # Loop through each field in the row
        for e in elements:

            # Each field will have an index number. Ex: e[0] = ID, e[1] = Date, etc.
            i = 0
            for l in self.lists:
                # Insert each field in its applicable list box
                # Insert a blank if the field is None, otherwise boxes won't align.
                if e[i] == None:
                    l.insert(index, " ")
                else:
                    l.insert(index, e[i])
                i = i + 1

    def size(self):
        return self.lists[0].size()

    def see(self, index):
        for l in self.lists:
            l.see(index)

    def selection_anchor(self, index):
        for l in self.lists:
            l.selection_anchor(index)

    def selection_clear(self, first, last=None):
        # Unselects the entire line by looping over each of the list boxes that make up the MLB.
        for l in self.lists:
            l.selection_clear(first, last)

    def selection_includes(self, index):
        return self.lists[0].selection_includes(index)

    def selection_set(self, first, last=None):
        # first = the index of the selected row

        # self.item_selected = a list whose 1st item is the index of the row, followed by the entire row.
        # Example:
        # [6,134,'2014-02-03','Publix','',188,'$0.00','$3.48',197,1]

        self.item_selected=[first,]+self.get(first)

        for l in self.lists:
            # Sets the selection to the same row in all list boxes
            l.selection_set(first, last)

    def not_focus(self):
        for l in self.lists:
            l['takefocus']=False

    def calc_total(self,column):
        # Returns the total of the selected column number. Column must contain numeric values only.
        total = 0
        for n in range(0,self.lists[column].size()):
            total = total + float(self.lists[column].get(n))
        return total

    def footer_val(self,column,value):
        # Sets the text to be displayed in the footer of the column specified.
        v = StringVar()
        self.footers[column].config(textvariable=v)
        v.set(value)

If you scrolled all the way through that huge block of code to get here, I’ll bet you are the kind of person who sits in a movie theater and waits for all the credits to finish, hoping for something fun or entertaining at the end, or a sneak peak at a new movie. So as to not disappoint you, here’s an relevant item:
Monty-Python-and-the-Holy-Grail-monty-python-and-the-holy-grail-4968359-845-468