Welcome, guest | Sign In | My Account | Store | Cart
# Version: 0.6
# Author: Miguel Martinez Lopez
# Uncomment the next line to see my email
# print("Author's email: %s"%"61706c69636163696f6e616d656469646140676d61696c2e636f6d".decode("hex"))

import platform

try:
    from Tkinter import Frame, BOTH ,N,E,S, W, END, CENTER, Entry
    import tkFont as tkFont
    from ttk import Treeview, Scrollbar, Style
except ImportError:
    from tkinter import Frame, BOTH ,N,E,S, W, END, CENTER, Entry
    import tkinter.font as tkFont
    from tkinter.ttk import Treeview, Scrollbar, Style

def make_autoscroll(sbar, first, last):
    """Hide and show scrollbar as needed."""
    first, last = float(first), float(last)
    if first <= 0 and last >= 1:
        sbar.grid_remove()
    else:
        sbar.grid()
    sbar.set(first, last)

OS = platform.system()


class Table_Row(object):
    def __init__(self, multicolumn_listbox):
        self._multicolumn_listbox = multicolumn_listbox
        
    def get(self, index):
        return self._multicolumn_listbox.get_row(index)

    def add(self, data, index=END):
        self._multicolumn_listbox.add_row(data, index)

    def delete(self, index):
        self._multicolumn_listbox.delete_row(index)

    def update(self, index, data):
        self._multicolumn_listbox.update_row(index, data)

    def select(self, index):
        self._multicolumn_listbox.select_row(index)

    def deselect(self, index):
        self._multicolumn_listbox.deselect_row(index)

    def set_selection(self, indices):
        self._multicolumn_listbox.set_selection(indices)

    def __getitem__(self, index): 
        return self._multicolumn_listbox.get_row(index)

    def __setitem__(self, index, value): 
        return self._multicolumn_listbox.update_row(index, value)

    def __delitem__(self, index): 
        self._multicolumn_listbox.delete_row(index)

    def __len__(self): 
        return self._multicolumn_listbox.number_of_rows

class Table_Column(object):
    def __init__(self, multicolumn_listbox):
        self._multicolumn_listbox = multicolumn_listbox    
        
    def get(self, index):
        return self._multicolumn_listbox.get_column(index)

    def add(self, data, index=END):
        self._multicolumn_listbox.add_column(data, index)

    def delete(self, index):
        self._multicolumn_listbox.delete_column(index)

    def update(self, index, data):
        self._multicolumn_listbox.update_column(index, data)

    def __getitem__(self, index): 
        return self._multicolumn_listbox.get_column(index)

    def __setitem__(self, index, value): 
        return self._multicolumn_listbox.update_column(index, value)

    def __delitem__(self, index): 
        self._multicolumn_listbox.delete_column(index)

    def __len__(self): 
        return self._multicolumn_listbox.number_of_columns

def bind_function_onMouseWheel(scrolled_widget, orient, binding_widget=None, callback=None, factor = 1):
    if not scrolled_widget:
        binding_widget = scrolled_widget

    view_command = getattr(scrolled_widget, orient+'view')
    
    if OS == 'Linux':
        if callback:
            def onMouseWheel(event):
                if event.num == 4:
                    view_command("scroll",(-1)*factor,"units" )
                elif event.num == 5:
                    view_command("scroll",factor,"units" ) 
                
                callback()
        else:
            def onMouseWheel(event):
                if event.num == 4:
                    view_command("scroll",(-1)*factor,"units" )
                elif event.num == 5:
                    view_command("scroll",factor,"units" ) 

    elif OS == 'Windows':
        if callback:
            def onMouseWheel(event):        
                view_command("scroll",(-1)*int((event.delta/120)*factor),"units" )
                callback()
        else:
            def onMouseWheel(event):        
                view_command("scroll",(-1)*int((event.delta/120)*factor),"units" )
    
    elif OS == 'Darwin':
        if callback:
            def onMouseWheel(event):        
                view_command("scroll",event.delta,"units" )             
                callback()
        else:
            def onMouseWheel(event):        
                view_command("scroll",event.delta,"units" )             

    if OS == "Linux" :
        binding_widget.bind('<4>', onMouseWheel, add='+')
        binding_widget.bind('<5>', onMouseWheel, add='+')
    else:
        # Windows and MacOS
        binding_widget.bind("<MouseWheel>", onMouseWheel,  add='+')
        
    return onMouseWheel
            
