From 8b58416d1d69827d815386cec6b6107a6583b9ba Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Sep 2013 12:27:01 -0400 Subject: [PATCH 01/10] minor edits --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 7657a6bd..d7fd49e5 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -183,7 +183,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: @@ -352,7 +352,7 @@ 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** From 84a845185eb60aa8ad846ed8dc762ec6a2b613d0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 20 Oct 2013 11:06:57 -0400 Subject: [PATCH 02/10] Fix: when ViewBox is resized, update range if it is linked to another view --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index d7fd49e5..e57ea1ee 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -308,7 +308,10 @@ class ViewBox(GraphicsWidget): ch.setParentItem(None) def resizeEvent(self, ev): + #print self.name, "ViewBox.resizeEvent", self.size() #self.setRange(self.range, padding=0) + self.linkedXChanged() + self.linkedYChanged() self.updateAutoRange() self.updateMatrix() self.sigStateChanged.emit(self) @@ -365,6 +368,7 @@ class ViewBox(GraphicsWidget): ============= ===================================================================== """ + #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding changes = {} @@ -770,6 +774,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() From 662af1a9c5b6e1d419790f807360c9e171c4fc9b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 25 Oct 2013 10:31:30 -0400 Subject: [PATCH 03/10] ignore test directories in top-level __init__ imports --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 12a4f90f..f6eafb60 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]) From ab1b1c6adf61ae358d8ad376f48f2b657b39ba72 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 25 Oct 2013 10:33:41 -0400 Subject: [PATCH 04/10] Added ViewBox test suite --- pyqtgraph/graphicsItems/tests/ViewBox.py | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 pyqtgraph/graphicsItems/tests/ViewBox.py diff --git a/pyqtgraph/graphicsItems/tests/ViewBox.py b/pyqtgraph/graphicsItems/tests/ViewBox.py new file mode 100644 index 00000000..a04e9a29 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/ViewBox.py @@ -0,0 +1,74 @@ +""" +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() +win = pg.GraphicsWindow() +vb = win.addViewBox(name="image view") +vb.setAspectLocked() +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() +imgData = pg.np.zeros((10, 10)) +imgData[0] = 1 +imgData[-1] = 1 +imgData[:,0] = 1 +imgData[:,-1] = 1 +img = pg.ImageItem(imgData) +vb.addItem(img) +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) + + +#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() From a4103dd1526ad4d159ae3a9c74cff7e859efafb0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Nov 2013 17:10:59 -0500 Subject: [PATCH 05/10] Mid-way through overhaul. Proposed code path looks like: setRange -> updateViewRange -> matrix dirty -> sigRangeChanged ... -> prepareForPaint -> updateAutoRange, updateMatrix if dirty --- pyqtgraph/graphicsItems/GraphicsObject.py | 3 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 314 +++++++++++++++------ pyqtgraph/graphicsItems/tests/ViewBox.py | 83 ++++-- 3 files changed, 280 insertions(+), 120 deletions(-) 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/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index e57ea1ee..5241489c 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) @@ -195,6 +199,21 @@ 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): + #print "prepare" + pass def getState(self, copy=True): """Return the current state of the ViewBox. @@ -308,12 +327,14 @@ class ViewBox(GraphicsWidget): ch.setParentItem(None) def resizeEvent(self, ev): - #print self.name, "ViewBox.resizeEvent", self.size() + print self.name, "ViewBox.resizeEvent", self.size() #self.setRange(self.range, padding=0) + x,y = self.targetRange() + self.setRange(xRange=x, yRange=y, padding=0) self.linkedXChanged() self.linkedYChanged() self.updateAutoRange() - self.updateMatrix() + #self.updateMatrix() self.sigStateChanged.emit(self) self.background.setRect(self.rect()) #self._itemBoundsCache.clear() @@ -357,77 +378,97 @@ class ViewBox(GraphicsWidget): Set the visible range of the ViewBox. 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 + 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: + # 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]: + self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? + elif changed[1] and self.state['autoVisibleOnly'][0]: + self.updateAutoRange() + + # If nothing has changed, we are done. + if not any(changed): + #if update and self.matrixNeedsUpdate: + #self.updateMatrix(changed) + return + + # Disable auto-range if needed + if disableAutoRange: if all(changed): ax = ViewBox.XYAxes elif changed[0]: @@ -436,26 +477,26 @@ class ViewBox(GraphicsWidget): ax = ViewBox.YAxis self.enableAutoRange(ax, False) - self.sigStateChanged.emit(self) - self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) + # Update target rect for debugging + if self.target.isVisible(): + 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 + ## Update view matrix only if requested + #if update: + #self.updateMatrix(changed) + ## Otherwise, indicate that the matrix needs to be updated + #else: + #self.matrixNeedsUpdate = True - for ax, range in changes.items(): - link = self.linkedView(ax) - if link is not None: - link.linkedViewChanged(self, ax) + ## 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): """ @@ -572,7 +613,7 @@ class ViewBox(GraphicsWidget): - 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). @@ -584,25 +625,40 @@ 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: + for ax in axes: + if self.state['autoRange'][ax] != enable: + self.state['autoRange'][ax] = enable + needAutoRangeUpdate |= (enable is not False) + + if needAutoRangeUpdate: self.updateAutoRange() + self.sigStateChanged.emit(self) def disableAutoRange(self, axis=None): @@ -728,6 +784,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 @@ -738,6 +795,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() @@ -1233,47 +1291,126 @@ 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. - - def updateMatrix(self, changed=None): - ## Make the childGroup's transform match the requested range. + viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] - if changed is None: - changed = [False, False] - changed = list(changed) + # Make correction for aspect ratio constraint + 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: + if aspect is not False and tr.width() != 0 and bounds.width() != 0: ## 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: + # Decide which range to keep unchanged + print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] + if all(setRequested): + ax = 0 if targetRatio > viewRatio else 1 + print "ax:", ax + elif setRequested[0]: + ax = 0 + else: + ax = 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] - ] + self.state['targetRange'][1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] + changed[1] = True 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][:] - ] + self.state['targetRange'][0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] + changed[0] = True + + + ## 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] + #changed[1-ax] = True + + self.state['viewRange'] = viewRange + + # emit range change signals here! + 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']) + + # 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) + + 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: @@ -1294,15 +1431,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 index a04e9a29..91d9b617 100644 --- a/pyqtgraph/graphicsItems/tests/ViewBox.py +++ b/pyqtgraph/graphicsItems/tests/ViewBox.py @@ -15,41 +15,58 @@ ViewBox test cases: import pyqtgraph as pg app = pg.mkQApp() -win = pg.GraphicsWindow() -vb = win.addViewBox(name="image view") -vb.setAspectLocked() -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() imgData = pg.np.zeros((10, 10)) -imgData[0] = 1 -imgData[-1] = 1 -imgData[:,0] = 1 -imgData[:,-1] = 1 -img = pg.ImageItem(imgData) -vb.addItem(img) -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) +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) @@ -72,3 +89,7 @@ print "add items views match:", viewsMatch() #win.resize(600, 100) #app.processEvents() #print vb.viewRange() + + +if __name__ == '__main__': + testLinkWithAspectLock() From 608352138761a44871740f3b2bd092c0ef300a4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Nov 2013 17:55:57 -0500 Subject: [PATCH 06/10] Initial success. Testing and further reorganization to follow. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 119 ++++++++++----------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5241489c..aed0cb8b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -91,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 = { @@ -212,8 +213,11 @@ class ViewBox(GraphicsWidget): return ret def prepareForPaint(self): - #print "prepare" - pass + autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) + if self._autoRangeNeedsUpdate and autoRangeEnabled: + self.updateAutoRange() + if self._matrixNeedsUpdate: + self.updateMatrix() def getState(self, copy=True): """Return the current state of the ViewBox. @@ -327,13 +331,14 @@ class ViewBox(GraphicsWidget): ch.setParentItem(None) def resizeEvent(self, ev): - print self.name, "ViewBox.resizeEvent", self.size() + #print self.name, "ViewBox.resizeEvent", self.size() #self.setRange(self.range, padding=0) - x,y = self.targetRange() - self.setRange(xRange=x, yRange=y, padding=0) + #x,y = self.targetRange() + #self.setRange(xRange=x, yRange=y, padding=0) self.linkedXChanged() self.linkedYChanged() self.updateAutoRange() + self.updateViewRange() #self.updateMatrix() self.sigStateChanged.emit(self) self.background.setRect(self.rect()) @@ -393,7 +398,7 @@ class ViewBox(GraphicsWidget): ================== ===================================================================== """ - print self.name, "ViewBox.setRange", rect, xRange, yRange, padding + #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding changes = {} # axes setRequested = [False, False] @@ -454,19 +459,6 @@ class ViewBox(GraphicsWidget): lockY = False self.updateViewRange(lockX, lockY) - # 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]: - self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? - elif changed[1] and self.state['autoVisibleOnly'][0]: - self.updateAutoRange() - - # If nothing has changed, we are done. - if not any(changed): - #if update and self.matrixNeedsUpdate: - #self.updateMatrix(changed) - return - # Disable auto-range if needed if disableAutoRange: if all(changed): @@ -476,13 +468,27 @@ class ViewBox(GraphicsWidget): elif changed[1]: ax = ViewBox.YAxis self.enableAutoRange(ax, False) + 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())) - 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]: + self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? + elif changed[1] and self.state['autoVisibleOnly'][0]: + self.updateAutoRange() + ## Update view matrix only if requested #if update: #self.updateMatrix(changed) @@ -746,6 +752,7 @@ class ViewBox(GraphicsWidget): args['disableAutoRange'] = False self.setRange(**args) finally: + self._autoRangeNeedsUpdate = False self._updatingRange = False def setXLink(self, view): @@ -891,7 +898,9 @@ class ViewBox(GraphicsWidget): def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) - self.updateAutoRange() + self._autoRangeNeedsUpdate = True + self.update() + #self.updateAutoRange() def invertY(self, b=True): """ @@ -1293,17 +1302,19 @@ class ViewBox(GraphicsWidget): def updateViewRange(self, forceX=False, forceY=False): ## Update viewRange to match targetRange as closely as possible, given - ## aspect ratio constraints. + ## aspect ratio constraints. The *force* arguments are used to indicate + ## which axis (if any) should be unchanged when applying constraints. viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] + changed = [False, False] # 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() if aspect is not False and tr.width() != 0 and bounds.width() != 0: - ## aspect is (widget w/h) / (view range w/h) - ## This is the view range aspect ratio we have requested targetRatio = tr.width() / tr.height() @@ -1311,14 +1322,15 @@ class ViewBox(GraphicsWidget): viewRatio = (bounds.width() / bounds.height()) / aspect # Decide which range to keep unchanged - print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] - if all(setRequested): - ax = 0 if targetRatio > viewRatio else 1 - print "ax:", ax - elif setRequested[0]: + #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] + if forceX: ax = 0 - else: + 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: @@ -1326,44 +1338,31 @@ class ViewBox(GraphicsWidget): dy = 0.5 * (tr.width() / viewRatio - tr.height()) if dy != 0: changed[1] = True - self.state['targetRange'][1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] - changed[1] = True + 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['targetRange'][0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - changed[0] = True + viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - - ## 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] - #changed[1-ax] = True - + 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 here! + # 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, range in changes.items(): + # 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) @@ -1377,7 +1376,7 @@ class ViewBox(GraphicsWidget): #changed = [False, False] #changed = list(changed) #tr = self.targetRect() - #bounds = self.rect() + bounds = self.rect() ## set viewRect, given targetRect and possibly aspect ratio constraint #self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] From 0daae425a39e353fe86df2b4c9db2c0abf54c61a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Nov 2013 19:41:05 -0500 Subject: [PATCH 07/10] A bit more flow reorganization --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index aed0cb8b..8dbaf9ef 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -246,7 +246,8 @@ class ViewBox(GraphicsWidget): del state['linkedViews'] self.state.update(state) - self.updateMatrix() + #self.updateMatrix() + self.updateViewRange() self.sigStateChanged.emit(self) @@ -485,9 +486,11 @@ class ViewBox(GraphicsWidget): # 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]: - self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? + self._autoRangeNeedsUpdate = True + #self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? elif changed[1] and self.state['autoVisibleOnly'][0]: - self.updateAutoRange() + self._autoRangeNeedsUpdate = True + #self.updateAutoRange() ## Update view matrix only if requested #if update: @@ -907,7 +910,8 @@ class ViewBox(GraphicsWidget): 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): @@ -933,7 +937,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): From 96a4ff7cd5235441b890433154c72972a7977fc9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 4 Nov 2013 22:07:43 -0500 Subject: [PATCH 08/10] Fixes: - ROI updates on sigTransformChanged - ViewBox is more careful about accepting all auto-range changes up to the point it is disabled, even if the auto-range calculation is deferred. --- examples/ROIExamples.py | 2 +- examples/crosshair.py | 4 ++- pyqtgraph/graphicsItems/ROI.py | 7 ++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 38 ++++++++++++---------- 4 files changed, 27 insertions(+), 24 deletions(-) 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/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/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 8dbaf9ef..f0ea1e57 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -213,8 +213,9 @@ class ViewBox(GraphicsWidget): return ret def prepareForPaint(self): - autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) - if self._autoRangeNeedsUpdate and autoRangeEnabled: + #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() @@ -332,22 +333,15 @@ class ViewBox(GraphicsWidget): ch.setParentItem(None) def resizeEvent(self, ev): - #print self.name, "ViewBox.resizeEvent", self.size() - #self.setRange(self.range, padding=0) - #x,y = self.targetRange() - #self.setRange(xRange=x, yRange=y, padding=0) self.linkedXChanged() self.linkedYChanged() self.updateAutoRange() self.updateViewRange() - #self.updateMatrix() 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 @@ -485,10 +479,10 @@ class ViewBox(GraphicsWidget): # 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]: + 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]: + elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): self._autoRangeNeedsUpdate = True #self.updateAutoRange() @@ -662,11 +656,18 @@ class ViewBox(GraphicsWidget): 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 - needAutoRangeUpdate |= (enable is not False) - - if needAutoRangeUpdate: - self.updateAutoRange() + self._autoRangeNeedsUpdate |= (enable is not False) + self.update() + + + #if needAutoRangeUpdate: + # self.updateAutoRange() self.sigStateChanged.emit(self) @@ -901,8 +902,9 @@ class ViewBox(GraphicsWidget): def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) - self._autoRangeNeedsUpdate = True - self.update() + 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): From ea8079334fb3b013f3330c78375e1c6f168a8cde Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Nov 2013 15:35:24 -0500 Subject: [PATCH 09/10] Correct ViewBox.translate to use setRange(x, y) when possible rather than making two calls. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index f0ea1e57..419b6306 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -608,11 +608,10 @@ 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) From 31928e70a5f5e3a8e5edd9f07da14f6373610721 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Nov 2013 23:14:27 -0500 Subject: [PATCH 10/10] Bugfixes: - GraphicsView.render now correctly invokes GraphicsScene.prepareForPaint - Fixed RemoteGraphicsView renderer to use new PyQt QImage API. - multiprocess.Process now pipes stdout/err directly to console when in debugging mode --- examples/RemoteGraphicsView.py | 3 ++- pyqtgraph/__init__.py | 6 ++++-- pyqtgraph/multiprocess/bootstrap.py | 4 +--- pyqtgraph/multiprocess/processes.py | 15 +++++++++++---- pyqtgraph/widgets/GraphicsView.py | 5 +++++ pyqtgraph/widgets/RemoteGraphicsView.py | 12 ++++++++++-- 6 files changed, 33 insertions(+), 12 deletions(-) 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/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f6eafb60..810fecec 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -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/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())