Welcome, guest | Sign In | My Account | Store | Cart
# Version: 0.9
# 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
    from tkFont import Font, nametofont
    from ttk import Treeview, Scrollbar, Style
except ImportError:
    from tkinter import Frame, BOTH ,N,E,S, W, END, CENTER, Entry
    from tkinter.font import Font, nametofont
    from tkinter.ttk import Treeview, Scrollbar, Style

# Python 3 compatibility
try:
  basestring
except NameError:
  basestring = str

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 insert(self, data, index=END):
        self._multicolumn_listbox.insert_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 insert(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, data=None, command=None, editable=True, sort=True, select_mode=None, autoscroll=True, vscrollbar=True, hscrollbar=False, heading_anchor = CENTER, anchor=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, field_background=None, heading_font= None, heading_background=None, heading_foreground=None, extra_cell_height=2):

        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:
            if not isinstance(font, Font):
                if isinstance(font, basestring):
                    font = nametofont(font)
                else:
                    if len(font) == 1:
                        font = Font(family=font[0])
                    elif len(font) == 2:
                        font = Font(family=font[0], size=font[1])
                        
                    elif len(font) == 3:
                        font = Font(family=font[0], size=font[1], weight=font[2])
                    else:
                        raise ValueError("Not possible more than 3 values for font")

            style_config["font"] = font

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

        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=heading_anchor)
                
            if adjust_heading_to_content:
                self._treeview.column(i, width=Font().measure(columns[i]))

            self._treeview.column(i, anchor=anchor)
        
        self.row = Table_Row(self)
        self.column = Table_Column(self)
        
        if data is not None:
            for row in data:
                self.insert_row(row)

        if font is None:
            font_name = s.lookup(style_name, "font")
            font = nametofont(font_name)

        s.configure(style_name, rowheight=font.metrics("linespace")+extra_cell_height)

    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
        cell_data = self._item_ID_to_row_data(item_ID)[column_number]


        self._entry_popup = Entry(self._treeview, exportselection=True, selectbackground=self._entry_background, borderwidth=0, font="TkDefaultFont")
        self._entry_popup.place(x=x, y=y, width=width, height=height)
        
        self._entry_popup.insert(0, cell_data)
        self._entry_popup.focus_force()

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

        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>", self._on_update_cell)
        
        self._selected_cell = item_ID, column, column_number

    def _on_select_entry_data(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 _on_update_cell(self, event):
        item_ID, column, column_number = self._selected_cell

        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[0], self._selected_cell[1])
            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 insert_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(self, data):
        self.clear()

        for row in data:
            self.insert_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(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):
        """Get the value of a table cell"""

        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):
        """Set the value of a table cell"""

        if isinstance(index, tuple):
            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)
        else:
            raise Exception("Row and column indices are required")

    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.insert_row([1,2,3])
    show_info("table.insert_row([1,2,3])")
    
    table.row.insert([4,5,7])
    show_info("table.row.insert([4,5,7])")

    table.update_row(0, [7,8,9])
    show_info("table.update_row(0, [4,5,6])")
    
    table.update([[1,2,3], [4,5,6]])
    show_info("table.update([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.insert_row([1,2,3])
    
    show_info("We created an editable and zebra style table. Click on a cell to edit.")

    root.mainloop()

Diff to Previous Revision

--- revision 13 2017-02-23 21:46:33
+++ revision 14 2017-02-23 21:49:24
@@ -609,6 +609,8 @@
         self._treeview.heading(col, command=lambda col=col: self._sort_by(col, not descending))
     
     def __getitem__(self, index):
+        """Get the value of a table cell"""
+
         if isinstance(index, tuple):
             row, column = index
             return self._treeview.set(self._treeview.get_children('')[row], column)
@@ -616,13 +618,18 @@
             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)
+        """Set the value of a table cell"""
+
+        if isinstance(index, tuple):
+            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)
+        else:
+            raise Exception("Row and column indices are required")
 
     def bind(self, event, handler):
         self._treeview.bind(event, handler)

History