class Multicolumn_Listbox(Frame, object):
    _style_index = 0

    def __init__(self, master, columns, command=None, editable=True, sort=True, select_mode=None, autoscroll=True, vscrollbar=True, hscrollbar=False, anchor_heading = CENTER, anchor_data = W, style=None, scrollbar_background=None, scrollbar_troughcolor=None, height=None, padding=None, adjust_heading_to_content=False, stripped_rows=None, entry_background="#1BA1E2", selection_background=None, selection_foreground=None, background=None, foreground=None, font=None, fieldbackground=None, heading_font= None, heading_background=None, heading_foreground=None):

        Frame.__init__(self, master, class_="Multicolumn_Listbox")
        
        self._stripped_rows = stripped_rows
        self._entry_background = entry_background
        
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

        treeview_kwargs = {}
        s = Style()

        if style is None:
            style_name = "Multicolumn_Listbox%s.Treeview"%self._style_index
            self._style_index += 1
        else:
            style_name = style

        treeview_kwargs["style"] = style_name

        if height is not None:
            treeview_kwargs["height"] = height
            
        if padding is not None:
            treeview_kwargs["padding"] = padding
        
        if select_mode is not None:
            treeview_kwargs["selectmode"] = select_mode
        
        style_map = {}
        if selection_background is not None:
            style_map["background"] = [('selected', selection_background)]
            
        if selection_foreground is not None:
            style_map["foeground"] = [('selected', selection_foreground)]

        if style_map is not None:
            s.map(style_name, **style_map)

        style_config = {}
        if background is not None:
            style_config["background"] = background

        if foreground is not None:
            style_config["foreground"] = foreground

        if font is not None:
            style_config["font"] = font

        if fieldbackground is not None:
            style_config["fieldbackground"] = fieldbackground

        s.configure(style_name, **style_config)

        heading_style_config = {}
        if heading_font is not None:
            heading_style_config["font"] = heading_font
        if heading_background is not None:
            heading_style_config["background"] = heading_background
        if heading_foreground is not None:
            heading_style_config["foreground"] = heading_foreground

        heading_style_name = style_name + ".Heading"
        s.configure(heading_style_name, **heading_style_config)
        
        self._treeview = Treeview(self,columns=columns, show="headings", **treeview_kwargs)
        self._treeview.grid(row=0, column=0, sticky= N+E+W+S)

        if command is not None:
            self._command = command
            self._treeview.bind("<<TreeviewSelect>>", self._on_select)

        if editable:
            self._selected_cell = None
            self._entry_popup = None

            self._treeview.bind("<1>", self._edit_cell)
            
            def configure(event):
                """
                if self._entry_popup:
                    self._entry_popup.destroy()
                return
                """

                self._treeview.update_idletasks()
                self._update_position_of_entry()

            self._treeview.bind("<Configure>", configure)
            
        scrollbar_kwargs = {}
        if scrollbar_background is not None:
            scrollbar_kwargs["background"] = scrollbar_background
            
        if scrollbar_troughcolor is not None:
            scrollbar_kwargs["throughcolor"] = scrollbar_troughcolor

        if vscrollbar:
            if editable:
                yview_command = self._yview
            else:
                yview_command = self._treeview.yview

            self._vbar=Scrollbar(self,takefocus=0, command=yview_command, **scrollbar_kwargs)
            self._vbar.grid(row=0, column=1, sticky= N+S)
            
            if autoscroll:
                if editable:
                    def yscrollcommand(f,l, self=self):
                        make_autoscroll(self._vbar, f, l)
                        self._update_position_of_entry()
                else:
                    def yscrollcommand(f,l, vbar=self._vbar):
                        make_autoscroll(vbar, f, l)

                self._treeview.config(yscrollcommand=yscrollcommand)
            else:
                self._treeview.config(yscrollcommand=self._vbar.set)

        if hscrollbar:
            if editable:
                xview_command = self._xview
            else:
                xview_command = self._treeview.xview

            self._hbar=Scrollbar(self,takefocus=0, command=xview_command, **scrollbar_kwargs)
            self._hbar.grid(row=0, column=1, sticky= E+W)
            
            if autoscroll:
                if editable:
                    def xscrollcommand(f,l, self=self):
                        make_autoscroll(self._hbar, f, l)
                        self._update_position_of_entry()
                else:
                    def xscrollcommand(f,l, hbar=self._hbar):
                        make_autoscroll(hbar, f, l)

                self._treeview.config(xscrollcommand=xscrollcommand)
            else:
                self._treeview.config(xscrollcommand=self._hbar.set)

        self._columns = columns
        self._number_of_columns = len(columns)

        for i in range(0, len(columns)):

            if sort:
                self._treeview.heading(i, text=columns[i], command=lambda col=i: self._sort_by(col, descending=False))
            else:
                self._treeview.heading(i, text=columns[i], anchor=anchor_heading)
                
            if adjust_heading_to_content:
                self._treeview.column(i, width=tkFont.Font().measure(columns[i]))

            self._treeview.column(i, anchor=anchor_data)
        
        self.row = Table_Row(self)
        self.column = Table_Column(self)

    # No funciona la rueda encima del entry
    def _edit_cell(self, event):
        '''Executed, when a row is clicked. Opens an entry popup above the cell, so it is possible
        to select text '''

        # close previous popups
        if self._entry_popup:
            self._entry_popup.destroy()

        # what row and column was clicked on
        item_ID = self._treeview.identify_row(event.y)
        if not item_ID: return

        column = self._treeview.identify_column(event.x)
               
        # get column position info
        x,y,width,height = self._treeview.bbox(item_ID, column)
       
        # place Entry popup properly
        column_number = int(column[1:])-1
        data = self._item_ID_to_row_data(item_ID)[column_number]
        
        
        self._entry_popup = Entry(self._treeview, exportselection=True, selectbackground=self._entry_background)
        self._entry_popup.place(x=x, y=y, width=width, height=height)
        
        self._entry_popup.insert(0, data)
        self._entry_popup.focus_force()

        self._entry_popup.bind("<Control-a>", self._on_entry_data_selection)
        self._entry_popup.bind("<Escape>", lambda event: self._destroy_entry())

        # No funciona bien udpate position
        bind_function_onMouseWheel(self._treeview, "y", binding_widget=self._entry_popup, callback=self._update_position_of_entry)
        #self.bind('<Unmap>', lambda *ignore: self.destroy())
        
        self._entry_popup.bind("<Return>", lambda event, itemd_ID=item_ID, column_number=column_number: self._update_cell(item_ID, column_number))
        
        self._selected_cell = item_ID, column

    def _on_entry_data_selection(self, event):
        ''' Set selection on the whole text '''
        self._entry_popup.selection_range(0, 'end')

        # returns 'break' to interrupt default key-bindings
        return 'break'

    def _destroy_entry(self):
        self._entry_popup.destroy()

        self._entry_popup = None
        self._selected_cell = None

    def _update_cell(self, item_ID, column_number):
        data = self._entry_popup.get()

        row_data = self._item_ID_to_row_data(item_ID)
        row_data[column_number] = data        
        self._treeview.item(item_ID, values=row_data)
        
        self._destroy_entry()

    def _xview(self, *args):
        self._treeview.xview(*args)
        self._update_position_of_entry()

    def _yview(self, *args):
        self._treeview.yview(*args)
        self._update_position_of_entry()
        
    def _update_position_of_entry(self):
        if self._selected_cell:
            bbox = self._treeview.bbox(*self._selected_cell)
            if bbox == "":
                self._entry_popup.place_forget()
            else:
                x,y,width,height = bbox
                self._entry_popup.place(x=x, y=y, width=width, height=height)

    def configure_column(self, index, width=None, minwidth=None, anchor=None, stretch=None):
        kwargs = {}
        for config_name in ("width", "anchor", "stretch", "minwidth"):
            config_value = locals()[config_name]
            if config_value is not None:
                kwargs[config_name] = config_value
            
        self._treeview.column('#%s'%index, **kwargs)

    def get_row(self, index):
        try:
            item_ID = self._treeview.get_children()[index]
        except IndexError:
            raise ValueError("Index out of range: %d"%index)        

        return self._treeview.item(self._item_ID_to_row_data(item_ID))
            
    def update_row(self, index, data):
        try:
            item = self._treeview.get_children()[index]
        except IndexError:
            raise ValueError("Index out of range: %d"%index)
            
        if len(data) == len(self._columns):
            self._treeview.item(item, values=data)
        else:
            raise ValueError("The multicolumn listbox has only %d columns"%self._number_of_columns)

    def delete_row(self, index):
        list_of_items = self._treeview.get_children()
        number_of_rows = len(list_of_items)

        if index == END:
            index = number_of_rows-1
        elif index < 0:
            index = 0
        elif index >= number_of_rows:
            index = number_of_rows-1
        
        item_ID = list_of_items[index]

        self._treeview.delete(item_ID)

        if self._stripped_rows:
            
            for i in range(index+1, number_of_rows):
                self._treeview.tag_configure(list_of_items[i], background=self._stripped_rows[(i-1)%2])

    def add_row(self, data, index=END):
        if len(data) != self._number_of_columns:
            raise ValueError("The multicolumn listbox has only %d columns"%self._number_of_columns)

        item_ID = self._treeview.insert('', index, values=data)
        self._treeview.item(item_ID, tags=item_ID)
        
        if self._stripped_rows:
            list_of_items = self._treeview.get_children()
            number_of_rows = len(list_of_items)

            if index == END:
                index = number_of_rows-1
            elif index < 0:
                index = 0
            elif index >= number_of_rows:
                index = number_of_rows-1
            
            self._treeview.tag_configure(item_ID, background=self._stripped_rows[index%2])

            for i in range(index+1, number_of_rows):
                self._treeview.tag_configure(list_of_items[i], background=self._stripped_rows[(i+1)%2])

    def get_column(self, index):
        return [self._treeview.set(child_ID, index) for child_ID in self._treeview.get_children('')]

    def update_column(self, index, data):
        for i, item_ID in enumerate(self._treeview.get_children()): 
            data_row = self._item_ID_to_row_data(item_ID)
            data_row[index] = data[i]

            self._treeview.item(item_ID, values=data_row)

        return data

    def clear(self):
        # Another possibility:
        #  self._treeview.delete(*self._treeview.get_children())

        for row in self._treeview.get_children():
            self._treeview.delete(row)
            
    def update_table(self, data):
        self.clear()

        for row in data:
            self.add_row(row)
            
    def focus(self, row=None):
        if row is None:
            return self._treeview.item(self._treeview.focus())
        else:
            item = self._treeview.get_children()[row]
            self._treeview.focus(item)

    def state(self, state=None):
        if stateSpec is None:
            return self._treeview.state()
        else:
            self._treeview.state(state)

    @property
    def number_of_rows(self):
        return len(self._treeview.get_children())
        
    @property
    def number_of_columns(self):
        return self._number_of_columns
        
    def select_row(self, index):
        list_of_items = self._treeview.get_children()
        
        try:
            item_ID = list_of_items[index]
        except IndexError:
            raise ValueError("Index out of range: %d"%index)

        self._treeview.selection_add(item_ID)

    def deselect_row(self, index):
        list_of_items = self._treeview.get_children()
        
        try:
            item_ID = list_of_items[index]
        except IndexError:
            raise ValueError("Index out of range: %d"%index)

        self._treeview.selection_remove(item_ID)

    def set_selection(self, indices):
        list_of_items = self._treeview.get_children()

        self._treeview.selection_set(" ".join(list_of_items[row_index] for row_index in indices))

    @property
    def selected_rows(self):
        data = []
        for item_ID in self._treeview.selection():
            data_row = self._item_ID_to_row_data(item_ID)
            data.append(data_row)
        
        return data

    def _on_select(self, event):
        for item_ID in event.widget.selection():
            data_row = self._item_ID_to_row_data(item_ID)
            self._command(data_row)

    def _item_ID_to_row_data(self, item_ID):
        item = self._treeview.item(item_ID)
        return item["values"]
    
    @property
    def table_data(self):
        data = []

        for item_ID in self._treeview.get_children():        
            data_row = self._item_ID_to_row_data(item_ID)
            data.append(data_row)

        return data
    
    @table_data.setter
    def table_data(self, data):
        self.update_table(data)

    def _sort_by(self, col, descending):
        """
        sort tree contents when a column header is clicked
        """
        # grab values to sort
        data = [(self._treeview.set(child_ID, col), child_ID) for child_ID in self._treeview.get_children('')]
        
        # if the data to be sorted is numeric change to float
        try:
            data = [(float(number), child_ID) for number, child_ID in data]
        except ValueError:
            pass

        # now sort the data in place
        data.sort(reverse=descending)
        for idx, item in enumerate(data):
            self._treeview.move(item[1], '', idx)
    
        # switch the heading so that it will sort in the opposite direction
        self._treeview.heading(col, command=lambda col=col: self._sort_by(col, not descending))
    
    def __getitem__(self, index):
        if isinstance(index, tuple):
            row, column = index
            return self._treeview.set(self._treeview.get_children('')[row], column)
        else:
            raise Exception("Row and column indices are required")
        
    def __setitem__(self, index, value):
        row, column = index
        item_ID = self._treeview.get_children()[row]
        
        data = self._item_ID_to_row_data(item_ID)
        
        data[column] = value
        self._treeview.item(item_ID, values=data)

    def bind(self, event, handler):
        self._treeview.bind(event, handler)

