# Author: Miguel Martinez Lopez # Version: 0.14 try: from Tkinter import Frame, Canvas, Label, Message, StringVar from ttk import Scrollbar from Tkconstants import * except ImportError: from tkinter import Frame, Canvas, Label, Message, StringVar from tkinter.ttk import Scrollbar from tkinter.constants import * import platform OS = platform.system() def make_mouse_wheel_handler(widget, orient, factor = 1, what="units"): view_command = getattr(widget, orient+'view') if OS == 'Linux': def onMouseWheel(event): if event.num == 4: view_command("scroll",(-1)*factor, what) elif event.num == 5: view_command("scroll",factor, what) elif OS == 'Windows': def onMouseWheel(event): view_command("scroll",(-1)*int((event.delta/120)*factor), what) elif OS == 'Darwin': def onMouseWheel(event): view_command("scroll",event.delta, what) return onMouseWheel class Mousewheel_Support(object): # implemetnation of singleton pattern _instance = None def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = object.__new__(cls) return cls._instance def __init__(self, root, horizontal_factor = 2, vertical_factor=2): self._active_area = None if isinstance(horizontal_factor, int): self.horizontal_factor = horizontal_factor else: raise Exception("Vertical factor must be an integer.") if isinstance(vertical_factor, int): self.vertical_factor = vertical_factor else: raise Exception("Horizontal factor must be an integer.") if OS == "Linux" : root.bind_all('<4>', self._on_mouse_wheel, add='+') root.bind_all('<5>', self._on_mouse_wheel, add='+') else: # Windows and MacOS root.bind_all("<MouseWheel>", self._on_mouse_wheel, add='+') def _on_mouse_wheel(self,event): if self._active_area: self._active_area.onMouseWheel(event) def _on_mouse_enter_scrolling_area(self, widget): self._active_area = widget def _on_mouse_leave_scrolling_area(self): self._active_area = None def add_support_to(self, widget, xscrollbar=None, yscrollbar=None, what="units", horizontal_factor=None, vertical_factor=None): if xscrollbar is not None and not hasattr(xscrollbar, 'onMouseWheel'): horizontal_factor = horizontal_factor or self.horizontal_factor xscrollbar.onMouseWheel = make_mouse_wheel_handler(widget,'x', self.horizontal_factor, what) xscrollbar.bind('<Enter>', lambda event, scrollbar=xscrollbar: self._on_mouse_enter_scrolling_area(scrollbar) ) xscrollbar.bind('<Leave>', lambda event: self._on_mouse_leave_scrolling_area()) if yscrollbar is not None and not hasattr(yscrollbar, 'onMouseWheel'): vertical_factor = vertical_factor or self.vertical_factor yscrollbar.onMouseWheel = make_mouse_wheel_handler(widget,'y', self.vertical_factor, what) yscrollbar.bind('<Enter>', lambda event, scrollbar=yscrollbar: self._on_mouse_enter_scrolling_area(scrollbar) ) yscrollbar.bind('<Leave>', lambda event: self._on_mouse_leave_scrolling_area()) main_scrollbar = yscrollbar if yscrollbar is not None else xscrollbar if main_scrollbar is not None: widget.bind('<Enter>',lambda event: self._on_mouse_enter_scrolling_area(widget)) widget.bind('<Leave>', lambda event: self._on_mouse_leave_scrolling_area()) widget.onMouseWheel = main_scrollbar.onMouseWheel class Scrolling_Area(Frame, object): def __init__(self, master, width=None, height=None, mousewheel_speed = 2, scroll_horizontally=True, xscrollbar=None, scroll_vertically=True, yscrollbar=None, outer_background=None, inner_frame=Frame, window_minwidth=0, window_minheight = 0, **kw): Frame.__init__(self, master) if outer_background: self.configure(background=outer_background) self._width = width self._height = height self._canvas = Canvas(self, highlightthickness=0, width=width, height=height) self._canvas.grid(row=0, column=0, sticky=N+E+W+S) self._window_minwidth = window_minwidth self._window_minheight = window_minheight if scroll_vertically: if yscrollbar is not None: self.yscrollbar = yscrollbar else: self.yscrollbar = Scrollbar(self, orient=VERTICAL) self.yscrollbar.grid(row=0, column=1,sticky=N+S) self._canvas.configure(yscrollcommand=self.yscrollbar.set) self.yscrollbar['command']=self._canvas.yview else: self.yscrollbar = None if scroll_horizontally: if xscrollbar is not None: self.xscrollbar = xscrollbar else: self.xscrollbar = Scrollbar(self, orient=HORIZONTAL) self.xscrollbar.grid(row=1, column=0, sticky=E+W) self._canvas.configure(xscrollcommand=self.xscrollbar.set) self.xscrollbar['command']=self._canvas.xview else: self.xscrollbar = None self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.innerFrame= inner_frame(self._canvas, **kw) self.innerFrame.pack() self._canvas.create_window(0, 0, window=self.innerFrame, anchor='nw', tags="inner_frame") self._canvas.bind('<Configure>', self._on_configure) Mousewheel_Support(self).add_support_to(self._canvas, xscrollbar=self.xscrollbar, yscrollbar=self.yscrollbar) def _on_configure(self, event): width = max(self.innerFrame.winfo_reqwidth(), event.width, self._window_minwidth) height = max(self.innerFrame.winfo_reqheight(), event.height, self._window_minheight) self._canvas.configure(scrollregion="0 0 %s %s" % (width, height)) self._canvas.itemconfigure("inner_frame", width=width, height=height) def update_viewport(self): self.update() height = self.innerFrame.winfo_reqheight() width = self.innerFrame.winfo_reqwidth() canvas_width = self._width if canvas_width is None: canvas_width = width canvas_height = self._height if canvas_height is None: canvas_height = height self._canvas.configure(width=canvas_width, height=canvas_height, scrollregion="0 0 %s %s" % (width, height)) self._canvas.itemconfigure("inner_frame", width=width, height=height) class Cell(Frame): """Base class for cells""" class Data_Cell(Cell): def __init__(self, master, variable, anchor=W, bordercolor=None, borderwidth=1, padx=0, pady=0, background=None, foreground=None, font=None): Cell.__init__(self, master, background=background, highlightbackground=bordercolor, highlightcolor=bordercolor, highlightthickness=borderwidth, bd= 0) self._message_widget = Message(self, textvariable=variable, font=font, background=background, foreground=foreground) self._message_widget.pack(expand=True, padx=padx, pady=pady, anchor=anchor) self.bind("<Configure>", self._on_configure) def _on_configure(self, event): self._message_widget.configure(width=event.width) class Header_Cell(Cell): def __init__(self, master, text, bordercolor=None, borderwidth=1, padx=0, pady=0, background=None, foreground=None, font=None, anchor=CENTER, separator=True): Cell.__init__(self, master, background=background, highlightbackground=bordercolor, highlightcolor=bordercolor, highlightthickness=borderwidth, bd= 0) self.pack_propagate(False) self._header_label = Label(self, text=text, background=background, foreground=foreground, font=font) self._header_label.pack(padx=padx, pady=pady, expand=True) if separator and bordercolor is not None: separator = Frame(self, height=2, background=bordercolor, bd=0, highlightthickness=0, class_="Separator") separator.pack(fill=X, anchor=anchor) self.update() height = self._header_label.winfo_reqheight() + 2*padx width = self._header_label.winfo_reqwidth() + 2*pady self.configure(height=height, width=width) class Table(Frame): def __init__(self, master, columns, column_weights=None, column_minwidths=None, height=None, minwidth=20, minheight=20, padx=5, pady=5, cell_font=None, cell_foreground="black", cell_background="white", cell_anchor=W, header_font=None, header_background="white", header_foreground="black", header_anchor=CENTER, bordercolor = "#999999", innerborder=True, outerborder=True, stripped_rows=("#EEEEEE", "white"), on_change_data=None, mousewheel_speed = 2, scroll_horizontally=False, scroll_vertically=True): outerborder_width = 1 if outerborder else 0 Frame.__init__(self,master, bd= 0) self._cell_background = cell_background self._cell_foreground = cell_foreground self._cell_font = cell_font self._cell_anchor = cell_anchor self._stripped_rows = stripped_rows self._padx = padx self._pady = pady self._bordercolor = bordercolor self._innerborder_width = 1 if innerborder else 0 self._data_vars = [] self._columns = columns self._number_of_rows = 0 self._number_of_columns = len(columns) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(1, weight=1) self._head = Frame(self, highlightbackground=bordercolor, highlightcolor=bordercolor, highlightthickness=outerborder_width, bd= 0) self._head.grid(row=0, column=0, sticky=E+W) header_separator = False if outerborder else True for j in range(len(columns)): column_name = columns[j] header_cell = Header_Cell(self._head, text=column_name, borderwidth=self._innerborder_width, font=header_font, background=header_background, foreground=header_foreground, padx=padx, pady=pady, bordercolor=bordercolor, anchor=header_anchor, separator=header_separator) header_cell.grid(row=0, column=j, sticky=N+E+W+S) if scroll_horizontally or scroll_vertically: if scroll_horizontally: xscrollbar = Scrollbar(self, orient=HORIZONTAL) xscrollbar.grid(row=2, column=0, sticky=E+W) else: xscrollbar = None if scroll_vertically: yscrollbar = Scrollbar(self, orient=VERTICAL) yscrollbar.grid(row=1, column=1, sticky=N+S) else: yscrollbar = None scrolling_area = Scrolling_Area(self, height=height, scroll_horizontally=scroll_horizontally, xscrollbar=xscrollbar, scroll_vertically=scroll_vertically, yscrollbar=yscrollbar) scrolling_area.grid(row=1, column=0, sticky=E+W) self._body = Frame(scrolling_area.innerFrame, highlightbackground=bordercolor, highlightcolor=bordercolor, highlightthickness=outerborder_width, bd= 0) self._body.pack() if on_change_data is None: on_change_data = scrolling_area.update_viewport else: _on_change_data = on_change_data def on_change_data(): scrolling_area.update_viewport() _on_change_data() else: self._body = Frame(self, height=height, highlightbackground=bordercolor, highlightcolor=bordercolor, highlightthickness=outerborder_width, bd= 0) self._body.grid(row=1, column=0, sticky=E+W) if column_weights is None: for j in range(len(columns)): self._body.grid_columnconfigure(j, weight=1) else: for j, weight in enumerate(column_weights): self._body.grid_columnconfigure(j, weight=weight) self.update_idletasks() if column_minwidths is not None: for j, minwidth in enumerate(column_minwidths): if minwidth is None: header_cell = self._head.grid_slaves(row=0, column=j)[0] minwidth = header_cell.winfo_reqwidth() self._body.grid_columnconfigure(j, minsize=minwidth) else: for j in range(len(columns)): header_cell = self._head.grid_slaves(row=0, column=j)[0] minwidth = header_cell.winfo_reqwidth() self._body.grid_columnconfigure(j, minsize=minwidth) self._on_change_data = on_change_data def _append_n_rows(self, n): number_of_rows = self._number_of_rows for i in range(number_of_rows, number_of_rows+n): list_of_vars = [] for j in range(self.number_of_columns): var = StringVar() list_of_vars.append(var) if self._stripped_rows: cell = Data_Cell(self._body, borderwidth=self._innerborder_width, variable=var, bordercolor=self._bordercolor, padx=self._padx, pady=self._pady, background=self._stripped_rows[i%2], foreground=self._cell_foreground, font=self._cell_font, anchor=self._cell_anchor) else: cell = Data_Cell(self._body, borderwidth=self._innerborder_width, variable=var, bordercolor=self._bordercolor, padx=self._padx, pady=self._pady, background=self._cell_background, foreground=self._cell_foreground, font=self._cell_font, anchor=self._cell_anchor) cell.grid(row=i, column=j, sticky=N+E+W+S) self._data_vars.append(list_of_vars) if number_of_rows == 0: for j in range(self.number_of_columns): header_cell = self._head.grid_slaves(row=0, column=j)[0] data_cell = self._body.grid_slaves(row=0, column=j)[0] data_cell.bind("<Configure>", lambda event, header_cell=header_cell: header_cell.configure(width=event.width), add="+") self._number_of_rows += n def _pop_n_rows(self, n): number_of_rows = self._number_of_rows for i in range(number_of_rows-n, number_of_rows): for j in range(self.number_of_columns): self._body.grid_slaves(row=i, column=j)[0].destroy() self._data_vars.pop() self._number_of_rows -= n def set_data(self, data): n = len(data) m = len(data[0]) number_of_rows = self._number_of_rows if number_of_rows > n: self._pop_n_rows(number_of_rows-n) elif number_of_rows < n: self._append_n_rows(n-number_of_rows) for i in range(n): for j in range(m): self._data_vars[i][j].set(data[i][j]) if self._on_change_data is not None: self._on_change_data() def get_data(self): number_of_rows = self._number_of_rows number_of_columns = self.number_of_columns data = [] for i in range(number_of_rows): row = [] row_of_vars = self._data_vars[i] for j in range(number_of_columns): cell_data = row_of_vars[j].get() row.append(cell_data) data.append(row) return data @property def number_of_rows(self): return self._number_of_rows @property def number_of_columns(self): return self._number_of_columns def row(self, index, data=None): if data is None: row = [] row_of_vars = self._data_vars[index] for j in range(self.number_of_columns): row.append(row_of_vars[j].get()) return row else: number_of_columns = self.number_of_columns if len(data) != number_of_columns: raise ValueError("data has no %d elements: %s"%(number_of_columns, data)) row_of_vars = self._data_vars[index] for j in range(number_of_columns): row_of_vars[index][j].set(data[j]) if self._on_change_data is not None: self._on_change_data() def column(self, index, data=None): number_of_rows = self._number_of_rows if data is None: column= [] for i in range(number_of_rows): column.append(self._data_vars[i][index].get()) return column else: if len(data) != number_of_rows: raise ValueError("data has no %d elements: %s"%(number_of_rows, data)) for i in range(number_of_columns): self._data_vars[i][index].set(data[i]) if self._on_change_data is not None: self._on_change_data() def clear(self): for i in range(self._number_of_rows): for j in range(self.number_of_columns): self._data_vars[i][j].set("") if self._on_change_data is not None: self._on_change_data() def delete_row(self, index): i = index while i < self._number_of_rows: row_of_vars_1 = self._data_vars[i] row_of_vars_2 = self._data_vars[i+1] j = 0 while j <self.number_of_columns: row_of_vars_1[j].set(row_of_vars_2[j]) i += 1 self._pop_n_rows(1) if self._on_change_data is not None: self._on_change_data() def insert_row(self, data, index=END): self._append_n_rows(1) if index == END: index = self._number_of_rows - 1 i = self._number_of_rows-1 while i > index: row_of_vars_1 = self._data_vars[i-1] row_of_vars_2 = self._data_vars[i] j = 0 while j < self.number_of_columns: row_of_vars_2[j].set(row_of_vars_1[j]) j += 1 i -= 1 list_of_cell_vars = self._data_vars[index] for cell_var, cell_data in zip(list_of_cell_vars, data): cell_var.set(cell_data) if self._on_change_data is not None: self._on_change_data() def cell(self, row, column, data=None): """Get the value of a table cell""" if data is None: return self._data_vars[row][column].get() else: self._data_vars[row][column].set(data) if self._on_change_data is not None: self._on_change_data() def __getitem__(self, index): if isinstance(index, tuple): row, column = index return self.cell(row, column) else: raise Exception("Row and column indices are required") def __setitem__(self, index, value): if isinstance(index, tuple): row, column = index self.cell(row, column, value) else: raise Exception("Row and column indices are required") def on_change_data(self, callback): self._on_change_data = callback if __name__ == "__main__": try: from Tkinter import Tk except ImportError: from tkinter import Tk root = Tk() table = Table(root, ["column A", "column B", "column C"], column_minwidths=[None, None, None] ) table.pack(padx=10,pady=10) table.set_data([[1,2,3],[4,5,6], [7,8,9], [10,11,12]]) table.cell(0,0, " a fdas fasd fasdf asdf asdfasdf asdf asdfa sdfas asd sadf ") root.mainloop()