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
|
||||
|
||||
from .python2_3 import asUnicode
|
||||
|
||||
## Automatically determine whether to use PyQt or PySide.
|
||||
## 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.
|
||||
@ -31,6 +33,10 @@ else:
|
||||
|
||||
if USE_PYSIDE:
|
||||
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
||||
try:
|
||||
from PySide import QtTest
|
||||
except ImportError:
|
||||
pass
|
||||
import PySide
|
||||
try:
|
||||
from PySide import shiboken
|
||||
@ -56,18 +62,29 @@ if USE_PYSIDE:
|
||||
# Credit:
|
||||
# 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):
|
||||
"""
|
||||
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 xml.etree.ElementTree as xml
|
||||
from io import StringIO
|
||||
#from io import StringIO
|
||||
|
||||
parsed = xml.parse(uiFile)
|
||||
widget_class = parsed.find('widget').get('class')
|
||||
form_class = parsed.find('class').text
|
||||
|
||||
|
||||
with open(uiFile, 'r') as f:
|
||||
o = StringIO()
|
||||
frame = {}
|
||||
@ -93,6 +110,10 @@ else:
|
||||
from PyQt4 import QtOpenGL
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from PyQt4 import QtTest
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
import sip
|
||||
|
@ -2,6 +2,7 @@
|
||||
from .Qt import QtCore
|
||||
from .ptime import time
|
||||
from . import ThreadsafeTimer
|
||||
import weakref
|
||||
|
||||
__all__ = ['SignalProxy']
|
||||
|
||||
@ -34,7 +35,7 @@ class SignalProxy(QtCore.QObject):
|
||||
self.timer = ThreadsafeTimer.ThreadsafeTimer()
|
||||
self.timer.timeout.connect(self.flush)
|
||||
self.block = False
|
||||
self.slot = slot
|
||||
self.slot = weakref.ref(slot)
|
||||
self.lastFlushTime = None
|
||||
if slot is not None:
|
||||
self.sigDelayed.connect(slot)
|
||||
@ -80,7 +81,7 @@ class SignalProxy(QtCore.QObject):
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self.sigDelayed.disconnect(self.slot)
|
||||
self.sigDelayed.disconnect(self.slot())
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -257,7 +257,7 @@ from .graphicsWindows import *
|
||||
from .SignalProxy import *
|
||||
from .colormap import *
|
||||
from .ptime import time
|
||||
from pyqtgraph.Qt import isQObjectAlive
|
||||
from .Qt import isQObjectAlive
|
||||
|
||||
|
||||
##############################################################
|
||||
|
@ -22,6 +22,9 @@ class Container(object):
|
||||
return None
|
||||
|
||||
def insert(self, new, pos=None, neighbor=None):
|
||||
# remove from existing parent first
|
||||
new.setParent(None)
|
||||
|
||||
if not isinstance(new, list):
|
||||
new = [new]
|
||||
if neighbor is None:
|
||||
|
112
dockarea/Dock.py
112
dockarea/Dock.py
@ -8,11 +8,13 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
|
||||
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)
|
||||
DockDrop.__init__(self)
|
||||
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.moveLabel = True ## If false, the dock is no longer allowed to move the label.
|
||||
self.autoOrient = autoOrientation
|
||||
@ -35,30 +37,30 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
#self.titlePos = 'top'
|
||||
self.raiseOverlay()
|
||||
self.hStyle = """
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-top-width: 0px;
|
||||
}"""
|
||||
self.vStyle = """
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-left-width: 0px;
|
||||
}"""
|
||||
self.nStyle = """
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
Dock > QWidget {
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
}"""
|
||||
self.dragStyle = """
|
||||
Dock > QWidget {
|
||||
border: 4px solid #00F;
|
||||
border-radius: 5px;
|
||||
Dock > QWidget {
|
||||
border: 4px solid #00F;
|
||||
border-radius: 5px;
|
||||
}"""
|
||||
self.setAutoFillBackground(False)
|
||||
self.widgetArea.setStyleSheet(self.hStyle)
|
||||
@ -79,7 +81,7 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
|
||||
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
|
||||
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.
|
||||
Must be one of 'auto', 'horizontal', or 'vertical'.
|
||||
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
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if row is None:
|
||||
@ -239,11 +241,13 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
def dropEvent(self, *args):
|
||||
DockDrop.dropEvent(self, *args)
|
||||
|
||||
|
||||
class DockLabel(VerticalLabel):
|
||||
|
||||
sigClicked = QtCore.Signal(object, object)
|
||||
sigCloseClicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, text, dock):
|
||||
def __init__(self, text, dock, showCloseButton):
|
||||
self.dim = False
|
||||
self.fixedWidth = False
|
||||
VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False)
|
||||
@ -251,10 +255,13 @@ class DockLabel(VerticalLabel):
|
||||
self.dock = dock
|
||||
self.updateStyle()
|
||||
self.setAutoFillBackground(False)
|
||||
self.startedDrag = False
|
||||
|
||||
#def minimumSizeHint(self):
|
||||
##sh = QtGui.QWidget.minimumSizeHint(self)
|
||||
#return QtCore.QSize(20, 20)
|
||||
self.closeButton = None
|
||||
if showCloseButton:
|
||||
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):
|
||||
r = '3px'
|
||||
@ -268,28 +275,28 @@ class DockLabel(VerticalLabel):
|
||||
border = '#55B'
|
||||
|
||||
if self.orientation == 'vertical':
|
||||
self.vStyle = """DockLabel {
|
||||
background-color : %s;
|
||||
color : %s;
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: %s;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: %s;
|
||||
border-width: 0px;
|
||||
self.vStyle = """DockLabel {
|
||||
background-color : %s;
|
||||
color : %s;
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: %s;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: %s;
|
||||
border-width: 0px;
|
||||
border-right: 2px solid %s;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}""" % (bg, fg, r, r, border)
|
||||
self.setStyleSheet(self.vStyle)
|
||||
else:
|
||||
self.hStyle = """DockLabel {
|
||||
background-color : %s;
|
||||
color : %s;
|
||||
border-top-right-radius: %s;
|
||||
border-top-left-radius: %s;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-width: 0px;
|
||||
self.hStyle = """DockLabel {
|
||||
background-color : %s;
|
||||
color : %s;
|
||||
border-top-right-radius: %s;
|
||||
border-top-left-radius: %s;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-width: 0px;
|
||||
border-bottom: 2px solid %s;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
@ -315,11 +322,9 @@ class DockLabel(VerticalLabel):
|
||||
if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
|
||||
self.dock.startDrag()
|
||||
ev.accept()
|
||||
#print ev.pos()
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
if not self.startedDrag:
|
||||
#self.emit(QtCore.SIGNAL('clicked'), self, ev)
|
||||
self.sigClicked.emit(self, ev)
|
||||
ev.accept()
|
||||
|
||||
@ -327,13 +332,14 @@ class DockLabel(VerticalLabel):
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
self.dock.float()
|
||||
|
||||
#def paintEvent(self, ev):
|
||||
#p = QtGui.QPainter(self)
|
||||
##p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200)))
|
||||
#p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100)))
|
||||
#p.drawRect(self.rect().adjusted(0, 0, -1, -1))
|
||||
|
||||
#VerticalLabel.paintEvent(self, ev)
|
||||
|
||||
|
||||
|
||||
def resizeEvent (self, ev):
|
||||
if self.closeButton:
|
||||
if self.orientation == 'vertical':
|
||||
size = ev.size().width()
|
||||
pos = QtCore.QPoint(0, 0)
|
||||
else:
|
||||
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])
|
||||
for i in range(numRows):
|
||||
for d in data:
|
||||
if i < len(d[0]):
|
||||
fd.write(numFormat % d[0][i] + sep + numFormat % d[1][i] + sep)
|
||||
else:
|
||||
fd.write(' %s %s' % (sep, sep))
|
||||
for j in [0, 1]:
|
||||
if i < len(d[j]):
|
||||
fd.write(numFormat % d[j][i] + sep)
|
||||
else:
|
||||
fd.write(' %s' % sep)
|
||||
fd.write('\n')
|
||||
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()
|
||||
|
||||
result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype)
|
||||
nd = data.ndim
|
||||
md = x.shape[-1]
|
||||
|
||||
|
@ -55,6 +55,8 @@ class AxisItem(GraphicsWidget):
|
||||
],
|
||||
'showValues': showValues,
|
||||
'tickLength': maxTickLength,
|
||||
'maxTickLevel': 2,
|
||||
'maxTextLevel': 2,
|
||||
}
|
||||
|
||||
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
|
||||
@ -68,6 +70,7 @@ class AxisItem(GraphicsWidget):
|
||||
self.tickFont = None
|
||||
|
||||
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.autoSIPrefix = True
|
||||
self.autoSIPrefixScale = 1.0
|
||||
@ -161,7 +164,11 @@ class AxisItem(GraphicsWidget):
|
||||
self.scene().removeItem(self)
|
||||
|
||||
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.picture = None
|
||||
self.prepareGeometryChange()
|
||||
@ -229,7 +236,7 @@ class AxisItem(GraphicsWidget):
|
||||
without any scaling prefix (eg, 'V' instead of 'mV'). The
|
||||
scaling prefix will be automatically prepended based on the
|
||||
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.
|
||||
============== =============================================================
|
||||
|
||||
@ -454,7 +461,10 @@ class AxisItem(GraphicsWidget):
|
||||
else:
|
||||
if newRange is None:
|
||||
newRange = view.viewRange()[0]
|
||||
self.setRange(*newRange)
|
||||
if view.xInverted():
|
||||
self.setRange(*newRange[::-1])
|
||||
else:
|
||||
self.setRange(*newRange)
|
||||
|
||||
def boundingRect(self):
|
||||
linkedView = self.linkedView()
|
||||
@ -510,6 +520,37 @@ class AxisItem(GraphicsWidget):
|
||||
self.picture = None
|
||||
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):
|
||||
"""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)
|
||||
if dif == 0:
|
||||
return []
|
||||
@ -550,12 +595,13 @@ class AxisItem(GraphicsWidget):
|
||||
#(intervals[minorIndex], 0) ## Pretty, but eats up CPU
|
||||
]
|
||||
|
||||
## decide whether to include the last level of ticks
|
||||
minSpacing = min(size / 20., 30.)
|
||||
maxTickCount = size / minSpacing
|
||||
if dif / intervals[minorIndex] <= maxTickCount:
|
||||
levels.append((intervals[minorIndex], 0))
|
||||
return levels
|
||||
if self.style['maxTickLevel'] >= 2:
|
||||
## decide whether to include the last level of ticks
|
||||
minSpacing = min(size / 20., 30.)
|
||||
maxTickCount = size / minSpacing
|
||||
if dif / intervals[minorIndex] <= maxTickCount:
|
||||
levels.append((intervals[minorIndex], 0))
|
||||
return levels
|
||||
|
||||
|
||||
|
||||
@ -581,8 +627,6 @@ class AxisItem(GraphicsWidget):
|
||||
#(intervals[intIndexes[0]], 0)
|
||||
#]
|
||||
|
||||
|
||||
|
||||
def tickValues(self, minVal, maxVal, size):
|
||||
"""
|
||||
Return the values and spacing of ticks to draw::
|
||||
@ -756,8 +800,6 @@ class AxisItem(GraphicsWidget):
|
||||
values.append(val)
|
||||
strings.append(strn)
|
||||
|
||||
textLevel = 1 ## draw text at this scale level
|
||||
|
||||
## determine mapping between tick values and local coordinates
|
||||
dif = self.range[1] - self.range[0]
|
||||
if dif == 0:
|
||||
@ -846,7 +888,7 @@ class AxisItem(GraphicsWidget):
|
||||
if not self.style['showValues']:
|
||||
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
|
||||
if tickStrings is None:
|
||||
spacing, values = tickLevels[i]
|
||||
|
@ -8,15 +8,7 @@ __all__ = ['ErrorBarItem']
|
||||
class ErrorBarItem(GraphicsObject):
|
||||
def __init__(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.
|
||||
|
||||
If height is specified, it overrides top and bottom.
|
||||
If width is specified, it overrides left and right.
|
||||
All keyword arguments are passed to setData().
|
||||
"""
|
||||
GraphicsObject.__init__(self)
|
||||
self.opts = dict(
|
||||
@ -31,14 +23,37 @@ class ErrorBarItem(GraphicsObject):
|
||||
beam=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.path = None
|
||||
self.update()
|
||||
self.prepareGeometryChange()
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
def setOpts(self, **opts):
|
||||
# for backward compatibility
|
||||
self.setData(**opts)
|
||||
|
||||
def drawPath(self):
|
||||
p = QtGui.QPainterPath()
|
||||
|
||||
|
@ -102,7 +102,7 @@ class GraphicsItem(object):
|
||||
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.
|
||||
return self._exportOpts['painter'].deviceTransform()
|
||||
return self._exportOpts['painter'].deviceTransform() * self.sceneTransform()
|
||||
|
||||
if viewportTransform is None:
|
||||
view = self.getViewWidget()
|
||||
@ -318,6 +318,8 @@ class GraphicsItem(object):
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
if isinstance(obj, QtCore.QPoint):
|
||||
obj = QtCore.QPointF(obj)
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.map(obj)
|
||||
|
||||
|
@ -17,6 +17,7 @@ from .. import functions as fn
|
||||
import numpy as np
|
||||
from .. import debug as debug
|
||||
|
||||
import weakref
|
||||
|
||||
__all__ = ['HistogramLUTItem']
|
||||
|
||||
@ -42,7 +43,7 @@ class HistogramLUTItem(GraphicsWidget):
|
||||
"""
|
||||
GraphicsWidget.__init__(self)
|
||||
self.lut = None
|
||||
self.imageItem = None
|
||||
self.imageItem = lambda: None # fake a dead weakref
|
||||
|
||||
self.layout = QtGui.QGraphicsGridLayout()
|
||||
self.setLayout(self.layout)
|
||||
@ -138,7 +139,7 @@ class HistogramLUTItem(GraphicsWidget):
|
||||
#self.region.setBounds([vr.top(), vr.bottom()])
|
||||
|
||||
def setImageItem(self, img):
|
||||
self.imageItem = img
|
||||
self.imageItem = weakref.ref(img)
|
||||
img.sigImageChanged.connect(self.imageChanged)
|
||||
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
||||
#self.gradientChanged()
|
||||
@ -150,11 +151,11 @@ class HistogramLUTItem(GraphicsWidget):
|
||||
self.update()
|
||||
|
||||
def gradientChanged(self):
|
||||
if self.imageItem is not None:
|
||||
if self.imageItem() is not None:
|
||||
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:
|
||||
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
|
||||
#if self.imageItem is not None:
|
||||
@ -178,14 +179,14 @@ class HistogramLUTItem(GraphicsWidget):
|
||||
#self.update()
|
||||
|
||||
def regionChanging(self):
|
||||
if self.imageItem is not None:
|
||||
self.imageItem.setLevels(self.region.getRegion())
|
||||
if self.imageItem() is not None:
|
||||
self.imageItem().setLevels(self.region.getRegion())
|
||||
self.sigLevelsChanged.emit(self)
|
||||
self.update()
|
||||
|
||||
def imageChanged(self, autoLevel=False, autoRange=False):
|
||||
profiler = debug.Profiler()
|
||||
h = self.imageItem.getHistogram()
|
||||
h = self.imageItem().getHistogram()
|
||||
profiler('get histogram')
|
||||
if h[0] is None:
|
||||
return
|
||||
|
@ -59,7 +59,9 @@ class InfiniteLine(GraphicsObject):
|
||||
|
||||
if pen is None:
|
||||
pen = (200, 200, 100)
|
||||
|
||||
self.setPen(pen)
|
||||
self.setHoverPen(color=(255,0,0), width=self.pen.width())
|
||||
self.currentPen = self.pen
|
||||
#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
|
||||
for :func:`mkPen <pyqtgraph.mkPen>`."""
|
||||
self.pen = fn.mkPen(*args, **kwargs)
|
||||
self.currentPen = self.pen
|
||||
self.update()
|
||||
if not self.mouseHovering:
|
||||
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):
|
||||
"""
|
||||
@ -168,8 +184,9 @@ class InfiniteLine(GraphicsObject):
|
||||
px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
|
||||
if px is None:
|
||||
px = 0
|
||||
br.setBottom(-px*4)
|
||||
br.setTop(px*4)
|
||||
w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px
|
||||
br.setBottom(-w)
|
||||
br.setTop(w)
|
||||
return br.normalized()
|
||||
|
||||
def paint(self, p, *args):
|
||||
@ -183,25 +200,6 @@ class InfiniteLine(GraphicsObject):
|
||||
return None ## x axis should never be auto-scaled
|
||||
else:
|
||||
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):
|
||||
if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
||||
@ -239,12 +237,12 @@ class InfiniteLine(GraphicsObject):
|
||||
self.setMouseHover(False)
|
||||
|
||||
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:
|
||||
return
|
||||
self.mouseHovering = hover
|
||||
if hover:
|
||||
self.currentPen = fn.mkPen(255, 0,0)
|
||||
self.currentPen = self.hoverPen
|
||||
else:
|
||||
self.currentPen = self.pen
|
||||
self.update()
|
||||
|
@ -35,11 +35,6 @@ class IsocurveItem(GraphicsObject):
|
||||
self.setPen(pen)
|
||||
self.setData(data, level)
|
||||
|
||||
|
||||
|
||||
#if data is not None and level is not None:
|
||||
#self.updateLines(data, level)
|
||||
|
||||
|
||||
def setData(self, data, level=None):
|
||||
"""
|
||||
@ -65,6 +60,7 @@ class IsocurveItem(GraphicsObject):
|
||||
"""Set the level at which the isocurve is drawn."""
|
||||
self.level = level
|
||||
self.path = None
|
||||
self.prepareGeometryChange()
|
||||
self.update()
|
||||
|
||||
|
||||
|
@ -75,7 +75,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||
sample = item
|
||||
else:
|
||||
sample = ItemSample(item)
|
||||
row = len(self.items)
|
||||
row = self.layout.rowCount()
|
||||
self.items.append((sample, label))
|
||||
self.layout.addItem(sample, row, 0)
|
||||
self.layout.addItem(label, row, 1)
|
||||
|
@ -546,7 +546,7 @@ class PlotDataItem(GraphicsObject):
|
||||
if view is None or not view.autoRangeEnabled()[0]:
|
||||
# this option presumes that x-values have uniform spacing
|
||||
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)
|
||||
# clip to visible region extended by downsampling value
|
||||
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:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
|
||||
:func:`invertY <pyqtgraph.ViewBox.invertY>`,
|
||||
:func:`invertX <pyqtgraph.ViewBox.invertX>`,
|
||||
:func:`register <pyqtgraph.ViewBox.register>`,
|
||||
:func:`unregister <pyqtgraph.ViewBox.unregister>`
|
||||
|
||||
@ -299,7 +300,7 @@ class PlotItem(GraphicsWidget):
|
||||
for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE:
|
||||
'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please
|
||||
'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 method(self, *args, **kwargs):
|
||||
|
@ -1804,13 +1804,56 @@ class PolyLineROI(ROI):
|
||||
self.segments = []
|
||||
ROI.__init__(self, pos, size=[1,1], **args)
|
||||
|
||||
for p in positions:
|
||||
self.addFreeHandle(p)
|
||||
self.setPoints(positions)
|
||||
#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
|
||||
for i in range(start, len(self.handles)-1):
|
||||
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):
|
||||
seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False)
|
||||
if index is None:
|
||||
@ -1936,6 +1979,8 @@ class PolyLineROI(ROI):
|
||||
for seg in self.segments:
|
||||
seg.setPen(*args, **kwds)
|
||||
|
||||
|
||||
|
||||
class LineSegmentROI(ROI):
|
||||
"""
|
||||
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 getConfigOption
|
||||
import sys
|
||||
from pyqtgraph.Qt import isQObjectAlive
|
||||
from ...Qt import isQObjectAlive
|
||||
|
||||
__all__ = ['ViewBox']
|
||||
|
||||
@ -74,12 +74,11 @@ class ViewBox(GraphicsWidget):
|
||||
|
||||
Features:
|
||||
|
||||
- Scaling contents by mouse or auto-scale when contents change
|
||||
- View linking--multiple views display the same data ranges
|
||||
- Configurable by context menu
|
||||
- Item coordinate mapping methods
|
||||
* Scaling contents by mouse or auto-scale when contents change
|
||||
* View linking--multiple views display the same data ranges
|
||||
* Configurable by context menu
|
||||
* Item coordinate mapping methods
|
||||
|
||||
Not really compatible with GraphicsView having the same functionality.
|
||||
"""
|
||||
|
||||
sigYRangeChanged = QtCore.Signal(object, object)
|
||||
@ -104,7 +103,7 @@ class ViewBox(GraphicsWidget):
|
||||
NamedViews = weakref.WeakValueDictionary() # name: ViewBox
|
||||
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:**
|
||||
@ -115,11 +114,16 @@ class ViewBox(GraphicsWidget):
|
||||
coorinates to. (or False to allow the ratio to change)
|
||||
*enableMouse* (bool) Whether mouse can be used to scale/pan the view
|
||||
*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)
|
||||
self.name = None
|
||||
self.linksBlocked = False
|
||||
@ -139,6 +143,7 @@ class ViewBox(GraphicsWidget):
|
||||
'viewRange': [[0,1], [0,1]], ## actual range viewed
|
||||
|
||||
'yInverted': invertY,
|
||||
'xInverted': invertX,
|
||||
'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio.
|
||||
'autoRange': [True, True], ## False if auto range is disabled,
|
||||
## otherwise float gives the fraction of data that is visible
|
||||
@ -218,7 +223,11 @@ class ViewBox(GraphicsWidget):
|
||||
def register(self, name):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
ViewBox.AllViews[self] = None
|
||||
@ -662,7 +671,10 @@ class ViewBox(GraphicsWidget):
|
||||
Added in version 0.9.9
|
||||
"""
|
||||
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']:
|
||||
#if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]:
|
||||
#self.state['limits'][kwd] = kwds[kwd]
|
||||
@ -996,7 +1008,10 @@ class ViewBox(GraphicsWidget):
|
||||
x2 = vr.right()
|
||||
else: ## views overlap; line them up
|
||||
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
|
||||
self.enableAutoRange(ViewBox.XAxis, False)
|
||||
self.setXRange(x1, x2, padding=0)
|
||||
@ -1054,10 +1069,27 @@ class ViewBox(GraphicsWidget):
|
||||
#self.updateMatrix(changed=(False, True))
|
||||
self.updateViewRange()
|
||||
self.sigStateChanged.emit(self)
|
||||
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
|
||||
|
||||
def yInverted(self):
|
||||
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):
|
||||
"""
|
||||
If the aspect ratio is locked, view scaling must always preserve the aspect ratio.
|
||||
@ -1280,6 +1312,8 @@ class ViewBox(GraphicsWidget):
|
||||
ev.ignore()
|
||||
|
||||
def scaleHistory(self, d):
|
||||
if len(self.axHistory) == 0:
|
||||
return
|
||||
ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d))
|
||||
if ptr != self.axHistoryPointer:
|
||||
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:
|
||||
|
||||
## 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
|
||||
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
|
||||
#print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange']
|
||||
@ -1481,7 +1516,8 @@ class ViewBox(GraphicsWidget):
|
||||
if dx != 0:
|
||||
changed[0] = True
|
||||
viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
|
||||
|
||||
|
||||
|
||||
# ----------- Make corrections for view limits -----------
|
||||
|
||||
limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits'])
|
||||
@ -1532,7 +1568,7 @@ class ViewBox(GraphicsWidget):
|
||||
changed[axis] = True
|
||||
|
||||
#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)]
|
||||
self.state['viewRange'] = viewRange
|
||||
|
||||
@ -1554,6 +1590,7 @@ class ViewBox(GraphicsWidget):
|
||||
if link is not None:
|
||||
link.linkedViewChanged(self, ax)
|
||||
|
||||
self.update()
|
||||
self._matrixNeedsUpdate = True
|
||||
|
||||
def updateMatrix(self, changed=None):
|
||||
@ -1566,6 +1603,8 @@ class ViewBox(GraphicsWidget):
|
||||
scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height())
|
||||
if not self.state['yInverted']:
|
||||
scale = scale * Point(1, -1)
|
||||
if self.state['xInverted']:
|
||||
scale = scale * Point(-1, 1)
|
||||
m = QtGui.QTransform()
|
||||
|
||||
## First center the viewport at 0
|
||||
|
@ -56,7 +56,7 @@ class ViewBoxMenu(QtGui.QMenu):
|
||||
for sig, fn in connects:
|
||||
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)
|
||||
## exporting is handled by GraphicsScene now
|
||||
#self.export = QtGui.QMenu("Export")
|
||||
@ -139,8 +139,9 @@ class ViewBoxMenu(QtGui.QMenu):
|
||||
|
||||
self.ctrl[i].autoPanCheck.setChecked(state['autoPan'][i])
|
||||
self.ctrl[i].visibleOnlyCheck.setChecked(state['autoVisibleOnly'][i])
|
||||
|
||||
self.ctrl[1].invertCheck.setChecked(state['yInverted'])
|
||||
xy = ['x', 'y'][i]
|
||||
self.ctrl[i].invertCheck.setChecked(state.get(xy+'Inverted', False))
|
||||
|
||||
self.valid = True
|
||||
|
||||
def popup(self, *args):
|
||||
@ -217,19 +218,19 @@ class ViewBoxMenu(QtGui.QMenu):
|
||||
def yInvertToggled(self, b):
|
||||
self.view().invertY(b)
|
||||
|
||||
def xInvertToggled(self, b):
|
||||
self.view().invertX(b)
|
||||
|
||||
def exportMethod(self):
|
||||
act = self.sender()
|
||||
self.exportMethods[str(act.text())]()
|
||||
|
||||
|
||||
def set3ButtonMode(self):
|
||||
self.view().setLeftButtonAction('pan')
|
||||
|
||||
def set1ButtonMode(self):
|
||||
self.view().setLeftButtonAction('rect')
|
||||
|
||||
|
||||
def setViewList(self, views):
|
||||
names = ['']
|
||||
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
|
||||
- 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:
|
||||
from .ImageViewTemplate_pyside import *
|
||||
else:
|
||||
@ -24,25 +26,14 @@ from ..graphicsItems.ROI import *
|
||||
from ..graphicsItems.LinearRegionItem import *
|
||||
from ..graphicsItems.InfiniteLine import *
|
||||
from ..graphicsItems.ViewBox import *
|
||||
#from widgets import ROI
|
||||
import sys
|
||||
#from numpy import ndarray
|
||||
from .. import ptime as ptime
|
||||
import numpy as np
|
||||
from .. import debug as debug
|
||||
|
||||
from ..SignalProxy import SignalProxy
|
||||
|
||||
try:
|
||||
from bottleneck import nanmin, nanmax
|
||||
except ImportError:
|
||||
from numpy import nanmin, nanmax
|
||||
|
||||
#try:
|
||||
#from .. import metaarray as metaarray
|
||||
#HAVE_METAARRAY = True
|
||||
#except:
|
||||
#HAVE_METAARRAY = False
|
||||
|
||||
|
||||
class PlotROI(ROI):
|
||||
@ -72,6 +63,16 @@ class ImageView(QtGui.QWidget):
|
||||
imv = pg.ImageView()
|
||||
imv.show()
|
||||
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)
|
||||
sigProcessingChanged = QtCore.Signal(object)
|
||||
@ -79,8 +80,31 @@ class ImageView(QtGui.QWidget):
|
||||
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
|
||||
and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem. Custom items may be given instead
|
||||
by specifying the *view* and/or *imageItem* arguments.
|
||||
and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem.
|
||||
|
||||
============= =========================================================
|
||||
**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)
|
||||
self.levelMax = 4096
|
||||
@ -165,6 +189,7 @@ class ImageView(QtGui.QWidget):
|
||||
self.normRoi.sigRegionChangeFinished.connect(self.updateNorm)
|
||||
|
||||
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]
|
||||
|
||||
@ -318,7 +343,7 @@ class ImageView(QtGui.QWidget):
|
||||
self.ui.histogram.setLevels(min, max)
|
||||
|
||||
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()
|
||||
self.view.autoRange()
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
from pyqtgraph.Qt import QtGui
|
||||
import pyqtgraph.functions as fn
|
||||
from ..Qt import QtGui
|
||||
from .. import functions as fn
|
||||
import numpy as np
|
||||
|
||||
class MeshData(object):
|
||||
@ -501,4 +501,4 @@ class MeshData(object):
|
||||
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
|
||||
|
||||
return MeshData(vertexes=verts, faces=faces)
|
||||
|
||||
|
||||
|
@ -25,13 +25,21 @@ class GLImageItem(GLGraphicsItem):
|
||||
"""
|
||||
|
||||
self.smooth = smooth
|
||||
self.data = data
|
||||
self._needUpdate = False
|
||||
GLGraphicsItem.__init__(self)
|
||||
self.setData(data)
|
||||
self.setGLOptions(glOptions)
|
||||
|
||||
def initializeGL(self):
|
||||
glEnable(GL_TEXTURE_2D)
|
||||
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)
|
||||
if self.smooth:
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||
@ -63,7 +71,8 @@ class GLImageItem(GLGraphicsItem):
|
||||
|
||||
|
||||
def paint(self):
|
||||
|
||||
if self._needUpdate:
|
||||
self._updateTexture()
|
||||
glEnable(GL_TEXTURE_2D)
|
||||
glBindTexture(GL_TEXTURE_2D, self.texture)
|
||||
|
||||
|
@ -59,7 +59,7 @@ class GLScatterPlotItem(GLGraphicsItem):
|
||||
w = 64
|
||||
def fn(x,y):
|
||||
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[:] = 255
|
||||
pData[:,:,3] = np.fromfunction(fn, pData.shape[:2])
|
||||
|
@ -2,6 +2,7 @@ from OpenGL.GL import *
|
||||
from .. GLGraphicsItem import GLGraphicsItem
|
||||
from ...Qt import QtGui
|
||||
import numpy as np
|
||||
from ... import debug
|
||||
|
||||
__all__ = ['GLVolumeItem']
|
||||
|
||||
@ -25,13 +26,22 @@ class GLVolumeItem(GLGraphicsItem):
|
||||
|
||||
self.sliceDensity = sliceDensity
|
||||
self.smooth = smooth
|
||||
self.data = data
|
||||
self.data = None
|
||||
self._needUpload = False
|
||||
self.texture = None
|
||||
GLGraphicsItem.__init__(self)
|
||||
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)
|
||||
self.texture = glGenTextures(1)
|
||||
if self.texture is None:
|
||||
self.texture = glGenTextures(1)
|
||||
glBindTexture(GL_TEXTURE_3D, self.texture)
|
||||
if self.smooth:
|
||||
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||
@ -60,9 +70,16 @@ class GLVolumeItem(GLGraphicsItem):
|
||||
glNewList(l, GL_COMPILE)
|
||||
self.drawVolume(ax, d)
|
||||
glEndList()
|
||||
|
||||
|
||||
|
||||
self._needUpload = False
|
||||
|
||||
def paint(self):
|
||||
if self.data is None:
|
||||
return
|
||||
|
||||
if self._needUpload:
|
||||
self._uploadData()
|
||||
|
||||
self.setupGLState()
|
||||
|
||||
glEnable(GL_TEXTURE_3D)
|
||||
|
@ -7,9 +7,16 @@ from .ParameterItem import ParameterItem
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
============== ========================================================
|
||||
**Arguments:**
|
||||
parent (QWidget) An optional parent widget
|
||||
showHeader (bool) If True, then the QTreeView header is displayed.
|
||||
============== ========================================================
|
||||
"""
|
||||
TreeWidget.__init__(self, parent)
|
||||
self.setVerticalScrollMode(self.ScrollPerPixel)
|
||||
self.setHorizontalScrollMode(self.ScrollPerPixel)
|
||||
@ -25,10 +32,35 @@ class ParameterTree(TreeWidget):
|
||||
self.setRootIsDecorated(False)
|
||||
|
||||
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.addParameters(param, showTop=showTop)
|
||||
|
||||
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)
|
||||
if root is None:
|
||||
root = self.invisibleRootItem()
|
||||
@ -45,11 +77,14 @@ class ParameterTree(TreeWidget):
|
||||
self.addParameters(ch, root=item, depth=depth+1)
|
||||
|
||||
def clear(self):
|
||||
self.invisibleRootItem().takeChildren()
|
||||
|
||||
"""
|
||||
Remove all parameters from the tree.
|
||||
"""
|
||||
self.invisibleRootItem().takeChildren()
|
||||
|
||||
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:
|
||||
parent = item.parent()
|
||||
if parent is None:
|
||||
|
@ -125,6 +125,7 @@ class WidgetParameterItem(ParameterItem):
|
||||
w.sigChanged = w.toggled
|
||||
w.value = w.isChecked
|
||||
w.setValue = w.setChecked
|
||||
w.setEnabled(not opts.get('readonly', False))
|
||||
self.hideWidget = False
|
||||
elif t == 'str':
|
||||
w = QtGui.QLineEdit()
|
||||
@ -140,6 +141,7 @@ class WidgetParameterItem(ParameterItem):
|
||||
w.setValue = w.setColor
|
||||
self.hideWidget = False
|
||||
w.setFlat(True)
|
||||
w.setEnabled(not opts.get('readonly', False))
|
||||
elif t == 'colormap':
|
||||
from ..widgets.GradientWidget import GradientWidget ## need this here to avoid import loop
|
||||
w = GradientWidget(orientation='bottom')
|
||||
@ -274,6 +276,8 @@ class WidgetParameterItem(ParameterItem):
|
||||
|
||||
if 'readonly' in opts:
|
||||
self.updateDefaultBtn()
|
||||
if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)):
|
||||
w.setEnabled(not opts['readonly'])
|
||||
|
||||
## If widget is a SpinBox, pass options straight through
|
||||
if isinstance(self.widget, SpinBox):
|
||||
@ -281,6 +285,9 @@ class WidgetParameterItem(ParameterItem):
|
||||
opts['suffix'] = opts['units']
|
||||
self.widget.setOpts(**opts)
|
||||
self.updateDisplayLabel()
|
||||
|
||||
|
||||
|
||||
|
||||
class EventProxy(QtCore.QObject):
|
||||
def __init__(self, qobj, callback):
|
||||
@ -532,8 +539,8 @@ class ListParameter(Parameter):
|
||||
self.forward, self.reverse = self.mapping(limits)
|
||||
|
||||
Parameter.setLimits(self, limits)
|
||||
#print self.name(), self.value(), limits
|
||||
if len(self.reverse) > 0 and self.value() not in self.reverse[0]:
|
||||
#print self.name(), self.value(), limits, self.reverse
|
||||
if len(self.reverse[0]) > 0 and self.value() not in self.reverse[0]:
|
||||
self.setValue(self.reverse[0][0])
|
||||
|
||||
#def addItem(self, name, value=None):
|
||||
@ -636,6 +643,7 @@ class TextParameterItem(WidgetParameterItem):
|
||||
def makeWidget(self):
|
||||
self.textBox = QtGui.QTextEdit()
|
||||
self.textBox.setMaximumHeight(100)
|
||||
self.textBox.setReadOnly(self.param.opts.get('readonly', False))
|
||||
self.textBox.value = lambda: str(self.textBox.toPlainText())
|
||||
self.textBox.setValue = self.textBox.setPlainText
|
||||
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 gc
|
||||
import gc, os
|
||||
|
||||
app = pg.mkQApp()
|
||||
|
||||
def test_isQObjectAlive():
|
||||
o1 = pg.QtCore.QObject()
|
||||
@ -8,3 +10,14 @@ def test_isQObjectAlive():
|
||||
del o1
|
||||
gc.collect()
|
||||
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)
|
||||
|
||||
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()
|
||||
return QtGui.QWidget.wheelEvent(self, ev)
|
||||
|
||||
@ -243,6 +243,7 @@ class Renderer(GraphicsView):
|
||||
def wheelEvent(self, pos, gpos, d, btns, mods, ori):
|
||||
btns = QtCore.Qt.MouseButtons(btns)
|
||||
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))
|
||||
|
||||
def keyEvent(self, typ, mods, text, autorep, count):
|
||||
|
Loading…
Reference in New Issue
Block a user