if __name__ == '__main__':
    try:
        from Tkinter import Tk
        import tkMessageBox as messagebox
    except ImportError:
        from tkinter import Tk
        from tkinter import messagebox

    root = Tk()
    
    def on_select(data):
        print(data)
        
    def show_info(msg):
        messagebox.showinfo("Table Data", msg)
        
    table = Multicolumn_Listbox(root, columns=["column one","column two", "column three"], command=on_select)
    table.pack(expand=True, fill=BOTH)
    
    table.add_row([1,2,3])
    show_info("table.add_row([1,2,3])")

    table.update_row(0, [4,5,6])
    show_info("table.update_row(0, [4,5,6])")
    
    table.update_table([[1,2,3], [4,5,6]])
    show_info("table.update_table([1,2,3], [4,5,6])")
    
    table.select_row(0)
    show_info("table.select_row(0)")

    print(table.selected_rows)
    print(table.table_data)
    
    print(table[0,1])
    table.column[1] = ["item1", "item2"]

    table.update_column(2, [8,9])
    show_info("table.update_column(2, [8,9])")
    
    table.clear()
    show_info("table.clear()")
    
    table.table_data = [[1,2,3], [4,5,6], [7,8,9]]
    show_info("table.table_data = [[1,2,3], [4,5,6], [7,8,9]]")
    
    table.destroy()

    # The next table is editable: Click on the table to edit the cell
    table = Multicolumn_Listbox(root, columns=["column one","column two", "column three"], stripped_rows = ("white","#f2f2f2"), select_mode="none")
    table.pack(expand=True, fill=BOTH)
    
    
    table.table_data = [[0, 1, 2],
 [3, 4, 5],
 [6, 7, 8],
 [9, 10, 11],
 [12, 13, 14],
 [15, 16, 17],
 [18, 19, 20],
 [21, 22, 23],
 [24, 25, 26],
 [27, 28, 29]]
    
    table.add_row([1,2,3])
    
    show_info("We create an editable (click on a cell) and zebra style table")
    root.mainloop()

