ViewBox overhaul. Cleaned up code pathway:

- setRange now only affects target range
  - updateViewRange only affects view range
  - updateMatrix only affects childGroup transform
  - updateMatrix is only called before a render

Pathway now looks like:
        setRange -> updateViewRange -> matrix dirty -> ...
                                    -> sigRangeChanged

        ... -> prepareForPaint -> updateAutoRange, updateMatrix if dirty
This commit is contained in:
Luke Campagnola 2013-11-06 23:26:57 -05:00
commit 58ed8ee7bd
12 changed files with 393 additions and 128 deletions

View File

@ -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.<br>\n

View File

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

View File

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

View File

@ -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__() <pyqtgraph.console.ConsoleWidget.__init__>`.
"""
mkQApp()
from . import console
c = console.ConsoleWidget()
c = console.ConsoleWidget(*args, **kwds)
c.catchAllExceptions()
c.show()
global consoles

View File

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

View File

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

View File

@ -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() <pyqtgraph.ViewBox.register>`)
Remove this ViewBox from the list of linkable views. (see :func:`register() <pyqtgraph.ViewBox.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 <<This should be moved>>
#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:

View File

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

View File

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

View File

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

View File

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

View File

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