Fix TableWidget sorting behavior when appending and editing data
Merge branch 'tablewidget_fix' into develop
This commit is contained in:
commit
62b506c63c
@ -80,6 +80,7 @@ pyqtgraph-0.9.9 [unreleased]
|
|||||||
- Fixed GLGridItem.setSize
|
- Fixed GLGridItem.setSize
|
||||||
- Fixed parametertree.Parameter.sigValueChanging
|
- Fixed parametertree.Parameter.sigValueChanging
|
||||||
- Fixed AxisItem.__init__(showValues=False)
|
- Fixed AxisItem.__init__(showValues=False)
|
||||||
|
- Fixed TableWidget append / sort issues
|
||||||
|
|
||||||
pyqtgraph-0.9.8 2013-11-24
|
pyqtgraph-0.9.8 2013-11-24
|
||||||
|
|
||||||
|
@ -9,7 +9,28 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
HAVE_METAARRAY = False
|
HAVE_METAARRAY = False
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['TableWidget']
|
__all__ = ['TableWidget']
|
||||||
|
|
||||||
|
|
||||||
|
def _defersort(fn):
|
||||||
|
def defersort(self, *args, **kwds):
|
||||||
|
# may be called recursively; only the first call needs to block sorting
|
||||||
|
setSorting = False
|
||||||
|
if self._sorting is None:
|
||||||
|
self._sorting = self.isSortingEnabled()
|
||||||
|
setSorting = True
|
||||||
|
self.setSortingEnabled(False)
|
||||||
|
try:
|
||||||
|
return fn(self, *args, **kwds)
|
||||||
|
finally:
|
||||||
|
if setSorting:
|
||||||
|
self.setSortingEnabled(self._sorting)
|
||||||
|
self._sorting = None
|
||||||
|
|
||||||
|
return defersort
|
||||||
|
|
||||||
|
|
||||||
class TableWidget(QtGui.QTableWidget):
|
class TableWidget(QtGui.QTableWidget):
|
||||||
"""Extends QTableWidget with some useful functions for automatic data handling
|
"""Extends QTableWidget with some useful functions for automatic data handling
|
||||||
and copy / export context menu. Can automatically format and display a variety
|
and copy / export context menu. Can automatically format and display a variety
|
||||||
@ -18,14 +39,40 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
|
"""
|
||||||
|
All positional arguments are passed to QTableWidget.__init__().
|
||||||
|
|
||||||
|
===================== =================================================
|
||||||
|
**Keyword Arguments**
|
||||||
|
editable (bool) If True, cells in the table can be edited
|
||||||
|
by the user. Default is False.
|
||||||
|
sortable (bool) If True, the table may be soted by
|
||||||
|
clicking on column headers. Note that this also
|
||||||
|
causes rows to appear initially shuffled until
|
||||||
|
a sort column is selected. Default is True.
|
||||||
|
*(added in version 0.9.9)*
|
||||||
|
===================== =================================================
|
||||||
|
"""
|
||||||
|
|
||||||
QtGui.QTableWidget.__init__(self, *args)
|
QtGui.QTableWidget.__init__(self, *args)
|
||||||
|
|
||||||
self.setVerticalScrollMode(self.ScrollPerPixel)
|
self.setVerticalScrollMode(self.ScrollPerPixel)
|
||||||
self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection)
|
self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection)
|
||||||
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
||||||
self.setSortingEnabled(True)
|
|
||||||
self.clear()
|
self.clear()
|
||||||
editable = kwds.get('editable', False)
|
|
||||||
self.setEditable(editable)
|
kwds.setdefault('sortable', True)
|
||||||
|
kwds.setdefault('editable', False)
|
||||||
|
self.setEditable(kwds.pop('editable'))
|
||||||
|
self.setSortingEnabled(kwds.pop('sortable'))
|
||||||
|
|
||||||
|
if len(kwds) > 0:
|
||||||
|
raise TypeError("Invalid keyword arguments '%s'" % kwds.keys())
|
||||||
|
|
||||||
|
self._sorting = None
|
||||||
|
|
||||||
|
self.itemChanged.connect(self.handleItemChanged)
|
||||||
|
|
||||||
self.contextMenu = QtGui.QMenu()
|
self.contextMenu = QtGui.QMenu()
|
||||||
self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel)
|
self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel)
|
||||||
self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll)
|
self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll)
|
||||||
@ -40,6 +87,7 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
self.items = []
|
self.items = []
|
||||||
self.setRowCount(0)
|
self.setRowCount(0)
|
||||||
self.setColumnCount(0)
|
self.setColumnCount(0)
|
||||||
|
self.sortModes = {}
|
||||||
|
|
||||||
def setData(self, data):
|
def setData(self, data):
|
||||||
"""Set the data displayed in the table.
|
"""Set the data displayed in the table.
|
||||||
@ -56,12 +104,16 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
self.appendData(data)
|
self.appendData(data)
|
||||||
self.resizeColumnsToContents()
|
self.resizeColumnsToContents()
|
||||||
|
|
||||||
|
@_defersort
|
||||||
def appendData(self, data):
|
def appendData(self, data):
|
||||||
"""Types allowed:
|
|
||||||
1 or 2D numpy array or metaArray
|
|
||||||
1D numpy record array
|
|
||||||
list-of-lists, list-of-dicts or dict-of-lists
|
|
||||||
"""
|
"""
|
||||||
|
Add new rows to the table.
|
||||||
|
|
||||||
|
See :func:`setData() <pyqtgraph.TableWidget.setData>` for accepted
|
||||||
|
data types.
|
||||||
|
"""
|
||||||
|
startRow = self.rowCount()
|
||||||
|
|
||||||
fn0, header0 = self.iteratorFn(data)
|
fn0, header0 = self.iteratorFn(data)
|
||||||
if fn0 is None:
|
if fn0 is None:
|
||||||
self.clear()
|
self.clear()
|
||||||
@ -80,18 +132,22 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
self.setColumnCount(len(firstVals))
|
self.setColumnCount(len(firstVals))
|
||||||
|
|
||||||
if not self.verticalHeadersSet and header0 is not None:
|
if not self.verticalHeadersSet and header0 is not None:
|
||||||
self.setRowCount(len(header0))
|
labels = [self.verticalHeaderItem(i).text() for i in range(self.rowCount())]
|
||||||
self.setVerticalHeaderLabels(header0)
|
self.setRowCount(startRow + len(header0))
|
||||||
|
self.setVerticalHeaderLabels(labels + header0)
|
||||||
self.verticalHeadersSet = True
|
self.verticalHeadersSet = True
|
||||||
if not self.horizontalHeadersSet and header1 is not None:
|
if not self.horizontalHeadersSet and header1 is not None:
|
||||||
self.setHorizontalHeaderLabels(header1)
|
self.setHorizontalHeaderLabels(header1)
|
||||||
self.horizontalHeadersSet = True
|
self.horizontalHeadersSet = True
|
||||||
|
|
||||||
self.setRow(0, firstVals)
|
i = startRow
|
||||||
i = 1
|
self.setRow(i, firstVals)
|
||||||
for row in it0:
|
for row in it0:
|
||||||
self.setRow(i, [x for x in fn1(row)])
|
|
||||||
i += 1
|
i += 1
|
||||||
|
self.setRow(i, [x for x in fn1(row)])
|
||||||
|
|
||||||
|
if self._sorting and self.horizontalHeader().sortIndicatorSection() >= self.columnCount():
|
||||||
|
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||||
|
|
||||||
def setEditable(self, editable=True):
|
def setEditable(self, editable=True):
|
||||||
self.editable = editable
|
self.editable = editable
|
||||||
@ -135,21 +191,46 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
def appendRow(self, data):
|
def appendRow(self, data):
|
||||||
self.appendData([data])
|
self.appendData([data])
|
||||||
|
|
||||||
|
@_defersort
|
||||||
def addRow(self, vals):
|
def addRow(self, vals):
|
||||||
row = self.rowCount()
|
row = self.rowCount()
|
||||||
self.setRowCount(row + 1)
|
self.setRowCount(row + 1)
|
||||||
self.setRow(row, vals)
|
self.setRow(row, vals)
|
||||||
|
|
||||||
|
@_defersort
|
||||||
def setRow(self, row, vals):
|
def setRow(self, row, vals):
|
||||||
if row > self.rowCount() - 1:
|
if row > self.rowCount() - 1:
|
||||||
self.setRowCount(row + 1)
|
self.setRowCount(row + 1)
|
||||||
for col in range(len(vals)):
|
for col in range(len(vals)):
|
||||||
val = vals[col]
|
val = vals[col]
|
||||||
item = TableWidgetItem(val)
|
item = TableWidgetItem(val, row)
|
||||||
item.setEditable(self.editable)
|
item.setEditable(self.editable)
|
||||||
|
sortMode = self.sortModes.get(col, None)
|
||||||
|
if sortMode is not None:
|
||||||
|
item.setSortMode(sortMode)
|
||||||
self.items.append(item)
|
self.items.append(item)
|
||||||
self.setItem(row, col, item)
|
self.setItem(row, col, item)
|
||||||
|
|
||||||
|
def setSortMode(self, column, mode):
|
||||||
|
"""
|
||||||
|
Set the mode used to sort *column*.
|
||||||
|
|
||||||
|
============== ========================================================
|
||||||
|
**Sort Modes**
|
||||||
|
value Compares item.value if available; falls back to text
|
||||||
|
comparison.
|
||||||
|
text Compares item.text()
|
||||||
|
index Compares by the order in which items were inserted.
|
||||||
|
============== ========================================================
|
||||||
|
|
||||||
|
Added in version 0.9.9
|
||||||
|
"""
|
||||||
|
for r in range(self.rowCount()):
|
||||||
|
item = self.item(r, column)
|
||||||
|
if hasattr(item, 'setSortMode'):
|
||||||
|
item.setSortMode(mode)
|
||||||
|
self.sortModes[column] = mode
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
# based on http://stackoverflow.com/a/7195443/54056
|
# based on http://stackoverflow.com/a/7195443/54056
|
||||||
width = sum(self.columnWidth(i) for i in range(self.columnCount()))
|
width = sum(self.columnWidth(i) for i in range(self.columnCount()))
|
||||||
@ -234,25 +315,56 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
else:
|
else:
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
|
|
||||||
|
def handleItemChanged(self, item):
|
||||||
|
try:
|
||||||
|
item.value = type(item.value)(item.text())
|
||||||
|
except ValueError:
|
||||||
|
item.value = str(item.text())
|
||||||
|
|
||||||
|
|
||||||
class TableWidgetItem(QtGui.QTableWidgetItem):
|
class TableWidgetItem(QtGui.QTableWidgetItem):
|
||||||
def __init__(self, val):
|
def __init__(self, val, index):
|
||||||
if isinstance(val, float) or isinstance(val, np.floating):
|
if isinstance(val, float) or isinstance(val, np.floating):
|
||||||
s = "%0.3g" % val
|
s = "%0.3g" % val
|
||||||
else:
|
else:
|
||||||
s = asUnicode(val)
|
s = asUnicode(val)
|
||||||
QtGui.QTableWidgetItem.__init__(self, s)
|
QtGui.QTableWidgetItem.__init__(self, s)
|
||||||
|
self.sortMode = 'value'
|
||||||
self.value = val
|
self.value = val
|
||||||
|
self.index = index
|
||||||
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
||||||
self.setFlags(flags)
|
self.setFlags(flags)
|
||||||
|
|
||||||
def setEditable(self, editable):
|
def setEditable(self, editable):
|
||||||
|
"""
|
||||||
|
Set whether this item is user-editable.
|
||||||
|
"""
|
||||||
if editable:
|
if editable:
|
||||||
self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable)
|
self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable)
|
||||||
else:
|
else:
|
||||||
self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable)
|
self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||||
|
|
||||||
|
def setSortMode(self, mode):
|
||||||
|
"""
|
||||||
|
Set the mode used to sort this item against others in its column.
|
||||||
|
|
||||||
|
============== ========================================================
|
||||||
|
**Sort Modes**
|
||||||
|
value Compares item.value if available; falls back to text
|
||||||
|
comparison.
|
||||||
|
text Compares item.text()
|
||||||
|
index Compares by the order in which items were inserted.
|
||||||
|
============== ========================================================
|
||||||
|
"""
|
||||||
|
modes = ('value', 'text', 'index', None)
|
||||||
|
if mode not in modes:
|
||||||
|
raise ValueError('Sort mode must be one of %s' % str(modes))
|
||||||
|
self.sortMode = mode
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
if hasattr(other, 'value'):
|
if self.sortMode == 'index' and hasattr(other, 'index'):
|
||||||
|
return self.index < other.index
|
||||||
|
if self.sortMode == 'value' and hasattr(other, 'value'):
|
||||||
return self.value < other.value
|
return self.value < other.value
|
||||||
else:
|
else:
|
||||||
return self.text() < other.text()
|
return self.text() < other.text()
|
||||||
|
87
pyqtgraph/widgets/tests/test_tablewidget.py
Normal file
87
pyqtgraph/widgets/tests/test_tablewidget.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import pyqtgraph as pg
|
||||||
|
import numpy as np
|
||||||
|
from pyqtgraph.pgcollections import OrderedDict
|
||||||
|
|
||||||
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
|
||||||
|
listOfTuples = [('text_%d' % i, i, i/10.) for i in range(12)]
|
||||||
|
listOfLists = [list(row) for row in listOfTuples]
|
||||||
|
plainArray = np.array(listOfLists, dtype=object)
|
||||||
|
recordArray = np.array(listOfTuples, dtype=[('string', object),
|
||||||
|
('integer', int),
|
||||||
|
('floating', float)])
|
||||||
|
dictOfLists = OrderedDict([(name, list(recordArray[name])) for name in recordArray.dtype.names])
|
||||||
|
listOfDicts = [OrderedDict([(name, rec[name]) for name in recordArray.dtype.names]) for rec in recordArray]
|
||||||
|
transposed = [[row[col] for row in listOfTuples] for col in range(len(listOfTuples[0]))]
|
||||||
|
|
||||||
|
def assertTableData(table, data):
|
||||||
|
assert len(data) == table.rowCount()
|
||||||
|
rows = list(range(table.rowCount()))
|
||||||
|
columns = list(range(table.columnCount()))
|
||||||
|
for r in rows:
|
||||||
|
assert len(data[r]) == table.columnCount()
|
||||||
|
row = []
|
||||||
|
for c in columns:
|
||||||
|
item = table.item(r, c)
|
||||||
|
if item is not None:
|
||||||
|
row.append(item.value)
|
||||||
|
else:
|
||||||
|
row.append(None)
|
||||||
|
assert row == list(data[r])
|
||||||
|
|
||||||
|
|
||||||
|
def test_TableWidget():
|
||||||
|
w = pg.TableWidget(sortable=False)
|
||||||
|
|
||||||
|
# Test all input data types
|
||||||
|
w.setData(listOfTuples)
|
||||||
|
assertTableData(w, listOfTuples)
|
||||||
|
|
||||||
|
w.setData(listOfLists)
|
||||||
|
assertTableData(w, listOfTuples)
|
||||||
|
|
||||||
|
w.setData(plainArray)
|
||||||
|
assertTableData(w, listOfTuples)
|
||||||
|
|
||||||
|
w.setData(recordArray)
|
||||||
|
assertTableData(w, listOfTuples)
|
||||||
|
|
||||||
|
w.setData(dictOfLists)
|
||||||
|
assertTableData(w, transposed)
|
||||||
|
|
||||||
|
w.appendData(dictOfLists)
|
||||||
|
assertTableData(w, transposed * 2)
|
||||||
|
|
||||||
|
w.setData(listOfDicts)
|
||||||
|
assertTableData(w, listOfTuples)
|
||||||
|
|
||||||
|
w.appendData(listOfDicts)
|
||||||
|
assertTableData(w, listOfTuples * 2)
|
||||||
|
|
||||||
|
# Test sorting
|
||||||
|
w.setData(listOfTuples)
|
||||||
|
w.sortByColumn(0, pg.QtCore.Qt.AscendingOrder)
|
||||||
|
assertTableData(w, sorted(listOfTuples, key=lambda a: a[0]))
|
||||||
|
|
||||||
|
w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder)
|
||||||
|
assertTableData(w, sorted(listOfTuples, key=lambda a: a[1]))
|
||||||
|
|
||||||
|
w.sortByColumn(2, pg.QtCore.Qt.AscendingOrder)
|
||||||
|
assertTableData(w, sorted(listOfTuples, key=lambda a: a[2]))
|
||||||
|
|
||||||
|
w.setSortMode(1, 'text')
|
||||||
|
w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder)
|
||||||
|
assertTableData(w, sorted(listOfTuples, key=lambda a: str(a[1])))
|
||||||
|
|
||||||
|
w.setSortMode(1, 'index')
|
||||||
|
w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder)
|
||||||
|
assertTableData(w, listOfTuples)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
w = pg.TableWidget(editable=True)
|
||||||
|
w.setData(listOfTuples)
|
||||||
|
w.resize(600, 600)
|
||||||
|
w.show()
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user