Diff to Previous Revision

--- revision 7 2017-02-11 23:22:46
+++ revision 8 2017-02-20 23:30:56
@@ -1,17 +1,18 @@
-# Version: 0.5
+# Version: 0.6
 # Author: Miguel Martinez Lopez
 # Uncomment the next line to see my email
 # print("Author's email: %s"%"61706c69636163696f6e616d656469646140676d61696c2e636f6d".decode("hex"))
 
+import platform
+
 try:
-    from Tkinter import Frame, BOTH ,N,E,S, W, CENTER
+    from Tkinter import Frame, BOTH ,N,E,S, W, END, CENTER, Entry
     import tkFont as tkFont
-    from ttk import Treeview, Scrollbar
+    from ttk import Treeview, Scrollbar, Style
 except ImportError:
-    from tkinter import Frame, BOTH ,N,E,S, W, CENTER
+    from tkinter import Frame, BOTH ,N,E,S, W, END, CENTER, Entry
     import tkinter.font as tkFont
-    from tkinter.ttk import Treeview, Scrollbar
-
+    from tkinter.ttk import Treeview, Scrollbar, Style
 
 def make_autoscroll(sbar, first, last):
     """Hide and show scrollbar as needed."""
@@ -22,18 +23,146 @@
         sbar.grid()
     sbar.set(first, last)
 
