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:
commit
58ed8ee7bd
@ -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
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
95
pyqtgraph/graphicsItems/tests/ViewBox.py
Normal file
95
pyqtgraph/graphicsItems/tests/ViewBox.py
Normal 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()
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user