Merge remote-tracking branch 'pyqtgraph/develop' into core

This commit is contained in:
Luke Campagnola 2014-08-06 10:11:21 -04:00
commit f9c85dae42
34 changed files with 908 additions and 187 deletions

23
Qt.py
View File

@ -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,13 +62,24 @@ 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')
@ -93,6 +110,10 @@ else:
from PyQt4 import QtOpenGL
except ImportError:
pass
try:
from PyQt4 import QtTest
except ImportError:
pass
import sip

View File

@ -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

View File

@ -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
##############################################################

View File

@ -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:

View File

@ -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
@ -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'
@ -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)

View File

@ -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()

View 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()

View File

@ -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]

View File

@ -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]

View File

@ -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 setOpts(self, **opts):
def setData(self, **opts):
"""
Update the data in the item. All arguments are optional.
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()

View File

@ -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)

View File

@ -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

View File

@ -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):
@ -184,25 +201,6 @@ class InfiniteLine(GraphicsObject):
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:
if ev.isStart():
@ -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()

View File

@ -36,11 +36,6 @@ class IsocurveItem(GraphicsObject):
self.setData(data, level)
#if data is not None and level is not None:
#self.updateLines(data, level)
def setData(self, data, level=None):
"""
Set the data/image to draw isocurves for.
@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -1804,13 +1804,56 @@ class PolyLineROI(ROI):
self.segments = []
ROI.__init__(self, pos, size=[1,1], **args)
for p in positions:
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.

View File

@ -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']
@ -1482,6 +1517,7 @@ class ViewBox(GraphicsWidget):
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'])
@ -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

View File

@ -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])
xy = ['x', 'y'][i]
self.ctrl[i].invertCheck.setChecked(state.get(xy+'Inverted', False))
self.ctrl[1].invertCheck.setChecked(state['yInverted'])
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()

View 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()

View File

@ -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,13 +26,8 @@ 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:
@ -38,12 +35,6 @@ try:
except ImportError:
from numpy import nanmin, nanmax
#try:
#from .. import metaarray as metaarray
#HAVE_METAARRAY = True
#except:
#HAVE_METAARRAY = False
class PlotROI(ROI):
def __init__(self, size):
@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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])

View File

@ -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)
@ -61,8 +71,15 @@ class GLVolumeItem(GLGraphicsItem):
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)

View File

@ -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):
"""
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:

View File

@ -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):
@ -282,6 +286,9 @@ class WidgetParameterItem(ParameterItem):
self.widget.setOpts(**opts)
self.updateDisplayLabel()
class EventProxy(QtCore.QObject):
def __init__(self, qobj, callback):
QtCore.QObject.__init__(self)
@ -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

View 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

View File

@ -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
View 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
View 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
View 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>

View File

@ -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):