+OS = platform.system()
+
+
+class Table_Row(object):
+    def __init__(self, multicolumn_listbox):
+        self._multicolumn_listbox = multicolumn_listbox
+        
+    def get(self, index):
+        return self._multicolumn_listbox.get_row(index)
+
+    def add(self, data, index=END):
+        self._multicolumn_listbox.add_row(data, index)
+
+    def delete(self, index):
+        self._multicolumn_listbox.delete_row(index)
+
+    def update(self, index, data):
+        self._multicolumn_listbox.update_row(index, data)
+
+    def select(self, index):
+        self._multicolumn_listbox.select_row(index)
+
+    def deselect(self, index):
+        self._multicolumn_listbox.deselect_row(index)
+
+    def set_selection(self, indices):
+        self._multicolumn_listbox.set_selection(indices)
+
+    def __getitem__(self, index): 
+        return self._multicolumn_listbox.get_row(index)
+
+    def __setitem__(self, index, value): 
+        return self._multicolumn_listbox.update_row(index, value)
+
+    def __delitem__(self, index): 
+        self._multicolumn_listbox.delete_row(index)
+
+    def __len__(self): 
+        return self._multicolumn_listbox.number_of_rows
+
+class Table_Column(object):
+    def __init__(self, multicolumn_listbox):
+        self._multicolumn_listbox = multicolumn_listbox    
+        
+    def get(self, index):
+        return self._multicolumn_listbox.get_column(index)
+
+    def add(self, data, index=END):
+        self._multicolumn_listbox.add_column(data, index)
+
+    def delete(self, index):
+        self._multicolumn_listbox.delete_column(index)
+
+    def update(self, index, data):
+        self._multicolumn_listbox.update_column(index, data)
+
+    def __getitem__(self, index): 
+        return self._multicolumn_listbox.get_column(index)
+
+    def __setitem__(self, index, value): 
+        return self._multicolumn_listbox.update_column(index, value)
+
+    def __delitem__(self, index): 
+        self._multicolumn_listbox.delete_column(index)
+
+    def __len__(self): 
+        return self._multicolumn_listbox.number_of_columns
+
+def bind_function_onMouseWheel(scrolled_widget, orient, binding_widget=None, callback=None, factor = 1):
+    if not scrolled_widget:
+        binding_widget = scrolled_widget
+
+    view_command = getattr(scrolled_widget, orient+'view')
+    
+    if OS == 'Linux':
+        if callback:
+            def onMouseWheel(event):
+                if event.num == 4:
+                    view_command("scroll",(-1)*factor,"units" )
+                elif event.num == 5:
+                    view_command("scroll",factor,"units" ) 
+                
+                callback()
+        else:
+            def onMouseWheel(event):
+                if event.num == 4:
+                    view_command("scroll",(-1)*factor,"units" )
+                elif event.num == 5:
+                    view_command("scroll",factor,"units" ) 
+
+    elif OS == 'Windows':
+        if callback:
+            def onMouseWheel(event):        
+                view_command("scroll",(-1)*int((event.delta/120)*factor),"units" )
+                callback()
+        else:
+            def onMouseWheel(event):        
+                view_command("scroll",(-1)*int((event.delta/120)*factor),"units" )
+    
+    elif OS == 'Darwin':
+        if callback:
+            def onMouseWheel(event):        
+                view_command("scroll",event.delta,"units" )             
+                callback()
+        else:
+            def onMouseWheel(event):        
+                view_command("scroll",event.delta,"units" )             
+
+    if OS == "Linux" :
+        binding_widget.bind('<4>', onMouseWheel, add='+')
+        binding_widget.bind('<5>', onMouseWheel, add='+')
+    else:
+        # Windows and MacOS
+        binding_widget.bind("<MouseWheel>", onMouseWheel,  add='+')
+        
+    return onMouseWheel
+            
 class Multicolumn_Listbox(Frame, object):
-
-    def __init__(self, master, columns, anchor_heading = CENTER, anchor_data = W, style=None, autoscroll=True, vscrollbar=True, hscrollbar=False, scrollbar_background=None, scrollbar_troughcolor=None, height=None, padding=None, select_mode=None, command=None, sort=True, adjust_heading_to_content=False):
+    _style_index = 0
+
+    def __init__(self, master, columns, command=None, editable=True, sort=True, select_mode=None, autoscroll=True, vscrollbar=True, hscrollbar=False, anchor_heading = CENTER, anchor_data = W, style=None, scrollbar_background=None, scrollbar_troughcolor=None, height=None, padding=None, adjust_heading_to_content=False, stripped_rows=None, entry_background="#1BA1E2", selection_background=None, selection_foreground=None, background=None, foreground=None, font=None, fieldbackground=None, heading_font= None, heading_background=None, heading_foreground=None):
 
         Frame.__init__(self, master, class_="Multicolumn_Listbox")
