def main(): root = Tk() sizex = 800 sizey = 600 posx = 100 posy = 100 root.wm_geometry("%dx%d+%d+%d" % (sizex, sizey, posx, posy)) root.grid() root.rowconfigure(0, weight=1) root.columnconfigure(0, weight=1) myframe = Frame(root, relief=GROOVE, bd=1) myframe.grid(row=0, column=0, sticky="NESW") myframe.rowconfigure(0, weight=1) myframe.columnconfigure(0, weight=1) myframe.columnconfigure(1, weight=0) canvas = Canvas(myframe) canvas.grid(row=0, column=0, sticky="NESW") frame = Frame(canvas) myscrollbar = Scrollbar(myframe, orient="vertical", command=canvas.yview) myscrollbar.grid(row=0, column=1, sticky="NESW") canvas.configure(yscrollcommand=myscrollbar.set) canvas.create_window((0, 0), window=frame, anchor='nw') frame.bind("<Configure>", lambda e, c=canvas: myfunction(c, e)) data(frame) root.mainloop()
class Table(object): """ A display widget for a table of values, based on a ``MultiListbox`` widget. For many purposes, ``Table`` can be treated as a list-of-lists. E.g., table[i] is a list of the values for row i; and table.append(row) adds a new row with the given lits of values. Individual cells can be accessed using table[i,j], which refers to the j-th column of the i-th row. This can be used to both read and write values from the table. E.g.: >>> table[i,j] = 'hello' The column (j) can be given either as an index number, or as a column name. E.g., the following prints the value in the 3rd row for the 'First Name' column: >>> print(table[3, 'First Name']) John You can configure the colors for individual rows, columns, or cells using ``rowconfig()``, ``columnconfig()``, and ``itemconfig()``. The color configuration for each row will be preserved if the table is modified; however, when new rows are added, any color configurations that have been made for *columns* will not be applied to the new row. Note: Although ``Table`` acts like a widget in some ways (e.g., it defines ``grid()``, ``pack()``, and ``bind()``), it is not itself a widget; it just contains one. This is because widgets need to define ``__getitem__()``, ``__setitem__()``, and ``__nonzero__()`` in a way that's incompatible with the fact that ``Table`` behaves as a list-of-lists. :ivar _mlb: The multi-column listbox used to display this table's data. :ivar _rows: A list-of-lists used to hold the cell values of this table. Each element of _rows is a row value, i.e., a list of cell values, one for each column in the row. """ def __init__(self, master, column_names, rows=None, column_weights=None, scrollbar=True, click_to_sort=True, reprfunc=None, cnf={}, **kw): """ Construct a new Table widget. :type master: Tkinter.Widget :param master: The widget that should contain the new table. :type column_names: list(str) :param column_names: A list of names for the columns; these names will be used to create labels for each column; and can be used as an index when reading or writing cell values from the table. :type rows: list(list) :param rows: A list of row values used to initialze the table. Each row value should be a tuple of cell values, one for each column in the row. :type scrollbar: bool :param scrollbar: If true, then create a scrollbar for the new table widget. :type click_to_sort: bool :param click_to_sort: If true, then create bindings that will sort the table's rows by a given column's values if the user clicks on that colum's label. :type reprfunc: function :param reprfunc: If specified, then use this function to convert each table cell value to a string suitable for display. ``reprfunc`` has the following signature: reprfunc(row_index, col_index, cell_value) -> str (Note that the column is specified by index, not by name.) :param cnf, kw: Configuration parameters for this widget's contained ``MultiListbox``. See ``MultiListbox.__init__()`` for details. """ self._num_columns = len(column_names) self._reprfunc = reprfunc self._frame = Frame(master) self._column_name_to_index = dict( (c, i) for (i, c) in enumerate(column_names)) # Make a copy of the rows & check that it's valid. if rows is None: self._rows = [] else: self._rows = [[v for v in row] for row in rows] for row in self._rows: self._checkrow(row) # Create our multi-list box. self._mlb = MultiListbox(self._frame, column_names, column_weights, cnf, **kw) self._mlb.pack(side='left', expand=True, fill='both') # Optional scrollbar if scrollbar: sb = Scrollbar(self._frame, orient='vertical', command=self._mlb.yview) self._mlb.listboxes[0]['yscrollcommand'] = sb.set #for listbox in self._mlb.listboxes: # listbox['yscrollcommand'] = sb.set sb.pack(side='right', fill='y') self._scrollbar = sb # Set up sorting self._sortkey = None if click_to_sort: for i, l in enumerate(self._mlb.column_labels): l.bind('<Button-1>', self._sort) # Fill in our multi-list box. self._fill_table() #///////////////////////////////////////////////////////////////// #{ Widget-like Methods #///////////////////////////////////////////////////////////////// # These all just delegate to either our frame or our MLB. def pack(self, *args, **kwargs): """Position this table's main frame widget in its parent widget. See ``Tkinter.Frame.pack()`` for more info.""" self._frame.pack(*args, **kwargs) def grid(self, *args, **kwargs): """Position this table's main frame widget in its parent widget. See ``Tkinter.Frame.grid()`` for more info.""" self._frame.grid(*args, **kwargs) def focus(self): """Direct (keyboard) input foxus to this widget.""" self._mlb.focus() def bind(self, sequence=None, func=None, add=None): """Add a binding to this table's main frame that will call ``func`` in response to the event sequence.""" self._mlb.bind(sequence, func, add) def rowconfigure(self, row_index, cnf={}, **kw): """:see: ``MultiListbox.rowconfigure()``""" self._mlb.rowconfigure(row_index, cnf, **kw) def columnconfigure(self, col_index, cnf={}, **kw): """:see: ``MultiListbox.columnconfigure()``""" col_index = self.column_index(col_index) self._mlb.columnconfigure(col_index, cnf, **kw) def itemconfigure(self, row_index, col_index, cnf=None, **kw): """:see: ``MultiListbox.itemconfigure()``""" col_index = self.column_index(col_index) return self._mlb.itemconfigure(row_index, col_index, cnf, **kw) def bind_to_labels(self, sequence=None, func=None, add=None): """:see: ``MultiListbox.bind_to_labels()``""" return self._mlb.bind_to_labels(sequence, func, add) def bind_to_listboxes(self, sequence=None, func=None, add=None): """:see: ``MultiListbox.bind_to_listboxes()``""" return self._mlb.bind_to_listboxes(sequence, func, add) def bind_to_columns(self, sequence=None, func=None, add=None): """:see: ``MultiListbox.bind_to_columns()``""" return self._mlb.bind_to_columns(sequence, func, add) rowconfig = rowconfigure columnconfig = columnconfigure itemconfig = itemconfigure #///////////////////////////////////////////////////////////////// #{ Table as list-of-lists #///////////////////////////////////////////////////////////////// def insert(self, row_index, rowvalue): """ Insert a new row into the table, so that its row index will be ``row_index``. If the table contains any rows whose row index is greater than or equal to ``row_index``, then they will be shifted down. :param rowvalue: A tuple of cell values, one for each column in the new row. """ self._checkrow(rowvalue) self._rows.insert(row_index, rowvalue) if self._reprfunc is not None: rowvalue = [ self._reprfunc(row_index, j, v) for (j, v) in enumerate(rowvalue) ] self._mlb.insert(row_index, rowvalue) if self._DEBUG: self._check_table_vs_mlb() def extend(self, rowvalues): """ Add new rows at the end of the table. :param rowvalues: A list of row values used to initialze the table. Each row value should be a tuple of cell values, one for each column in the row. """ for rowvalue in rowvalues: self.append(rowvalue) if self._DEBUG: self._check_table_vs_mlb() def append(self, rowvalue): """ Add a new row to the end of the table. :param rowvalue: A tuple of cell values, one for each column in the new row. """ self.insert(len(self._rows), rowvalue) if self._DEBUG: self._check_table_vs_mlb() def clear(self): """ Delete all rows in this table. """ self._rows = [] self._mlb.delete(0, 'end') if self._DEBUG: self._check_table_vs_mlb() def __getitem__(self, index): """ Return the value of a row or a cell in this table. If ``index`` is an integer, then the row value for the ``index``th row. This row value consists of a tuple of cell values, one for each column in the row. If ``index`` is a tuple of two integers, ``(i,j)``, then return the value of the cell in the ``i``th row and the ``j``th column. """ if isinstance(index, slice): raise ValueError('Slicing not supported') elif isinstance(index, tuple) and len(index) == 2: return self._rows[index[0]][self.column_index(index[1])] else: return tuple(self._rows[index]) def __setitem__(self, index, val): """ Replace the value of a row or a cell in this table with ``val``. If ``index`` is an integer, then ``val`` should be a row value (i.e., a tuple of cell values, one for each column). In this case, the values of the ``index``th row of the table will be replaced with the values in ``val``. If ``index`` is a tuple of integers, ``(i,j)``, then replace the value of the cell in the ``i``th row and ``j``th column with ``val``. """ if isinstance(index, slice): raise ValueError('Slicing not supported') # table[i,j] = val elif isinstance(index, tuple) and len(index) == 2: i, j = index[0], self.column_index(index[1]) config_cookie = self._save_config_info([i]) self._rows[i][j] = val if self._reprfunc is not None: val = self._reprfunc(i, j, val) self._mlb.listboxes[j].insert(i, val) self._mlb.listboxes[j].delete(i + 1) self._restore_config_info(config_cookie) # table[i] = val else: config_cookie = self._save_config_info([index]) self._checkrow(val) self._rows[index] = list(val) if self._reprfunc is not None: val = [ self._reprfunc(index, j, v) for (j, v) in enumerate(val) ] self._mlb.insert(index, val) self._mlb.delete(index + 1) self._restore_config_info(config_cookie) def __delitem__(self, row_index): """ Delete the ``row_index``th row from this table. """ if isinstance(row_index, slice): raise ValueError('Slicing not supported') if isinstance(row_index, tuple) and len(row_index) == 2: raise ValueError('Cannot delete a single cell!') del self._rows[row_index] self._mlb.delete(row_index) if self._DEBUG: self._check_table_vs_mlb() def __len__(self): """ :return: the number of rows in this table. """ return len(self._rows) def _checkrow(self, rowvalue): """ Helper function: check that a given row value has the correct number of elements; and if not, raise an exception. """ if len(rowvalue) != self._num_columns: raise ValueError('Row %r has %d columns; expected %d' % (rowvalue, len(rowvalue), self._num_columns)) #///////////////////////////////////////////////////////////////// # Columns #///////////////////////////////////////////////////////////////// @property def column_names(self): """A list of the names of the columns in this table.""" return self._mlb.column_names def column_index(self, i): """ If ``i`` is a valid column index integer, then return it as is. Otherwise, check if ``i`` is used as the name for any column; if so, return that column's index. Otherwise, raise a ``KeyError`` exception. """ if isinstance(i, int) and 0 <= i < self._num_columns: return i else: # This raises a key error if the column is not found. return self._column_name_to_index[i] def hide_column(self, column_index): """:see: ``MultiListbox.hide_column()``""" self._mlb.hide_column(self.column_index(column_index)) def show_column(self, column_index): """:see: ``MultiListbox.show_column()``""" self._mlb.show_column(self.column_index(column_index)) #///////////////////////////////////////////////////////////////// # Selection #///////////////////////////////////////////////////////////////// def selected_row(self): """ Return the index of the currently selected row, or None if no row is selected. To get the row value itself, use ``table[table.selected_row()]``. """ sel = self._mlb.curselection() if sel: return int(sel[0]) else: return None def select(self, index=None, delta=None, see=True): """:see: ``MultiListbox.select()``""" self._mlb.select(index, delta, see) #///////////////////////////////////////////////////////////////// # Sorting #///////////////////////////////////////////////////////////////// def sort_by(self, column_index, order='toggle'): """ Sort the rows in this table, using the specified column's values as a sort key. :param column_index: Specifies which column to sort, using either a column index (int) or a column's label name (str). :param order: Specifies whether to sort the values in ascending or descending order: - ``'ascending'``: Sort from least to greatest. - ``'descending'``: Sort from greatest to least. - ``'toggle'``: If the most recent call to ``sort_by()`` sorted the table by the same column (``column_index``), then reverse the rows; otherwise sort in ascending order. """ if order not in ('ascending', 'descending', 'toggle'): raise ValueError('sort_by(): order should be "ascending", ' '"descending", or "toggle".') column_index = self.column_index(column_index) config_cookie = self._save_config_info(index_by_id=True) # Sort the rows. if order == 'toggle' and column_index == self._sortkey: self._rows.reverse() else: self._rows.sort(key=operator.itemgetter(column_index), reverse=(order == 'descending')) self._sortkey = column_index # Redraw the table. self._fill_table() self._restore_config_info(config_cookie, index_by_id=True, see=True) if self._DEBUG: self._check_table_vs_mlb() def _sort(self, event): """Event handler for clicking on a column label -- sort by that column.""" column_index = event.widget.column_index # If they click on the far-left of far-right of a column's # label, then resize rather than sorting. if self._mlb._resize_column(event): return 'continue' # Otherwise, sort. else: self.sort_by(column_index) return 'continue' #///////////////////////////////////////////////////////////////// #{ Table Drawing Helpers #///////////////////////////////////////////////////////////////// def _fill_table(self, save_config=True): """ Re-draw the table from scratch, by clearing out the table's multi-column listbox; and then filling it in with values from ``self._rows``. Note that any cell-, row-, or column-specific color configuration that has been done will be lost. The selection will also be lost -- i.e., no row will be selected after this call completes. """ self._mlb.delete(0, 'end') for i, row in enumerate(self._rows): if self._reprfunc is not None: row = [self._reprfunc(i, j, v) for (j, v) in enumerate(row)] self._mlb.insert('end', row) def _get_itemconfig(self, r, c): return dict((k, self._mlb.itemconfig(r, c, k)[-1]) for k in ('foreground', 'selectforeground', 'background', 'selectbackground')) def _save_config_info(self, row_indices=None, index_by_id=False): """ Return a 'cookie' containing information about which row is selected, and what color configurations have been applied. this information can the be re-applied to the table (after making modifications) using ``_restore_config_info()``. Color configuration information will be saved for any rows in ``row_indices``, or in the entire table, if ``row_indices=None``. If ``index_by_id=True``, the the cookie will associate rows with their configuration information based on the rows' python id. This is useful when performing operations that re-arrange the rows (e.g. ``sort``). If ``index_by_id=False``, then it is assumed that all rows will be in the same order when ``_restore_config_info()`` is called. """ # Default value for row_indices is all rows. if row_indices is None: row_indices = list(range(len(self._rows))) # Look up our current selection. selection = self.selected_row() if index_by_id and selection is not None: selection = id(self._rows[selection]) # Look up the color configuration info for each row. if index_by_id: config = dict(( id(self._rows[r]), [self._get_itemconfig(r, c) for c in range(self._num_columns)]) for r in row_indices) else: config = dict(( r, [self._get_itemconfig(r, c) for c in range(self._num_columns)]) for r in row_indices) return selection, config def _restore_config_info(self, cookie, index_by_id=False, see=False): """ Restore selection & color configuration information that was saved using ``_save_config_info``. """ selection, config = cookie # Clear the selection. if selection is None: self._mlb.selection_clear(0, 'end') # Restore selection & color config if index_by_id: for r, row in enumerate(self._rows): if id(row) in config: for c in range(self._num_columns): self._mlb.itemconfigure(r, c, config[id(row)][c]) if id(row) == selection: self._mlb.select(r, see=see) else: if selection is not None: self._mlb.select(selection, see=see) for r in config: for c in range(self._num_columns): self._mlb.itemconfigure(r, c, config[r][c]) #///////////////////////////////////////////////////////////////// # Debugging (Invariant Checker) #///////////////////////////////////////////////////////////////// _DEBUG = False """If true, then run ``_check_table_vs_mlb()`` after any operation that modifies the table.""" def _check_table_vs_mlb(self): """ Verify that the contents of the table's ``_rows`` variable match the contents of its multi-listbox (``_mlb``). This is just included for debugging purposes, to make sure that the list-modifying operations are working correctly. """ for col in self._mlb.listboxes: assert len(self) == col.size() for row in self: assert len(row) == self._num_columns assert self._num_columns == len(self._mlb.column_names) #assert self._column_names == self._mlb.column_names for i, row in enumerate(self): for j, cell in enumerate(row): if self._reprfunc is not None: cell = self._reprfunc(i, j, cell) assert self._mlb.get(i)[j] == cell
class Table(object): """ A display widget for a table of values, based on a ``MultiListbox`` widget. For many purposes, ``Table`` can be treated as a list-of-lists. E.g., table[i] is a list of the values for row i; and table.append(row) adds a new row with the given lits of values. Individual cells can be accessed using table[i,j], which refers to the j-th column of the i-th row. This can be used to both read and write values from the table. E.g.: >>> table[i,j] = 'hello' The column (j) can be given either as an index number, or as a column name. E.g., the following prints the value in the 3rd row for the 'First Name' column: >>> print(table[3, 'First Name']) John You can configure the colors for individual rows, columns, or cells using ``rowconfig()``, ``columnconfig()``, and ``itemconfig()``. The color configuration for each row will be preserved if the table is modified; however, when new rows are added, any color configurations that have been made for *columns* will not be applied to the new row. Note: Although ``Table`` acts like a widget in some ways (e.g., it defines ``grid()``, ``pack()``, and ``bind()``), it is not itself a widget; it just contains one. This is because widgets need to define ``__getitem__()``, ``__setitem__()``, and ``__nonzero__()`` in a way that's incompatible with the fact that ``Table`` behaves as a list-of-lists. :ivar _mlb: The multi-column listbox used to display this table's data. :ivar _rows: A list-of-lists used to hold the cell values of this table. Each element of _rows is a row value, i.e., a list of cell values, one for each column in the row. """ def __init__(self, master, column_names, rows=None, column_weights=None, scrollbar=True, click_to_sort=True, reprfunc=None, cnf={}, **kw): """ Construct a new Table widget. :type master: Tkinter.Widget :param master: The widget that should contain the new table. :type column_names: list(str) :param column_names: A list of names for the columns; these names will be used to create labels for each column; and can be used as an index when reading or writing cell values from the table. :type rows: list(list) :param rows: A list of row values used to initialze the table. Each row value should be a tuple of cell values, one for each column in the row. :type scrollbar: bool :param scrollbar: If true, then create a scrollbar for the new table widget. :type click_to_sort: bool :param click_to_sort: If true, then create bindings that will sort the table's rows by a given column's values if the user clicks on that colum's label. :type reprfunc: function :param reprfunc: If specified, then use this function to convert each table cell value to a string suitable for display. ``reprfunc`` has the following signature: reprfunc(row_index, col_index, cell_value) -> str (Note that the column is specified by index, not by name.) :param cnf, kw: Configuration parameters for this widget's contained ``MultiListbox``. See ``MultiListbox.__init__()`` for details. """ self._num_columns = len(column_names) self._reprfunc = reprfunc self._frame = Frame(master) self._column_name_to_index = dict((c,i) for (i,c) in enumerate(column_names)) # Make a copy of the rows & check that it's valid. if rows is None: self._rows = [] else: self._rows = [[v for v in row] for row in rows] for row in self._rows: self._checkrow(row) # Create our multi-list box. self._mlb = MultiListbox(self._frame, column_names, column_weights, cnf, **kw) self._mlb.pack(side='left', expand=True, fill='both') # Optional scrollbar if scrollbar: sb = Scrollbar(self._frame, orient='vertical', command=self._mlb.yview) self._mlb.listboxes[0]['yscrollcommand'] = sb.set #for listbox in self._mlb.listboxes: # listbox['yscrollcommand'] = sb.set sb.pack(side='right', fill='y') self._scrollbar = sb # Set up sorting self._sortkey = None if click_to_sort: for i, l in enumerate(self._mlb.column_labels): l.bind('<Button-1>', self._sort) # Fill in our multi-list box. self._fill_table() #///////////////////////////////////////////////////////////////// #{ Widget-like Methods #///////////////////////////////////////////////////////////////// # These all just delegate to either our frame or our MLB. def pack(self, *args, **kwargs): """Position this table's main frame widget in its parent widget. See ``Tkinter.Frame.pack()`` for more info.""" self._frame.pack(*args, **kwargs) def grid(self, *args, **kwargs): """Position this table's main frame widget in its parent widget. See ``Tkinter.Frame.grid()`` for more info.""" self._frame.grid(*args, **kwargs) def focus(self): """Direct (keyboard) input foxus to this widget.""" self._mlb.focus() def bind(self, sequence=None, func=None, add=None): """Add a binding to this table's main frame that will call ``func`` in response to the event sequence.""" self._mlb.bind(sequence, func, add) def rowconfigure(self, row_index, cnf={}, **kw): """:see: ``MultiListbox.rowconfigure()``""" self._mlb.rowconfigure(row_index, cnf, **kw) def columnconfigure(self, col_index, cnf={}, **kw): """:see: ``MultiListbox.columnconfigure()``""" col_index = self.column_index(col_index) self._mlb.columnconfigure(col_index, cnf, **kw) def itemconfigure(self, row_index, col_index, cnf=None, **kw): """:see: ``MultiListbox.itemconfigure()``""" col_index = self.column_index(col_index) return self._mlb.itemconfigure(row_index, col_index, cnf, **kw) def bind_to_labels(self, sequence=None, func=None, add=None): """:see: ``MultiListbox.bind_to_labels()``""" return self._mlb.bind_to_labels(sequence, func, add) def bind_to_listboxes(self, sequence=None, func=None, add=None): """:see: ``MultiListbox.bind_to_listboxes()``""" return self._mlb.bind_to_listboxes(sequence, func, add) def bind_to_columns(self, sequence=None, func=None, add=None): """:see: ``MultiListbox.bind_to_columns()``""" return self._mlb.bind_to_columns(sequence, func, add) rowconfig = rowconfigure columnconfig = columnconfigure itemconfig = itemconfigure #///////////////////////////////////////////////////////////////// #{ Table as list-of-lists #///////////////////////////////////////////////////////////////// def insert(self, row_index, rowvalue): """ Insert a new row into the table, so that its row index will be ``row_index``. If the table contains any rows whose row index is greater than or equal to ``row_index``, then they will be shifted down. :param rowvalue: A tuple of cell values, one for each column in the new row. """ self._checkrow(rowvalue) self._rows.insert(row_index, rowvalue) if self._reprfunc is not None: rowvalue = [self._reprfunc(row_index,j,v) for (j,v) in enumerate(rowvalue)] self._mlb.insert(row_index, rowvalue) if self._DEBUG: self._check_table_vs_mlb() def extend(self, rowvalues): """ Add new rows at the end of the table. :param rowvalues: A list of row values used to initialze the table. Each row value should be a tuple of cell values, one for each column in the row. """ for rowvalue in rowvalues: self.append(rowvalue) if self._DEBUG: self._check_table_vs_mlb() def append(self, rowvalue): """ Add a new row to the end of the table. :param rowvalue: A tuple of cell values, one for each column in the new row. """ self.insert(len(self._rows), rowvalue) if self._DEBUG: self._check_table_vs_mlb() def clear(self): """ Delete all rows in this table. """ self._rows = [] self._mlb.delete(0, 'end') if self._DEBUG: self._check_table_vs_mlb() def __getitem__(self, index): """ Return the value of a row or a cell in this table. If ``index`` is an integer, then the row value for the ``index``th row. This row value consists of a tuple of cell values, one for each column in the row. If ``index`` is a tuple of two integers, ``(i,j)``, then return the value of the cell in the ``i``th row and the ``j``th column. """ if isinstance(index, slice): raise ValueError('Slicing not supported') elif isinstance(index, tuple) and len(index)==2: return self._rows[index[0]][self.column_index(index[1])] else: return tuple(self._rows[index]) def __setitem__(self, index, val): """ Replace the value of a row or a cell in this table with ``val``. If ``index`` is an integer, then ``val`` should be a row value (i.e., a tuple of cell values, one for each column). In this case, the values of the ``index``th row of the table will be replaced with the values in ``val``. If ``index`` is a tuple of integers, ``(i,j)``, then replace the value of the cell in the ``i``th row and ``j``th column with ``val``. """ if isinstance(index, slice): raise ValueError('Slicing not supported') # table[i,j] = val elif isinstance(index, tuple) and len(index)==2: i, j = index[0], self.column_index(index[1]) config_cookie = self._save_config_info([i]) self._rows[i][j] = val if self._reprfunc is not None: val = self._reprfunc(i, j, val) self._mlb.listboxes[j].insert(i, val) self._mlb.listboxes[j].delete(i+1) self._restore_config_info(config_cookie) # table[i] = val else: config_cookie = self._save_config_info([index]) self._checkrow(val) self._rows[index] = list(val) if self._reprfunc is not None: val = [self._reprfunc(index,j,v) for (j,v) in enumerate(val)] self._mlb.insert(index, val) self._mlb.delete(index+1) self._restore_config_info(config_cookie) def __delitem__(self, row_index): """ Delete the ``row_index``th row from this table. """ if isinstance(row_index, slice): raise ValueError('Slicing not supported') if isinstance(row_index, tuple) and len(row_index)==2: raise ValueError('Cannot delete a single cell!') del self._rows[row_index] self._mlb.delete(row_index) if self._DEBUG: self._check_table_vs_mlb() def __len__(self): """ :return: the number of rows in this table. """ return len(self._rows) def _checkrow(self, rowvalue): """ Helper function: check that a given row value has the correct number of elements; and if not, raise an exception. """ if len(rowvalue) != self._num_columns: raise ValueError('Row %r has %d columns; expected %d' % (rowvalue, len(rowvalue), self._num_columns)) #///////////////////////////////////////////////////////////////// # Columns #///////////////////////////////////////////////////////////////// @property def column_names(self): """A list of the names of the columns in this table.""" return self._mlb.column_names def column_index(self, i): """ If ``i`` is a valid column index integer, then return it as is. Otherwise, check if ``i`` is used as the name for any column; if so, return that column's index. Otherwise, raise a ``KeyError`` exception. """ if isinstance(i, int) and 0 <= i < self._num_columns: return i else: # This raises a key error if the column is not found. return self._column_name_to_index[i] def hide_column(self, column_index): """:see: ``MultiListbox.hide_column()``""" self._mlb.hide_column(self.column_index(column_index)) def show_column(self, column_index): """:see: ``MultiListbox.show_column()``""" self._mlb.show_column(self.column_index(column_index)) #///////////////////////////////////////////////////////////////// # Selection #///////////////////////////////////////////////////////////////// def selected_row(self): """ Return the index of the currently selected row, or None if no row is selected. To get the row value itself, use ``table[table.selected_row()]``. """ sel = self._mlb.curselection() if sel: return int(sel[0]) else: return None def select(self, index=None, delta=None, see=True): """:see: ``MultiListbox.select()``""" self._mlb.select(index, delta, see) #///////////////////////////////////////////////////////////////// # Sorting #///////////////////////////////////////////////////////////////// def sort_by(self, column_index, order='toggle'): """ Sort the rows in this table, using the specified column's values as a sort key. :param column_index: Specifies which column to sort, using either a column index (int) or a column's label name (str). :param order: Specifies whether to sort the values in ascending or descending order: - ``'ascending'``: Sort from least to greatest. - ``'descending'``: Sort from greatest to least. - ``'toggle'``: If the most recent call to ``sort_by()`` sorted the table by the same column (``column_index``), then reverse the rows; otherwise sort in ascending order. """ if order not in ('ascending', 'descending', 'toggle'): raise ValueError('sort_by(): order should be "ascending", ' '"descending", or "toggle".') column_index = self.column_index(column_index) config_cookie = self._save_config_info(index_by_id=True) # Sort the rows. if order == 'toggle' and column_index == self._sortkey: self._rows.reverse() else: self._rows.sort(key=operator.itemgetter(column_index), reverse=(order=='descending')) self._sortkey = column_index # Redraw the table. self._fill_table() self._restore_config_info(config_cookie, index_by_id=True, see=True) if self._DEBUG: self._check_table_vs_mlb() def _sort(self, event): """Event handler for clicking on a column label -- sort by that column.""" column_index = event.widget.column_index # If they click on the far-left of far-right of a column's # label, then resize rather than sorting. if self._mlb._resize_column(event): return 'continue' # Otherwise, sort. else: self.sort_by(column_index) return 'continue' #///////////////////////////////////////////////////////////////// #{ Table Drawing Helpers #///////////////////////////////////////////////////////////////// def _fill_table(self, save_config=True): """ Re-draw the table from scratch, by clearing out the table's multi-column listbox; and then filling it in with values from ``self._rows``. Note that any cell-, row-, or column-specific color configuration that has been done will be lost. The selection will also be lost -- i.e., no row will be selected after this call completes. """ self._mlb.delete(0, 'end') for i, row in enumerate(self._rows): if self._reprfunc is not None: row = [self._reprfunc(i,j,v) for (j,v) in enumerate(row)] self._mlb.insert('end', row) def _get_itemconfig(self, r, c): return dict( (k, self._mlb.itemconfig(r, c, k)[-1]) for k in ('foreground', 'selectforeground', 'background', 'selectbackground') ) def _save_config_info(self, row_indices=None, index_by_id=False): """ Return a 'cookie' containing information about which row is selected, and what color configurations have been applied. this information can the be re-applied to the table (after making modifications) using ``_restore_config_info()``. Color configuration information will be saved for any rows in ``row_indices``, or in the entire table, if ``row_indices=None``. If ``index_by_id=True``, the the cookie will associate rows with their configuration information based on the rows' python id. This is useful when performing operations that re-arrange the rows (e.g. ``sort``). If ``index_by_id=False``, then it is assumed that all rows will be in the same order when ``_restore_config_info()`` is called. """ # Default value for row_indices is all rows. if row_indices is None: row_indices = list(range(len(self._rows))) # Look up our current selection. selection = self.selected_row() if index_by_id and selection is not None: selection = id(self._rows[selection]) # Look up the color configuration info for each row. if index_by_id: config = dict((id(self._rows[r]), [self._get_itemconfig(r, c) for c in range(self._num_columns)]) for r in row_indices) else: config = dict((r, [self._get_itemconfig(r, c) for c in range(self._num_columns)]) for r in row_indices) return selection, config def _restore_config_info(self, cookie, index_by_id=False, see=False): """ Restore selection & color configuration information that was saved using ``_save_config_info``. """ selection, config = cookie # Clear the selection. if selection is None: self._mlb.selection_clear(0, 'end') # Restore selection & color config if index_by_id: for r, row in enumerate(self._rows): if id(row) in config: for c in range(self._num_columns): self._mlb.itemconfigure(r, c, config[id(row)][c]) if id(row) == selection: self._mlb.select(r, see=see) else: if selection is not None: self._mlb.select(selection, see=see) for r in config: for c in range(self._num_columns): self._mlb.itemconfigure(r, c, config[r][c]) #///////////////////////////////////////////////////////////////// # Debugging (Invariant Checker) #///////////////////////////////////////////////////////////////// _DEBUG = False """If true, then run ``_check_table_vs_mlb()`` after any operation that modifies the table.""" def _check_table_vs_mlb(self): """ Verify that the contents of the table's ``_rows`` variable match the contents of its multi-listbox (``_mlb``). This is just included for debugging purposes, to make sure that the list-modifying operations are working correctly. """ for col in self._mlb.listboxes: assert len(self) == col.size() for row in self: assert len(row) == self._num_columns assert self._num_columns == len(self._mlb.column_names) #assert self._column_names == self._mlb.column_names for i, row in enumerate(self): for j, cell in enumerate(row): if self._reprfunc is not None: cell = self._reprfunc(i, j, cell) assert self._mlb.get(i)[j] == cell
def __init__(self, root, *args, **kw): GUIDialog.__init__(self, master=root, *args, **kw) self.qom_type_var = root.qom_type_var self.title(_("Device Tree")) self.grid() self.columnconfigure(0, weight=1, minsize=300) self.columnconfigure(2, weight=1, minsize=100) self.rowconfigure(0, weight=1) geom = "+" + str(int(root.winfo_rootx())) \ + "+" + str(int(root.winfo_rooty())) self.geometry(geom) self.focus() self.device_tree = dt = VarTreeview(self, selectmode="browse") dt["columns"] = "Macros" dt.heading("#0", text=_("Devices")) dt.heading("Macros", text=_("Macros")) dt.bind("<ButtonPress-1>", self.on_b1_press_dt) dt.grid(row=0, column=0, sticky="NEWS") add_scrollbars_native(self, dt) column_fr = Frame(self, borderwidth=0) column_fr.grid(row=0, column=2, rowspan=2, sticky="SEWN") column_fr.columnconfigure(0, weight=1) column_fr.rowconfigure(0, weight=1) column_fr.rowconfigure(1, weight=1, minsize=100) fr_at = VarLabelFrame(column_fr, text=_("Architecture filter")) fr_at.grid(row=0, column=0, sticky="SEWN") self.fr_qt = VarLabelFrame(column_fr, text=_("Select QOM type")) self.fr_qt.grid(row=1, column=0, sticky="SEWN") self.add_button = VarButton(column_fr, text=_("Select"), command=self.on_select_qom_type) self.add_button.grid(row=2, column=0, sticky="EW") self.add_button.config(state="disabled") qtype_dt = self.qtype_dt = qvd_get( root.mach.project.build_path, version=root.mach.project.target_version).qvc.device_tree arch_buttons = Frame(fr_at, borderwidth=0) arch_buttons.pack(fill="x") arch_buttons.columnconfigure(0, weight=1, minsize=60) arch_buttons.columnconfigure(1, weight=1, minsize=60) arch_buttons.columnconfigure(2, weight=1, minsize=60) bt_all_arches = VarButton(arch_buttons, text=_("All"), command=self.select_arches) bt_all_arches.grid(row=0, column=0, sticky="EW") bt_none_arches = VarButton(arch_buttons, text=_("None"), command=self.deselect_arches) bt_none_arches.grid(row=0, column=1, sticky="EW") bt_invert_arches = VarButton(arch_buttons, text=_("Invert"), command=self.invert_arches) bt_invert_arches.grid(row=0, column=2, sticky="EW") if not qtype_dt.arches: bt_all_arches.config(state="disabled") bt_none_arches.config(state="disabled") bt_invert_arches.config(state="disabled") arch_selector_outer = Frame(fr_at, borderwidth=0) arch_selector_outer.pack(fill="both", anchor="w") arch_selector, scrolltag = add_scrollbars_with_tags( arch_selector_outer, GUIFrame) ac = self.arches_checkbox = [] for a in sorted(list(qtype_dt.arches)): v = IntVar() c = Checkbutton(arch_selector, text = a, variable = v, command = lambda arch = a, var = v: \ self.on_toggle_arch(arch, var) ) c.pack(expand=1, anchor="w") c.bindtags((scrolltag, ) + c.bindtags()) c.select() ac.append(c) # key: item # value: ItemDesc self.detached_items = {} self.all_items = {} self.disabled_arches = set() self.qom_create_tree("", qtype_dt.children)