Setting a table item value to a unicode value with non ascii chars would cause the tablewidget contents to not be displayed (no exception thrown either). Changed all instances of str(..) or map(str,...) to use asUnicode and issue seems to be fixed
289 lines
9.8 KiB
Python
289 lines
9.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
from ..Qt import QtGui, QtCore
|
|
from ..python2_3 import asUnicode
|
|
|
|
import numpy as np
|
|
try:
|
|
import metaarray
|
|
HAVE_METAARRAY = True
|
|
except ImportError:
|
|
HAVE_METAARRAY = False
|
|
|
|
__all__ = ['TableWidget']
|
|
class TableWidget(QtGui.QTableWidget):
|
|
"""Extends QTableWidget with some useful functions for automatic data handling
|
|
and copy / export context menu. Can automatically format and display a variety
|
|
of data types (see :func:`setData() <pyqtgraph.TableWidget.setData>` for more
|
|
information.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwds):
|
|
QtGui.QTableWidget.__init__(self, *args)
|
|
self.setVerticalScrollMode(self.ScrollPerPixel)
|
|
self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection)
|
|
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
|
self.setSortingEnabled(True)
|
|
self.clear()
|
|
editable = kwds.get('editable', False)
|
|
self.setEditable(editable)
|
|
self.contextMenu = QtGui.QMenu()
|
|
self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel)
|
|
self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll)
|
|
self.contextMenu.addAction('Save Selection').triggered.connect(self.saveSel)
|
|
self.contextMenu.addAction('Save All').triggered.connect(self.saveAll)
|
|
|
|
def clear(self):
|
|
"""Clear all contents from the table."""
|
|
QtGui.QTableWidget.clear(self)
|
|
self.verticalHeadersSet = False
|
|
self.horizontalHeadersSet = False
|
|
self.items = []
|
|
self.setRowCount(0)
|
|
self.setColumnCount(0)
|
|
|
|
def setData(self, data):
|
|
"""Set the data displayed in the table.
|
|
Allowed formats are:
|
|
|
|
* numpy arrays
|
|
* numpy record arrays
|
|
* metaarrays
|
|
* list-of-lists [[1,2,3], [4,5,6]]
|
|
* dict-of-lists {'x': [1,2,3], 'y': [4,5,6]}
|
|
* list-of-dicts [{'x': 1, 'y': 4}, {'x': 2, 'y': 5}, ...]
|
|
"""
|
|
self.clear()
|
|
self.appendData(data)
|
|
self.resizeColumnsToContents()
|
|
|
|
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
|
|
"""
|
|
fn0, header0 = self.iteratorFn(data)
|
|
if fn0 is None:
|
|
self.clear()
|
|
return
|
|
it0 = fn0(data)
|
|
try:
|
|
first = next(it0)
|
|
except StopIteration:
|
|
return
|
|
fn1, header1 = self.iteratorFn(first)
|
|
if fn1 is None:
|
|
self.clear()
|
|
return
|
|
|
|
firstVals = [x for x in fn1(first)]
|
|
self.setColumnCount(len(firstVals))
|
|
|
|
if not self.verticalHeadersSet and header0 is not None:
|
|
self.setRowCount(len(header0))
|
|
self.setVerticalHeaderLabels(header0)
|
|
self.verticalHeadersSet = True
|
|
if not self.horizontalHeadersSet and header1 is not None:
|
|
self.setHorizontalHeaderLabels(header1)
|
|
self.horizontalHeadersSet = True
|
|
|
|
self.setRow(0, firstVals)
|
|
i = 1
|
|
for row in it0:
|
|
self.setRow(i, [x for x in fn1(row)])
|
|
i += 1
|
|
|
|
def setEditable(self, editable=True):
|
|
self.editable = editable
|
|
for item in self.items:
|
|
item.setEditable(editable)
|
|
|
|
def iteratorFn(self, data):
|
|
## Return 1) a function that will provide an iterator for data and 2) a list of header strings
|
|
if isinstance(data, list) or isinstance(data, tuple):
|
|
return lambda d: d.__iter__(), None
|
|
elif isinstance(data, dict):
|
|
return lambda d: iter(d.values()), list(map(asUnicode, data.keys()))
|
|
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
|
if data.axisHasColumns(0):
|
|
header = [asUnicode(data.columnName(0, i)) for i in range(data.shape[0])]
|
|
elif data.axisHasValues(0):
|
|
header = list(map(asUnicode, data.xvals(0)))
|
|
else:
|
|
header = None
|
|
return self.iterFirstAxis, header
|
|
elif isinstance(data, np.ndarray):
|
|
return self.iterFirstAxis, None
|
|
elif isinstance(data, np.void):
|
|
return self.iterate, list(map(asUnicode, data.dtype.names))
|
|
elif data is None:
|
|
return (None,None)
|
|
else:
|
|
msg = "Don't know how to iterate over data type: {!s}".format(type(data))
|
|
raise TypeError(msg)
|
|
|
|
def iterFirstAxis(self, data):
|
|
for i in range(data.shape[0]):
|
|
yield data[i]
|
|
|
|
def iterate(self, data):
|
|
# for numpy.void, which can be iterated but mysteriously
|
|
# has no __iter__ (??)
|
|
for x in data:
|
|
yield x
|
|
|
|
def appendRow(self, data):
|
|
self.appendData([data])
|
|
|
|
def addRow(self, vals):
|
|
row = self.rowCount()
|
|
self.setRowCount(row + 1)
|
|
self.setRow(row, vals)
|
|
|
|
def setRow(self, row, vals):
|
|
if row > self.rowCount() - 1:
|
|
self.setRowCount(row + 1)
|
|
for col in range(len(vals)):
|
|
val = vals[col]
|
|
item = TableWidgetItem(val)
|
|
item.setEditable(self.editable)
|
|
self.items.append(item)
|
|
self.setItem(row, col, item)
|
|
|
|
def sizeHint(self):
|
|
# based on http://stackoverflow.com/a/7195443/54056
|
|
width = sum(self.columnWidth(i) for i in range(self.columnCount()))
|
|
width += self.verticalHeader().sizeHint().width()
|
|
width += self.verticalScrollBar().sizeHint().width()
|
|
width += self.frameWidth() * 2
|
|
height = sum(self.rowHeight(i) for i in range(self.rowCount()))
|
|
height += self.verticalHeader().sizeHint().height()
|
|
height += self.horizontalScrollBar().sizeHint().height()
|
|
return QtCore.QSize(width, height)
|
|
|
|
def serialize(self, useSelection=False):
|
|
"""Convert entire table (or just selected area) into tab-separated text values"""
|
|
if useSelection:
|
|
selection = self.selectedRanges()[0]
|
|
rows = list(range(selection.topRow(),
|
|
selection.bottomRow() + 1))
|
|
columns = list(range(selection.leftColumn(),
|
|
selection.rightColumn() + 1))
|
|
else:
|
|
rows = list(range(self.rowCount()))
|
|
columns = list(range(self.columnCount()))
|
|
|
|
|
|
data = []
|
|
if self.horizontalHeadersSet:
|
|
row = []
|
|
if self.verticalHeadersSet:
|
|
row.append(asUnicode(''))
|
|
|
|
for c in columns:
|
|
row.append(asUnicode(self.horizontalHeaderItem(c).text()))
|
|
data.append(row)
|
|
|
|
for r in rows:
|
|
row = []
|
|
if self.verticalHeadersSet:
|
|
row.append(asUnicode(self.verticalHeaderItem(r).text()))
|
|
for c in columns:
|
|
item = self.item(r, c)
|
|
if item is not None:
|
|
row.append(asUnicode(item.value))
|
|
else:
|
|
row.append(asUnicode(''))
|
|
data.append(row)
|
|
|
|
s = ''
|
|
for row in data:
|
|
s += ('\t'.join(row) + '\n')
|
|
return s
|
|
|
|
def copySel(self):
|
|
"""Copy selected data to clipboard."""
|
|
QtGui.QApplication.clipboard().setText(self.serialize(useSelection=True))
|
|
|
|
def copyAll(self):
|
|
"""Copy all data to clipboard."""
|
|
QtGui.QApplication.clipboard().setText(self.serialize(useSelection=False))
|
|
|
|
def saveSel(self):
|
|
"""Save selected data to file."""
|
|
self.save(self.serialize(useSelection=True))
|
|
|
|
def saveAll(self):
|
|
"""Save all data to file."""
|
|
self.save(self.serialize(useSelection=False))
|
|
|
|
def save(self, data):
|
|
fileName = QtGui.QFileDialog.getSaveFileName(self, "Save As..", "", "Tab-separated values (*.tsv)")
|
|
if fileName == '':
|
|
return
|
|
open(fileName, 'w').write(data)
|
|
|
|
|
|
def contextMenuEvent(self, ev):
|
|
self.contextMenu.popup(ev.globalPos())
|
|
|
|
def keyPressEvent(self, ev):
|
|
if ev.text() == 'c' and ev.modifiers() == QtCore.Qt.ControlModifier:
|
|
ev.accept()
|
|
self.copy()
|
|
else:
|
|
ev.ignore()
|
|
|
|
class TableWidgetItem(QtGui.QTableWidgetItem):
|
|
def __init__(self, val):
|
|
if isinstance(val, float) or isinstance(val, np.floating):
|
|
s = "%0.3g" % val
|
|
else:
|
|
s = asUnicode(val)
|
|
QtGui.QTableWidgetItem.__init__(self, s)
|
|
self.value = val
|
|
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
|
self.setFlags(flags)
|
|
|
|
def setEditable(self, editable):
|
|
if editable:
|
|
self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable)
|
|
else:
|
|
self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable)
|
|
|
|
def __lt__(self, other):
|
|
if hasattr(other, 'value'):
|
|
return self.value < other.value
|
|
else:
|
|
return self.text() < other.text()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app = QtGui.QApplication([])
|
|
win = QtGui.QMainWindow()
|
|
t = TableWidget()
|
|
win.setCentralWidget(t)
|
|
win.resize(800,600)
|
|
win.show()
|
|
|
|
ll = [[1,2,3,4,5]] * 20
|
|
ld = [{'x': 1, 'y': 2, 'z': 3}] * 20
|
|
dl = {'x': list(range(20)), 'y': list(range(20)), 'z': list(range(20))}
|
|
|
|
a = np.ones((20, 5))
|
|
ra = np.ones((20,), dtype=[('x', int), ('y', int), ('z', int)])
|
|
|
|
t.setData(ll)
|
|
|
|
if HAVE_METAARRAY:
|
|
ma = metaarray.MetaArray(np.ones((20, 3)), info=[
|
|
{'values': np.linspace(1, 5, 20)},
|
|
{'cols': [
|
|
{'name': 'x'},
|
|
{'name': 'y'},
|
|
{'name': 'z'},
|
|
]}
|
|
])
|
|
t.setData(ma)
|
|
|