+        
+        self._stripped_rows = stripped_rows
+        self._entry_background = entry_background
         
         self.grid_rowconfigure(0, weight=1)
         self.grid_columnconfigure(0, weight=1)
 
         treeview_kwargs = {}
-        if style is not None:
-            treeview_kwargs["style"] = style
+        s = Style()
+
+        if style is None:
+            style_name = "Multicolumn_Listbox%s.Treeview"%self._style_index
+            self._style_index += 1
+        else:
+            style_name = style
+
+        treeview_kwargs["style"] = style_name
 
         if height is not None:
             treeview_kwargs["height"] = height
@@ -43,10 +172,68 @@
         
         if select_mode is not None:
             treeview_kwargs["selectmode"] = select_mode
-
+        
+        style_map = {}
+        if selection_background is not None:
+            style_map["background"] = [('selected', selection_background)]
+            
+        if selection_foreground is not None:
+            style_map["foeground"] = [('selected', selection_foreground)]
+
+        if style_map is not None:
+            s.map(style_name, **style_map)
+
+        style_config = {}
+        if background is not None:
+            style_config["background"] = background
+
+        if foreground is not None:
+            style_config["foreground"] = foreground
+
+        if font is not None:
+            style_config["font"] = font
+
+        if fieldbackground is not None:
+            style_config["fieldbackground"] = fieldbackground
+
+        s.configure(style_name, **style_config)
+
+        heading_style_config = {}
+        if heading_font is not None:
+            heading_style_config["font"] = heading_font
+        if heading_background is not None:
+            heading_style_config["background"] = heading_background
+        if heading_foreground is not None:
+            heading_style_config["foreground"] = heading_foreground
+
+        heading_style_name = style_name + ".Heading"
+        s.configure(heading_style_name, **heading_style_config)
+        
         self._treeview = Treeview(self,columns=columns, show="headings", **treeview_kwargs)
         self._treeview.grid(row=0, column=0, sticky= N+E+W+S)
-        
+
+        if command is not None:
+            self._command = command
+            self._treeview.bind("<<TreeviewSelect>>", self._on_select)
+
+        if editable:
+            self._selected_cell = None
+            self._entry_popup = None
+
+            self._treeview.bind("<1>", self._edit_cell)
+            
+            def configure(event):
+                """
+                if self._entry_popup:
+                    self._entry_popup.destroy()
+                return
+                """
+
+                self._treeview.update_idletasks()
+                self._update_position_of_entry()
+
+            self._treeview.bind("<Configure>", configure)
+            
         scrollbar_kwargs = {}
         if scrollbar_background is not None:
             scrollbar_kwargs["background"] = scrollbar_background
@@ -55,27 +242,49 @@
             scrollbar_kwargs["throughcolor"] = scrollbar_troughcolor
 
         if vscrollbar:
-            self._vbar=Scrollbar(self,takefocus=0, command=self._treeview.yview, **scrollbar_kwargs)
+            if editable:
+                yview_command = self._yview
+            else:
+                yview_command = self._treeview.yview
+
+            self._vbar=Scrollbar(self,takefocus=0, command=yview_command, **scrollbar_kwargs)
             self._vbar.grid(row=0, column=1, sticky= N+S)
             
             if autoscroll:
-                self._treeview.config(yscrollcommand=lambda f, l: make_autoscroll(self._vbar, f, l))
+                if editable:
+                    def yscrollcommand(f,l, self=self):
+                        make_autoscroll(self._vbar, f, l)
+                        self._update_position_of_entry()
+                else:
+                    def yscrollcommand(f,l, vbar=self._vbar):
+                        make_autoscroll(vbar, f, l)
+
+                self._treeview.config(yscrollcommand=yscrollcommand)
             else:
                 self._treeview.config(yscrollcommand=self._vbar.set)
 
         if hscrollbar:
-            self._hbar=Scrollbar(self,takefocus=0, command=self._treeview.xview, **scrollbar_kwargs)
+            if editable:
+                xview_command = self._xview
+            else:
+                xview_command = self._treeview.xview
+
+            self._hbar=Scrollbar(self,takefocus=0, command=xview_command, **scrollbar_kwargs)
             self._hbar.grid(row=0, column=1, sticky= E+W)
             
             if autoscroll:
-                self._treeview.config(xscrollcommand=lambda f, l: make_autoscroll(self._hbar, f, l))
+                if editable:
+                    def xscrollcommand(f,l, self=self):
+                        make_autoscroll(self._hbar, f, l)
+                        self._update_position_of_entry()
+                else:
+                    def xscrollcommand(f,l, hbar=self._hbar):
+                        make_autoscroll(hbar, f, l)
+
+                self._treeview.config(xscrollcommand=xscrollcommand)
             else:
                 self._treeview.config(xscrollcommand=self._hbar.set)
-        
-        if command is not None:
-            self.on_select = command
-            self._treeview.bind("<<TreeviewSelect>>", self._on_select)
-        
+
         self._columns = columns
         self._number_of_columns = len(columns)
 
@@ -90,6 +299,88 @@
                 self._treeview.column(i, width=tkFont.Font().measure(columns[i]))
 
             self._treeview.column(i, anchor=anchor_data)
