Mid-way through overhaul. Proposed code path looks like:

setRange -> updateViewRange -> matrix dirty
                            -> sigRangeChanged

    ... -> prepareForPaint -> updateAutoRange, updateMatrix if dirty
This commit is contained in:
Luke Campagnola 2013-11-03 17:10:59 -05:00
parent ab1b1c6adf
commit a4103dd152
3 changed files with 280 additions and 120 deletions

View File

@ -12,6 +12,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
""" """
_qtBaseClass = QtGui.QGraphicsObject _qtBaseClass = QtGui.QGraphicsObject
def __init__(self, *args): def __init__(self, *args):
self.__inform_view_on_changes = True
QtGui.QGraphicsObject.__init__(self, *args) QtGui.QGraphicsObject.__init__(self, *args)
self.setFlag(self.ItemSendsGeometryChanges) self.setFlag(self.ItemSendsGeometryChanges)
GraphicsItem.__init__(self) GraphicsItem.__init__(self)
@ -20,7 +21,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
ret = QtGui.QGraphicsObject.itemChange(self, change, value) ret = QtGui.QGraphicsObject.itemChange(self, change, value)
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
self.parentChanged() self.parentChanged()
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: if self.__inform_view_on_changes and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
self.informViewBoundsChanged() self.informViewBoundsChanged()
## workaround for pyqt bug: ## workaround for pyqt bug:

View File

