Merge remote-tracking branch 'pyqtgraph/develop' into core
This commit is contained in:
commit
f9c85dae42
25
Qt.py
25
Qt.py
@ -11,6 +11,8 @@ This module exists to smooth out some of the differences between PySide and PyQt
|
|||||||
|
|
||||||
import sys, re
|
import sys, re
|
||||||
|
|
||||||
|
from .python2_3 import asUnicode
|
||||||
|
|
||||||
## Automatically determine whether to use PyQt or PySide.
|
## Automatically determine whether to use PyQt or PySide.
|
||||||
## This is done by first checking to see whether one of the libraries
|
## This is done by first checking to see whether one of the libraries
|
||||||
## is already imported. If not, then attempt to import PyQt4, then PySide.
|
## is already imported. If not, then attempt to import PyQt4, then PySide.
|
||||||
@ -31,6 +33,10 @@ else:
|
|||||||
|
|
||||||
if USE_PYSIDE:
|
if USE_PYSIDE:
|
||||||
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
||||||
|
try:
|
||||||
|
from PySide import QtTest
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
import PySide
|
import PySide
|
||||||
try:
|
try:
|
||||||
from PySide import shiboken
|
from PySide import shiboken
|
||||||
@ -56,18 +62,29 @@ if USE_PYSIDE:
|
|||||||
# Credit:
|
# Credit:
|
||||||
# http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313
|
# http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313
|
||||||
|
|
||||||
|
class StringIO(object):
|
||||||
|
"""Alternative to built-in StringIO needed to circumvent unicode/ascii issues"""
|
||||||
|
def __init__(self):
|
||||||
|
self.data = []
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.data.append(data)
|
||||||
|
|
||||||
|
def getvalue(self):
|
||||||
|
return ''.join(map(asUnicode, self.data)).encode('utf8')
|
||||||
|
|
||||||
def loadUiType(uiFile):
|
def loadUiType(uiFile):
|
||||||
"""
|
"""
|
||||||
Pyside "loadUiType" command like PyQt4 has one, so we have to convert the ui file to py code in-memory first and then execute it in a special frame to retrieve the form_class.
|
Pyside "loadUiType" command like PyQt4 has one, so we have to convert the ui file to py code in-memory first and then execute it in a special frame to retrieve the form_class.
|
||||||
"""
|
"""
|
||||||
import pysideuic
|
import pysideuic
|
||||||
import xml.etree.ElementTree as xml
|
import xml.etree.ElementTree as xml
|
||||||
from io import StringIO
|
#from io import StringIO
|
||||||
|
|
||||||
parsed = xml.parse(uiFile)
|
parsed = xml.parse(uiFile)
|
||||||
widget_class = parsed.find('widget').get('class')
|
widget_class = parsed.find('widget').get('class')
|
||||||
form_class = parsed.find('class').text
|
form_class = parsed.find('class').text
|
||||||
|
|
||||||
with open(uiFile, 'r') as f:
|
with open(uiFile, 'r') as f:
|
||||||
o = StringIO()
|
o = StringIO()
|
||||||
frame = {}
|
frame = {}
|
||||||
@ -93,6 +110,10 @@ else:
|
|||||||
from PyQt4 import QtOpenGL
|
from PyQt4 import QtOpenGL
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
from PyQt4 import QtTest
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
import sip
|
import sip
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from .Qt import QtCore
|
from .Qt import QtCore
|
||||||
from .ptime import time
|
from .ptime import time
|
||||||
from . import ThreadsafeTimer
|
from . import ThreadsafeTimer
|
||||||
|
import weakref
|
||||||
|
|
||||||
__all__ = ['SignalProxy']
|
__all__ = ['SignalProxy']
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ class SignalProxy(QtCore.QObject):
|
|||||||
self.timer = ThreadsafeTimer.ThreadsafeTimer()
|
self.timer = ThreadsafeTimer.ThreadsafeTimer()
|
||||||
self.timer.timeout.connect(self.flush)
|
self.timer.timeout.connect(self.flush)
|
||||||
self.block = False
|
self.block = False
|
||||||
self.slot = slot
|
self.slot = weakref.ref(slot)
|
||||||
self.lastFlushTime = None
|
self.lastFlushTime = None
|
||||||
if slot is not None:
|
if slot is not None:
|
||||||
self.sigDelayed.connect(slot)
|
self.sigDelayed.connect(slot)
|
||||||
@ -80,7 +81,7 @@ class SignalProxy(QtCore.QObject):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
self.sigDelayed.disconnect(self.slot)
|
self.sigDelayed.disconnect(self.slot())
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ from .graphicsWindows import *
|
|||||||
from .SignalProxy import *
|
from .SignalProxy import *
|
||||||
from .colormap import *
|
from .colormap import *
|
||||||
from .ptime import time
|
from .ptime import time
|
||||||
from pyqtgraph.Qt import isQObjectAlive
|
from .Qt import isQObjectAlive
|
||||||
|
|
||||||
|
|
||||||
##############################################################
|
##############################################################
|
||||||
|
@ -22,6 +22,9 @@ class Container(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def insert(self, new, pos=None, neighbor=None):
|
def insert(self, new, pos=None, neighbor=None):
|
||||||
|
# remove from existing parent first
|
||||||
|
new.setParent(None)
|
||||||
|
|
||||||
if not isinstance(new, list):
|
if not isinstance(new, list):
|
||||||
new = [new]
|
new = [new]
|
||||||
if neighbor is None:
|
if neighbor is None:
|
||||||
|
112
dockarea/Dock.py
112
dockarea/Dock.py
@ -8,11 +8,13 @@ class Dock(QtGui.QWidget, DockDrop):
|
|||||||
|
|
||||||
sigStretchChanged = QtCore.Signal()
|
sigStretchChanged = QtCore.Signal()
|
||||||
|
|
||||||
def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True):
|
def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False):
|
||||||
QtGui.QWidget.__init__(self)
|
QtGui.QWidget.__init__(self)
|
||||||
DockDrop.__init__(self)
|
DockDrop.__init__(self)
|
||||||
self.area = area
|
self.area = area
|
||||||
self.label = DockLabel(name, self)
|
self.label = DockLabel(name, self, closable)
|
||||||
|
if closable:
|
||||||
|
self.label.sigCloseClicked.connect(self.close)
|
||||||
self.labelHidden = False
|
self.labelHidden = False
|
||||||
self.moveLabel = True ## If false, the dock is no longer allowed to move the label.
|
self.moveLabel = True ## If false, the dock is no longer allowed to move the label.
|
||||||
self.autoOrient = autoOrientation
|
self.autoOrient = autoOrientation
|
||||||
@ -35,30 +37,30 @@ class Dock(QtGui.QWidget, DockDrop):
|
|||||||
#self.titlePos = 'top'
|
#self.titlePos = 'top'
|
||||||
self.raiseOverlay()
|
self.raiseOverlay()
|
||||||
self.hStyle = """
|
self.hStyle = """
|
||||||
Dock > QWidget {
|
Dock > QWidget {
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border-top-left-radius: 0px;
|
border-top-left-radius: 0px;
|
||||||
border-top-right-radius: 0px;
|
border-top-right-radius: 0px;
|
||||||
border-top-width: 0px;
|
border-top-width: 0px;
|
||||||
}"""
|
}"""
|
||||||
self.vStyle = """
|
self.vStyle = """
|
||||||
Dock > QWidget {
|
Dock > QWidget {
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border-top-left-radius: 0px;
|
border-top-left-radius: 0px;
|
||||||
border-bottom-left-radius: 0px;
|
border-bottom-left-radius: 0px;
|
||||||
border-left-width: 0px;
|
border-left-width: 0px;
|
||||||
}"""
|
}"""
|
||||||
self.nStyle = """
|
self.nStyle = """
|
||||||
Dock > QWidget {
|
Dock > QWidget {
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}"""
|
}"""
|
||||||
self.dragStyle = """
|
self.dragStyle = """
|
||||||
Dock > QWidget {
|
Dock > QWidget {
|
||||||
border: 4px solid #00F;
|
border: 4px solid #00F;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}"""
|
}"""
|
||||||
self.setAutoFillBackground(False)
|
self.setAutoFillBackground(False)
|
||||||
self.widgetArea.setStyleSheet(self.hStyle)
|
self.widgetArea.setStyleSheet(self.hStyle)
|
||||||
@ -79,7 +81,7 @@ class Dock(QtGui.QWidget, DockDrop):
|
|||||||
|
|
||||||
def setStretch(self, x=None, y=None):
|
def setStretch(self, x=None, y=None):
|
||||||
"""
|
"""
|
||||||
Set the 'target' size for this Dock.
|
Set the 'target' size for this Dock.
|
||||||
The actual size will be determined by comparing this Dock's
|
The actual size will be determined by comparing this Dock's
|
||||||
stretch value to the rest of the docks it shares space with.
|
stretch value to the rest of the docks it shares space with.
|
||||||
"""
|
"""
|
||||||
@ -130,7 +132,7 @@ class Dock(QtGui.QWidget, DockDrop):
|
|||||||
Sets the orientation of the title bar for this Dock.
|
Sets the orientation of the title bar for this Dock.
|
||||||
Must be one of 'auto', 'horizontal', or 'vertical'.
|
Must be one of 'auto', 'horizontal', or 'vertical'.
|
||||||
By default ('auto'), the orientation is determined
|
By default ('auto'), the orientation is determined
|
||||||
based on the aspect ratio of the Dock.
|
based on the aspect ratio of the Dock.
|
||||||
"""
|
"""
|
||||||
#print self.name(), "setOrientation", o, force
|
#print self.name(), "setOrientation", o, force
|
||||||
if o == 'auto' and self.autoOrient:
|
if o == 'auto' and self.autoOrient:
|
||||||
@ -175,7 +177,7 @@ class Dock(QtGui.QWidget, DockDrop):
|
|||||||
|
|
||||||
def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
|
def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
|
||||||
"""
|
"""
|
||||||
Add a new widget to the interior of this Dock.
|
Add a new widget to the interior of this Dock.
|
||||||
Each Dock uses a QGridLayout to arrange widgets within.
|
Each Dock uses a QGridLayout to arrange widgets within.
|
||||||
"""
|
"""
|
||||||
if row is None:
|
if row is None:
|
||||||
@ -239,11 +241,13 @@ class Dock(QtGui.QWidget, DockDrop):
|
|||||||
def dropEvent(self, *args):
|
def dropEvent(self, *args):
|
||||||
DockDrop.dropEvent(self, *args)
|
DockDrop.dropEvent(self, *args)
|
||||||
|
|
||||||
|
|
||||||
class DockLabel(VerticalLabel):
|
class DockLabel(VerticalLabel):
|
||||||
|
|
||||||
sigClicked = QtCore.Signal(object, object)
|
sigClicked = QtCore.Signal(object, object)
|
||||||
|
sigCloseClicked = QtCore.Signal()
|
||||||
|
|
||||||
def __init__(self, text, dock):
|
def __init__(self, text, dock, showCloseButton):
|
||||||
self.dim = False
|
self.dim = False
|
||||||
self.fixedWidth = False
|
self.fixedWidth = False
|
||||||
VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False)
|
VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False)
|
||||||
@ -251,10 +255,13 @@ class DockLabel(VerticalLabel):
|
|||||||
self.dock = dock
|
self.dock = dock
|
||||||
self.updateStyle()
|
self.updateStyle()
|
||||||
self.setAutoFillBackground(False)
|
self.setAutoFillBackground(False)
|
||||||
|
self.startedDrag = False
|
||||||
|
|
||||||
#def minimumSizeHint(self):
|
self.closeButton = None
|
||||||
##sh = QtGui.QWidget.minimumSizeHint(self)
|
if showCloseButton:
|
||||||
#return QtCore.QSize(20, 20)
|
self.closeButton = QtGui.QToolButton(self)
|
||||||
|
self.closeButton.clicked.connect(self.sigCloseClicked)
|
||||||
|
self.closeButton.setIcon(QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_TitleBarCloseButton))
|
||||||
|
|
||||||
def updateStyle(self):
|
def updateStyle(self):
|
||||||
r = '3px'
|
r = '3px'
|
||||||
@ -268,28 +275,28 @@ class DockLabel(VerticalLabel):
|
|||||||
border = '#55B'
|
border = '#55B'
|
||||||
|
|
||||||
if self.orientation == 'vertical':
|
if self.orientation == 'vertical':
|
||||||
self.vStyle = """DockLabel {
|
self.vStyle = """DockLabel {
|
||||||
background-color : %s;
|
background-color : %s;
|
||||||
color : %s;
|
color : %s;
|
||||||
border-top-right-radius: 0px;
|
border-top-right-radius: 0px;
|
||||||
border-top-left-radius: %s;
|
border-top-left-radius: %s;
|
||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
border-bottom-left-radius: %s;
|
border-bottom-left-radius: %s;
|
||||||
border-width: 0px;
|
border-width: 0px;
|
||||||
border-right: 2px solid %s;
|
border-right: 2px solid %s;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
padding-bottom: 3px;
|
padding-bottom: 3px;
|
||||||
}""" % (bg, fg, r, r, border)
|
}""" % (bg, fg, r, r, border)
|
||||||
self.setStyleSheet(self.vStyle)
|
self.setStyleSheet(self.vStyle)
|
||||||
else:
|
else:
|
||||||
self.hStyle = """DockLabel {
|
self.hStyle = """DockLabel {
|
||||||
background-color : %s;
|
background-color : %s;
|
||||||
color : %s;
|
color : %s;
|
||||||
border-top-right-radius: %s;
|
border-top-right-radius: %s;
|
||||||
border-top-left-radius: %s;
|
border-top-left-radius: %s;
|
||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
border-bottom-left-radius: 0px;
|
border-bottom-left-radius: 0px;
|
||||||
border-width: 0px;
|
border-width: 0px;
|
||||||
border-bottom: 2px solid %s;
|
border-bottom: 2px solid %s;
|
||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
padding-right: 3px;
|
padding-right: 3px;
|
||||||
@ -315,11 +322,9 @@ class DockLabel(VerticalLabel):
|
|||||||
if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
|
if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
|
||||||
self.dock.startDrag()
|
self.dock.startDrag()
|
||||||
ev.accept()
|
ev.accept()
|
||||||
#print ev.pos()
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
if not self.startedDrag:
|
if not self.startedDrag:
|
||||||
#self.emit(QtCore.SIGNAL('clicked'), self, ev)
|
|
||||||
self.sigClicked.emit(self, ev)
|
self.sigClicked.emit(self, ev)
|
||||||
ev.accept()
|
ev.accept()
|
||||||
|
|
||||||
@ -327,13 +332,14 @@ class DockLabel(VerticalLabel):
|
|||||||
if ev.button() == QtCore.Qt.LeftButton:
|
if ev.button() == QtCore.Qt.LeftButton:
|
||||||
self.dock.float()
|
self.dock.float()
|
||||||
|
|
||||||
#def paintEvent(self, ev):
|
def resizeEvent (self, ev):
|
||||||
#p = QtGui.QPainter(self)
|
if self.closeButton:
|
||||||
##p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200)))
|
if self.orientation == 'vertical':
|
||||||
#p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100)))
|
size = ev.size().width()
|
||||||
#p.drawRect(self.rect().adjusted(0, 0, -1, -1))
|
pos = QtCore.QPoint(0, 0)
|
||||||
|
else:
|
||||||
#VerticalLabel.paintEvent(self, ev)
|
size = ev.size().height()
|
||||||
|
pos = QtCore.QPoint(ev.size().width() - size, 0)
|
||||||
|
self.closeButton.setFixedSize(QtCore.QSize(size, size))
|
||||||
|
self.closeButton.move(pos)
|
||||||
|
super(DockLabel,self).resizeEvent(ev)
|
||||||
|
@ -52,10 +52,11 @@ class CSVExporter(Exporter):
|
|||||||
numRows = max([len(d[0]) for d in data])
|
numRows = max([len(d[0]) for d in data])
|
||||||
for i in range(numRows):
|
for i in range(numRows):
|
||||||
for d in data:
|
for d in data:
|
||||||
if i < len(d[0]):
|
for j in [0, 1]:
|
||||||
fd.write(numFormat % d[0][i] + sep + numFormat % d[1][i] + sep)
|
if i < len(d[j]):
|
||||||
else:
|
fd.write(numFormat % d[j][i] + sep)
|
||||||
fd.write(' %s %s' % (sep, sep))
|
else:
|
||||||
|
fd.write(' %s' % sep)
|
||||||
fd.write('\n')
|
fd.write('\n')
|
||||||
fd.close()
|
fd.close()
|
||||||
|
|
||||||
|
49
exporters/tests/test_csv.py
Normal file
49
exporters/tests/test_csv.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
SVG export test
|
||||||
|
"""
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import pyqtgraph.exporters
|
||||||
|
import csv
|
||||||
|
|
||||||
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
def approxeq(a, b):
|
||||||
|
return (a-b) <= ((a + b) * 1e-6)
|
||||||
|
|
||||||
|
def test_CSVExporter():
|
||||||
|
plt = pg.plot()
|
||||||
|
y1 = [1,3,2,3,1,6,9,8,4,2]
|
||||||
|
plt.plot(y=y1, name='myPlot')
|
||||||
|
|
||||||
|
y2 = [3,4,6,1,2,4,2,3,5,3,5,1,3]
|
||||||
|
x2 = pg.np.linspace(0, 1.0, len(y2))
|
||||||
|
plt.plot(x=x2, y=y2)
|
||||||
|
|
||||||
|
y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3]
|
||||||
|
x3 = pg.np.linspace(0, 1.0, len(y3)+1)
|
||||||
|
plt.plot(x=x3, y=y3, stepMode=True)
|
||||||
|
|
||||||
|
ex = pg.exporters.CSVExporter(plt.plotItem)
|
||||||
|
ex.export(fileName='test.csv')
|
||||||
|
|
||||||
|
r = csv.reader(open('test.csv', 'r'))
|
||||||
|
lines = [line for line in r]
|
||||||
|
header = lines.pop(0)
|
||||||
|
assert header == ['myPlot_x', 'myPlot_y', 'x', 'y', 'x', 'y']
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for vals in lines:
|
||||||
|
vals = list(map(str.strip, vals))
|
||||||
|
assert (i >= len(y1) and vals[0] == '') or approxeq(float(vals[0]), i)
|
||||||
|
assert (i >= len(y1) and vals[1] == '') or approxeq(float(vals[1]), y1[i])
|
||||||
|
|
||||||
|
assert (i >= len(x2) and vals[2] == '') or approxeq(float(vals[2]), x2[i])
|
||||||
|
assert (i >= len(y2) and vals[3] == '') or approxeq(float(vals[3]), y2[i])
|
||||||
|
|
||||||
|
assert (i >= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i])
|
||||||
|
assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_CSVExporter()
|
||||||
|
|
@ -538,7 +538,6 @@ def interpolateArray(data, x, default=0.0):
|
|||||||
|
|
||||||
prof = debug.Profiler()
|
prof = debug.Profiler()
|
||||||
|
|
||||||
result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype)
|
|
||||||
nd = data.ndim
|
nd = data.ndim
|
||||||
md = x.shape[-1]
|
md = x.shape[-1]
|
||||||
|
|
||||||
|
@ -55,6 +55,8 @@ class AxisItem(GraphicsWidget):
|
|||||||
],
|
],
|
||||||
'showValues': showValues,
|
'showValues': showValues,
|
||||||
'tickLength': maxTickLength,
|
'tickLength': maxTickLength,
|
||||||
|
'maxTickLevel': 2,
|
||||||
|
'maxTextLevel': 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
|
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
|
||||||
@ -68,6 +70,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
self.tickFont = None
|
self.tickFont = None
|
||||||
|
|
||||||
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
|
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
|
||||||
|
self._tickSpacing = None # used to override default tickSpacing method
|
||||||
self.scale = 1.0
|
self.scale = 1.0
|
||||||
self.autoSIPrefix = True
|
self.autoSIPrefix = True
|
||||||
self.autoSIPrefixScale = 1.0
|
self.autoSIPrefixScale = 1.0
|
||||||
@ -161,7 +164,11 @@ class AxisItem(GraphicsWidget):
|
|||||||
self.scene().removeItem(self)
|
self.scene().removeItem(self)
|
||||||
|
|
||||||
def setGrid(self, grid):
|
def setGrid(self, grid):
|
||||||
"""Set the alpha value for the grid, or False to disable."""
|
"""Set the alpha value (0-255) for the grid, or False to disable.
|
||||||
|
|
||||||
|
When grid lines are enabled, the axis tick lines are extended to cover
|
||||||
|
the extent of the linked ViewBox, if any.
|
||||||
|
"""
|
||||||
self.grid = grid
|
self.grid = grid
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
@ -229,7 +236,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
without any scaling prefix (eg, 'V' instead of 'mV'). The
|
without any scaling prefix (eg, 'V' instead of 'mV'). The
|
||||||
scaling prefix will be automatically prepended based on the
|
scaling prefix will be automatically prepended based on the
|
||||||
range of data displayed.
|
range of data displayed.
|
||||||
**args All extra keyword arguments become CSS style options for
|
**args All extra keyword arguments become CSS style options for
|
||||||
the <span> tag which will surround the axis label and units.
|
the <span> tag which will surround the axis label and units.
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
|
|
||||||
@ -454,7 +461,10 @@ class AxisItem(GraphicsWidget):
|
|||||||
else:
|
else:
|
||||||
if newRange is None:
|
if newRange is None:
|
||||||
newRange = view.viewRange()[0]
|
newRange = view.viewRange()[0]
|
||||||
self.setRange(*newRange)
|
if view.xInverted():
|
||||||
|
self.setRange(*newRange[::-1])
|
||||||
|
else:
|
||||||
|
self.setRange(*newRange)
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
linkedView = self.linkedView()
|
linkedView = self.linkedView()
|
||||||
@ -510,6 +520,37 @@ class AxisItem(GraphicsWidget):
|
|||||||
self.picture = None
|
self.picture = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
def setTickSpacing(self, major=None, minor=None, levels=None):
|
||||||
|
"""
|
||||||
|
Explicitly determine the spacing of major and minor ticks. This
|
||||||
|
overrides the default behavior of the tickSpacing method, and disables
|
||||||
|
the effect of setTicks(). Arguments may be either *major* and *minor*,
|
||||||
|
or *levels* which is a list of (spacing, offset) tuples for each
|
||||||
|
tick level desired.
|
||||||
|
|
||||||
|
If no arguments are given, then the default behavior of tickSpacing
|
||||||
|
is enabled.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
# two levels, all offsets = 0
|
||||||
|
axis.setTickSpacing(5, 1)
|
||||||
|
# three levels, all offsets = 0
|
||||||
|
axis.setTickSpacing([(3, 0), (1, 0), (0.25, 0)])
|
||||||
|
# reset to default
|
||||||
|
axis.setTickSpacing()
|
||||||
|
"""
|
||||||
|
|
||||||
|
if levels is None:
|
||||||
|
if major is None:
|
||||||
|
levels = None
|
||||||
|
else:
|
||||||
|
levels = [(major, 0), (minor, 0)]
|
||||||
|
self._tickSpacing = levels
|
||||||
|
self.picture = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
def tickSpacing(self, minVal, maxVal, size):
|
def tickSpacing(self, minVal, maxVal, size):
|
||||||
"""Return values describing the desired spacing and offset of ticks.
|
"""Return values describing the desired spacing and offset of ticks.
|
||||||
|
|
||||||
@ -525,6 +566,10 @@ class AxisItem(GraphicsWidget):
|
|||||||
...
|
...
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
# First check for override tick spacing
|
||||||
|
if self._tickSpacing is not None:
|
||||||
|
return self._tickSpacing
|
||||||
|
|
||||||
dif = abs(maxVal - minVal)
|
dif = abs(maxVal - minVal)
|
||||||
if dif == 0:
|
if dif == 0:
|
||||||
return []
|
return []
|
||||||
@ -550,12 +595,13 @@ class AxisItem(GraphicsWidget):
|
|||||||
#(intervals[minorIndex], 0) ## Pretty, but eats up CPU
|
#(intervals[minorIndex], 0) ## Pretty, but eats up CPU
|
||||||
]
|
]
|
||||||
|
|
||||||
## decide whether to include the last level of ticks
|
if self.style['maxTickLevel'] >= 2:
|
||||||
minSpacing = min(size / 20., 30.)
|
## decide whether to include the last level of ticks
|
||||||
maxTickCount = size / minSpacing
|
minSpacing = min(size / 20., 30.)
|
||||||
if dif / intervals[minorIndex] <= maxTickCount:
|
maxTickCount = size / minSpacing
|
||||||
levels.append((intervals[minorIndex], 0))
|
if dif / intervals[minorIndex] <= maxTickCount:
|
||||||
return levels
|
levels.append((intervals[minorIndex], 0))
|
||||||
|
return levels
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -581,8 +627,6 @@ class AxisItem(GraphicsWidget):
|
|||||||
#(intervals[intIndexes[0]], 0)
|
#(intervals[intIndexes[0]], 0)
|
||||||
#]
|
#]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def tickValues(self, minVal, maxVal, size):
|
def tickValues(self, minVal, maxVal, size):
|
||||||
"""
|
"""
|
||||||
Return the values and spacing of ticks to draw::
|
Return the values and spacing of ticks to draw::
|
||||||
@ -756,8 +800,6 @@ class AxisItem(GraphicsWidget):
|
|||||||
values.append(val)
|
values.append(val)
|
||||||
strings.append(strn)
|
strings.append(strn)
|
||||||
|
|
||||||
textLevel = 1 ## draw text at this scale level
|
|
||||||
|
|
||||||
## determine mapping between tick values and local coordinates
|
## determine mapping between tick values and local coordinates
|
||||||
dif = self.range[1] - self.range[0]
|
dif = self.range[1] - self.range[0]
|
||||||
if dif == 0:
|
if dif == 0:
|
||||||
@ -846,7 +888,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
if not self.style['showValues']:
|
if not self.style['showValues']:
|
||||||
return (axisSpec, tickSpecs, textSpecs)
|
return (axisSpec, tickSpecs, textSpecs)
|
||||||
|
|
||||||
for i in range(len(tickLevels)):
|
for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)):
|
||||||
## Get the list of strings to display for this level
|
## Get the list of strings to display for this level
|
||||||
if tickStrings is None:
|
if tickStrings is None:
|
||||||
spacing, values = tickLevels[i]
|
spacing, values = tickLevels[i]
|
||||||
|
@ -8,15 +8,7 @@ __all__ = ['ErrorBarItem']
|
|||||||
class ErrorBarItem(GraphicsObject):
|
class ErrorBarItem(GraphicsObject):
|
||||||
def __init__(self, **opts):
|
def __init__(self, **opts):
|
||||||
"""
|
"""
|
||||||
Valid keyword options are:
|
All keyword arguments are passed to setData().
|
||||||
x, y, height, width, top, bottom, left, right, beam, pen
|
|
||||||
|
|
||||||
x and y must be numpy arrays specifying the coordinates of data points.
|
|
||||||
height, width, top, bottom, left, right, and beam may be numpy arrays,
|
|
||||||
single values, or None to disable. All values should be positive.
|
|
||||||
|
|
||||||
If height is specified, it overrides top and bottom.
|
|
||||||
If width is specified, it overrides left and right.
|
|
||||||
"""
|
"""
|
||||||
GraphicsObject.__init__(self)
|
GraphicsObject.__init__(self)
|
||||||
self.opts = dict(
|
self.opts = dict(
|
||||||
@ -31,14 +23,37 @@ class ErrorBarItem(GraphicsObject):
|
|||||||
beam=None,
|
beam=None,
|
||||||
pen=None
|
pen=None
|
||||||
)
|
)
|
||||||
self.setOpts(**opts)
|
self.setData(**opts)
|
||||||
|
|
||||||
|
def setData(self, **opts):
|
||||||
|
"""
|
||||||
|
Update the data in the item. All arguments are optional.
|
||||||
|
|
||||||
def setOpts(self, **opts):
|
Valid keyword options are:
|
||||||
|
x, y, height, width, top, bottom, left, right, beam, pen
|
||||||
|
|
||||||
|
* x and y must be numpy arrays specifying the coordinates of data points.
|
||||||
|
* height, width, top, bottom, left, right, and beam may be numpy arrays,
|
||||||
|
single values, or None to disable. All values should be positive.
|
||||||
|
* top, bottom, left, and right specify the lengths of bars extending
|
||||||
|
in each direction.
|
||||||
|
* If height is specified, it overrides top and bottom.
|
||||||
|
* If width is specified, it overrides left and right.
|
||||||
|
* beam specifies the width of the beam at the end of each bar.
|
||||||
|
* pen may be any single argument accepted by pg.mkPen().
|
||||||
|
|
||||||
|
This method was added in version 0.9.9. For prior versions, use setOpts.
|
||||||
|
"""
|
||||||
self.opts.update(opts)
|
self.opts.update(opts)
|
||||||
self.path = None
|
self.path = None
|
||||||
self.update()
|
self.update()
|
||||||
|
self.prepareGeometryChange()
|
||||||
self.informViewBoundsChanged()
|
self.informViewBoundsChanged()
|
||||||
|
|
||||||
|
def setOpts(self, **opts):
|
||||||
|
# for backward compatibility
|
||||||
|
self.setData(**opts)
|
||||||
|
|
||||||
def drawPath(self):
|
def drawPath(self):
|
||||||
p = QtGui.QPainterPath()
|
p = QtGui.QPainterPath()
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ class GraphicsItem(object):
|
|||||||
Extends deviceTransform to automatically determine the viewportTransform.
|
Extends deviceTransform to automatically determine the viewportTransform.
|
||||||
"""
|
"""
|
||||||
if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different.
|
if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different.
|
||||||
return self._exportOpts['painter'].deviceTransform()
|
return self._exportOpts['painter'].deviceTransform() * self.sceneTransform()
|
||||||
|
|
||||||
if viewportTransform is None:
|
if viewportTransform is None:
|
||||||
view = self.getViewWidget()
|
view = self.getViewWidget()
|
||||||
@ -318,6 +318,8 @@ class GraphicsItem(object):
|
|||||||
vt = self.deviceTransform()
|
vt = self.deviceTransform()
|
||||||
if vt is None:
|
if vt is None:
|
||||||
return None
|
return None
|
||||||
|
if isinstance(obj, QtCore.QPoint):
|
||||||
|
obj = QtCore.QPointF(obj)
|
||||||
vt = fn.invertQTransform(vt)
|
vt = fn.invertQTransform(vt)
|
||||||
return vt.map(obj)
|
return vt.map(obj)
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ from .. import functions as fn
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from .. import debug as debug
|
from .. import debug as debug
|
||||||
|
|
||||||
|
import weakref
|
||||||
|
|
||||||
__all__ = ['HistogramLUTItem']
|
__all__ = ['HistogramLUTItem']
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ class HistogramLUTItem(GraphicsWidget):
|
|||||||
"""
|
"""
|
||||||
GraphicsWidget.__init__(self)
|
GraphicsWidget.__init__(self)
|
||||||
self.lut = None
|
self.lut = None
|
||||||
self.imageItem = None
|
self.imageItem = lambda: None # fake a dead weakref
|
||||||
|
|
||||||
self.layout = QtGui.QGraphicsGridLayout()
|
self.layout = QtGui.QGraphicsGridLayout()
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
@ -138,7 +139,7 @@ class HistogramLUTItem(GraphicsWidget):
|
|||||||
#self.region.setBounds([vr.top(), vr.bottom()])
|
#self.region.setBounds([vr.top(), vr.bottom()])
|
||||||
|
|
||||||
def setImageItem(self, img):
|
def setImageItem(self, img):
|
||||||
self.imageItem = img
|
self.imageItem = weakref.ref(img)
|
||||||
img.sigImageChanged.connect(self.imageChanged)
|
img.sigImageChanged.connect(self.imageChanged)
|
||||||
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
||||||
#self.gradientChanged()
|
#self.gradientChanged()
|
||||||
@ -150,11 +151,11 @@ class HistogramLUTItem(GraphicsWidget):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def gradientChanged(self):
|
def gradientChanged(self):
|
||||||
if self.imageItem is not None:
|
if self.imageItem() is not None:
|
||||||
if self.gradient.isLookupTrivial():
|
if self.gradient.isLookupTrivial():
|
||||||
self.imageItem.setLookupTable(None) #lambda x: x.astype(np.uint8))
|
self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8))
|
||||||
else:
|
else:
|
||||||
self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
||||||
|
|
||||||
self.lut = None
|
self.lut = None
|
||||||
#if self.imageItem is not None:
|
#if self.imageItem is not None:
|
||||||
@ -178,14 +179,14 @@ class HistogramLUTItem(GraphicsWidget):
|
|||||||
#self.update()
|
#self.update()
|
||||||
|
|
||||||
def regionChanging(self):
|
def regionChanging(self):
|
||||||
if self.imageItem is not None:
|
if self.imageItem() is not None:
|
||||||
self.imageItem.setLevels(self.region.getRegion())
|
self.imageItem().setLevels(self.region.getRegion())
|
||||||
self.sigLevelsChanged.emit(self)
|
self.sigLevelsChanged.emit(self)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def imageChanged(self, autoLevel=False, autoRange=False):
|
def imageChanged(self, autoLevel=False, autoRange=False):
|
||||||
profiler = debug.Profiler()
|
profiler = debug.Profiler()
|
||||||
h = self.imageItem.getHistogram()
|
h = self.imageItem().getHistogram()
|
||||||
profiler('get histogram')
|
profiler('get histogram')
|
||||||
if h[0] is None:
|
if h[0] is None:
|
||||||
return
|
return
|
||||||
|
@ -59,7 +59,9 @@ class InfiniteLine(GraphicsObject):
|
|||||||
|
|
||||||
if pen is None:
|
if pen is None:
|
||||||
pen = (200, 200, 100)
|
pen = (200, 200, 100)
|
||||||
|
|
||||||
self.setPen(pen)
|
self.setPen(pen)
|
||||||
|
self.setHoverPen(color=(255,0,0), width=self.pen.width())
|
||||||
self.currentPen = self.pen
|
self.currentPen = self.pen
|
||||||
#self.setFlag(self.ItemSendsScenePositionChanges)
|
#self.setFlag(self.ItemSendsScenePositionChanges)
|
||||||
|
|
||||||
@ -77,8 +79,22 @@ class InfiniteLine(GraphicsObject):
|
|||||||
"""Set the pen for drawing the line. Allowable arguments are any that are valid
|
"""Set the pen for drawing the line. Allowable arguments are any that are valid
|
||||||
for :func:`mkPen <pyqtgraph.mkPen>`."""
|
for :func:`mkPen <pyqtgraph.mkPen>`."""
|
||||||
self.pen = fn.mkPen(*args, **kwargs)
|
self.pen = fn.mkPen(*args, **kwargs)
|
||||||
self.currentPen = self.pen
|
if not self.mouseHovering:
|
||||||
self.update()
|
self.currentPen = self.pen
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def setHoverPen(self, *args, **kwargs):
|
||||||
|
"""Set the pen for drawing the line while the mouse hovers over it.
|
||||||
|
Allowable arguments are any that are valid
|
||||||
|
for :func:`mkPen <pyqtgraph.mkPen>`.
|
||||||
|
|
||||||
|
If the line is not movable, then hovering is also disabled.
|
||||||
|
|
||||||
|
Added in version 0.9.9."""
|
||||||
|
self.hoverPen = fn.mkPen(*args, **kwargs)
|
||||||
|
if self.mouseHovering:
|
||||||
|
self.currentPen = self.hoverPen
|
||||||
|
self.update()
|
||||||
|
|
||||||
def setAngle(self, angle):
|
def setAngle(self, angle):
|
||||||
"""
|
"""
|
||||||
@ -168,8 +184,9 @@ class InfiniteLine(GraphicsObject):
|
|||||||
px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
|
px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
|
||||||
if px is None:
|
if px is None:
|
||||||
px = 0
|
px = 0
|
||||||
br.setBottom(-px*4)
|
w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px
|
||||||
br.setTop(px*4)
|
br.setBottom(-w)
|
||||||
|
br.setTop(w)
|
||||||
return br.normalized()
|
return br.normalized()
|
||||||
|
|
||||||
def paint(self, p, *args):
|
def paint(self, p, *args):
|
||||||
@ -183,25 +200,6 @@ class InfiniteLine(GraphicsObject):
|
|||||||
return None ## x axis should never be auto-scaled
|
return None ## x axis should never be auto-scaled
|
||||||
else:
|
else:
|
||||||
return (0,0)
|
return (0,0)
|
||||||
|
|
||||||
#def mousePressEvent(self, ev):
|
|
||||||
#if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
|
||||||
#ev.accept()
|
|
||||||
#self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p)
|
|
||||||
#else:
|
|
||||||
#ev.ignore()
|
|
||||||
|
|
||||||
#def mouseMoveEvent(self, ev):
|
|
||||||
#self.setPos(self.mapToParent(ev.pos()) - self.pressDelta)
|
|
||||||
##self.emit(QtCore.SIGNAL('dragged'), self)
|
|
||||||
#self.sigDragged.emit(self)
|
|
||||||
#self.hasMoved = True
|
|
||||||
|
|
||||||
#def mouseReleaseEvent(self, ev):
|
|
||||||
#if self.hasMoved and ev.button() == QtCore.Qt.LeftButton:
|
|
||||||
#self.hasMoved = False
|
|
||||||
##self.emit(QtCore.SIGNAL('positionChangeFinished'), self)
|
|
||||||
#self.sigPositionChangeFinished.emit(self)
|
|
||||||
|
|
||||||
def mouseDragEvent(self, ev):
|
def mouseDragEvent(self, ev):
|
||||||
if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
||||||
@ -239,12 +237,12 @@ class InfiniteLine(GraphicsObject):
|
|||||||
self.setMouseHover(False)
|
self.setMouseHover(False)
|
||||||
|
|
||||||
def setMouseHover(self, hover):
|
def setMouseHover(self, hover):
|
||||||
## Inform the item that the mouse is(not) hovering over it
|
## Inform the item that the mouse is (not) hovering over it
|
||||||
if self.mouseHovering == hover:
|
if self.mouseHovering == hover:
|
||||||
return
|
return
|
||||||
self.mouseHovering = hover
|
self.mouseHovering = hover
|
||||||
if hover:
|
if hover:
|
||||||
self.currentPen = fn.mkPen(255, 0,0)
|
self.currentPen = self.hoverPen
|
||||||
else:
|
else:
|
||||||
self.currentPen = self.pen
|
self.currentPen = self.pen
|
||||||
self.update()
|
self.update()
|
||||||
|
@ -35,11 +35,6 @@ class IsocurveItem(GraphicsObject):
|
|||||||
self.setPen(pen)
|
self.setPen(pen)
|
||||||
self.setData(data, level)
|
self.setData(data, level)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#if data is not None and level is not None:
|
|
||||||
#self.updateLines(data, level)
|
|
||||||
|
|
||||||
|
|
||||||
def setData(self, data, level=None):
|
def setData(self, data, level=None):
|
||||||
"""
|
"""
|
||||||
@ -65,6 +60,7 @@ class IsocurveItem(GraphicsObject):
|
|||||||
"""Set the level at which the isocurve is drawn."""
|
"""Set the level at which the isocurve is drawn."""
|
||||||
self.level = level
|
self.level = level
|
||||||
self.path = None
|
self.path = None
|
||||||
|
self.prepareGeometryChange()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
|||||||
sample = item
|
sample = item
|
||||||
else:
|
else:
|
||||||
sample = ItemSample(item)
|
sample = ItemSample(item)
|
||||||
row = len(self.items)
|
row = self.layout.rowCount()
|
||||||
self.items.append((sample, label))
|
self.items.append((sample, label))
|
||||||
self.layout.addItem(sample, row, 0)
|
self.layout.addItem(sample, row, 0)
|
||||||
self.layout.addItem(label, row, 1)
|
self.layout.addItem(label, row, 1)
|
||||||
|
@ -546,7 +546,7 @@ class PlotDataItem(GraphicsObject):
|
|||||||
if view is None or not view.autoRangeEnabled()[0]:
|
if view is None or not view.autoRangeEnabled()[0]:
|
||||||
# this option presumes that x-values have uniform spacing
|
# this option presumes that x-values have uniform spacing
|
||||||
range = self.viewRect()
|
range = self.viewRect()
|
||||||
if range is not None:
|
if range is not None and len(x) > 1:
|
||||||
dx = float(x[-1]-x[0]) / (len(x)-1)
|
dx = float(x[-1]-x[0]) / (len(x)-1)
|
||||||
# clip to visible region extended by downsampling value
|
# clip to visible region extended by downsampling value
|
||||||
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
|
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
|
||||||
|
@ -78,6 +78,7 @@ class PlotItem(GraphicsWidget):
|
|||||||
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
|
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
|
||||||
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
|
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
|
||||||
:func:`invertY <pyqtgraph.ViewBox.invertY>`,
|
:func:`invertY <pyqtgraph.ViewBox.invertY>`,
|
||||||
|
:func:`invertX <pyqtgraph.ViewBox.invertX>`,
|
||||||
:func:`register <pyqtgraph.ViewBox.register>`,
|
:func:`register <pyqtgraph.ViewBox.register>`,
|
||||||
:func:`unregister <pyqtgraph.ViewBox.unregister>`
|
:func:`unregister <pyqtgraph.ViewBox.unregister>`
|
||||||
|
|
||||||
@ -299,7 +300,7 @@ class PlotItem(GraphicsWidget):
|
|||||||
for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE:
|
for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE:
|
||||||
'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please
|
'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please
|
||||||
'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring
|
'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring
|
||||||
'setAspectLocked', 'invertY', 'register', 'unregister']: # as well.
|
'setAspectLocked', 'invertY', 'invertX', 'register', 'unregister']: # as well.
|
||||||
|
|
||||||
def _create_method(name):
|
def _create_method(name):
|
||||||
def method(self, *args, **kwargs):
|
def method(self, *args, **kwargs):
|
||||||
|
@ -1804,13 +1804,56 @@ class PolyLineROI(ROI):
|
|||||||
self.segments = []
|
self.segments = []
|
||||||
ROI.__init__(self, pos, size=[1,1], **args)
|
ROI.__init__(self, pos, size=[1,1], **args)
|
||||||
|
|
||||||
for p in positions:
|
self.setPoints(positions)
|
||||||
self.addFreeHandle(p)
|
#for p in positions:
|
||||||
|
#self.addFreeHandle(p)
|
||||||
|
|
||||||
|
#start = -1 if self.closed else 0
|
||||||
|
#for i in range(start, len(self.handles)-1):
|
||||||
|
#self.addSegment(self.handles[i]['item'], self.handles[i+1]['item'])
|
||||||
|
|
||||||
|
def setPoints(self, points, closed=None):
|
||||||
|
"""
|
||||||
|
Set the complete sequence of points displayed by this ROI.
|
||||||
|
|
||||||
|
============= =========================================================
|
||||||
|
**Arguments**
|
||||||
|
points List of (x,y) tuples specifying handle locations to set.
|
||||||
|
closed If bool, then this will set whether the ROI is closed
|
||||||
|
(the last point is connected to the first point). If
|
||||||
|
None, then the closed mode is left unchanged.
|
||||||
|
============= =========================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
if closed is not None:
|
||||||
|
self.closed = closed
|
||||||
|
|
||||||
|
for p in points:
|
||||||
|
self.addFreeHandle(p)
|
||||||
|
|
||||||
start = -1 if self.closed else 0
|
start = -1 if self.closed else 0
|
||||||
for i in range(start, len(self.handles)-1):
|
for i in range(start, len(self.handles)-1):
|
||||||
self.addSegment(self.handles[i]['item'], self.handles[i+1]['item'])
|
self.addSegment(self.handles[i]['item'], self.handles[i+1]['item'])
|
||||||
|
|
||||||
|
|
||||||
|
def clearPoints(self):
|
||||||
|
"""
|
||||||
|
Remove all handles and segments.
|
||||||
|
"""
|
||||||
|
while len(self.handles) > 0:
|
||||||
|
self.removeHandle(self.handles[0]['item'])
|
||||||
|
|
||||||
|
def saveState(self):
|
||||||
|
state = ROI.saveState(self)
|
||||||
|
state['closed'] = self.closed
|
||||||
|
state['points'] = [tuple(h.pos()) for h in self.getHandles()]
|
||||||
|
return state
|
||||||
|
|
||||||
|
def setState(self, state):
|
||||||
|
ROI.setState(self, state)
|
||||||
|
self.clearPoints()
|
||||||
|
self.setPoints(state['points'], closed=state['closed'])
|
||||||
|
|
||||||
def addSegment(self, h1, h2, index=None):
|
def addSegment(self, h1, h2, index=None):
|
||||||
seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False)
|
seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False)
|
||||||
if index is None:
|
if index is None:
|
||||||
@ -1936,6 +1979,8 @@ class PolyLineROI(ROI):
|
|||||||
for seg in self.segments:
|
for seg in self.segments:
|
||||||
seg.setPen(*args, **kwds)
|
seg.setPen(*args, **kwds)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LineSegmentROI(ROI):
|
class LineSegmentROI(ROI):
|
||||||
"""
|
"""
|
||||||
ROI subclass with two freely-moving handles defining a line.
|
ROI subclass with two freely-moving handles defining a line.
|
||||||
|
@ -10,7 +10,7 @@ from copy import deepcopy
|
|||||||
from ... import debug as debug
|
from ... import debug as debug
|
||||||
from ... import getConfigOption
|
from ... import getConfigOption
|
||||||
import sys
|
import sys
|
||||||
from pyqtgraph.Qt import isQObjectAlive
|
from ...Qt import isQObjectAlive
|
||||||
|
|
||||||
__all__ = ['ViewBox']
|
__all__ = ['ViewBox']
|
||||||
|
|
||||||
@ -74,12 +74,11 @@ class ViewBox(GraphicsWidget):
|
|||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
|
||||||
- Scaling contents by mouse or auto-scale when contents change
|
* Scaling contents by mouse or auto-scale when contents change
|
||||||
- View linking--multiple views display the same data ranges
|
* View linking--multiple views display the same data ranges
|
||||||
- Configurable by context menu
|
* Configurable by context menu
|
||||||
- Item coordinate mapping methods
|
* Item coordinate mapping methods
|
||||||
|
|
||||||
Not really compatible with GraphicsView having the same functionality.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sigYRangeChanged = QtCore.Signal(object, object)
|
sigYRangeChanged = QtCore.Signal(object, object)
|
||||||
@ -104,7 +103,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
NamedViews = weakref.WeakValueDictionary() # name: ViewBox
|
NamedViews = weakref.WeakValueDictionary() # name: ViewBox
|
||||||
AllViews = weakref.WeakKeyDictionary() # ViewBox: None
|
AllViews = weakref.WeakKeyDictionary() # ViewBox: None
|
||||||
|
|
||||||
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None):
|
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None, invertX=False):
|
||||||
"""
|
"""
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
@ -115,11 +114,16 @@ class ViewBox(GraphicsWidget):
|
|||||||
coorinates to. (or False to allow the ratio to change)
|
coorinates to. (or False to allow the ratio to change)
|
||||||
*enableMouse* (bool) Whether mouse can be used to scale/pan the view
|
*enableMouse* (bool) Whether mouse can be used to scale/pan the view
|
||||||
*invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
|
*invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
|
||||||
|
*invertX* (bool) See :func:`invertX <pyqtgraph.ViewBox.invertX>`
|
||||||
|
*enableMenu* (bool) Whether to display a context menu when
|
||||||
|
right-clicking on the ViewBox background.
|
||||||
|
*name* (str) Used to register this ViewBox so that it appears
|
||||||
|
in the "Link axis" dropdown inside other ViewBox
|
||||||
|
context menus. This allows the user to manually link
|
||||||
|
the axes of any other view to this one.
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GraphicsWidget.__init__(self, parent)
|
GraphicsWidget.__init__(self, parent)
|
||||||
self.name = None
|
self.name = None
|
||||||
self.linksBlocked = False
|
self.linksBlocked = False
|
||||||
@ -139,6 +143,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
'viewRange': [[0,1], [0,1]], ## actual range viewed
|
'viewRange': [[0,1], [0,1]], ## actual range viewed
|
||||||
|
|
||||||
'yInverted': invertY,
|
'yInverted': invertY,
|
||||||
|
'xInverted': invertX,
|
||||||
'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio.
|
'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio.
|
||||||
'autoRange': [True, True], ## False if auto range is disabled,
|
'autoRange': [True, True], ## False if auto range is disabled,
|
||||||
## otherwise float gives the fraction of data that is visible
|
## otherwise float gives the fraction of data that is visible
|
||||||
@ -218,7 +223,11 @@ class ViewBox(GraphicsWidget):
|
|||||||
def register(self, name):
|
def register(self, name):
|
||||||
"""
|
"""
|
||||||
Add this ViewBox to the registered list of views.
|
Add this ViewBox to the registered list of views.
|
||||||
*name* will appear in the drop-down lists for axis linking in all other views.
|
|
||||||
|
This allows users to manually link the axes of any other ViewBox to
|
||||||
|
this one. The specified *name* will appear in the drop-down lists for
|
||||||
|
axis linking in the context menus of all other views.
|
||||||
|
|
||||||
The same can be accomplished by initializing the ViewBox with the *name* attribute.
|
The same can be accomplished by initializing the ViewBox with the *name* attribute.
|
||||||
"""
|
"""
|
||||||
ViewBox.AllViews[self] = None
|
ViewBox.AllViews[self] = None
|
||||||
@ -662,7 +671,10 @@ class ViewBox(GraphicsWidget):
|
|||||||
Added in version 0.9.9
|
Added in version 0.9.9
|
||||||
"""
|
"""
|
||||||
update = False
|
update = False
|
||||||
|
allowed = ['xMin', 'xMax', 'yMin', 'yMax', 'minXRange', 'maxXRange', 'minYRange', 'maxYRange']
|
||||||
|
for kwd in kwds:
|
||||||
|
if kwd not in allowed:
|
||||||
|
raise ValueError("Invalid keyword argument '%s'." % kwd)
|
||||||
#for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']:
|
#for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']:
|
||||||
#if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]:
|
#if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]:
|
||||||
#self.state['limits'][kwd] = kwds[kwd]
|
#self.state['limits'][kwd] = kwds[kwd]
|
||||||
@ -996,7 +1008,10 @@ class ViewBox(GraphicsWidget):
|
|||||||
x2 = vr.right()
|
x2 = vr.right()
|
||||||
else: ## views overlap; line them up
|
else: ## views overlap; line them up
|
||||||
upp = float(vr.width()) / vg.width()
|
upp = float(vr.width()) / vg.width()
|
||||||
x1 = vr.left() + (sg.x()-vg.x()) * upp
|
if self.xInverted():
|
||||||
|
x1 = vr.left() + (sg.right()-vg.right()) * upp
|
||||||
|
else:
|
||||||
|
x1 = vr.left() + (sg.x()-vg.x()) * upp
|
||||||
x2 = x1 + sg.width() * upp
|
x2 = x1 + sg.width() * upp
|
||||||
self.enableAutoRange(ViewBox.XAxis, False)
|
self.enableAutoRange(ViewBox.XAxis, False)
|
||||||
self.setXRange(x1, x2, padding=0)
|
self.setXRange(x1, x2, padding=0)
|
||||||
@ -1054,10 +1069,27 @@ class ViewBox(GraphicsWidget):
|
|||||||
#self.updateMatrix(changed=(False, True))
|
#self.updateMatrix(changed=(False, True))
|
||||||
self.updateViewRange()
|
self.updateViewRange()
|
||||||
self.sigStateChanged.emit(self)
|
self.sigStateChanged.emit(self)
|
||||||
|
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
|
||||||
|
|
||||||
def yInverted(self):
|
def yInverted(self):
|
||||||
return self.state['yInverted']
|
return self.state['yInverted']
|
||||||
|
|
||||||
|
def invertX(self, b=True):
|
||||||
|
"""
|
||||||
|
By default, the positive x-axis points rightward on the screen. Use invertX(True) to reverse the x-axis.
|
||||||
|
"""
|
||||||
|
if self.state['xInverted'] == b:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.state['xInverted'] = b
|
||||||
|
#self.updateMatrix(changed=(False, True))
|
||||||
|
self.updateViewRange()
|
||||||
|
self.sigStateChanged.emit(self)
|
||||||
|
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
|
||||||
|
|
||||||
|
def xInverted(self):
|
||||||
|
return self.state['xInverted']
|
||||||
|
|
||||||
def setAspectLocked(self, lock=True, ratio=1):
|
def setAspectLocked(self, lock=True, ratio=1):
|
||||||
"""
|
"""
|
||||||
If the aspect ratio is locked, view scaling must always preserve the aspect ratio.
|
If the aspect ratio is locked, view scaling must always preserve the aspect ratio.
|
||||||
@ -1280,6 +1312,8 @@ class ViewBox(GraphicsWidget):
|
|||||||
ev.ignore()
|
ev.ignore()
|
||||||
|
|
||||||
def scaleHistory(self, d):
|
def scaleHistory(self, d):
|
||||||
|
if len(self.axHistory) == 0:
|
||||||
|
return
|
||||||
ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d))
|
ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d))
|
||||||
if ptr != self.axHistoryPointer:
|
if ptr != self.axHistoryPointer:
|
||||||
self.axHistoryPointer = ptr
|
self.axHistoryPointer = ptr
|
||||||
@ -1454,9 +1488,10 @@ class ViewBox(GraphicsWidget):
|
|||||||
if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0:
|
if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0:
|
||||||
|
|
||||||
## This is the view range aspect ratio we have requested
|
## This is the view range aspect ratio we have requested
|
||||||
targetRatio = tr.width() / tr.height()
|
targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1
|
||||||
## This is the view range aspect ratio we need to obey aspect constraint
|
## This is the view range aspect ratio we need to obey aspect constraint
|
||||||
viewRatio = (bounds.width() / bounds.height()) / aspect
|
viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect
|
||||||
|
viewRatio = 1 if viewRatio == 0 else viewRatio
|
||||||
|
|
||||||
# Decide which range to keep unchanged
|
# Decide which range to keep unchanged
|
||||||
#print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange']
|
#print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange']
|
||||||
@ -1481,7 +1516,8 @@ class ViewBox(GraphicsWidget):
|
|||||||
if dx != 0:
|
if dx != 0:
|
||||||
changed[0] = True
|
changed[0] = True
|
||||||
viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
|
viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
|
||||||
|
|
||||||
|
|
||||||
# ----------- Make corrections for view limits -----------
|
# ----------- Make corrections for view limits -----------
|
||||||
|
|
||||||
limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits'])
|
limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits'])
|
||||||
@ -1532,7 +1568,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
changed[axis] = True
|
changed[axis] = True
|
||||||
|
|
||||||
#print "after applying edge limits:", viewRange[axis]
|
#print "after applying edge limits:", viewRange[axis]
|
||||||
|
|
||||||
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
|
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
|
||||||
self.state['viewRange'] = viewRange
|
self.state['viewRange'] = viewRange
|
||||||
|
|
||||||
@ -1554,6 +1590,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
if link is not None:
|
if link is not None:
|
||||||
link.linkedViewChanged(self, ax)
|
link.linkedViewChanged(self, ax)
|
||||||
|
|
||||||
|
self.update()
|
||||||
self._matrixNeedsUpdate = True
|
self._matrixNeedsUpdate = True
|
||||||
|
|
||||||
def updateMatrix(self, changed=None):
|
def updateMatrix(self, changed=None):
|
||||||
@ -1566,6 +1603,8 @@ class ViewBox(GraphicsWidget):
|
|||||||
scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height())
|
scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height())
|
||||||
if not self.state['yInverted']:
|
if not self.state['yInverted']:
|
||||||
scale = scale * Point(1, -1)
|
scale = scale * Point(1, -1)
|
||||||
|
if self.state['xInverted']:
|
||||||
|
scale = scale * Point(-1, 1)
|
||||||
m = QtGui.QTransform()
|
m = QtGui.QTransform()
|
||||||
|
|
||||||
## First center the viewport at 0
|
## First center the viewport at 0
|
||||||
|
@ -56,7 +56,7 @@ class ViewBoxMenu(QtGui.QMenu):
|
|||||||
for sig, fn in connects:
|
for sig, fn in connects:
|
||||||
sig.connect(getattr(self, axis.lower()+fn))
|
sig.connect(getattr(self, axis.lower()+fn))
|
||||||
|
|
||||||
self.ctrl[0].invertCheck.hide() ## no invert for x-axis
|
self.ctrl[0].invertCheck.toggled.connect(self.xInvertToggled)
|
||||||
self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled)
|
self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled)
|
||||||
## exporting is handled by GraphicsScene now
|
## exporting is handled by GraphicsScene now
|
||||||
#self.export = QtGui.QMenu("Export")
|
#self.export = QtGui.QMenu("Export")
|
||||||
@ -139,8 +139,9 @@ class ViewBoxMenu(QtGui.QMenu):
|
|||||||
|
|
||||||
self.ctrl[i].autoPanCheck.setChecked(state['autoPan'][i])
|
self.ctrl[i].autoPanCheck.setChecked(state['autoPan'][i])
|
||||||
self.ctrl[i].visibleOnlyCheck.setChecked(state['autoVisibleOnly'][i])
|
self.ctrl[i].visibleOnlyCheck.setChecked(state['autoVisibleOnly'][i])
|
||||||
|
xy = ['x', 'y'][i]
|
||||||
self.ctrl[1].invertCheck.setChecked(state['yInverted'])
|
self.ctrl[i].invertCheck.setChecked(state.get(xy+'Inverted', False))
|
||||||
|
|
||||||
self.valid = True
|
self.valid = True
|
||||||
|
|
||||||
def popup(self, *args):
|
def popup(self, *args):
|
||||||
@ -217,19 +218,19 @@ class ViewBoxMenu(QtGui.QMenu):
|
|||||||
def yInvertToggled(self, b):
|
def yInvertToggled(self, b):
|
||||||
self.view().invertY(b)
|
self.view().invertY(b)
|
||||||
|
|
||||||
|
def xInvertToggled(self, b):
|
||||||
|
self.view().invertX(b)
|
||||||
|
|
||||||
def exportMethod(self):
|
def exportMethod(self):
|
||||||
act = self.sender()
|
act = self.sender()
|
||||||
self.exportMethods[str(act.text())]()
|
self.exportMethods[str(act.text())]()
|
||||||
|
|
||||||
|
|
||||||
def set3ButtonMode(self):
|
def set3ButtonMode(self):
|
||||||
self.view().setLeftButtonAction('pan')
|
self.view().setLeftButtonAction('pan')
|
||||||
|
|
||||||
def set1ButtonMode(self):
|
def set1ButtonMode(self):
|
||||||
self.view().setLeftButtonAction('rect')
|
self.view().setLeftButtonAction('rect')
|
||||||
|
|
||||||
|
|
||||||
def setViewList(self, views):
|
def setViewList(self, views):
|
||||||
names = ['']
|
names = ['']
|
||||||
self.viewMap.clear()
|
self.viewMap.clear()
|
||||||
|
85
graphicsItems/ViewBox/tests/test_ViewBox.py
Normal file
85
graphicsItems/ViewBox/tests/test_ViewBox.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#import PySide
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
app = pg.mkQApp()
|
||||||
|
qtest = pg.Qt.QtTest.QTest
|
||||||
|
|
||||||
|
def assertMapping(vb, r1, r2):
|
||||||
|
assert vb.mapFromView(r1.topLeft()) == r2.topLeft()
|
||||||
|
assert vb.mapFromView(r1.bottomLeft()) == r2.bottomLeft()
|
||||||
|
assert vb.mapFromView(r1.topRight()) == r2.topRight()
|
||||||
|
assert vb.mapFromView(r1.bottomRight()) == r2.bottomRight()
|
||||||
|
|
||||||
|
def test_ViewBox():
|
||||||
|
global app, win, vb
|
||||||
|
QRectF = pg.QtCore.QRectF
|
||||||
|
|
||||||
|
win = pg.GraphicsWindow()
|
||||||
|
win.ci.layout.setContentsMargins(0,0,0,0)
|
||||||
|
win.resize(200, 200)
|
||||||
|
win.show()
|
||||||
|
vb = win.addViewBox()
|
||||||
|
|
||||||
|
vb.setRange(xRange=[0, 10], yRange=[0, 10], padding=0)
|
||||||
|
|
||||||
|
# required to make mapFromView work properly.
|
||||||
|
qtest.qWaitForWindowShown(win)
|
||||||
|
vb.update()
|
||||||
|
|
||||||
|
g = pg.GridItem()
|
||||||
|
vb.addItem(g)
|
||||||
|
|
||||||
|
app.processEvents()
|
||||||
|
|
||||||
|
w = vb.geometry().width()
|
||||||
|
h = vb.geometry().height()
|
||||||
|
view1 = QRectF(0, 0, 10, 10)
|
||||||
|
size1 = QRectF(0, h, w, -h)
|
||||||
|
assertMapping(vb, view1, size1)
|
||||||
|
|
||||||
|
# test resize
|
||||||
|
win.resize(400, 400)
|
||||||
|
app.processEvents()
|
||||||
|
w = vb.geometry().width()
|
||||||
|
h = vb.geometry().height()
|
||||||
|
size1 = QRectF(0, h, w, -h)
|
||||||
|
assertMapping(vb, view1, size1)
|
||||||
|
|
||||||
|
# now lock aspect
|
||||||
|
vb.setAspectLocked()
|
||||||
|
|
||||||
|
# test wide resize
|
||||||
|
win.resize(800, 400)
|
||||||
|
app.processEvents()
|
||||||
|
w = vb.geometry().width()
|
||||||
|
h = vb.geometry().height()
|
||||||
|
view1 = QRectF(-5, 0, 20, 10)
|
||||||
|
size1 = QRectF(0, h, w, -h)
|
||||||
|
assertMapping(vb, view1, size1)
|
||||||
|
|
||||||
|
# test tall resize
|
||||||
|
win.resize(400, 800)
|
||||||
|
app.processEvents()
|
||||||
|
w = vb.geometry().width()
|
||||||
|
h = vb.geometry().height()
|
||||||
|
view1 = QRectF(0, -5, 10, 20)
|
||||||
|
size1 = QRectF(0, h, w, -h)
|
||||||
|
assertMapping(vb, view1, size1)
|
||||||
|
|
||||||
|
# test limits + resize (aspect ratio constraint has priority over limits
|
||||||
|
win.resize(400, 400)
|
||||||
|
app.processEvents()
|
||||||
|
vb.setLimits(xMin=0, xMax=10, yMin=0, yMax=10)
|
||||||
|
win.resize(800, 400)
|
||||||
|
app.processEvents()
|
||||||
|
w = vb.geometry().width()
|
||||||
|
h = vb.geometry().height()
|
||||||
|
view1 = QRectF(-5, 0, 20, 10)
|
||||||
|
size1 = QRectF(0, h, w, -h)
|
||||||
|
assertMapping(vb, view1, size1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import user,sys
|
||||||
|
test_ViewBox()
|
||||||
|
|
@ -12,8 +12,10 @@ Widget used for displaying 2D or 3D data. Features:
|
|||||||
- ROI plotting
|
- ROI plotting
|
||||||
- Image normalization through a variety of methods
|
- Image normalization through a variety of methods
|
||||||
"""
|
"""
|
||||||
from ..Qt import QtCore, QtGui, USE_PYSIDE
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..Qt import QtCore, QtGui, USE_PYSIDE
|
||||||
if USE_PYSIDE:
|
if USE_PYSIDE:
|
||||||
from .ImageViewTemplate_pyside import *
|
from .ImageViewTemplate_pyside import *
|
||||||
else:
|
else:
|
||||||
@ -24,25 +26,14 @@ from ..graphicsItems.ROI import *
|
|||||||
from ..graphicsItems.LinearRegionItem import *
|
from ..graphicsItems.LinearRegionItem import *
|
||||||
from ..graphicsItems.InfiniteLine import *
|
from ..graphicsItems.InfiniteLine import *
|
||||||
from ..graphicsItems.ViewBox import *
|
from ..graphicsItems.ViewBox import *
|
||||||
#from widgets import ROI
|
|
||||||
import sys
|
|
||||||
#from numpy import ndarray
|
|
||||||
from .. import ptime as ptime
|
from .. import ptime as ptime
|
||||||
import numpy as np
|
|
||||||
from .. import debug as debug
|
from .. import debug as debug
|
||||||
|
|
||||||
from ..SignalProxy import SignalProxy
|
from ..SignalProxy import SignalProxy
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from bottleneck import nanmin, nanmax
|
from bottleneck import nanmin, nanmax
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from numpy import nanmin, nanmax
|
from numpy import nanmin, nanmax
|
||||||
|
|
||||||
#try:
|
|
||||||
#from .. import metaarray as metaarray
|
|
||||||
#HAVE_METAARRAY = True
|
|
||||||
#except:
|
|
||||||
#HAVE_METAARRAY = False
|
|
||||||
|
|
||||||
|
|
||||||
class PlotROI(ROI):
|
class PlotROI(ROI):
|
||||||
@ -72,6 +63,16 @@ class ImageView(QtGui.QWidget):
|
|||||||
imv = pg.ImageView()
|
imv = pg.ImageView()
|
||||||
imv.show()
|
imv.show()
|
||||||
imv.setImage(data)
|
imv.setImage(data)
|
||||||
|
|
||||||
|
**Keyboard interaction**
|
||||||
|
|
||||||
|
* left/right arrows step forward/backward 1 frame when pressed,
|
||||||
|
seek at 20fps when held.
|
||||||
|
* up/down arrows seek at 100fps
|
||||||
|
* pgup/pgdn seek at 1000fps
|
||||||
|
* home/end seek immediately to the first/last frame
|
||||||
|
* space begins playing frames. If time values (in seconds) are given
|
||||||
|
for each frame, then playback is in realtime.
|
||||||
"""
|
"""
|
||||||
sigTimeChanged = QtCore.Signal(object, object)
|
sigTimeChanged = QtCore.Signal(object, object)
|
||||||
sigProcessingChanged = QtCore.Signal(object)
|
sigProcessingChanged = QtCore.Signal(object)
|
||||||
@ -79,8 +80,31 @@ class ImageView(QtGui.QWidget):
|
|||||||
def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args):
|
def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args):
|
||||||
"""
|
"""
|
||||||
By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data
|
By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data
|
||||||
and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem. Custom items may be given instead
|
and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem.
|
||||||
by specifying the *view* and/or *imageItem* arguments.
|
|
||||||
|
============= =========================================================
|
||||||
|
**Arguments**
|
||||||
|
parent (QWidget) Specifies the parent widget to which
|
||||||
|
this ImageView will belong. If None, then the ImageView
|
||||||
|
is created with no parent.
|
||||||
|
name (str) The name used to register both the internal ViewBox
|
||||||
|
and the PlotItem used to display ROI data. See the *name*
|
||||||
|
argument to :func:`ViewBox.__init__()
|
||||||
|
<pyqtgraph.ViewBox.__init__>`.
|
||||||
|
view (ViewBox or PlotItem) If specified, this will be used
|
||||||
|
as the display area that contains the displayed image.
|
||||||
|
Any :class:`ViewBox <pyqtgraph.ViewBox>`,
|
||||||
|
:class:`PlotItem <pyqtgraph.PlotItem>`, or other
|
||||||
|
compatible object is acceptable.
|
||||||
|
imageItem (ImageItem) If specified, this object will be used to
|
||||||
|
display the image. Must be an instance of ImageItem
|
||||||
|
or other compatible object.
|
||||||
|
============= =========================================================
|
||||||
|
|
||||||
|
Note: to display axis ticks inside the ImageView, instantiate it
|
||||||
|
with a PlotItem instance as its view::
|
||||||
|
|
||||||
|
pg.ImageView(view=pg.PlotItem())
|
||||||
"""
|
"""
|
||||||
QtGui.QWidget.__init__(self, parent, *args)
|
QtGui.QWidget.__init__(self, parent, *args)
|
||||||
self.levelMax = 4096
|
self.levelMax = 4096
|
||||||
@ -165,6 +189,7 @@ class ImageView(QtGui.QWidget):
|
|||||||
self.normRoi.sigRegionChangeFinished.connect(self.updateNorm)
|
self.normRoi.sigRegionChangeFinished.connect(self.updateNorm)
|
||||||
|
|
||||||
self.ui.roiPlot.registerPlot(self.name + '_ROI')
|
self.ui.roiPlot.registerPlot(self.name + '_ROI')
|
||||||
|
self.view.register(self.name)
|
||||||
|
|
||||||
self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]
|
self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]
|
||||||
|
|
||||||
@ -318,7 +343,7 @@ class ImageView(QtGui.QWidget):
|
|||||||
self.ui.histogram.setLevels(min, max)
|
self.ui.histogram.setLevels(min, max)
|
||||||
|
|
||||||
def autoRange(self):
|
def autoRange(self):
|
||||||
"""Auto scale and pan the view around the image."""
|
"""Auto scale and pan the view around the image such that the image fills the view."""
|
||||||
image = self.getProcessedImage()
|
image = self.getProcessedImage()
|
||||||
self.view.autoRange()
|
self.view.autoRange()
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from pyqtgraph.Qt import QtGui
|
from ..Qt import QtGui
|
||||||
import pyqtgraph.functions as fn
|
from .. import functions as fn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
class MeshData(object):
|
class MeshData(object):
|
||||||
@ -501,4 +501,4 @@ class MeshData(object):
|
|||||||
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
|
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
|
||||||
|
|
||||||
return MeshData(vertexes=verts, faces=faces)
|
return MeshData(vertexes=verts, faces=faces)
|
||||||
|
|
||||||
|
@ -25,13 +25,21 @@ class GLImageItem(GLGraphicsItem):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self.smooth = smooth
|
self.smooth = smooth
|
||||||
self.data = data
|
self._needUpdate = False
|
||||||
GLGraphicsItem.__init__(self)
|
GLGraphicsItem.__init__(self)
|
||||||
|
self.setData(data)
|
||||||
self.setGLOptions(glOptions)
|
self.setGLOptions(glOptions)
|
||||||
|
|
||||||
def initializeGL(self):
|
def initializeGL(self):
|
||||||
glEnable(GL_TEXTURE_2D)
|
glEnable(GL_TEXTURE_2D)
|
||||||
self.texture = glGenTextures(1)
|
self.texture = glGenTextures(1)
|
||||||
|
|
||||||
|
def setData(self, data):
|
||||||
|
self.data = data
|
||||||
|
self._needUpdate = True
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def _updateTexture(self):
|
||||||
glBindTexture(GL_TEXTURE_2D, self.texture)
|
glBindTexture(GL_TEXTURE_2D, self.texture)
|
||||||
if self.smooth:
|
if self.smooth:
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||||
@ -63,7 +71,8 @@ class GLImageItem(GLGraphicsItem):
|
|||||||
|
|
||||||
|
|
||||||
def paint(self):
|
def paint(self):
|
||||||
|
if self._needUpdate:
|
||||||
|
self._updateTexture()
|
||||||
glEnable(GL_TEXTURE_2D)
|
glEnable(GL_TEXTURE_2D)
|
||||||
glBindTexture(GL_TEXTURE_2D, self.texture)
|
glBindTexture(GL_TEXTURE_2D, self.texture)
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class GLScatterPlotItem(GLGraphicsItem):
|
|||||||
w = 64
|
w = 64
|
||||||
def fn(x,y):
|
def fn(x,y):
|
||||||
r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5
|
r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5
|
||||||
return 200 * (w/2. - np.clip(r, w/2.-1.0, w/2.))
|
return 255 * (w/2. - np.clip(r, w/2.-1.0, w/2.))
|
||||||
pData = np.empty((w, w, 4))
|
pData = np.empty((w, w, 4))
|
||||||
pData[:] = 255
|
pData[:] = 255
|
||||||
pData[:,:,3] = np.fromfunction(fn, pData.shape[:2])
|
pData[:,:,3] = np.fromfunction(fn, pData.shape[:2])
|
||||||
|
@ -2,6 +2,7 @@ from OpenGL.GL import *
|
|||||||
from .. GLGraphicsItem import GLGraphicsItem
|
from .. GLGraphicsItem import GLGraphicsItem
|
||||||
from ...Qt import QtGui
|
from ...Qt import QtGui
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from ... import debug
|
||||||
|
|
||||||
__all__ = ['GLVolumeItem']
|
__all__ = ['GLVolumeItem']
|
||||||
|
|
||||||
@ -25,13 +26,22 @@ class GLVolumeItem(GLGraphicsItem):
|
|||||||
|
|
||||||
self.sliceDensity = sliceDensity
|
self.sliceDensity = sliceDensity
|
||||||
self.smooth = smooth
|
self.smooth = smooth
|
||||||
self.data = data
|
self.data = None
|
||||||
|
self._needUpload = False
|
||||||
|
self.texture = None
|
||||||
GLGraphicsItem.__init__(self)
|
GLGraphicsItem.__init__(self)
|
||||||
self.setGLOptions(glOptions)
|
self.setGLOptions(glOptions)
|
||||||
|
self.setData(data)
|
||||||
|
|
||||||
def initializeGL(self):
|
def setData(self, data):
|
||||||
|
self.data = data
|
||||||
|
self._needUpload = True
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def _uploadData(self):
|
||||||
glEnable(GL_TEXTURE_3D)
|
glEnable(GL_TEXTURE_3D)
|
||||||
self.texture = glGenTextures(1)
|
if self.texture is None:
|
||||||
|
self.texture = glGenTextures(1)
|
||||||
glBindTexture(GL_TEXTURE_3D, self.texture)
|
glBindTexture(GL_TEXTURE_3D, self.texture)
|
||||||
if self.smooth:
|
if self.smooth:
|
||||||
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||||
@ -60,9 +70,16 @@ class GLVolumeItem(GLGraphicsItem):
|
|||||||
glNewList(l, GL_COMPILE)
|
glNewList(l, GL_COMPILE)
|
||||||
self.drawVolume(ax, d)
|
self.drawVolume(ax, d)
|
||||||
glEndList()
|
glEndList()
|
||||||
|
|
||||||
|
self._needUpload = False
|
||||||
|
|
||||||
def paint(self):
|
def paint(self):
|
||||||
|
if self.data is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._needUpload:
|
||||||
|
self._uploadData()
|
||||||
|
|
||||||
self.setupGLState()
|
self.setupGLState()
|
||||||
|
|
||||||
glEnable(GL_TEXTURE_3D)
|
glEnable(GL_TEXTURE_3D)
|
||||||
|
@ -7,9 +7,16 @@ from .ParameterItem import ParameterItem
|
|||||||
|
|
||||||
|
|
||||||
class ParameterTree(TreeWidget):
|
class ParameterTree(TreeWidget):
|
||||||
"""Widget used to display or control data from a ParameterSet"""
|
"""Widget used to display or control data from a hierarchy of Parameters"""
|
||||||
|
|
||||||
def __init__(self, parent=None, showHeader=True):
|
def __init__(self, parent=None, showHeader=True):
|
||||||
|
"""
|
||||||
|
============== ========================================================
|
||||||
|
**Arguments:**
|
||||||
|
parent (QWidget) An optional parent widget
|
||||||
|
showHeader (bool) If True, then the QTreeView header is displayed.
|
||||||
|
============== ========================================================
|
||||||
|
"""
|
||||||
TreeWidget.__init__(self, parent)
|
TreeWidget.__init__(self, parent)
|
||||||
self.setVerticalScrollMode(self.ScrollPerPixel)
|
self.setVerticalScrollMode(self.ScrollPerPixel)
|
||||||
self.setHorizontalScrollMode(self.ScrollPerPixel)
|
self.setHorizontalScrollMode(self.ScrollPerPixel)
|
||||||
@ -25,10 +32,35 @@ class ParameterTree(TreeWidget):
|
|||||||
self.setRootIsDecorated(False)
|
self.setRootIsDecorated(False)
|
||||||
|
|
||||||
def setParameters(self, param, showTop=True):
|
def setParameters(self, param, showTop=True):
|
||||||
|
"""
|
||||||
|
Set the top-level :class:`Parameter <pyqtgraph.parametertree.Parameter>`
|
||||||
|
to be displayed in this ParameterTree.
|
||||||
|
|
||||||
|
If *showTop* is False, then the top-level parameter is hidden and only
|
||||||
|
its children will be visible. This is a convenience method equivalent
|
||||||
|
to::
|
||||||
|
|
||||||
|
tree.clear()
|
||||||
|
tree.addParameters(param, showTop)
|
||||||
|
"""
|
||||||
self.clear()
|
self.clear()
|
||||||
self.addParameters(param, showTop=showTop)
|
self.addParameters(param, showTop=showTop)
|
||||||
|
|
||||||
def addParameters(self, param, root=None, depth=0, showTop=True):
|
def addParameters(self, param, root=None, depth=0, showTop=True):
|
||||||
|
"""
|
||||||
|
Adds one top-level :class:`Parameter <pyqtgraph.parametertree.Parameter>`
|
||||||
|
to the view.
|
||||||
|
|
||||||
|
============== ==========================================================
|
||||||
|
**Arguments:**
|
||||||
|
param The :class:`Parameter <pyqtgraph.parametertree.Parameter>`
|
||||||
|
to add.
|
||||||
|
root The item within the tree to which *param* should be added.
|
||||||
|
By default, *param* is added as a top-level item.
|
||||||
|
showTop If False, then *param* will be hidden, and only its
|
||||||
|
children will be visible in the tree.
|
||||||
|
============== ==========================================================
|
||||||
|
"""
|
||||||
item = param.makeTreeItem(depth=depth)
|
item = param.makeTreeItem(depth=depth)
|
||||||
if root is None:
|
if root is None:
|
||||||
root = self.invisibleRootItem()
|
root = self.invisibleRootItem()
|
||||||
@ -45,11 +77,14 @@ class ParameterTree(TreeWidget):
|
|||||||
self.addParameters(ch, root=item, depth=depth+1)
|
self.addParameters(ch, root=item, depth=depth+1)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.invisibleRootItem().takeChildren()
|
"""
|
||||||
|
Remove all parameters from the tree.
|
||||||
|
"""
|
||||||
|
self.invisibleRootItem().takeChildren()
|
||||||
|
|
||||||
def focusNext(self, item, forward=True):
|
def focusNext(self, item, forward=True):
|
||||||
## Give input focus to the next (or previous) item after 'item'
|
"""Give input focus to the next (or previous) item after *item*
|
||||||
|
"""
|
||||||
while True:
|
while True:
|
||||||
parent = item.parent()
|
parent = item.parent()
|
||||||
if parent is None:
|
if parent is None:
|
||||||
|
@ -125,6 +125,7 @@ class WidgetParameterItem(ParameterItem):
|
|||||||
w.sigChanged = w.toggled
|
w.sigChanged = w.toggled
|
||||||
w.value = w.isChecked
|
w.value = w.isChecked
|
||||||
w.setValue = w.setChecked
|
w.setValue = w.setChecked
|
||||||
|
w.setEnabled(not opts.get('readonly', False))
|
||||||
self.hideWidget = False
|
self.hideWidget = False
|
||||||
elif t == 'str':
|
elif t == 'str':
|
||||||
w = QtGui.QLineEdit()
|
w = QtGui.QLineEdit()
|
||||||
@ -140,6 +141,7 @@ class WidgetParameterItem(ParameterItem):
|
|||||||
w.setValue = w.setColor
|
w.setValue = w.setColor
|
||||||
self.hideWidget = False
|
self.hideWidget = False
|
||||||
w.setFlat(True)
|
w.setFlat(True)
|
||||||
|
w.setEnabled(not opts.get('readonly', False))
|
||||||
elif t == 'colormap':
|
elif t == 'colormap':
|
||||||
from ..widgets.GradientWidget import GradientWidget ## need this here to avoid import loop
|
from ..widgets.GradientWidget import GradientWidget ## need this here to avoid import loop
|
||||||
w = GradientWidget(orientation='bottom')
|
w = GradientWidget(orientation='bottom')
|
||||||
@ -274,6 +276,8 @@ class WidgetParameterItem(ParameterItem):
|
|||||||
|
|
||||||
if 'readonly' in opts:
|
if 'readonly' in opts:
|
||||||
self.updateDefaultBtn()
|
self.updateDefaultBtn()
|
||||||
|
if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)):
|
||||||
|
w.setEnabled(not opts['readonly'])
|
||||||
|
|
||||||
## If widget is a SpinBox, pass options straight through
|
## If widget is a SpinBox, pass options straight through
|
||||||
if isinstance(self.widget, SpinBox):
|
if isinstance(self.widget, SpinBox):
|
||||||
@ -281,6 +285,9 @@ class WidgetParameterItem(ParameterItem):
|
|||||||
opts['suffix'] = opts['units']
|
opts['suffix'] = opts['units']
|
||||||
self.widget.setOpts(**opts)
|
self.widget.setOpts(**opts)
|
||||||
self.updateDisplayLabel()
|
self.updateDisplayLabel()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class EventProxy(QtCore.QObject):
|
class EventProxy(QtCore.QObject):
|
||||||
def __init__(self, qobj, callback):
|
def __init__(self, qobj, callback):
|
||||||
@ -532,8 +539,8 @@ class ListParameter(Parameter):
|
|||||||
self.forward, self.reverse = self.mapping(limits)
|
self.forward, self.reverse = self.mapping(limits)
|
||||||
|
|
||||||
Parameter.setLimits(self, limits)
|
Parameter.setLimits(self, limits)
|
||||||
#print self.name(), self.value(), limits
|
#print self.name(), self.value(), limits, self.reverse
|
||||||
if len(self.reverse) > 0 and self.value() not in self.reverse[0]:
|
if len(self.reverse[0]) > 0 and self.value() not in self.reverse[0]:
|
||||||
self.setValue(self.reverse[0][0])
|
self.setValue(self.reverse[0][0])
|
||||||
|
|
||||||
#def addItem(self, name, value=None):
|
#def addItem(self, name, value=None):
|
||||||
@ -636,6 +643,7 @@ class TextParameterItem(WidgetParameterItem):
|
|||||||
def makeWidget(self):
|
def makeWidget(self):
|
||||||
self.textBox = QtGui.QTextEdit()
|
self.textBox = QtGui.QTextEdit()
|
||||||
self.textBox.setMaximumHeight(100)
|
self.textBox.setMaximumHeight(100)
|
||||||
|
self.textBox.setReadOnly(self.param.opts.get('readonly', False))
|
||||||
self.textBox.value = lambda: str(self.textBox.toPlainText())
|
self.textBox.value = lambda: str(self.textBox.toPlainText())
|
||||||
self.textBox.setValue = self.textBox.setPlainText
|
self.textBox.setValue = self.textBox.setPlainText
|
||||||
self.textBox.sigChanged = self.textBox.textChanged
|
self.textBox.sigChanged = self.textBox.textChanged
|
||||||
|
18
parametertree/tests/test_parametertypes.py
Normal file
18
parametertree/tests/test_parametertypes.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import pyqtgraph.parametertree as pt
|
||||||
|
import pyqtgraph as pg
|
||||||
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
def test_opts():
|
||||||
|
paramSpec = [
|
||||||
|
dict(name='bool', type='bool', readonly=True),
|
||||||
|
dict(name='color', type='color', readonly=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
param = pt.Parameter.create(name='params', type='group', children=paramSpec)
|
||||||
|
tree = pt.ParameterTree()
|
||||||
|
tree.setParameters(param)
|
||||||
|
|
||||||
|
assert param.param('bool').items.keys()[0].widget.isEnabled() is False
|
||||||
|
assert param.param('color').items.keys()[0].widget.isEnabled() is False
|
||||||
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
|||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import gc
|
import gc, os
|
||||||
|
|
||||||
|
app = pg.mkQApp()
|
||||||
|
|
||||||
def test_isQObjectAlive():
|
def test_isQObjectAlive():
|
||||||
o1 = pg.QtCore.QObject()
|
o1 = pg.QtCore.QObject()
|
||||||
@ -8,3 +10,14 @@ def test_isQObjectAlive():
|
|||||||
del o1
|
del o1
|
||||||
gc.collect()
|
gc.collect()
|
||||||
assert not pg.Qt.isQObjectAlive(o2)
|
assert not pg.Qt.isQObjectAlive(o2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_loadUiType():
|
||||||
|
path = os.path.dirname(__file__)
|
||||||
|
formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui'))
|
||||||
|
w = baseClass()
|
||||||
|
ui = formClass()
|
||||||
|
ui.setupUi(w)
|
||||||
|
w.show()
|
||||||
|
app.processEvents()
|
||||||
|
|
||||||
|
77
tests/test_ref_cycles.py
Normal file
77
tests/test_ref_cycles.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
Test for unwanted reference cycles
|
||||||
|
|
||||||
|
"""
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import numpy as np
|
||||||
|
import gc, weakref
|
||||||
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
def assert_alldead(refs):
|
||||||
|
for ref in refs:
|
||||||
|
assert ref() is None
|
||||||
|
|
||||||
|
def qObjectTree(root):
|
||||||
|
"""Return root and its entire tree of qobject children"""
|
||||||
|
childs = [root]
|
||||||
|
for ch in pg.QtCore.QObject.children(root):
|
||||||
|
childs += qObjectTree(ch)
|
||||||
|
return childs
|
||||||
|
|
||||||
|
def mkrefs(*objs):
|
||||||
|
"""Return a list of weakrefs to each object in *objs.
|
||||||
|
QObject instances are expanded to include all child objects.
|
||||||
|
"""
|
||||||
|
allObjs = {}
|
||||||
|
for obj in objs:
|
||||||
|
if isinstance(obj, pg.QtCore.QObject):
|
||||||
|
obj = qObjectTree(obj)
|
||||||
|
else:
|
||||||
|
obj = [obj]
|
||||||
|
for o in obj:
|
||||||
|
allObjs[id(o)] = o
|
||||||
|
|
||||||
|
return map(weakref.ref, allObjs.values())
|
||||||
|
|
||||||
|
def test_PlotWidget():
|
||||||
|
def mkobjs(*args, **kwds):
|
||||||
|
w = pg.PlotWidget(*args, **kwds)
|
||||||
|
data = pg.np.array([1,5,2,4,3])
|
||||||
|
c = w.plot(data, name='stuff')
|
||||||
|
w.addLegend()
|
||||||
|
|
||||||
|
# test that connections do not keep objects alive
|
||||||
|
w.plotItem.vb.sigRangeChanged.connect(mkrefs)
|
||||||
|
app.focusChanged.connect(w.plotItem.vb.invertY)
|
||||||
|
|
||||||
|
# return weakrefs to a bunch of objects that should die when the scope exits.
|
||||||
|
return mkrefs(w, c, data, w.plotItem, w.plotItem.vb, w.plotItem.getMenu(), w.plotItem.getAxis('left'))
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
assert_alldead(mkobjs())
|
||||||
|
|
||||||
|
def test_ImageView():
|
||||||
|
def mkobjs():
|
||||||
|
iv = pg.ImageView()
|
||||||
|
data = np.zeros((10,10,5))
|
||||||
|
iv.setImage(data)
|
||||||
|
|
||||||
|
return mkrefs(iv, iv.imageItem, iv.view, iv.ui.histogram, data)
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
assert_alldead(mkobjs())
|
||||||
|
|
||||||
|
def test_GraphicsWindow():
|
||||||
|
def mkobjs():
|
||||||
|
w = pg.GraphicsWindow()
|
||||||
|
p1 = w.addPlot()
|
||||||
|
v1 = w.addViewBox()
|
||||||
|
return mkrefs(w, p1, v1)
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
assert_alldead(mkobjs())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ot = test_PlotItem()
|
160
tests/test_stability.py
Normal file
160
tests/test_stability.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
PyQt/PySide stress test:
|
||||||
|
|
||||||
|
Create lots of random widgets and graphics items, connect them together randomly,
|
||||||
|
the tear them down repeatedly.
|
||||||
|
|
||||||
|
The purpose of this is to attempt to generate segmentation faults.
|
||||||
|
"""
|
||||||
|
from PyQt4.QtTest import QTest
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from random import seed, randint
|
||||||
|
import sys, gc, weakref
|
||||||
|
|
||||||
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
seed(12345)
|
||||||
|
|
||||||
|
widgetTypes = [
|
||||||
|
pg.PlotWidget,
|
||||||
|
pg.ImageView,
|
||||||
|
pg.GraphicsView,
|
||||||
|
pg.QtGui.QWidget,
|
||||||
|
pg.QtGui.QTreeWidget,
|
||||||
|
pg.QtGui.QPushButton,
|
||||||
|
]
|
||||||
|
|
||||||
|
itemTypes = [
|
||||||
|
pg.PlotCurveItem,
|
||||||
|
pg.ImageItem,
|
||||||
|
pg.PlotDataItem,
|
||||||
|
pg.ViewBox,
|
||||||
|
pg.QtGui.QGraphicsRectItem
|
||||||
|
]
|
||||||
|
|
||||||
|
widgets = []
|
||||||
|
items = []
|
||||||
|
allWidgets = weakref.WeakSet()
|
||||||
|
|
||||||
|
|
||||||
|
def crashtest():
|
||||||
|
global allWidgets
|
||||||
|
try:
|
||||||
|
gc.disable()
|
||||||
|
actions = [
|
||||||
|
createWidget,
|
||||||
|
#setParent,
|
||||||
|
forgetWidget,
|
||||||
|
showWidget,
|
||||||
|
processEvents,
|
||||||
|
#raiseException,
|
||||||
|
#addReference,
|
||||||
|
]
|
||||||
|
|
||||||
|
thread = WorkThread()
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
action = randItem(actions)
|
||||||
|
action()
|
||||||
|
print('[%d widgets alive, %d zombie]' % (len(allWidgets), len(allWidgets) - len(widgets)))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Caught interrupt; send another to exit.")
|
||||||
|
try:
|
||||||
|
for i in range(100):
|
||||||
|
QTest.qWait(100)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
thread.terminate()
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
finally:
|
||||||
|
gc.enable()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WorkThread(pg.QtCore.QThread):
|
||||||
|
'''Intended to give the gc an opportunity to run from a non-gui thread.'''
|
||||||
|
def run(self):
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
i += 1
|
||||||
|
if (i % 1000000) == 0:
|
||||||
|
print('--worker--')
|
||||||
|
|
||||||
|
|
||||||
|
def randItem(items):
|
||||||
|
return items[randint(0, len(items)-1)]
|
||||||
|
|
||||||
|
def p(msg):
|
||||||
|
print(msg)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def createWidget():
|
||||||
|
p('create widget')
|
||||||
|
global widgets, allWidgets
|
||||||
|
if len(widgets) > 50:
|
||||||
|
return
|
||||||
|
widget = randItem(widgetTypes)()
|
||||||
|
widget.setWindowTitle(widget.__class__.__name__)
|
||||||
|
widgets.append(widget)
|
||||||
|
allWidgets.add(widget)
|
||||||
|
p(" %s" % widget)
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def setParent():
|
||||||
|
p('set parent')
|
||||||
|
global widgets
|
||||||
|
if len(widgets) < 2:
|
||||||
|
return
|
||||||
|
child = parent = None
|
||||||
|
while child is parent:
|
||||||
|
child = randItem(widgets)
|
||||||
|
parent = randItem(widgets)
|
||||||
|
p(" %s parent of %s" % (parent, child))
|
||||||
|
child.setParent(parent)
|
||||||
|
|
||||||
|
def forgetWidget():
|
||||||
|
p('forget widget')
|
||||||
|
global widgets
|
||||||
|
if len(widgets) < 1:
|
||||||
|
return
|
||||||
|
widget = randItem(widgets)
|
||||||
|
p(' %s' % widget)
|
||||||
|
widgets.remove(widget)
|
||||||
|
|
||||||
|
def showWidget():
|
||||||
|
p('show widget')
|
||||||
|
global widgets
|
||||||
|
if len(widgets) < 1:
|
||||||
|
return
|
||||||
|
widget = randItem(widgets)
|
||||||
|
p(' %s' % widget)
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
def processEvents():
|
||||||
|
p('process events')
|
||||||
|
QTest.qWait(25)
|
||||||
|
|
||||||
|
class TstException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def raiseException():
|
||||||
|
p('raise exception')
|
||||||
|
raise TstException("A test exception")
|
||||||
|
|
||||||
|
def addReference():
|
||||||
|
p('add reference')
|
||||||
|
global widgets
|
||||||
|
if len(widgets) < 1:
|
||||||
|
return
|
||||||
|
obj1 = randItem(widgets)
|
||||||
|
obj2 = randItem(widgets)
|
||||||
|
p(' %s -> %s' % (obj1, obj2))
|
||||||
|
obj1._testref = obj2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_stability()
|
53
tests/uictest.ui
Normal file
53
tests/uictest.ui
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>300</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<widget class="PlotWidget" name="widget" native="true">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>120</width>
|
||||||
|
<height>80</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="ImageView" name="widget_2" native="true">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>110</y>
|
||||||
|
<width>120</width>
|
||||||
|
<height>80</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>PlotWidget</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>pyqtgraph</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>ImageView</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>pyqtgraph</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
@ -108,7 +108,7 @@ class RemoteGraphicsView(QtGui.QWidget):
|
|||||||
return QtGui.QWidget.mouseMoveEvent(self, ev)
|
return QtGui.QWidget.mouseMoveEvent(self, ev)
|
||||||
|
|
||||||
def wheelEvent(self, ev):
|
def wheelEvent(self, ev):
|
||||||
self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), ev.orientation(), _callSync='off')
|
self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), int(ev.orientation()), _callSync='off')
|
||||||
ev.accept()
|
ev.accept()
|
||||||
return QtGui.QWidget.wheelEvent(self, ev)
|
return QtGui.QWidget.wheelEvent(self, ev)
|
||||||
|
|
||||||
@ -243,6 +243,7 @@ class Renderer(GraphicsView):
|
|||||||
def wheelEvent(self, pos, gpos, d, btns, mods, ori):
|
def wheelEvent(self, pos, gpos, d, btns, mods, ori):
|
||||||
btns = QtCore.Qt.MouseButtons(btns)
|
btns = QtCore.Qt.MouseButtons(btns)
|
||||||
mods = QtCore.Qt.KeyboardModifiers(mods)
|
mods = QtCore.Qt.KeyboardModifiers(mods)
|
||||||
|
ori = (None, QtCore.Qt.Horizontal, QtCore.Qt.Vertical)[ori]
|
||||||
return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori))
|
return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori))
|
||||||
|
|
||||||
def keyEvent(self, typ, mods, text, autorep, count):
|
def keyEvent(self, typ, mods, text, autorep, count):
|
||||||
|
Loading…
Reference in New Issue
Block a user