+        
+        self.row = Table_Row(self)
+        self.column = Table_Column(self)
+
+    # No funciona la rueda encima del entry
+    def _edit_cell(self, event):
+        '''Executed, when a row is clicked. Opens an entry popup above the cell, so it is possible
+        to select text '''
+
+        # close previous popups
+        if self._entry_popup:
+            self._entry_popup.destroy()
+
+        # what row and column was clicked on
+        item_ID = self._treeview.identify_row(event.y)
+        if not item_ID: return
+
+        column = self._treeview.identify_column(event.x)
+               
+        # get column position info
+        x,y,width,height = self._treeview.bbox(item_ID, column)
+       
+        # place Entry popup properly
+        column_number = int(column[1:])-1
+        data = self._item_ID_to_row_data(item_ID)[column_number]
+        
+        
+        self._entry_popup = Entry(self._treeview, exportselection=True, selectbackground=self._entry_background)
+        self._entry_popup.place(x=x, y=y, width=width, height=height)
+        
+        self._entry_popup.insert(0, data)
+        self._entry_popup.focus_force()
+
+        self._entry_popup.bind("<Control-a>", self._on_entry_data_selection)
+        self._entry_popup.bind("<Escape>", lambda event: self._destroy_entry())
+
+        # No funciona bien udpate position
+        bind_function_onMouseWheel(self._treeview, "y", binding_widget=self._entry_popup, callback=self._update_position_of_entry)
+        #self.bind('<Unmap>', lambda *ignore: self.destroy())
+        
+        self._entry_popup.bind("<Return>", lambda event, itemd_ID=item_ID, column_number=column_number: self._update_cell(item_ID, column_number))
+        
+        self._selected_cell = item_ID, column
+
+    def _on_entry_data_selection(self, event):
+        ''' Set selection on the whole text '''
+        self._entry_popup.selection_range(0, 'end')
+
+        # returns 'break' to interrupt default key-bindings
+        return 'break'
+
+    def _destroy_entry(self):
+        self._entry_popup.destroy()
+
+        self._entry_popup = None
+        self._selected_cell = None
+
+    def _update_cell(self, item_ID, column_number):
+        data = self._entry_popup.get()
+
+        row_data = self._item_ID_to_row_data(item_ID)
+        row_data[column_number] = data        
+        self._treeview.item(item_ID, values=row_data)
+        
+        self._destroy_entry()
+
+    def _xview(self, *args):
+        self._treeview.xview(*args)
+        self._update_position_of_entry()
+
+    def _yview(self, *args):
+        self._treeview.yview(*args)
+        self._update_position_of_entry()
+        
+    def _update_position_of_entry(self):
+        if self._selected_cell:
+            bbox = self._treeview.bbox(*self._selected_cell)
+            if bbox == "":
+                self._entry_popup.place_forget()
+            else:
+                x,y,width,height = bbox
+                self._entry_popup.place(x=x, y=y, width=width, height=height)
 
     def configure_column(self, index, width=None, minwidth=None, anchor=None, stretch=None):
         kwargs = {}
@@ -107,12 +398,6 @@
             raise ValueError("Index out of range: %d"%index)        
 
         return self._treeview.item(self._item_ID_to_row_data(item_ID))
-
-    def add_row(self, data, position="end"):
-        if len(data) != self._number_of_columns:
-            raise ValueError("The multicolumn listbox has only %d columns"%self._number_of_columns)
-
-        self._treeview.insert('', position, values=data)
             
     def update_row(self, index, data):
         try:
@@ -124,15 +409,53 @@
             self._treeview.item(item, values=data)
         else:
             raise ValueError("The multicolumn listbox has only %d columns"%self._number_of_columns)
-            
+
     def delete_row(self, index):
-        try:
-            item_ID = self._treeview.get_children()[index]
-        except IndexError:
-            raise ValueError("Index out of range: %d"%index)
-        
+        list_of_items = self._treeview.get_children()
+        number_of_rows = len(list_of_items)
+
+        if index == END:
+            index = number_of_rows-1
+        elif index < 0:
+            index = 0
+        elif index >= number_of_rows:
+            index = number_of_rows-1
+        
+        item_ID = list_of_items[index]
+
         self._treeview.delete(item_ID)
-        
+
+        if self._stripped_rows:
+            
+            for i in range(index+1, number_of_rows):
+                self._treeview.tag_configure(list_of_items[i], background=self._stripped_rows[(i-1)%2])
+
+    def add_row(self, data, index=END):
+        if len(data) != self._number_of_columns:
+            raise ValueError("The multicolumn listbox has only %d columns"%self._number_of_columns)
+
+        item_ID = self._treeview.insert('', index, values=data)
+        self._treeview.item(item_ID, tags=item_ID)
+        
+        if self._stripped_rows:
+            list_of_items = self._treeview.get_children()
+            number_of_rows = len(list_of_items)
+
+            if index == END:
+                index = number_of_rows-1
+            elif index < 0:
+                index = 0
+            elif index >= number_of_rows:
+                index = number_of_rows-1
+            
+            self._treeview.tag_configure(item_ID, background=self._stripped_rows[index%2])
+
+            for i in range(index+1, number_of_rows):
+                self._treeview.tag_configure(list_of_items[i], background=self._stripped_rows[(i+1)%2])
+
+    def get_column(self, index):
+        return [self._treeview.set(child_ID, index) for child_ID in self._treeview.get_children('')]
+
     def update_column(self, index, data):
         for i, item_ID in enumerate(self._treeview.get_children()): 
             data_row = self._item_ID_to_row_data(item_ID)
