[PyKDE] ANNOUNCE: SIP/PyQt/PyKDE v0.13pre5
Pete Ware
ware at cis.ohio-state.edu
Thu Aug 31 14:58:22 BST 2000
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
-------------- next part --------------
#! /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 ()
More information about the PyQt
mailing list