@ -17,6 +17,10 @@ __all__ = ['ViewBox']
class ChildGroup(ItemGroup): class ChildGroup(ItemGroup):
sigItemsChanged = QtCore.Signal() 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): def itemChange(self, change, value):
ret = ItemGroup.itemChange(self, change, value) ret = ItemGroup.itemChange(self, change, value)
@ -195,6 +199,21 @@ class ViewBox(GraphicsWidget):
def implements(self, interface): def implements(self, interface):
return interface == 'ViewBox' 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): def getState(self, copy=True):
"""Return the current state of the ViewBox. """Return the current state of the ViewBox.
@ -308,12 +327,14 @@ class ViewBox(GraphicsWidget):
ch.setParentItem(None) ch.setParentItem(None)
def resizeEvent(self, ev): def resizeEvent(self, ev):
#print self.name, "ViewBox.resizeEvent", self.size() print self.name, "ViewBox.resizeEvent", self.size()
#self.setRange(self.range, padding=0) #self.setRange(self.range, padding=0)
x,y = self.targetRange()
self.setRange(xRange=x, yRange=y, padding=0)
self.linkedXChanged() self.linkedXChanged()
self.linkedYChanged() self.linkedYChanged()
self.updateAutoRange() self.updateAutoRange()
self.updateMatrix() #self.updateMatrix()
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
self.background.setRect(self.rect()) self.background.setRect(self.rect())
#self._itemBoundsCache.clear() #self._itemBoundsCache.clear()
@ -357,7 +378,7 @@ class ViewBox(GraphicsWidget):
Set the visible range of the ViewBox. Set the visible range of the ViewBox.
Must specify at least one of *rect*, *xRange*, or *yRange*. Must specify at least one of *rect*, *xRange*, or *yRange*.
============= ===================================================================== ================== =====================================================================
**Arguments** **Arguments**
*rect* (QRectF) The full range that should be visible in the view box. *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. *xRange* (min,max) The range that should be visible along the x-axis.
@ -365,69 +386,89 @@ class ViewBox(GraphicsWidget):
*padding* (float) Expand the view by a fraction of the requested range. *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 By default, this value is set between 0.02 and 0.1 depending on
the size of the ViewBox. 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: if rect is not None:
changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]} changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]}
setRequested = [True, True]
if xRange is not None: if xRange is not None:
changes[0] = xRange changes[0] = xRange
setRequested[0] = True
if yRange is not None: if yRange is not None:
changes[1] = yRange changes[1] = yRange
setRequested[1] = True
if len(changes) == 0: if len(changes) == 0:
print(rect) print(rect)
raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(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] changed = [False, False]
for ax, range in changes.items(): for ax, range in changes.items():
if padding is None:
xpad = self.suggestPadding(ax)
else:
xpad = padding
mn = min(range) mn = min(range)
mx = max(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] dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0]
if dy == 0: if dy == 0:
dy = 1 dy = 1
mn -= dy*0.5 mn -= dy*0.5
mx += dy*0.5 mx += dy*0.5
xpad = 0.0 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 p = (mx-mn) * xpad
mn -= p mn -= p
mx += p mx += p
# Set target range
if self.state['targetRange'][ax] != [mn, mx]: if self.state['targetRange'][ax] != [mn, mx]:
self.state['targetRange'][ax] = [mn, mx] self.state['targetRange'][ax] = [mn, mx]
changed[ax] = True changed[ax] = True
aspect = self.state['aspectLocked'] # size ratio / view ratio # Update viewRange to match targetRange as closely as possible while
if aspect is not False and len(changes) == 1: # accounting for aspect ratio constraint
## need to adjust orthogonal target range to match lockX, lockY = setRequested
size = [self.width(), self.height()] if lockX and lockY:
tr1 = self.state['targetRange'][ax] lockX = False
tr2 = self.state['targetRange'][1-ax] lockY = False
if size[1] == 0 or aspect == 0: self.updateViewRange(lockX, lockY)
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]
# 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 any(changed) and disableAutoRange: if disableAutoRange:
if all(changed): if all(changed):
ax = ViewBox.XYAxes ax = ViewBox.XYAxes
elif changed[0]: elif changed[0]:
@ -436,26 +477,26 @@ class ViewBox(GraphicsWidget):
ax = ViewBox.YAxis ax = ViewBox.YAxis
self.enableAutoRange(ax, False) self.enableAutoRange(ax, False)
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
# Update target rect for debugging
if self.target.isVisible():
self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect()))
if update and (any(changed) or self.matrixNeedsUpdate): ## Update view matrix only if requested
self.updateMatrix(changed) #if update:
#self.updateMatrix(changed)
## Otherwise, indicate that the matrix needs to be updated
#else:
#self.matrixNeedsUpdate = True
if not update and any(changed): ## Inform linked views that the range has changed <<This should be moved>>
self.matrixNeedsUpdate = True #for ax, range in changes.items():
#link = self.linkedView(ax)
#if link is not None:
#link.linkedViewChanged(self, ax)
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): 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 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). (if *axis* is omitted, both axes will be changed).
@ -585,24 +626,39 @@ class ViewBox(GraphicsWidget):
#import traceback #import traceback
#traceback.print_stack() #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: if enable is True:
enable = 1.0 enable = 1.0
if axis is None: if axis is None:
axis = ViewBox.XYAxes axis = ViewBox.XYAxes
needAutoRangeUpdate = False
if axis == ViewBox.XYAxes or axis == 'xy': if axis == ViewBox.XYAxes or axis == 'xy':
self.state['autoRange'][0] = enable axes = [0, 1]
self.state['autoRange'][1] = enable
elif axis == ViewBox.XAxis or axis == 'x': elif axis == ViewBox.XAxis or axis == 'x':
self.state['autoRange'][0] = enable axes = [0]
elif axis == ViewBox.YAxis or axis == 'y': elif axis == ViewBox.YAxis or axis == 'y':
self.state['autoRange'][1] = enable axes = [1]
else: else:
raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.') 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.updateAutoRange()
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
def disableAutoRange(self, axis=None): def disableAutoRange(self, axis=None):
@ -728,6 +784,7 @@ class ViewBox(GraphicsWidget):
if oldLink is not None: if oldLink is not None:
try: try:
getattr(oldLink, signal).disconnect(slot) getattr(oldLink, signal).disconnect(slot)
oldLink.sigResized.disconnect(slot)
except TypeError: except TypeError:
## This can occur if the view has been deleted already ## This can occur if the view has been deleted already
pass pass
@ -738,6 +795,7 @@ class ViewBox(GraphicsWidget):
else: else:
self.state['linkedViews'][axis] = weakref.ref(view) self.state['linkedViews'][axis] = weakref.ref(view)
getattr(view, signal).connect(slot) getattr(view, signal).connect(slot)
view.sigResized.connect(slot)
if view.autoRangeEnabled()[axis] is not False: if view.autoRangeEnabled()[axis] is not False:
self.enableAutoRange(axis, False) self.enableAutoRange(axis, False)
slot() 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]) bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0])
return bounds return bounds
def updateViewRange(self, forceX=False, forceY=False):
## Update viewRange to match targetRange as closely as possible, given
## aspect ratio constraints.
viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
def updateMatrix(self, changed=None): # Make correction for aspect ratio constraint
## Make the childGroup's transform match the requested range. aspect = self.state['aspectLocked'] # size ratio / view ratio
if changed is None:
changed = [False, False]
changed = list(changed)
tr = self.targetRect() tr = self.targetRect()
bounds = self.rect() bounds = self.rect()
if aspect is not False and tr.width() != 0 and bounds.width() != 0:
## 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) ## aspect is (widget w/h) / (view range w/h)
## This is the view range aspect ratio we have requested ## This is the view range aspect ratio we have requested
targetRatio = tr.width() / tr.height() targetRatio = tr.width() / tr.height()
## This is the view range aspect ratio we need to obey aspect constraint ## This is the view range aspect ratio we need to obey aspect constraint
viewRatio = (bounds.width() / bounds.height()) / aspect 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 ## view range needs to be taller than target
dy = 0.5 * (tr.width() / viewRatio - tr.height()) dy = 0.5 * (tr.width() / viewRatio - tr.height())
if dy != 0: if dy != 0:
changed[1] = True changed[1] = True
self.state['viewRange'] = [ self.state['targetRange'][1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]
self.state['targetRange'][0][:], changed[1] = True
[self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]
]
else: else:
## view range needs to be wider than target ## view range needs to be wider than target
dx = 0.5 * (tr.height() * viewRatio - tr.width()) dx = 0.5 * (tr.height() * viewRatio - tr.width())
if dx != 0: if dx != 0:
changed[0] = True changed[0] = True
self.state['viewRange'] = [ self.state['targetRange'][0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], changed[0] = True
self.state['targetRange'][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]
#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 <<This should be moved>>
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() vr = self.viewRect()
if vr.height() == 0 or vr.width() == 0: if vr.height() == 0 or vr.width() == 0:
@ -1294,15 +1431,16 @@ class ViewBox(GraphicsWidget):
self.childGroup.setTransform(m) self.childGroup.setTransform(m)
if changed[0]: # moved to viewRangeChanged
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) #if changed[0]:
if changed[1]: #self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) #if changed[1]:
if any(changed): #self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
self.sigRangeChanged.emit(self, self.state['viewRange']) #if any(changed):
#self.sigRangeChanged.emit(self, self.state['viewRange'])
self.sigTransformChanged.emit(self) ## segfaults here: 1 self.sigTransformChanged.emit(self) ## segfaults here: 1
self.matrixNeedsUpdate = False self._matrixNeedsUpdate = False
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
if self.border is not None: if self.border is not None:

View File

@ -15,39 +15,56 @@ ViewBox test cases:
import pyqtgraph as pg import pyqtgraph as pg
app = pg.mkQApp() 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(): 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()) r0 = pg.np.array(vb.viewRange())
r1 = pg.np.array(p1.vb.viewRange()[1]) r1 = pg.np.array(p1.vb.viewRange()[1])
r2 = pg.np.array(p2.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() match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all()
return match return match
p1.setYLink(vb) p1.setYLink(vb)
p2.setXLink(vb) p2.setXLink(vb)
print "link views match:", viewsMatch() print "link views match:", viewsMatch()
win.show() win.show()
print "show views match:", viewsMatch() print "show views match:", viewsMatch()
imgData = pg.np.zeros((10, 10)) img = pg.ImageItem(imgData)
imgData[0] = 1 vb.addItem(img)
imgData[-1] = 1 vb.autoRange()
imgData[:,0] = 1 p1.plot(x=imgData.sum(axis=0), y=range(10))
imgData[:,-1] = 1 p2.plot(x=range(10), y=imgData.sum(axis=1))
img = pg.ImageItem(imgData) print "add items views match:", viewsMatch()
vb.addItem(img) #p1.setAspectLocked()
p1.plot(x=imgData.sum(axis=0), y=range(10)) #grid = pg.GridItem()
p2.plot(x=range(10), y=imgData.sum(axis=1)) #vb.addItem(grid)
print "add items views match:", viewsMatch() pg.QtGui.QApplication.processEvents()
#p1.setAspectLocked() pg.QtGui.QApplication.processEvents()
#grid = pg.GridItem() #win.resize(801, 600)
#vb.addItem(grid)
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() #app.processEvents()
@ -72,3 +89,7 @@ print "add items views match:", viewsMatch()
#win.resize(600, 100) #win.resize(600, 100)
#app.processEvents() #app.processEvents()
#print vb.viewRange() #print vb.viewRange()
if __name__ == '__main__':
testLinkWithAspectLock()