@@ -167,12 +490,6 @@
             return self._treeview.state()
         else:
             self._treeview.state(state)
-    
-    def xview(self, *args):
-        self._treeview.xview(*args)
-
-    def yview(self):
-        self._treeview.yview(*args)
 
     @property
     def number_of_rows(self):
@@ -190,9 +507,19 @@
         except IndexError:
             raise ValueError("Index out of range: %d"%index)
 
-        self._treeview.selection_set(item_ID)
-
-    def select_rows(self, indices):
+        self._treeview.selection_add(item_ID)
+
+    def deselect_row(self, index):
+        list_of_items = self._treeview.get_children()
+        
+        try:
+            item_ID = list_of_items[index]
+        except IndexError:
+            raise ValueError("Index out of range: %d"%index)
+
+        self._treeview.selection_remove(item_ID)
+
+    def set_selection(self, indices):
         list_of_items = self._treeview.get_children()
 
         self._treeview.selection_set(" ".join(list_of_items[row_index] for row_index in indices))
@@ -209,14 +536,14 @@
     def _on_select(self, event):
         for item_ID in event.widget.selection():
             data_row = self._item_ID_to_row_data(item_ID)
-            self.on_select(data_row)
+            self._command(data_row)
 
     def _item_ID_to_row_data(self, item_ID):
         item = self._treeview.item(item_ID)
         return item["values"]
     
     @property
-    def data(self):
+    def table_data(self):
         data = []
 
         for item_ID in self._treeview.get_children():        
@@ -225,8 +552,8 @@
 
         return data
     
-    @data.setter
-    def data(self, data):
+    @table_data.setter
+    def table_data(self, data):
         self.update_table(data)
 
     def _sort_by(self, col, descending):
@@ -250,13 +577,22 @@
         # switch the heading so that it will sort in the opposite direction
         self._treeview.heading(col, command=lambda col=col: self._sort_by(col, not descending))
     
-    def __getitem__(self, col):
-        return [self._treeview.set(child_ID, col) for child_ID in self._treeview.get_children('')]
-        
-    def __setitem__(self, col, value):
-        for child_ID, cell_data in zip(self._treeview.get_children(''), value):
-            self._treeview.set(child_ID, col, cell_data)
-        
+    def __getitem__(self, index):
+        if isinstance(index, tuple):
+            row, column = index
+            return self._treeview.set(self._treeview.get_children('')[row], column)
+        else:
+            raise Exception("Row and column indices are required")
+        
+    def __setitem__(self, index, value):
+        row, column = index
+        item_ID = self._treeview.get_children()[row]
+        
+        data = self._item_ID_to_row_data(item_ID)
+        
+        data[column] = value
+        self._treeview.item(item_ID, values=data)
+
     def bind(self, event, handler):
         self._treeview.bind(event, handler)
 
@@ -280,30 +616,51 @@
     table.pack(expand=True, fill=BOTH)
     
     table.add_row([1,2,3])
-    show_info("""table.add_row([1,2,3])""")
+    show_info("table.add_row([1,2,3])")
 
     table.update_row(0, [4,5,6])
-    show_info("""table.update_row(0, [4,5,6])""")
+    show_info("table.update_row(0, [4,5,6])")
     
     table.update_table([[1,2,3], [4,5,6]])
-    show_info("""table.update_table([1,2,3], [4,5,6])""")
+    show_info("table.update_table([1,2,3], [4,5,6])")
     
     table.select_row(0)
-    show_info("""table.select_row(0)""")
+    show_info("table.select_row(0)")
 
     print(table.selected_rows)
-    print(table.data)
-    
-    print(table["column one"])
-    table[1] = ["item1", "item2"]
-    
+    print(table.table_data)
+    
+    print(table[0,1])
+    table.column[1] = ["item1", "item2"]
+
     table.update_column(2, [8,9])
-    show_info("""table.update_column(2, [8,9])""")
+    show_info("table.update_column(2, [8,9])")
     
     table.clear()
-    show_info("""table.clear()""")
-    
-    table.data = [[1,2,3], [4,5,6], [7,8,9]]
-    show_info("""table.data = [[1,2,3], [4,5,6], [7,8,9]]""")
-
+    show_info("table.clear()")
+    
+    table.table_data = [[1,2,3], [4,5,6], [7,8,9]]
+    show_info("table.table_data = [[1,2,3], [4,5,6], [7,8,9]]")
+    
+    table.destroy()
+
+    # The next table is editable: Click on the table to edit the cell
+    table = Multicolumn_Listbox(root, columns=["column one","column two", "column three"], stripped_rows = ("white","#f2f2f2"), select_mode="none")
+    table.pack(expand=True, fill=BOTH)
+    
+    
+    table.table_data = [[0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 8],
+ [9, 10, 11],
+ [12, 13, 14],
+ [15, 16, 17],
+ [18, 19, 20],
+ [21, 22, 23],
+ [24, 25, 26],
+ [27, 28, 29]]
+    
+    table.add_row([1,2,3])
+    
+    show_info("We create an editable (click on a cell) and zebra style table")
     root.mainloop()

History