Here is something I wrote to do a "Spreadsheet" like thing. I really haven't cleaned it up beyond just getting it to work for my specific needs. Typing "Return" allows you to edit a cell. Type "Esc" stops editting without saving changes to that cell. --pete
#! /bin/env python # -*- Python -*- import sys import types import UserList import qt class Table (qt.QScrollView): """Implement a spreadsheet like widget. Individual cells can be editted and arrow keys work for navigation.""" def __init__ (self, rows, cols, parent = None, name = ''): qt.QScrollView.__init__ (self, parent, name, 0x00800000 | qt.Qt.WidgetFlags.WNorthWestGravity) self.setResizePolicy (self.ResizePolicy.Manual) #self.num_rows = rows # avoid, if possible #self.num_cols = cols self.contents = map (lambda arg: (None, []), range (0,rows)) self.row_cur = 0 self.col_cur = 0 self.editor = None self.key_last = qt.Qt.Key.Key_Right self.update_func = None # called when user changes a cell's contents self.doupdates = 1 self.selectlist = [] # List of rows that are selected self.sort_col = None self.sort_increasing = 1 # # Create the left and top headers # self.header_left = qt.QHeader (rows, self) left = self.header_left left.setOrientation (qt.Qt.Orientation.Vertical) left.setTracking (1) left.setMovingEnabled (0) self.header_top = qt.QHeader (cols, self) top = self.header_top top.setOrientation (qt.Qt.Orientation.Horizontal) top.setTracking (1) top.setMovingEnabled (0) self.setMargins (30, self.fontMetrics().height() + 4, 0, 0) self.enableClipper (1) self.viewport().setBackgroundMode (qt.QWidget.BackgroundMode.PaletteBase) #self.contents.resize (self.) self.connect (self.horizontalScrollBar (), qt.SIGNAL ('valueChanged (int)'), self.header_top, qt.SLOT ('setOffset(int)')) self.connect (self.header_top, qt.SIGNAL ('clicked(int)'), self.sort) self.connect (self.header_left, qt.SIGNAL ('clicked(int)'), self.select_toggle) self.connect (self.header_left, qt.SIGNAL ('clicked(int)'), self.select_notify) self.connect (self.verticalScrollBar (), qt.SIGNAL ('valueChanged (int)'), self.header_left, qt.SLOT ('setOffset(int)')) self.connect (self.header_top, qt.SIGNAL ('sizeChange (int, int, int)'), self.col_width_changed) self.connect (self.header_left, qt.SIGNAL ('sizeChange (int, int, int)'), self.row_height_changed) # #------------------------------------------------------------ # The following re-implement methods from qt.QTableView and qt.QWidget #------------------------------------------------------------ # def contentsMousePressEvent (self, event): # get rid of the editor self.editor_save () self.editor_cancel () row_old = self.row_cur col_old = self.col_cur self.row_cur = self.row_at (event.pos().y()) self.col_cur = self.col_at (event.pos().x()) if self.row_cur == -1: self.row_cur = row_old if self.col_cur == -1: self.col_cur = col_old # if it is a new cell, repaint it if self.row_cur != row_old or self.col_cur != col_old: self.update_cell (row_old, col_old) self.update_cell (self.row_cur, self.col_cur) rh = self.row_height (self.row_cur) cw = self.col_width (self.col_cur) self.ensureVisible (self.col_pos (self.col_cur) + cw/2, self.row_pos (self.row_cur) + rh/2, cw/2, rh/2) return None def contentsMouseMoveEvent (self, event): self.contentsMousePressEvent (event) def focusInEvent (self, event): self.update_cell (self.row_cur, self.col_cur) def focusOutEvent (self, event): self.update_cell (self.row_cur, self.col_cur) def resizeEvent (self, event): qt.QScrollView.resizeEvent (self, event) self.update_geometries () def showEvent (self, event): qt.QScrollView.showEvent (self, event) (x, y, width, height) = self.cell_geometry (self.rows () - 1, self.cols () - 1) self.resizeContents (x + width, y + height) self.update_geometries () def focusNextPrevChild (self, next): dofocus = 0 if not next: next = -1 if self.editor: #self.editor_save () # If we are either in the top left or bottom right corner # Just move vertically. Make the self.key_last be vertical if self.col_cur == 0 and self.row_cur == 0: self.key_last = qt.Qt.Key.Key_Down return self.move_vertical (1, 1) if self.col_cur + 1== self.cols () and self.row_cur + 1 == self.rows (): self.key_last = qt.Qt.Key.Key_Up return self.move_vertical (-1, 1) # Based on the previous direction, keep moving in that # same direction. If we are at a boundary, than # "slide" along that boundary. if self.key_last == qt.Qt.Key.Key_Left: if next * -1 + self.col_cur < 0: return self.move_vertical (next * -1, 1) return self.move_horizontal (next * -1, 1) elif self.key_last == qt.Qt.Key.Key_Right: if next * 1 + self.col_cur >= self.cols (): return self.move_vertical (next * 1, 1) return self.move_horizontal (next * 1, 1) elif self.key_last == qt.Qt.Key.Key_Up: if next * - 1 + self.row_cur < 0: return self.move_horizontal (next * -1, 1) return self.move_vertical (next * -1, 1) else: #self.key_last == qt.Qt.Key.Key_Down: if next * 1 + self.row_cur >= self.rows (): return self.move_horizontal (next * 1, 1) return self.move_vertical (next * 1, 1) else: return qt.QScrollView.focusNextPrevChild (self, next > 0) def keyPressEvent (self, event): if self.editor: # # We will ignore most keys (except Escape) and # let the returnPressed() signal from QMultiLineEdit # tell us person wants to move on or the # focus change which is handled by # self.focusChangeEvent () if event.key () == qt.Qt.Key.Key_Escape: self.editor_cancel () return None #elif event.key () == qt.Qt.Key.Key_Return or event.key () == qt.Qt.Key.Key_Return: # self.editor_save () # FIX: If we wanted, we could handle Page_Up, etc here #return row_old = self.row_cur col_old = self.col_cur key = event.key () if key == qt.Qt.Key.Key_Tab: key = self.key_last if key == qt.Qt.Key.Key_Left: self.move_horizontal (-1, 0) self.key_last = key elif key == qt.Qt.Key.Key_Right: self.move_horizontal (1, 0) self.key_last = key elif key == qt.Qt.Key.Key_Up: self.move_vertical (-1, 0) self.key_last = key elif key == qt.Qt.Key.Key_Down: self.move_vertical (1, 0) self.key_last = key # FIX: Hmm, If this is a Key_Return, than create an editor and start editting # If the editor does not exist, then do not create one! # Otherwise, start editing with this new text. Do we really # want to just start editing on any key? elif key == qt.Qt.Key.Key_Enter or key == qt.Qt.Key.Key_Return: self.editor_display () #else: # if event.text ()[0].isPrint (): # t = repr (event.text ()) # if not t: # event.ignore () # return # self.editor_display (repr (event.text ())) return None def drawContents (self, painter, cell_x, cell_y, cell_width, cell_height): """Reimplement QScrollView.drawContents(). Draws all the cells within the rectangle defined by x,y,width, height.""" row_first = self.row_at (cell_y) row_last = self.row_at (cell_y + cell_height) col_first = self.col_at (cell_x) col_last = self.col_at (cell_x + cell_width) if row_first == -1 or col_first == -1: self.paint_empty (painter, cell_x, cell_y, cell_width, cell_height) return if row_last == -1: row_last = self.rows () if col_last == -1: col_last = self.cols () # Step through the rows for i in range (row_first, row_last+1): row_pos = self.row_pos (i) row_height = self.row_height (i); # Look at each column in the row for j in range (col_first, col_last+1): col_pos = self.col_pos (j) col_width = self.col_width (j) # Translate painter and draw the cell painter.saveWorldMatrix () painter.translate (col_pos, row_pos) self.cell_paint (painter, i, j, (col_pos, row_pos, col_width, row_height)) painter.restoreWorldMatrix () self.paint_empty (painter, cell_x, cell_y, cell_width, cell_height) # #------------------------------------------------------------ # The following is the "public" interface to this class #------------------------------------------------------------ # def cell_data (self, row): "Return the data the application associated with item displayed at ROW. Note: this is the currently displayed row, not the original order." return self.contents[row][0] def cell_text (self, row, col): "Return the contents of cell ROW, COL as a (string, wasstring)." val = self.cell_val (row, col) wasstring = 1 if not isinstance (val, types.StringType): if val != None: val = repr (val) wasstring = 0 else: val = '' return (val, wasstring) def cell_val (self, row, col): "Return the value associated with cell ROW, COL. Note this may not be a string value." if row >= len (self.contents): return None (data, vals) = self.contents[row] if col >= len (vals): #raise 'To long at (%d, %d)' % (row, col) val = None else: val = vals[col] return val def cell_row_set (self, row, values, data = []): "Set the contents of cells at ROW with each col being in VALUES[i]. At display time, a string representation of VALUES[i] is displayed. DATA is passed back to the application when a particular row is being acted upon." if not (isinstance (values, types.ListType) or isinstance (values, UserList.UserList)): raise 'values is not a listType' if len (self.contents) <= row: self.contents = self.contents + map (lambda arg: (None, []), range (len (self.contents), row+1)) self.contents[row] = (data, values) for col in range (0, self.cols ()): self.column_resize (col) self.row_resize (row) # FIX: How to update the contents? def cell_set (self, row, col, val, notify = 1): "Set the contents of cell at ROW, COL to VAL." if len (self.contents) <= row: raise 'Row %d is longer than number or rows (%d)' % (row, len (self.contents)) (data, rowdata) = self.contents[row] if col >= len (rowdata): raise 'Column %d is longer than number of columns (%d)' % (col, len (rowdata)) rowdata[col] = val for c in range (0, self.cols ()): self.column_resize (c) self.row_resize (row) self.repaint (self.row_pos (row), self.col_pos (col), self.row_height (row), self.col_width (col), 1) self.update_cell (row, col) # FIX: How to update the contents? def header_top_set (self, col, title): "Set the header in COL to the string TITLE." self.header_top.setLabel (col, title) self.column_resize (col) def header_row_set (self, row, title): "Set the header in ROW to the string TITLE." self.header_left.setLabel (row, title) self.row_resize (row) # #------------------------------------------------------------ # The following are used to implement this class. # They should be considered "internal". #------------------------------------------------------------ # def rows (self): "Return the number of rows in this table." return self.header_left.count () def row_at (self, y): "Return the row that is at pixel location Y." return min (self.header_left.sectionAt (y), self.rows ()) def row_height (self, row): "Return the height, in pixels, of ROW." return self.header_left.sectionSize (row) def row_height_changed (self, row, os, ns): self.updateContents (0, self.row_pos (row), self.contentsWidth (), self.contentsHeight ()) (width, height) = self.table_size () h = self.contentsHeight () self.resizeContents (width, height) if self.contentsHeight () < h: self.repaintContents (0, h, self.contentsWidth (), h - height) #self.repaintContents (0, self.contentsHeight (), self.contentsWidth (), h - height) # FIX: editor self.update_geometries () def row_resize (self, row): if not self.doupdates: return info = self.fontMetrics () h = info.height () for i in range (0, self.cols ()): r = info.height () h = max (r, h) self.header_left.resizeSection (row, h + 4) def row_pos (self, row): "Return the position, in pixels, of the top of ROW." return self.header_left.sectionPos (row) def cols (self): "Return the number of columns in this table." return self.header_top.count () def col_at (self, x): "Return the column that is at pixel location X." return min (self.header_top.sectionAt (x), self.cols ()) def col_header (self, col): "Return the header displayed about COL." return self.header_top.label (col) def col_pos (self, col): "Return the position, in pixels, of the COL." return self.header_top.sectionPos (col) def column_resize (self, col): if not self.doupdates: return cols = self.cols () info = self.fontMetrics () # Figure out how wide it should be plus enough for arrow # This is based on a peak at the qheader.cpp code. w = info.width (self.header_top.label (col)) + 16 + info.height ()/2 for i in range (0, self.rows ()): r = info.width (self.cell_text (i, col)[0]) w = max (r, w) if w + 8 != self.header_top.sectionSize (col): self.header_top.resizeSection (col, w + 8) self.updateContents (self.col_pos (col), 0, self.contentsWidth (), self.contentsHeight ()) #self.col_width_changed (col, self.header_top.sectionSize (col), w+8) def col_width (self, col): "Return the width, in pixels, of COL." return self.header_top.sectionSize (col) def col_width_changed (self, col, os, ns): self.updateContents (self.col_pos (col), 0, self.contentsWidth (), self.contentsHeight ()) (width, height) = self.table_size () w = self.contentsWidth () self.resizeContents (width, height) if self.contentsWidth () < w: self.repaintContents (width, 0, w - width + 1, self.contentsHeight (), 1) # FIX: editor self.update_geometries () def table_size (self): "Return the size of the table in pixels as a tuple: (width, height)." width = self.col_pos (self.cols () - 1) + self.col_width (self.cols () - 1) height = self.row_pos (self.rows () - 1) + self.row_height (self.rows () - 1) return (width, height) def paint_empty (self, painter, x, y, width, height): "Clear a region." reg = qt.QRegion (qt.QRect (x, y, width, height)) (tw, th) = self.table_size () r = qt.QRegion (qt.QRect (qt.QPoint (0, 0), qt.QSize (tw, th))) reg = reg.subtract (r) painter.save () painter.setClipRegion (reg) painter.fillRect (x, y, width, height, self.colorGroup().brush (qt.QColorGroup.ColorRole.Base)) painter.restore () def cell_paint (self, painter, row, col, rect): "In PAINTER, draw the cell number ROW, COL located in tuple RECT (x, y, width, height)." (x, y, width, height) = rect x2 = width - 1 y2 = height - 1 # Background if row in self.selectlist: fg_color = qt.QColorGroup.ColorRole.HighlightedText bg_color = qt.QColorGroup.ColorRole.Light else: fg_color = qt.QColorGroup.ColorRole.Text bg_color = qt.QColorGroup.ColorRole.Base painter.fillRect (0, 0, width, height, self.colorGroup().brush (bg_color)) pen = qt.QPen (painter.pen ()) painter.setPen (qt.Qt.gray) painter.drawLine (x2, 0, x2, y2) painter.drawLine (0, y2, x2, y2) #painter.setPen (fg_color) painter.setPen (pen) # If we are in the focus cell, draw indication if row == self.row_cur and col == self.col_cur: if self.hasFocus () or self.viewport().hasFocus (): painter.drawRect (0, 0, x2, y2) x = 0; #pix = qt.QPixmap pix( cellPixmap( row, col ) ); #if ( !pix.isNull() ) { # p->drawPixmap( 0, ( cr.height() - pix.height() ) / 2, pix ); # x = pix.width() + 2; #} (t, string) = self.cell_text (row, col) # Draw contents if string: flag = qt.Qt.AlignmentFlags.AlignLeft else: flag = qt.Qt.AlignmentFlags.AlignRight painter.drawText( x, 0, width - x, height, flag | qt.Qt.AlignmentFlags.AlignVCenter, t) def cell_geometry (self, row, col): "Return the cell location and size in pixels: (x, y, width, height)." return (self.col_pos (col), self.row_pos (row), self.col_width (col), self.row_height (row)) def move_redraw (self, row_new, col_new): "Move the currently selected item including redrawing any frames." # If at same spot, we don't need to change anything if self.row_cur == row_new and self.col_cur == col_new: return row_old = self.row_cur col_old = self.col_cur self.update_cell (row_new, col_new) self.row_cur = row_new self.col_cur = col_new self.editor_position () cw = self.col_width (self.col_cur) rh = self.row_height (self.row_cur) self.ensureVisible (self.col_pos (self.col_cur) + cw/2, self.row_pos (self.row_cur) + rh/2, cw/2, rh/2) self.update_cell (row_old, col_old) def move_horizontal (self, direction, focus): """Move left if DIRECTION < 0, right if DIRECTION > 0. If FOCUS is true and we are at the edge, then move to another widget. Relies on self.move_redraw() to redraw.""" dofocus = 0 col_new = self.col_cur if direction < 0: if self.col_cur == 0: dofocus = 1 else: col_new = self.col_cur - 1 else: if self.col_cur == self.cols() - 1: dofocus = 1 else: col_new = self.col_cur + 1 if focus and dofocus: return qt.QScrollView.focusNextPrevChild (self, direction > 0) else: #self.editor_display () #self.editor_position () self.move_redraw (self.row_cur, col_new) if self.editor: self.editor.selectAll () return 1 def move_vertical (self, direction, focus): """Move down if DIRECTION > 0, up if DIRECTION < 0. If FOCUS is true and we are at the edge, then move to another widget. Relies on self.move_redraw() to redraw.""" dofocus = 0 row_new = self.row_cur if direction < 0: if self.row_cur == 0: dofocus = 1 else: row_new = self.row_cur - 1 else: if self.row_cur == self.rows () - 1: dofocus = 1 else: row_new = self.row_cur + 1 if focus and dofocus: return qt.QScrollView.focusNextPrevChild (self, direction > 0) else: #self.editor_display () #self.editor_position () self.move_redraw (row_new, self.col_cur) if self.editor: self.editor.selectAll () return 1 def editor_display (self, new_text = ''): """If it doesn't exist, creates a QLineEdit (self.editor). Sets the text of the self.editor to this cell (self.cur_row, self.cur_col) plus TEXT. Then positions cell.""" if not self.editor: self.editor = qt.QLineEdit ('', self.viewport ()) self.connect (self.editor, qt.SIGNAL ('returnPressed()'), self.editor_done) self.editor.setFrame (0) self.editor_position () self.editor.selectAll () def editor_position (self, new_text = ''): if not self.editor: return t = self.cell_text (self.row_cur, self.col_cur)[0] + new_text self.editor.setText (t) self.moveChild (self.editor, self.col_pos (self.col_cur) + 1, self.row_pos (self.row_cur) + 1) self.editor.resize (self.col_width (self.col_cur) - 2, self.row_height (self.row_cur) - 2) self.editor.show () # FIX: Move this to when it is created? self.editor.setFocus () def editor_cancel (self): "Get rid of the editor without saving any results." if not self.editor: return self.viewport().removeChild (self.editor) self.removeChild (self.editor) self.editor.hide () del self.editor self.editor = None self.viewport().setFocus () def editor_done (self): "Same as self.editor_save() except it moves on to the next field." if not self.editor: return self.editor_save () self.focusNextPrevChild (1) def editor_save (self): "User is finished with this cell. Save the results." if not self.editor: return val = repr (self.editor.text ()) oldval = self.cell_val (self.row_cur, self.col_cur) if not isinstance (oldval, types.StringType): try: val = eval (val, {}, {}) except (SyntaxError, OverflowError, NameError): pass self.cell_set (self.row_cur, self.col_cur, val) if self.update_func: apply (self.update_func, (self, self.row_cur, self.col_cur)) #self.editor_cancel () def select_toggle (self, row): try: del self.selectlist[self.selectlist.index (row)] except ValueError: self.selectlist.append (row) self.repaintContents (0, self.row_pos (row), self.contentsWidth (), self.row_height (row), 1) def select_clear (self): for i in self.selectlist[0:]: self.select_toggle (i) def select_notify (self, row): self.emit (qt.PYSIGNAL ('selectionChanged'), ()) def select_data_get (self): return map (lambda i, data = self.contents: data[i][0], self.selectlist) def select_get (self): return self.selectlist[0:] def select_set (self, row): if row not in self.selectlist: self.select_toggle (row) def update_enable (self, start): old = self.doupdates if start and not self.doupdates: recalc = 1 else: recalc = 0 self.doupdates = start if not recalc: return old for row in range (0, self.rows ()): self.row_resize (row) for col in range (0, self.cols ()): self.column_resize (col) return old def update_cell (self, row, col): (x, y, width, height) = self.cell_geometry (row, col) r = qt.QRect (x, y, width, height) self.updateContents (x, y, width, height) def update_geometries (self): (width, height) = self.table_size () top_offset = self.header_top.offset () top_width = self.header_top.width () if top_offset and width < top_offset + top_width: self.horizontalScrollBar.setValue (width - top_width) left_offset = self.header_left.offset () left_height = self.header_left.height () if left_offset and height < left_offset + left_height: self.verticalScrollBar().setValue (height + left_height) self.header_left.setGeometry (self.leftMargin () -30 + 2, self.topMargin () + 2, 30, self.visibleHeight ()) self.header_top.setGeometry (self.leftMargin () + 2, self.topMargin () + 2 - self.fontMetrics ().height() - 4, self.visibleWidth (), self.fontMetrics().height() + 4) def sort (self, col): self.header_top.setSortIndicator (-1, 0) if self.sort_col == col: self.sort_increasing = not self.sort_increasing else: self.sort_increasing = 1 self.header_top.setSortIndicator (col, self.sort_increasing) self.sort_col = col l = [] row = 0 for (data, values) in self.contents: l.append ((values[col], row, data, values)) row = row + 1 l.sort () if not self.sort_increasing: l.reverse () newcontents = [] newselectlist = [] i = 0 for (field, row, data, values) in l: if row in self.selectlist: newselectlist.append (i) newcontents.append ((data, values)) i = i + 1 self.contents = newcontents self.selectlist = newselectlist self.viewport().update () self.emit (qt.PYSIGNAL ('sort'), (str (self.col_header (col)),)) w = None def test (): global w app = qt.QApplication (sys.argv) cols = 10 rows = 30 w = Table (rows, cols, None, 'Table') w.update_enable (0) for i in range (0, cols): w.header_top_set (i, '%d' % i) #top.setLabel (i, 'Header %d' % i) #top.resizeSection (i, 100) for i in range (0, rows): w.header_row_set (i, '%d' % i) #left.setLabel (i, str (i)) #left.resizeSection (i, 20) for row in range (0, rows): vals = map (lambda i, row = row, cols = cols: row * cols + i, range (0,cols)) w.cell_row_set (row, vals) w.update_enable (1) app.setMainWidget (w) w.show () return app if __name__ == '__main__': print 'starting app...', app = test () print 'done' app.exec_loop ()