Source code for seamm_widgets.scrolled_columns

# -*- coding: utf-8 -*-

"""A ttk-based widget for columns of widgets, with fixed titles and scrolling

This widgets has two areas: a row of titles across the top and a scrolled frame
below it. It is used to make a table of widgets with fixed column headers.
"""

import seamm_widgets as sw
import tkinter as tk
from tkinter import ttk


[docs]class ScrolledColumns(ttk.Frame): def __init__(self, parent, *args, **kwargs): """Initialize the widget """ class_ = kwargs.pop('class_', 'MScrolledColumns') super().__init__(parent, class_=class_) columns = kwargs.pop('columns', []) self.min_sizes = kwargs.pop('minsize', []) self._after_id = None # list (vector) of the header widgets, _ncolumns long self._header_widgets = [] # list of lists (row x column) of widgets in the table self._widgets = [] # Create the two subframes, linking them both to the # horizontal scrollbar at the bottom # self.headers = ttk.Frame(self) self.table = sw.ScrolledFrame( self, scroll_vertically=True, borderwidth=2, relief=tk.SUNKEN ) self.headers = sw.ScrolledFrame( self, scroll_vertically=False, borderwidth=2, relief=tk.RAISED, xscrollbar=self.table.xscrollbar, height=22 ) # and patch up the horizontal scrolling self.table.xscrollbar['command'] = self.xview self.headers.grid(row=0, column=0, sticky=tk.EW) self.table.grid(row=1, column=0, sticky=tk.NSEW) self.columnconfigure(0, weight=1) self.rowconfigure(1, weight=1) # Put in the column titles if given col = 0 header = self.headers.interior() for item in columns: if isinstance(item, str): item = ttk.Label(header, text=item) item.grid(row=0, column=col) col += 1 self._header_widgets.append(item)
[docs] def cell(self, row, column, value=None): """Return or set the widget at the given cell """ if value is None: try: result = self._widgets[row][column] except Exception: raise return result else: if column >= self.ncolumns: # pad the header and each row with None's extra = [None] * (column - self.ncolumns + 1) self._header_widgets.extend(extra) for row_of_widgets in self._widgets: row_of_widgets.extend(extra) if row >= self.nrows: # add rows 'till we get there extra = [None] * self.ncolumns for i in range(self.nrows, row + 1): self._widgets.append(extra) if isinstance(value, str): value = ttk.Label(self.table.interior(), text=value) value.grid(row=row, column=column) self._widgets[row][column] = value self._update_widths()
[docs] def clear(self): """Clear the contents of the widget. Returns ------- None """ for row in self._widgets: for item in row: if item is not None: item.destroy() self._widgets = []
[docs] def delete_row(self, index): for item in self._widgets[index]: if item is not None: item.destroy() del self._widgets[index] self._update_widths()
[docs] def delete_column(self, index): for row in self._widgets: item = row[index] if item is not None: item.destroy() del row[index] self._update_widths()
# Provide matrix-like access to the widgets to make # the code cleaner def __getitem__(self, index): """Allow [row,column] access to the widgets!""" 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): """Allow x[row,column] access to the data""" if isinstance(index, tuple): row, column = index self.cell(row, column, value) else: raise Exception("Row and column indices are required") def __delitem__(self, key): """Allow deletion of keys""" if isinstance(key, tuple): row, column = key widget = self.cell(row, column) widget.destroy() self.cell(row, column, None) self._update_widths() else: raise Exception("Row and column indices are required") @property def nrows(self): return len(self._widgets) @property def ncolumns(self): return len(self._header_widgets) def _update_widths_now(self): """Force the update of the column widths to happen now""" self._update_widths(when='now') def _update_widths(self, when='later'): """Make the column widths of header and table identical""" if when == 'later': if self._after_id is None: self._after_id = self.after_idle(self._update_widths_now) return self.after_cancel(self._after_id) self._after_id = None header = self.headers.interior() table = self.table.interior() # Remove any widths currently set for column in range(0, self.ncolumns): header.columnconfigure(column, minsize=0) table.columnconfigure(column, minsize=0) # Let everything settle and finds its size header.update_idletasks() # And make the sizes equal for column in range(0, self.ncolumns): # bbox returns (x, y, w, h) so we want the third item w1 = header.grid_bbox(column, 0)[2] w2 = table.grid_bbox(column, 0)[2] if w1 < w2: header.columnconfigure(column, minsize=w2) else: table.columnconfigure(column, minsize=w1) # ensure that the header area is large enough h = header.grid_bbox(0, 0)[3] self.headers.height = h + 5
[docs] def interior(self): """Where the user packs widgets""" return self.table.interior()
[docs] def xview(self, *args): """Connect the subwidgets to the single scrollbar""" self.headers.canvas.xview(*args) self.table.canvas.xview(*args)