diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py
index a67e279d..56b15bcf 100644
--- a/examples/ROIExamples.py
+++ b/examples/ROIExamples.py
@@ -27,7 +27,7 @@ arr += np.random.normal(size=(100,100))
## create GUI
app = QtGui.QApplication([])
-w = pg.GraphicsWindow(size=(800,800), border=True)
+w = pg.GraphicsWindow(size=(1000,800), border=True)
w.setWindowTitle('pyqtgraph example: ROI Examples')
text = """Data Selection From Image.
\n
diff --git a/examples/RemoteGraphicsView.py b/examples/RemoteGraphicsView.py
index a5d869c9..2b74a8c6 100644
--- a/examples/RemoteGraphicsView.py
+++ b/examples/RemoteGraphicsView.py
@@ -13,7 +13,8 @@ from pyqtgraph.widgets.RemoteGraphicsView import RemoteGraphicsView
app = pg.mkQApp()
## Create the widget
-v = RemoteGraphicsView(debug=False)
+v = RemoteGraphicsView(debug=False) # setting debug=True causes both processes to print information
+ # about interprocess communication
v.show()
v.setWindowTitle('pyqtgraph example: RemoteGraphicsView')
diff --git a/examples/crosshair.py b/examples/crosshair.py
index c41dfff1..67d3cc5f 100644
--- a/examples/crosshair.py
+++ b/examples/crosshair.py
@@ -23,7 +23,9 @@ p2 = win.addPlot(row=2, col=0)
region = pg.LinearRegionItem()
region.setZValue(10)
-p2.addItem(region)
+# Add the LinearRegionItem to the ViewBox, but tell the ViewBox to exclude this
+# item when doing auto-range calculations.
+p2.addItem(region, ignoreBounds=True)
#pg.dbg()
p1.setAutoVisible(y=True)
diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py
index 12a4f90f..810fecec 100644
--- a/pyqtgraph/__init__.py
+++ b/pyqtgraph/__init__.py
@@ -139,7 +139,7 @@ def importModules(path, globals, locals, excludes=()):
d = os.path.join(os.path.split(globals['__file__'])[0], path)
files = set()
for f in frozenSupport.listdir(d):
- if frozenSupport.isdir(os.path.join(d, f)) and f != '__pycache__':
+ if frozenSupport.isdir(os.path.join(d, f)) and f not in ['__pycache__', 'tests']:
files.add(f)
elif f[-3:] == '.py' and f != '__init__.py':
files.add(f[:-3])
@@ -311,13 +311,15 @@ def image(*args, **kargs):
return w
show = image ## for backward compatibility
-def dbg():
+def dbg(*args, **kwds):
"""
Create a console window and begin watching for exceptions.
+
+ All arguments are passed to :func:`ConsoleWidget.__init__() `.
"""
mkQApp()
from . import console
- c = console.ConsoleWidget()
+ c = console.ConsoleWidget(*args, **kwds)
c.catchAllExceptions()
c.show()
global consoles
diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py
index e4c5cd81..d8f55d27 100644
--- a/pyqtgraph/graphicsItems/GraphicsObject.py
+++ b/pyqtgraph/graphicsItems/GraphicsObject.py
@@ -12,6 +12,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
"""
_qtBaseClass = QtGui.QGraphicsObject
def __init__(self, *args):
+ self.__inform_view_on_changes = True
QtGui.QGraphicsObject.__init__(self, *args)
self.setFlag(self.ItemSendsGeometryChanges)
GraphicsItem.__init__(self)
@@ -20,7 +21,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
self.parentChanged()
- if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
+ if self.__inform_view_on_changes and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
self.informViewBoundsChanged()
## workaround for pyqt bug:
diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py
index 033aab42..f6ce4680 100644
--- a/pyqtgraph/graphicsItems/ROI.py
+++ b/pyqtgraph/graphicsItems/ROI.py
@@ -1320,7 +1320,6 @@ class Handle(UIGraphicsItem):
## determine rotation of transform
#m = self.sceneTransform() ## Qt bug: do not access sceneTransform() until we know this object has a scene.
#mi = m.inverted()[0]
-
dt = self.deviceTransform()
if dt is None:
@@ -1339,10 +1338,10 @@ class Handle(UIGraphicsItem):
return dti.map(tr.map(self.path))
- def viewRangeChanged(self):
- GraphicsObject.viewRangeChanged(self)
+ def viewTransformChanged(self):
+ GraphicsObject.viewTransformChanged(self)
self._shape = None ## invalidate shape, recompute later if requested.
- #self.updateShape()
+ self.update()
#def itemChange(self, change, value):
#if change == self.ItemScenePositionHasChanged:
diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
index 7657a6bd..419b6306 100644
--- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
+++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
@@ -17,6 +17,10 @@ __all__ = ['ViewBox']
class ChildGroup(ItemGroup):
sigItemsChanged = QtCore.Signal()
+ def __init__(self, parent):
+ ItemGroup.__init__(self, parent)
+ # excempt from telling view when transform changes
+ self._GraphicsObject__inform_view_on_change = False
def itemChange(self, change, value):
ret = ItemGroup.itemChange(self, change, value)
@@ -87,7 +91,8 @@ class ViewBox(GraphicsWidget):
self.addedItems = []
#self.gView = view
#self.showGrid = showGrid
- self.matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred
+ self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred
+ self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed.
self.state = {
@@ -183,7 +188,7 @@ class ViewBox(GraphicsWidget):
def unregister(self):
"""
- Remove this ViewBox forom the list of linkable views. (see :func:`register() `)
+ Remove this ViewBox from the list of linkable views. (see :func:`register() `)
"""
del ViewBox.AllViews[self]
if self.name is not None:
@@ -195,6 +200,25 @@ class ViewBox(GraphicsWidget):
def implements(self, interface):
return interface == 'ViewBox'
+ def itemChange(self, change, value):
+ ret = GraphicsWidget.itemChange(self, change, value)
+ if change == self.ItemSceneChange:
+ scene = self.scene()
+ if scene is not None:
+ scene.sigPrepareForPaint.disconnect(self.prepareForPaint)
+ elif change == self.ItemSceneHasChanged:
+ scene = self.scene()
+ if scene is not None:
+ scene.sigPrepareForPaint.connect(self.prepareForPaint)
+ return ret
+
+ def prepareForPaint(self):
+ #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False)
+ # don't check whether auto range is enabled here--only check when setting dirty flag.
+ if self._autoRangeNeedsUpdate: # and autoRangeEnabled:
+ self.updateAutoRange()
+ if self._matrixNeedsUpdate:
+ self.updateMatrix()
def getState(self, copy=True):
"""Return the current state of the ViewBox.
@@ -223,7 +247,8 @@ class ViewBox(GraphicsWidget):
del state['linkedViews']
self.state.update(state)
- self.updateMatrix()
+ #self.updateMatrix()
+ self.updateViewRange()
self.sigStateChanged.emit(self)
@@ -308,16 +333,15 @@ class ViewBox(GraphicsWidget):
ch.setParentItem(None)
def resizeEvent(self, ev):
- #self.setRange(self.range, padding=0)
+ self.linkedXChanged()
+ self.linkedYChanged()
self.updateAutoRange()
- self.updateMatrix()
+ self.updateViewRange()
self.sigStateChanged.emit(self)
self.background.setRect(self.rect())
- #self._itemBoundsCache.clear()
- #self.linkedXChanged()
- #self.linkedYChanged()
self.sigResized.emit(self)
+
def viewRange(self):
"""Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]"""
return [x[:] for x in self.state['viewRange']] ## return copy
@@ -352,78 +376,86 @@ class ViewBox(GraphicsWidget):
def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True):
"""
Set the visible range of the ViewBox.
- Must specify at least one of *range*, *xRange*, or *yRange*.
+ Must specify at least one of *rect*, *xRange*, or *yRange*.
- ============= =====================================================================
+ ================== =====================================================================
**Arguments**
- *rect* (QRectF) The full range that should be visible in the view box.
- *xRange* (min,max) The range that should be visible along the x-axis.
- *yRange* (min,max) The range that should be visible along the y-axis.
- *padding* (float) Expand the view by a fraction of the requested range.
- By default, this value is set between 0.02 and 0.1 depending on
- the size of the ViewBox.
- ============= =====================================================================
+ *rect* (QRectF) The full range that should be visible in the view box.
+ *xRange* (min,max) The range that should be visible along the x-axis.
+ *yRange* (min,max) The range that should be visible along the y-axis.
+ *padding* (float) Expand the view by a fraction of the requested range.
+ By default, this value is set between 0.02 and 0.1 depending on
+ the size of the ViewBox.
+ *update* (bool) If True, update the range of the ViewBox immediately.
+ Otherwise, the update is deferred until before the next render.
+ *disableAutoRange* (bool) If True, auto-ranging is diabled. Otherwise, it is left
+ unchanged.
+ ================== =====================================================================
"""
+ #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding
- changes = {}
+ changes = {} # axes
+ setRequested = [False, False]
if rect is not None:
changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]}
+ setRequested = [True, True]
if xRange is not None:
changes[0] = xRange
+ setRequested[0] = True
if yRange is not None:
changes[1] = yRange
+ setRequested[1] = True
if len(changes) == 0:
print(rect)
raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect)))
+ # Update axes one at a time
changed = [False, False]
for ax, range in changes.items():
- if padding is None:
- xpad = self.suggestPadding(ax)
- else:
- xpad = padding
mn = min(range)
mx = max(range)
- if mn == mx: ## If we requested 0 range, try to preserve previous scale. Otherwise just pick an arbitrary scale.
+
+ # If we requested 0 range, try to preserve previous scale.
+ # Otherwise just pick an arbitrary scale.
+ if mn == mx:
dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0]
if dy == 0:
dy = 1
mn -= dy*0.5
mx += dy*0.5
xpad = 0.0
- if any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])):
- raise Exception("Not setting range [%s, %s]" % (str(mn), str(mx)))
+ # Make sure no nan/inf get through
+ if not all(np.isfinite([mn, mx])):
+ raise Exception("Cannot set range [%s, %s]" % (str(mn), str(mx)))
+
+ # Apply padding
+ if padding is None:
+ xpad = self.suggestPadding(ax)
+ else:
+ xpad = padding
p = (mx-mn) * xpad
mn -= p
mx += p
+
+ # Set target range
if self.state['targetRange'][ax] != [mn, mx]:
self.state['targetRange'][ax] = [mn, mx]
changed[ax] = True
- aspect = self.state['aspectLocked'] # size ratio / view ratio
- if aspect is not False and len(changes) == 1:
- ## need to adjust orthogonal target range to match
- size = [self.width(), self.height()]
- tr1 = self.state['targetRange'][ax]
- tr2 = self.state['targetRange'][1-ax]
- if size[1] == 0 or aspect == 0:
- ratio = 1.0
- else:
- ratio = (size[0] / float(size[1])) / aspect
- if ax == 0:
- ratio = 1.0 / ratio
- w = (tr1[1]-tr1[0]) * ratio
- d = 0.5 * (w - (tr2[1]-tr2[0]))
- self.state['targetRange'][1-ax] = [tr2[0]-d, tr2[1]+d]
-
-
-
+ # Update viewRange to match targetRange as closely as possible while
+ # accounting for aspect ratio constraint
+ lockX, lockY = setRequested
+ if lockX and lockY:
+ lockX = False
+ lockY = False
+ self.updateViewRange(lockX, lockY)
- if any(changed) and disableAutoRange:
+ # Disable auto-range if needed
+ if disableAutoRange:
if all(changed):
ax = ViewBox.XYAxes
elif changed[0]:
@@ -431,27 +463,43 @@ class ViewBox(GraphicsWidget):
elif changed[1]:
ax = ViewBox.YAxis
self.enableAutoRange(ax, False)
-
-
- self.sigStateChanged.emit(self)
-
- self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect()))
-
- if update and (any(changed) or self.matrixNeedsUpdate):
- self.updateMatrix(changed)
-
- if not update and any(changed):
- self.matrixNeedsUpdate = True
-
- for ax, range in changes.items():
- link = self.linkedView(ax)
- if link is not None:
- link.linkedViewChanged(self, ax)
+ changed.append(True)
+
+ # If nothing has changed, we are done.
+ if any(changed):
+ #if update and self.matrixNeedsUpdate:
+ #self.updateMatrix(changed)
+ #return
+
+ self.sigStateChanged.emit(self)
+
+ # Update target rect for debugging
+ if self.target.isVisible():
+ self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect()))
+
+ # If ortho axes have auto-visible-only, update them now
+ # Note that aspect ratio constraints and auto-visible probably do not work together..
+ if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False):
+ self._autoRangeNeedsUpdate = True
+ #self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated?
+ elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False):
+ self._autoRangeNeedsUpdate = True
+ #self.updateAutoRange()
+
+ ## Update view matrix only if requested
+ #if update:
+ #self.updateMatrix(changed)
+ ## Otherwise, indicate that the matrix needs to be updated
+ #else:
+ #self.matrixNeedsUpdate = True
+
+ ## Inform linked views that the range has changed <>
+ #for ax, range in changes.items():
+ #link = self.linkedView(ax)
+ #if link is not None:
+ #link.linkedViewChanged(self, ax)
+
- if changed[0] and self.state['autoVisibleOnly'][1]:
- self.updateAutoRange()
- elif changed[1] and self.state['autoVisibleOnly'][0]:
- self.updateAutoRange()
def setYRange(self, min, max, padding=None, update=True):
"""
@@ -560,15 +608,14 @@ class ViewBox(GraphicsWidget):
self.setRange(vr.translated(t), padding=0)
else:
if x is not None:
- x1, x2 = vr.left()+x, vr.right()+x
- self.setXRange(x1, x2, padding=0)
+ x = vr.left()+x, vr.right()+x
if y is not None:
- y1, y2 = vr.top()+y, vr.bottom()+y
- self.setYRange(y1, y2, padding=0)
+ y = vr.top()+y, vr.bottom()+y
+ self.setRange(xRange=x, yRange=y, padding=0)
- def enableAutoRange(self, axis=None, enable=True):
+ def enableAutoRange(self, axis=None, enable=True, x=None, y=None):
"""
Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both
(if *axis* is omitted, both axes will be changed).
@@ -580,25 +627,47 @@ class ViewBox(GraphicsWidget):
#if not enable:
#import traceback
#traceback.print_stack()
-
+
+ # support simpler interface:
+ if x is not None or y is not None:
+ if x is not None:
+ self.enableAutoRange(ViewBox.XAxis, x)
+ if y is not None:
+ self.enableAutoRange(ViewBox.YAxis, y)
+ return
+
if enable is True:
enable = 1.0
if axis is None:
axis = ViewBox.XYAxes
+ needAutoRangeUpdate = False
+
if axis == ViewBox.XYAxes or axis == 'xy':
- self.state['autoRange'][0] = enable
- self.state['autoRange'][1] = enable
+ axes = [0, 1]
elif axis == ViewBox.XAxis or axis == 'x':
- self.state['autoRange'][0] = enable
+ axes = [0]
elif axis == ViewBox.YAxis or axis == 'y':
- self.state['autoRange'][1] = enable
+ axes = [1]
else:
raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.')
- if enable:
- self.updateAutoRange()
+ for ax in axes:
+ if self.state['autoRange'][ax] != enable:
+ # If we are disabling, do one last auto-range to make sure that
+ # previously scheduled auto-range changes are enacted
+ if enable is False and self._autoRangeNeedsUpdate:
+ self.updateAutoRange()
+
+ self.state['autoRange'][ax] = enable
+ self._autoRangeNeedsUpdate |= (enable is not False)
+ self.update()
+
+
+ #if needAutoRangeUpdate:
+ # self.updateAutoRange()
+
self.sigStateChanged.emit(self)
def disableAutoRange(self, axis=None):
@@ -686,6 +755,7 @@ class ViewBox(GraphicsWidget):
args['disableAutoRange'] = False
self.setRange(**args)
finally:
+ self._autoRangeNeedsUpdate = False
self._updatingRange = False
def setXLink(self, view):
@@ -724,6 +794,7 @@ class ViewBox(GraphicsWidget):
if oldLink is not None:
try:
getattr(oldLink, signal).disconnect(slot)
+ oldLink.sigResized.disconnect(slot)
except TypeError:
## This can occur if the view has been deleted already
pass
@@ -734,6 +805,7 @@ class ViewBox(GraphicsWidget):
else:
self.state['linkedViews'][axis] = weakref.ref(view)
getattr(view, signal).connect(slot)
+ view.sigResized.connect(slot)
if view.autoRangeEnabled()[axis] is not False:
self.enableAutoRange(axis, False)
slot()
@@ -770,6 +842,7 @@ class ViewBox(GraphicsWidget):
if self.linksBlocked or view is None:
return
+ #print self.name, "ViewBox.linkedViewChanged", axis, view.viewRange()[axis]
vr = view.viewRect()
vg = view.screenGeometry()
sg = self.screenGeometry()
@@ -828,14 +901,18 @@ class ViewBox(GraphicsWidget):
def itemBoundsChanged(self, item):
self._itemBoundsCache.pop(item, None)
- self.updateAutoRange()
+ if (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False):
+ self._autoRangeNeedsUpdate = True
+ self.update()
+ #self.updateAutoRange()
def invertY(self, b=True):
"""
By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis.
"""
self.state['yInverted'] = b
- self.updateMatrix(changed=(False, True))
+ #self.updateMatrix(changed=(False, True))
+ self.updateViewRange()
self.sigStateChanged.emit(self)
def yInverted(self):
@@ -861,7 +938,7 @@ class ViewBox(GraphicsWidget):
self.state['aspectLocked'] = ratio
if ratio != currentRatio: ## If this would change the current range, do that now
#self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1])
- self.updateMatrix()
+ self.updateViewRange()
self.sigStateChanged.emit(self)
def childTransform(self):
@@ -1228,47 +1305,116 @@ class ViewBox(GraphicsWidget):
bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0])
return bounds
+ def updateViewRange(self, forceX=False, forceY=False):
+ ## Update viewRange to match targetRange as closely as possible, given
+ ## aspect ratio constraints. The *force* arguments are used to indicate
+ ## which axis (if any) should be unchanged when applying constraints.
-
- def updateMatrix(self, changed=None):
- ## Make the childGroup's transform match the requested range.
+ viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
+ changed = [False, False]
- if changed is None:
- changed = [False, False]
- changed = list(changed)
+ # Make correction for aspect ratio constraint
+
+ ## aspect is (widget w/h) / (view range w/h)
+ aspect = self.state['aspectLocked'] # size ratio / view ratio
tr = self.targetRect()
bounds = self.rect()
-
- ## set viewRect, given targetRect and possibly aspect ratio constraint
- aspect = self.state['aspectLocked']
- if aspect is False or bounds.height() == 0:
- self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
- else:
- ## aspect is (widget w/h) / (view range w/h)
+ if aspect is not False and tr.width() != 0 and bounds.width() != 0:
## This is the view range aspect ratio we have requested
targetRatio = tr.width() / tr.height()
## This is the view range aspect ratio we need to obey aspect constraint
viewRatio = (bounds.width() / bounds.height()) / aspect
- if targetRatio > viewRatio:
+ # Decide which range to keep unchanged
+ #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange']
+ if forceX:
+ ax = 0
+ elif forceY:
+ ax = 1
+ else:
+ # if we are not required to keep a particular axis unchanged,
+ # then make the entire target range visible
+ ax = 0 if targetRatio > viewRatio else 1
+
+ #### these should affect viewRange, not targetRange!
+ if ax == 0:
## view range needs to be taller than target
dy = 0.5 * (tr.width() / viewRatio - tr.height())
if dy != 0:
changed[1] = True
- self.state['viewRange'] = [
- self.state['targetRange'][0][:],
- [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]
- ]
+ viewRange[1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]
else:
## view range needs to be wider than target
dx = 0.5 * (tr.height() * viewRatio - tr.width())
if dx != 0:
changed[0] = True
- self.state['viewRange'] = [
- [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx],
- self.state['targetRange'][1][:]
- ]
+ viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
+
+ changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
+ self.state['viewRange'] = viewRange
+
+ # emit range change signals
+ if changed[0]:
+ self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
+ if changed[1]:
+ self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
+
+ if any(changed):
+ self.sigRangeChanged.emit(self, self.state['viewRange'])
+ self.update()
+
+ # Inform linked views that the range has changed
+ for ax in [0, 1]:
+ if not changed[ax]:
+ continue
+ link = self.linkedView(ax)
+ if link is not None:
+ link.linkedViewChanged(self, ax)
+
+ self._matrixNeedsUpdate = True
+
+ def updateMatrix(self, changed=None):
+ ## Make the childGroup's transform match the requested viewRange.
+
+ #if changed is None:
+ #changed = [False, False]
+ #changed = list(changed)
+ #tr = self.targetRect()
+ bounds = self.rect()
+
+ ## set viewRect, given targetRect and possibly aspect ratio constraint
+ #self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
+
+ #aspect = self.state['aspectLocked']
+ #if aspect is False or bounds.height() == 0:
+ #self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
+ #else:
+ ### aspect is (widget w/h) / (view range w/h)
+
+ ### This is the view range aspect ratio we have requested
+ #targetRatio = tr.width() / tr.height()
+ ### This is the view range aspect ratio we need to obey aspect constraint
+ #viewRatio = (bounds.width() / bounds.height()) / aspect
+
+ #if targetRatio > viewRatio:
+ ### view range needs to be taller than target
+ #dy = 0.5 * (tr.width() / viewRatio - tr.height())
+ #if dy != 0:
+ #changed[1] = True
+ #self.state['viewRange'] = [
+ #self.state['targetRange'][0][:],
+ #[self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]
+ #]
+ #else:
+ ### view range needs to be wider than target
+ #dx = 0.5 * (tr.height() * viewRatio - tr.width())
+ #if dx != 0:
+ #changed[0] = True
+ #self.state['viewRange'] = [
+ #[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx],
+ #self.state['targetRange'][1][:]
+ #]
vr = self.viewRect()
if vr.height() == 0 or vr.width() == 0:
@@ -1289,15 +1435,16 @@ class ViewBox(GraphicsWidget):
self.childGroup.setTransform(m)
- if changed[0]:
- self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
- if changed[1]:
- self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
- if any(changed):
- self.sigRangeChanged.emit(self, self.state['viewRange'])
+ # moved to viewRangeChanged
+ #if changed[0]:
+ #self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
+ #if changed[1]:
+ #self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
+ #if any(changed):
+ #self.sigRangeChanged.emit(self, self.state['viewRange'])
self.sigTransformChanged.emit(self) ## segfaults here: 1
- self.matrixNeedsUpdate = False
+ self._matrixNeedsUpdate = False
def paint(self, p, opt, widget):
if self.border is not None:
diff --git a/pyqtgraph/graphicsItems/tests/ViewBox.py b/pyqtgraph/graphicsItems/tests/ViewBox.py
new file mode 100644
index 00000000..91d9b617
--- /dev/null
+++ b/pyqtgraph/graphicsItems/tests/ViewBox.py
@@ -0,0 +1,95 @@
+"""
+ViewBox test cases:
+
+* call setRange then resize; requested range must be fully visible
+* lockAspect works correctly for arbitrary aspect ratio
+* autoRange works correctly with aspect locked
+* call setRange with aspect locked, then resize
+* AutoRange with all the bells and whistles
+ * item moves / changes transformation / changes bounds
+ * pan only
+ * fractional range
+
+
+"""
+
+import pyqtgraph as pg
+app = pg.mkQApp()
+
+imgData = pg.np.zeros((10, 10))
+imgData[0] = 3
+imgData[-1] = 3
+imgData[:,0] = 3
+imgData[:,-1] = 3
+
+def testLinkWithAspectLock():
+ global win, vb
+ win = pg.GraphicsWindow()
+ vb = win.addViewBox(name="image view")
+ vb.setAspectLocked()
+ vb.enableAutoRange(x=False, y=False)
+ p1 = win.addPlot(name="plot 1")
+ p2 = win.addPlot(name="plot 2", row=1, col=0)
+ win.ci.layout.setRowFixedHeight(1, 150)
+ win.ci.layout.setColumnFixedWidth(1, 150)
+
+ def viewsMatch():
+ r0 = pg.np.array(vb.viewRange())
+ r1 = pg.np.array(p1.vb.viewRange()[1])
+ r2 = pg.np.array(p2.vb.viewRange()[1])
+ match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all()
+ return match
+
+ p1.setYLink(vb)
+ p2.setXLink(vb)
+ print "link views match:", viewsMatch()
+ win.show()
+ print "show views match:", viewsMatch()
+ img = pg.ImageItem(imgData)
+ vb.addItem(img)
+ vb.autoRange()
+ p1.plot(x=imgData.sum(axis=0), y=range(10))
+ p2.plot(x=range(10), y=imgData.sum(axis=1))
+ print "add items views match:", viewsMatch()
+ #p1.setAspectLocked()
+ #grid = pg.GridItem()
+ #vb.addItem(grid)
+ pg.QtGui.QApplication.processEvents()
+ pg.QtGui.QApplication.processEvents()
+ #win.resize(801, 600)
+
+def testAspectLock():
+ global win, vb
+ win = pg.GraphicsWindow()
+ vb = win.addViewBox(name="image view")
+ vb.setAspectLocked()
+ img = pg.ImageItem(imgData)
+ vb.addItem(img)
+
+
+#app.processEvents()
+#print "init views match:", viewsMatch()
+#p2.setYRange(-300, 300)
+#print "setRange views match:", viewsMatch()
+#app.processEvents()
+#print "setRange views match (after update):", viewsMatch()
+
+#print "--lock aspect--"
+#p1.setAspectLocked(True)
+#print "lockAspect views match:", viewsMatch()
+#p2.setYRange(-200, 200)
+#print "setRange views match:", viewsMatch()
+#app.processEvents()
+#print "setRange views match (after update):", viewsMatch()
+
+#win.resize(100, 600)
+#app.processEvents()
+#vb.setRange(xRange=[-10, 10], padding=0)
+#app.processEvents()
+#win.resize(600, 100)
+#app.processEvents()
+#print vb.viewRange()
+
+
+if __name__ == '__main__':
+ testLinkWithAspectLock()
diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py
index 4ecfb7da..b82debc2 100644
--- a/pyqtgraph/multiprocess/bootstrap.py
+++ b/pyqtgraph/multiprocess/bootstrap.py
@@ -20,10 +20,8 @@ if __name__ == '__main__':
if opts.pop('pyside', False):
import PySide
- #import pyqtgraph
- #import pyqtgraph.multiprocess.processes
+
targetStr = opts.pop('targetStr')
target = pickle.loads(targetStr) ## unpickling the target should import everything we need
- #target(name, port, authkey, ppid)
target(**opts) ## Send all other options to the target function
sys.exit(0)
diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py
index 7d147a1d..cf802352 100644
--- a/pyqtgraph/multiprocess/processes.py
+++ b/pyqtgraph/multiprocess/processes.py
@@ -48,9 +48,10 @@ class Process(RemoteEventHandler):
it must be picklable (bound methods are not).
copySysPath If True, copy the contents of sys.path to the remote process
debug If True, print detailed information about communication
- with the child process.
+ with the child process. Note that this option may cause
+ strange behavior on some systems due to a python bug:
+ http://bugs.python.org/issue3905
============ =============================================================
-
"""
if target is None:
target = startEventLoop
@@ -81,8 +82,14 @@ class Process(RemoteEventHandler):
self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap))
## note: we need all three streams to have their own PIPE due to this bug:
- ## http://bugs.python.org/issue3905
- self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ ## http://bugs.python.org/issue3905
+ if debug is True: # when debugging, we need to keep the usual stdout
+ stdout = sys.stdout
+ stderr = sys.stderr
+ else:
+ stdout = subprocess.PIPE
+ stderr = subprocess.PIPE
+ self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr)
targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to
## set its sys.path properly before unpickling the target
diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py
index 0c8921f6..fb535929 100644
--- a/pyqtgraph/widgets/GraphicsView.py
+++ b/pyqtgraph/widgets/GraphicsView.py
@@ -147,6 +147,11 @@ class GraphicsView(QtGui.QGraphicsView):
#print "GV: paint", ev.rect()
return QtGui.QGraphicsView.paintEvent(self, ev)
+ def render(self, *args, **kwds):
+ self.scene().prepareForPaint()
+ return QtGui.QGraphicsView.render(self, *args, **kwds)
+
+
def close(self):
self.centralWidget = None
self.scene().clear()
diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py
index 80f0fb4b..f8bbb6cf 100644
--- a/pyqtgraph/widgets/RemoteGraphicsView.py
+++ b/pyqtgraph/widgets/RemoteGraphicsView.py
@@ -18,12 +18,15 @@ class RemoteGraphicsView(QtGui.QWidget):
"""
def __init__(self, parent=None, *args, **kwds):
+ """
+ The keyword arguments 'debug' and 'name', if specified, are passed to QtProcess.__init__().
+ """
self._img = None
self._imgReq = None
self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView.
## without it, the widget will not compete for space against another GraphicsView.
QtGui.QWidget.__init__(self)
- self._proc = mp.QtProcess(debug=kwds.pop('debug', False))
+ self._proc = mp.QtProcess(debug=kwds.pop('debug', False), name=kwds.pop('name', None))
self.pg = self._proc._import('pyqtgraph')
self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS)
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
@@ -123,6 +126,7 @@ class Renderer(GraphicsView):
def __init__(self, *args, **kwds):
## Create shared memory for rendered image
+ #pg.dbg(namespace={'r': self})
if sys.platform.startswith('win'):
self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)])
self.shm = mmap.mmap(-1, mmap.PAGESIZE, self.shmtag) # use anonymous mmap on windows
@@ -184,7 +188,11 @@ class Renderer(GraphicsView):
self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
else:
address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0))
- self.img = QtGui.QImage(sip.voidptr(address), self.width(), self.height(), QtGui.QImage.Format_ARGB32)
+ try:
+ self.img = QtGui.QImage(sip.voidptr(address), self.width(), self.height(), QtGui.QImage.Format_ARGB32)
+ except TypeError:
+ # different versions of pyqt have different requirements here..
+ self.img = QtGui.QImage(memoryview(buffer(self.shm)), self.width(), self.height(), QtGui.QImage.Format_ARGB32)
self.img.fill(0xffffffff)
p = QtGui.QPainter(self.img)
self.render(p, self.viewRect(), self.rect())