From 7ada1ede4aec4939d85b2851e6b6e12f940eff8c Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 21 Jun 2012 21:24:44 -0400 Subject: [PATCH 01/24] reloadAll now raises exception if any modules fail to reload --- reload.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/reload.py b/reload.py index 85892c2d..91801fe1 100644 --- a/reload.py +++ b/reload.py @@ -34,6 +34,7 @@ def reloadAll(prefix=None, debug=False): - Skips reload if the file has not been updated (if .pyc is newer than .py) - if prefix is None, checks all loaded modules """ + failed = [] for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload if not inspect.ismodule(mod): continue @@ -58,7 +59,10 @@ def reloadAll(prefix=None, debug=False): reload(mod, debug=debug) except: printExc("Error while reloading module %s, skipping\n" % mod) - + failed.append(mod.__name__) + + if len(failed) > 0: + raise Exception("Some modules failed to reload: %s" % ', '.join(failed)) def reload(module, debug=False, lists=False, dicts=False): """Replacement for the builtin reload function: From cc93c7ba435f817069b6387cb94a14acc07af4f2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 21 Jun 2012 21:52:34 -0400 Subject: [PATCH 02/24] Minor edits and fixes --- console/CmdInput.py | 2 +- console/Console.py | 6 +- examples/initExample.py | 2 +- graphicsItems/AxisItem.py | 2 + graphicsItems/ScatterPlotItem.py | 4 +- parametertree/ParameterTree.py | 4 +- widgets/GraphicsView.py | 169 ------------------------------- widgets/ProgressDialog.py | 13 +-- 8 files changed, 19 insertions(+), 183 deletions(-) diff --git a/console/CmdInput.py b/console/CmdInput.py index d7a35d4f..70017289 100644 --- a/console/CmdInput.py +++ b/console/CmdInput.py @@ -1,4 +1,4 @@ -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui class CmdInput(QtGui.QLineEdit): diff --git a/console/Console.py b/console/Console.py index 44753f07..066a4073 100644 --- a/console/Console.py +++ b/console/Console.py @@ -1,13 +1,11 @@ from pyqtgraph.Qt import QtCore, QtGui -import sys, re, os, time, traceback +import sys, re, os, time, traceback, subprocess import pyqtgraph as pg import template import pyqtgraph.exceptionHandling as exceptionHandling import pickle -EDITOR = "pykate {fileName}:{lineNum}" - class ConsoleWidget(QtGui.QWidget): """ Widget displaying console output and accepting command input. @@ -258,7 +256,7 @@ class ConsoleWidget(QtGui.QWidget): tb = self.currentFrame() lineNum = tb.tb_lineno fileName = tb.tb_frame.f_code.co_filename - os.system(EDITOR.format(fileName=fileName, lineNum=lineNum)) + subprocess.Popen(EDITOR.format(fileName=fileName, lineNum=lineNum), shell=True) def allExceptionsHandler(self, *args): diff --git a/examples/initExample.py b/examples/initExample.py index 8f98f535..040dbeea 100644 --- a/examples/initExample.py +++ b/examples/initExample.py @@ -1,3 +1,3 @@ ## make this version of pyqtgraph importable before any others import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index fea281f3..ce2cb503 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -483,6 +483,8 @@ class AxisItem(GraphicsWidget): ## determine size of this item in pixels points = list(map(self.mapToDevice, span)) + if None in points: + return lengthInPixels = Point(points[1] - points[0]).length() if lengthInPixels == 0: return diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index 53075090..d1ad2a68 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -226,6 +226,7 @@ class ScatterPlotItem(GraphicsObject): self.setPointData(kargs['data'], dataSet=newData) #self.updateSpots() + self.prepareGeometryChange() self.bounds = [None, None] self.generateSpotItems() self.sigPlotChanged.emit(self) @@ -396,7 +397,7 @@ class ScatterPlotItem(GraphicsObject): if frac >= 1.0 and self.bounds[ax] is not None: return self.bounds[ax] - self.prepareGeometryChange() + #self.prepareGeometryChange() if self.data is None or len(self.data) == 0: return (None, None) @@ -464,6 +465,7 @@ class ScatterPlotItem(GraphicsObject): return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) def viewRangeChanged(self): + self.prepareGeometryChange() GraphicsObject.viewRangeChanged(self) self.bounds = [None, None] diff --git a/parametertree/ParameterTree.py b/parametertree/ParameterTree.py index 22c54d77..cd56ea00 100644 --- a/parametertree/ParameterTree.py +++ b/parametertree/ParameterTree.py @@ -113,4 +113,6 @@ class ParameterTree(TreeWidget): sel[0].selected(True) return TreeWidget.selectionChanged(self, *args) - + def wheelEvent(self, ev): + self.clearSelection() + return TreeWidget.wheelEvent(self, ev) diff --git a/widgets/GraphicsView.py b/widgets/GraphicsView.py index e12228c8..83a7f91b 100644 --- a/widgets/GraphicsView.py +++ b/widgets/GraphicsView.py @@ -75,13 +75,10 @@ class GraphicsView(QtGui.QGraphicsView): self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate) - #self.setSceneRect(QtCore.QRectF(-1e10, -1e10, 2e10, 2e10)) - self.lockedViewports = [] self.lastMousePos = None self.setMouseTracking(True) self.aspectLocked = False - #self.yInverted = True self.range = QtCore.QRectF(0, 0, 1, 1) self.autoPixelRange = True self.currentItem = None @@ -101,15 +98,10 @@ class GraphicsView(QtGui.QGraphicsView): self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) self.clickAccepted = False - #def paintEvent(self, *args): - #prof = debug.Profiler('GraphicsView.paintEvent '+str(id(self)), disabled=False) - #QtGui.QGraphicsView.paintEvent(self, *args) - #prof.finish() def close(self): self.centralWidget = None self.scene().clear() - #print " ", self.scene().itemCount() self.currentItem = None self.sceneObj = None self.closed = True @@ -123,11 +115,9 @@ class GraphicsView(QtGui.QGraphicsView): else: v = QtGui.QWidget() - #v.setStyleSheet("background-color: #000000;") self.setViewport(v) def keyPressEvent(self, ev): - #QtGui.QGraphicsView.keyPressEvent(self, ev) self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene ## (view likes to eat arrow key events) @@ -196,11 +186,6 @@ class GraphicsView(QtGui.QGraphicsView): scale = [sx, sy] if self.aspectLocked: scale[0] = scale[1] - #adj = (self.range.width()*0.5*(1.0-(1.0/scale[0])), self.range.height()*0.5*(1.0-(1.0/scale[1]))) - #print "======\n", scale, adj - #print self.range - #self.range.adjust(adj[0], adj[1], -adj[0], -adj[1]) - #print self.range if self.scaleCenter: center = None @@ -270,13 +255,6 @@ class GraphicsView(QtGui.QGraphicsView): r1.setBottom(r.bottom()) GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False) - #def invertY(self, invert=True): - ##if self.yInverted != invert: - ##self.scale[1] *= -1. - #self.yInverted = invert - #self.updateMatrix() - - def wheelEvent(self, ev): QtGui.QGraphicsView.wheelEvent(self, ev) if not self.mouseEnabled: @@ -289,39 +267,11 @@ class GraphicsView(QtGui.QGraphicsView): def setAspectLocked(self, s): self.aspectLocked = s - #def mouseDoubleClickEvent(self, ev): - #QtGui.QGraphicsView.mouseDoubleClickEvent(self, ev) - #pass - - ### This function is here because interactive mode is disabled due to bugs. - #def graphicsSceneEvent(self, ev, pev=None, fev=None): - #ev1 = GraphicsSceneMouseEvent() - #ev1.setPos(QtCore.QPointF(ev.pos().x(), ev.pos().y())) - #ev1.setButtons(ev.buttons()) - #ev1.setButton(ev.button()) - #ev1.setModifiers(ev.modifiers()) - #ev1.setScenePos(self.mapToScene(QtCore.QPoint(ev.pos()))) - #if pev is not None: - #ev1.setLastPos(pev.pos()) - #ev1.setLastScenePos(pev.scenePos()) - #ev1.setLastScreenPos(pev.screenPos()) - #if fev is not None: - #ev1.setButtonDownPos(fev.pos()) - #ev1.setButtonDownScenePos(fev.scenePos()) - #ev1.setButtonDownScreenPos(fev.screenPos()) - #return ev1 - def leaveEvent(self, ev): self.scene().leaveEvent(ev) ## inform scene when mouse leaves def mousePressEvent(self, ev): QtGui.QGraphicsView.mousePressEvent(self, ev) - - #print "Press over:" - #for i in self.items(ev.pos()): - # print i.zValue(), int(i.acceptedMouseButtons()), i, i.scenePos() - #print "Event accepted:", ev.isAccepted() - #print "Grabber:", self.scene().mouseGrabberItem() if not self.mouseEnabled: @@ -333,39 +283,14 @@ class GraphicsView(QtGui.QGraphicsView): self.scene().clearSelection() return ## Everything below disabled for now.. - #self.currentItem = None - #maxZ = None - #for i in self.items(ev.pos()): - #if maxZ is None or maxZ < i.zValue(): - #self.currentItem = i - #maxZ = i.zValue() - #print "make event" - #self.pev = self.graphicsSceneEvent(ev) - #self.fev = self.pev - #if self.currentItem is not None: - #self.currentItem.mousePressEvent(self.pev) - ##self.clearMouse() - ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - #self.emit(QtCore.SIGNAL("mousePressed(PyQt_PyObject)"), self.mouseTrail) - def mouseReleaseEvent(self, ev): QtGui.QGraphicsView.mouseReleaseEvent(self, ev) if not self.mouseEnabled: return - #self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - #self.emit(QtCore.SIGNAL("mouseReleased"), ev) self.sigMouseReleased.emit(ev) self.lastButtonReleased = ev.button() return ## Everything below disabled for now.. - ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - #self.emit(QtCore.SIGNAL("mouseReleased(PyQt_PyObject)"), self.mouseTrail) - #if self.currentItem is not None: - #pev = self.graphicsSceneEvent(ev, self.pev, self.fev) - #self.pev = pev - #self.currentItem.mouseReleaseEvent(pev) - #self.currentItem = None - def mouseMoveEvent(self, ev): if self.lastMousePos is None: self.lastMousePos = Point(ev.pos()) @@ -375,10 +300,7 @@ class GraphicsView(QtGui.QGraphicsView): QtGui.QGraphicsView.mouseMoveEvent(self, ev) if not self.mouseEnabled: return - #self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos())) self.sigSceneMouseMoved.emit(self.mapToScene(ev.pos())) - #print "moved. Grabber:", self.scene().mouseGrabberItem() - if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it. return @@ -386,10 +308,7 @@ class GraphicsView(QtGui.QGraphicsView): if ev.buttons() == QtCore.Qt.RightButton: delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50)) scale = 1.01 ** delta - #if self.yInverted: - #scale[0] = 1. / scale[0] self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) - #self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) self.sigRangeChanged.emit(self, self.range) elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. @@ -397,23 +316,8 @@ class GraphicsView(QtGui.QGraphicsView): tr = -delta * px self.translate(tr[0], tr[1]) - #self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) self.sigRangeChanged.emit(self, self.range) - #return ## Everything below disabled for now.. - - ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - #if self.currentItem is not None: - #pev = self.graphicsSceneEvent(ev, self.pev, self.fev) - #self.pev = pev - #self.currentItem.mouseMoveEvent(pev) - - #def paintEvent(self, ev): - #prof = debug.Profiler('GraphicsView.paintEvent (0x%x)' % id(self)) - #QtGui.QGraphicsView.paintEvent(self, ev) - #prof.finish() - - def pixelSize(self): """Return vector with the length and width of one view pixel in scene coordinates""" p0 = Point(0,0) @@ -423,80 +327,7 @@ class GraphicsView(QtGui.QGraphicsView): p11 = tr.map(p1) return Point(p11 - p01) - - #def writeSvg(self, fileName=None): - #if fileName is None: - #self.fileDialog = FileDialog() - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - #if GraphicsView.lastFileDir is not None: - #self.fileDialog.setDirectory(GraphicsView.lastFileDir) - #self.fileDialog.show() - #self.fileDialog.fileSelected.connect(self.writeSvg) - #return - #fileName = str(fileName) - #GraphicsView.lastFileDir = os.path.split(fileName)[0] - #self.svg = QtSvg.QSvgGenerator() - #self.svg.setFileName(fileName) - #self.svg.setSize(self.size()) - #self.svg.setResolution(600) - #painter = QtGui.QPainter(self.svg) - #self.render(painter) - - #def writeImage(self, fileName=None): - #if fileName is None: - #self.fileDialog = FileDialog() - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) ## this is the line that makes the fileDialog not show on mac - #if GraphicsView.lastFileDir is not None: - #self.fileDialog.setDirectory(GraphicsView.lastFileDir) - #self.fileDialog.show() - #self.fileDialog.fileSelected.connect(self.writeImage) - #return - #fileName = str(fileName) - #GraphicsView.lastFileDir = os.path.split(fileName)[0] - #self.png = QtGui.QImage(self.size(), QtGui.QImage.Format_ARGB32) - #painter = QtGui.QPainter(self.png) - #rh = self.renderHints() - #self.setRenderHints(QtGui.QPainter.Antialiasing) - #self.render(painter) - #self.setRenderHints(rh) - #self.png.save(fileName) - - #def writePs(self, fileName=None): - #if fileName is None: - #self.fileDialog = FileDialog() - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - #self.fileDialog.show() - #self.fileDialog.fileSelected.connect(self.writePs) - #return - ##if fileName is None: - ## fileName = str(QtGui.QFileDialog.getSaveFileName()) - #printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) - #printer.setOutputFileName(fileName) - #painter = QtGui.QPainter(printer) - #self.render(painter) - #painter.end() - def dragEnterEvent(self, ev): ev.ignore() ## not sure why, but for some reason this class likes to consume drag events - - - #def getFreehandLine(self): - - ## Wait for click - #self.clearMouse() - #while self.lastButtonReleased != QtCore.Qt.LeftButton: - #QtGui.qApp.sendPostedEvents() - #QtGui.qApp.processEvents() - #time.sleep(0.01) - #fl = vstack(self.mouseTrail) - #return fl - - #def getClick(self): - #fl = self.getFreehandLine() - #return fl[-1] - diff --git a/widgets/ProgressDialog.py b/widgets/ProgressDialog.py index d9901773..0f55e227 100644 --- a/widgets/ProgressDialog.py +++ b/widgets/ProgressDialog.py @@ -14,7 +14,7 @@ class ProgressDialog(QtGui.QProgressDialog): if dlg.wasCanceled(): raise Exception("Processing canceled by user") """ - def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False): + def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False): """ ============== ================================================================ **Arguments:** @@ -25,15 +25,16 @@ class ProgressDialog(QtGui.QProgressDialog): parent wait Length of time (im ms) to wait before displaying dialog busyCursor If True, show busy cursor until dialog finishes + disable If True, the progress dialog will not be displayed + and calls to wasCanceled() will always return False. + If ProgressDialog is entered from a non-gui thread, it will + always be disabled. ============== ================================================================ """ - isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() - if not isGuiThread: - self.disabled = True + self.disabled = disable or (not isGuiThread) + if self.disabled: return - - self.disabled = False noCancel = False if cancelText is None: From d1fdbadd1981ac537a71e991bc8217914d1f7ede Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 21 Jun 2012 22:00:04 -0400 Subject: [PATCH 03/24] Multiprocessing updates / fixes: - ForkedProcess is much more careful with inherited state -- closes file handles, removes atexit and excepthook callbacks - Remote processes copy sys.path from parent - Parallelizer has ProgressDialog support - Many docstring updates - Added some test code for remote GraphicsView rendering --- examples/RemoteGraphicsView.py | 20 +++ examples/multiprocess.py | 30 +---- examples/parallelize.py | 63 +++++++++ multiprocess/__init__.py | 2 + multiprocess/bootstrap.py | 15 +++ multiprocess/parallelizer.py | 165 ++++++++++++++++++----- multiprocess/processes.py | 168 +++++++++++++++++++---- multiprocess/remoteproxy.py | 239 +++++++++++++++++++++++++++------ widgets/RemoteGraphicsView.py | 70 ++++++++++ 9 files changed, 643 insertions(+), 129 deletions(-) create mode 100644 examples/RemoteGraphicsView.py create mode 100644 examples/parallelize.py create mode 100644 multiprocess/bootstrap.py create mode 100644 widgets/RemoteGraphicsView.py diff --git a/examples/RemoteGraphicsView.py b/examples/RemoteGraphicsView.py new file mode 100644 index 00000000..8a3346bf --- /dev/null +++ b/examples/RemoteGraphicsView.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +app = pg.mkQApp() + +v = pg.RemoteGraphicsView() +v.show() + +QtGui = v.pg.QtGui +rect = QtGui.QGraphicsRectItem(0,0,10,10) +rect.setPen(QtGui.QPen(QtGui.QColor(255,255,0))) +v.scene().addItem(rect) + + + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/multiprocess.py b/examples/multiprocess.py index 72337d20..0b2d7ed8 100644 --- a/examples/multiprocess.py +++ b/examples/multiprocess.py @@ -1,38 +1,10 @@ # -*- coding: utf-8 -*- import initExample ## Add path to library (just for examples; you do not need this) - import numpy as np import pyqtgraph.multiprocess as mp -from pyqtgraph.multiprocess.parallelizer import Parallelize #, Parallelizer +import pyqtgraph as pg import time -print "\n=================\nParallelize" -tasks = [1,2,4,8] -results = [None] * len(tasks) -size = 2000000 - -start = time.time() -with Parallelize(enumerate(tasks), results=results, workers=1) as tasker: - for i, x in tasker: - print i, x - tot = 0 - for j in xrange(size): - tot += j * x - results[i] = tot -print results -print "serial:", time.time() - start - -start = time.time() -with Parallelize(enumerate(tasks), results=results) as tasker: - for i, x in tasker: - print i, x - tot = 0 - for j in xrange(size): - tot += j * x - results[i] = tot -print results -print "parallel:", time.time() - start - diff --git a/examples/parallelize.py b/examples/parallelize.py new file mode 100644 index 00000000..d2ba0ce0 --- /dev/null +++ b/examples/parallelize.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) +import numpy as np +import pyqtgraph.multiprocess as mp +import pyqtgraph as pg +import time + +print "\n=================\nParallelize" + +## Do a simple task: +## for x in range(N): +## sum([x*i for i in range(M)]) +## +## We'll do this three times +## - once without Parallelize +## - once with Parallelize, but forced to use a single worker +## - once with Parallelize automatically determining how many workers to use +## + +tasks = range(10) +results = [None] * len(tasks) +results2 = results[:] +results3 = results[:] +size = 2000000 + +pg.mkQApp() + +### Purely serial processing +start = time.time() +with pg.ProgressDialog('processing serially..', maximum=len(tasks)) as dlg: + for i, x in enumerate(tasks): + tot = 0 + for j in xrange(size): + tot += j * x + results[i] = tot + dlg += 1 + if dlg.wasCanceled(): + raise Exception('processing canceled') +print "Serial time: %0.2f" % (time.time() - start) + +### Use parallelize, but force a single worker +### (this simulates the behavior seen on windows, which lacks os.fork) +start = time.time() +with mp.Parallelize(enumerate(tasks), results=results2, workers=1, progressDialog='processing serially (using Parallelizer)..') as tasker: + for i, x in tasker: + tot = 0 + for j in xrange(size): + tot += j * x + tasker.results[i] = tot +print "\nParallel time, 1 worker: %0.2f" % (time.time() - start) +print "Results match serial: ", results2 == results + +### Use parallelize with multiple workers +start = time.time() +with mp.Parallelize(enumerate(tasks), results=results3, progressDialog='processing in parallel..') as tasker: + for i, x in tasker: + tot = 0 + for j in xrange(size): + tot += j * x + tasker.results[i] = tot +print "\nParallel time, %d workers: %0.2f" % (mp.Parallelize.suggestedWorkerCount(), time.time() - start) +print "Results match serial: ", results3 == results + diff --git a/multiprocess/__init__.py b/multiprocess/__init__.py index 190c1b67..7dcf61a5 100644 --- a/multiprocess/__init__.py +++ b/multiprocess/__init__.py @@ -20,3 +20,5 @@ TODO: """ from processes import * +from parallelizer import Parallelize, CanceledError +from remoteproxy import proxy \ No newline at end of file diff --git a/multiprocess/bootstrap.py b/multiprocess/bootstrap.py new file mode 100644 index 00000000..196d34c8 --- /dev/null +++ b/multiprocess/bootstrap.py @@ -0,0 +1,15 @@ +"""For starting up remote processes""" +import sys, pickle + +if __name__ == '__main__': + name, port, authkey, targetStr, path = pickle.load(sys.stdin) + if path is not None: + ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. + while len(sys.path) > 0: + sys.path.pop() + sys.path.extend(path) + #import pyqtgraph + #import pyqtgraph.multiprocess.processes + target = pickle.loads(targetStr) ## unpickling the target should import everything we need + target(name, port, authkey) + sys.exit(0) diff --git a/multiprocess/parallelizer.py b/multiprocess/parallelizer.py index 19819cf2..66f01938 100644 --- a/multiprocess/parallelizer.py +++ b/multiprocess/parallelizer.py @@ -2,6 +2,10 @@ import os, sys, time, multiprocessing from processes import ForkedProcess from remoteproxy import ExitError +class CanceledError(Exception): + """Raised when the progress dialog is canceled during a processing operation.""" + pass + class Parallelize: """ Class for ultra-simple inline parallelization on multi-core CPUs @@ -29,35 +33,78 @@ class Parallelize: print results - The only major caveat is that *result* in the example above must be picklable. + The only major caveat is that *result* in the example above must be picklable, + since it is automatically sent via pipe back to the parent process. """ - def __init__(self, tasks, workers=None, block=True, **kwds): + def __init__(self, tasks, workers=None, block=True, progressDialog=None, **kwds): """ - Args: - tasks - list of objects to be processed (Parallelize will determine how to distribute the tasks) - workers - number of worker processes or None to use number of CPUs in the system - kwds - objects to be shared by proxy with child processes + =============== =================================================================== + Arguments: + tasks list of objects to be processed (Parallelize will determine how to + distribute the tasks) + workers number of worker processes or None to use number of CPUs in the + system + progressDialog optional dict of arguments for ProgressDialog + to update while tasks are processed + kwds objects to be shared by proxy with child processes (they will + appear as attributes of the tasker) + =============== =================================================================== """ - self.block = block + ## Generate progress dialog. + ## Note that we want to avoid letting forked child processes play with progress dialogs.. + self.showProgress = False + if progressDialog is not None: + self.showProgress = True + if isinstance(progressDialog, basestring): + progressDialog = {'labelText': progressDialog} + import pyqtgraph as pg + self.progressDlg = pg.ProgressDialog(**progressDialog) + if workers is None: - workers = multiprocessing.cpu_count() + workers = self.suggestedWorkerCount() if not hasattr(os, 'fork'): workers = 1 self.workers = workers self.tasks = list(tasks) - self.kwds = kwds + self.kwds = kwds.copy() + self.kwds['_taskStarted'] = self._taskStarted def __enter__(self): self.proc = None - workers = self.workers - if workers == 1: - return Tasker(None, self.tasks, self.kwds) - + if self.workers == 1: + return self.runSerial() + else: + return self.runParallel() + + def __exit__(self, *exc_info): + + if self.proc is not None: ## worker + try: + if exc_info[0] is not None: + sys.excepthook(*exc_info) + finally: + #print os.getpid(), 'exit' + os._exit(0) + + else: ## parent + if self.showProgress: + self.progressDlg.__exit__(None, None, None) + + def runSerial(self): + if self.showProgress: + self.progressDlg.__enter__() + self.progressDlg.setMaximum(len(self.tasks)) + self.progress = {os.getpid(): []} + return Tasker(None, self.tasks, self.kwds) + + + def runParallel(self): self.childs = [] ## break up tasks into one set per worker + workers = self.workers chunks = [[] for i in xrange(workers)] i = 0 for i in range(len(self.tasks)): @@ -72,30 +119,74 @@ class Parallelize: else: self.childs.append(proc) - ## process events from workers until all have exited. - activeChilds = self.childs[:] - while len(activeChilds) > 0: - for ch in activeChilds: + ## Keep track of the progress of each worker independently. + self.progress = {ch.childPid: [] for ch in self.childs} + ## for each child process, self.progress[pid] is a list + ## of task indexes. The last index is the task currently being + ## processed; all others are finished. + + + try: + if self.showProgress: + self.progressDlg.__enter__() + self.progressDlg.setMaximum(len(self.tasks)) + ## process events from workers until all have exited. + + activeChilds = self.childs[:] + pollInterval = 0.01 + while len(activeChilds) > 0: + waitingChildren = 0 rem = [] - try: - ch.processRequests() - except ExitError: - rem.append(ch) - for ch in rem: - activeChilds.remove(ch) - time.sleep(0.1) - + for ch in activeChilds: + try: + n = ch.processRequests() + if n > 0: + waitingChildren += 1 + except ExitError: + #print ch.childPid, 'process finished' + rem.append(ch) + if self.showProgress: + self.progressDlg += 1 + #print "remove:", [ch.childPid for ch in rem] + for ch in rem: + activeChilds.remove(ch) + os.waitpid(ch.childPid, 0) + #print [ch.childPid for ch in activeChilds] + + if self.showProgress and self.progressDlg.wasCanceled(): + for ch in activeChilds: + ch.kill() + raise CanceledError() + + ## adjust polling interval--prefer to get exactly 1 event per poll cycle. + if waitingChildren > 1: + pollInterval *= 0.7 + elif waitingChildren == 0: + pollInterval /= 0.7 + pollInterval = max(min(pollInterval, 0.5), 0.0005) ## but keep it within reasonable limits + + time.sleep(pollInterval) + finally: + if self.showProgress: + self.progressDlg.__exit__(None, None, None) return [] ## no tasks for parent process. - - def __exit__(self, *exc_info): - if exc_info[0] is not None: - sys.excepthook(*exc_info) - if self.proc is not None: - os._exit(0) - def wait(self): - ## wait for all child processes to finish - pass + + @staticmethod + def suggestedWorkerCount(): + return multiprocessing.cpu_count() ## is this really the best option? + + def _taskStarted(self, pid, i, **kwds): + ## called remotely by tasker to indicate it has started working on task i + #print pid, 'reported starting task', i + if self.showProgress: + if len(self.progress[pid]) > 0: + self.progressDlg += 1 + if pid == os.getpid(): ## single-worker process + if self.progressDlg.wasCanceled(): + raise CanceledError() + self.progress[pid].append(i) + class Tasker: def __init__(self, proc, tasks, kwds): @@ -106,9 +197,13 @@ class Tasker: def __iter__(self): ## we could fix this up such that tasks are retrieved from the parent process one at a time.. - for task in self.tasks: + for i, task in enumerate(self.tasks): + self.index = i + #print os.getpid(), 'starting task', i + self._taskStarted(os.getpid(), i, _callSync='off') yield task if self.proc is not None: + #print os.getpid(), 'no more tasks' self.proc.close() diff --git a/multiprocess/processes.py b/multiprocess/processes.py index 1b4f8106..c4356a80 100644 --- a/multiprocess/processes.py +++ b/multiprocess/processes.py @@ -1,10 +1,51 @@ from remoteproxy import RemoteEventHandler, ExitError, NoResultError, LocalObjectProxy, ObjectProxy -import subprocess, atexit, os, sys, time, random, socket +import subprocess, atexit, os, sys, time, random, socket, signal import cPickle as pickle import multiprocessing.connection +__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ExitError', 'NoResultError'] + class Process(RemoteEventHandler): - def __init__(self, name=None, target=None): + """ + Bases: RemoteEventHandler + + This class is used to spawn and control a new python interpreter. + It uses subprocess.Popen to start the new process and communicates with it + using multiprocessing.Connection objects over a network socket. + + By default, the remote process will immediately enter an event-processing + loop that carries out requests send from the parent process. + + Remote control works mainly through proxy objects:: + + proc = Process() ## starts process, returns handle + rsys = proc._import('sys') ## asks remote process to import 'sys', returns + ## a proxy which references the imported module + rsys.stdout.write('hello\n') ## This message will be printed from the remote + ## process. Proxy objects can usually be used + ## exactly as regular objects are. + proc.close() ## Request the remote process shut down + + Requests made via proxy objects may be synchronous or asynchronous and may + return objects either by proxy or by value (if they are picklable). See + ProxyObject for more information. + """ + + def __init__(self, name=None, target=None, copySysPath=True): + """ + ============ ============================================================= + Arguments: + name Optional name for this process used when printing messages + from the remote process. + target Optional function to call after starting remote process. + By default, this is startEventLoop(), which causes the remote + process to process requests from the parent process until it + is asked to quit. If you wish to specify a different target, + it must be picklable (bound methods are not). + copySysPath If true, copy the contents of sys.path to the remote process + ============ ============================================================= + + """ if target is None: target = startEventLoop if name is None: @@ -25,8 +66,12 @@ class Process(RemoteEventHandler): port += 1 ## start remote process, instruct it to run target function - self.proc = subprocess.Popen((sys.executable, __file__, 'remote'), stdin=subprocess.PIPE) - pickle.dump((name+'_child', port, authkey, target), self.proc.stdin) + sysPath = sys.path if copySysPath else None + bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) + self.proc = subprocess.Popen((sys.executable, bootstrap), stdin=subprocess.PIPE) + targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to + ## set its sys.path properly before unpickling the target + pickle.dump((name+'_child', port, authkey, targetStr, sysPath), self.proc.stdin) self.proc.stdin.close() ## open connection for remote process @@ -60,16 +105,29 @@ def startEventLoop(name, port, authkey): class ForkedProcess(RemoteEventHandler): """ ForkedProcess is a substitute for Process that uses os.fork() to generate a new process. - This is much faster than starting a completely new interpreter, but carries some caveats - and limitations: - - open file handles are shared with the parent process, which is potentially dangerous - - it is not possible to have a QApplication in both parent and child process - (unless both QApplications are created _after_ the call to fork()) - - generally not thread-safe. Also, threads are not copied by fork(); the new process - will have only one thread that starts wherever fork() was called in the parent process. - - forked processes are unceremoniously terminated when join() is called; they are not - given any opportunity to clean up. (This prevents them calling any cleanup code that - was only intended to be used by the parent process) + This is much faster than starting a completely new interpreter and child processes + automatically have a copy of the entire program state from before the fork. This + makes it an appealing approach when parallelizing expensive computations. (see + also Parallelizer) + + However, fork() comes with some caveats and limitations: + + - fork() is not available on Windows. + - It is not possible to have a QApplication in both parent and child process + (unless both QApplications are created _after_ the call to fork()) + Attempts by the forked process to access Qt GUI elements created by the parent + will most likely cause the child to crash. + - Likewise, database connections are unlikely to function correctly in a forked child. + - Threads are not copied by fork(); the new process + will have only one thread that starts wherever fork() was called in the parent process. + - Forked processes are unceremoniously terminated when join() is called; they are not + given any opportunity to clean up. (This prevents them calling any cleanup code that + was only intended to be used by the parent process) + - Normally when fork()ing, open file handles are shared with the parent process, + which is potentially dangerous. ForkedProcess is careful to close all file handles + that are not explicitly needed--stdout, stderr, and a single pipe to the parent + process. + """ def __init__(self, name=None, target=0, preProxy=None): @@ -101,16 +159,46 @@ class ForkedProcess(RemoteEventHandler): pid = os.fork() if pid == 0: self.isParent = False + ## We are now in the forked process; need to be extra careful what we touch while here. + ## - no reading/writing file handles/sockets owned by parent process (stdout is ok) + ## - don't touch QtGui or QApplication at all; these are landmines. + ## - don't let the process call exit handlers + ## - + + ## close all file handles we do not want shared with parent conn.close() sys.stdin.close() ## otherwise we screw with interactive prompts. + fid = remoteConn.fileno() + os.closerange(3, fid) + os.closerange(fid+1, 4096) ## just guessing on the maximum descriptor count.. + + ## Override any custom exception hooks + def excepthook(*args): + import traceback + traceback.print_exception(*args) + sys.excepthook = excepthook + + ## Make it harder to access QApplication instance + if 'PyQt4.QtGui' in sys.modules: + sys.modules['PyQt4.QtGui'].QApplication = None + sys.modules.pop('PyQt4.QtGui', None) + sys.modules.pop('PyQt4.QtCore', None) + + ## sabotage atexit callbacks + atexit._exithandlers = [] + atexit.register(lambda: os._exit(0)) + + RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid()) - if target is not None: - target() - + ppid = os.getppid() self.forkedProxies = {} for name, proxyId in proxyIDs.iteritems(): self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name])) + + if target is not None: + target() + else: self.isParent = True self.childPid = pid @@ -127,10 +215,11 @@ class ForkedProcess(RemoteEventHandler): self.processRequests() # exception raised when the loop should exit time.sleep(0.01) except ExitError: - sys.exit(0) + break except: print "Error occurred in forked event loop:" sys.excepthook(*sys.exc_info()) + sys.exit(0) def join(self, timeout=10): if self.hasJoined: @@ -138,10 +227,19 @@ class ForkedProcess(RemoteEventHandler): #os.kill(pid, 9) try: self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation. + os.waitpid(self.childPid, 0) except IOError: ## probably remote process has already quit pass self.hasJoined = True + def kill(self): + """Immediately kill the forked remote process. + This is generally safe because forked processes are already + expected to _avoid_ any cleanup at exit.""" + os.kill(self.childPid, signal.SIGKILL) + self.hasJoined = True + + ##Special set of subclasses that implement a Qt event loop instead. @@ -165,8 +263,33 @@ class RemoteQtEventHandler(RemoteEventHandler): #raise class QtProcess(Process): - def __init__(self, name=None): - Process.__init__(self, name, target=startQtEventLoop) + """ + QtProcess is essentially the same as Process, with two major differences: + + - The remote process starts by running startQtEventLoop() which creates a + QApplication in the remote process and uses a QTimer to trigger + remote event processing. This allows the remote process to have its own + GUI. + - A QTimer is also started on the parent process which polls for requests + from the child process. This allows Qt signals emitted within the child + process to invoke slots on the parent process and vice-versa. + + Example:: + + proc = QtProcess() + rQtGui = proc._import('PyQt4.QtGui') + btn = rQtGui.QPushButton('button on child process') + btn.show() + + def slot(): + print 'slot invoked on parent process' + btn.clicked.connect(proxy(slot)) # be sure to send a proxy of the slot + """ + + def __init__(self, **kwds): + if 'target' not in kwds: + kwds['target'] = startQtEventLoop + Process.__init__(self, **kwds) self.startEventTimer() def startEventTimer(self): @@ -201,8 +324,3 @@ def startQtEventLoop(name, port, authkey): app.exec_() -if __name__ == '__main__': - if len(sys.argv) == 2 and sys.argv[1] == 'remote': ## module has been invoked as script in new python interpreter. - name, port, authkey, target = pickle.load(sys.stdin) - target(name, port, authkey) - sys.exit(0) diff --git a/multiprocess/remoteproxy.py b/multiprocess/remoteproxy.py index f70d88d7..23e2fa8b 100644 --- a/multiprocess/remoteproxy.py +++ b/multiprocess/remoteproxy.py @@ -9,7 +9,26 @@ class NoResultError(Exception): class RemoteEventHandler(object): + """ + This class handles communication between two processes. One instance is present on + each process and listens for communication from the other process. This enables + (amongst other things) ObjectProxy instances to look up their attributes and call + their methods. + This class is responsible for carrying out actions on behalf of the remote process. + Each instance holds one end of a Connection which allows python + objects to be passed between processes. + + For the most common operations, see _import(), close(), and transfer() + + To handle and respond to incoming requests, RemoteEventHandler requires that its + processRequests method is called repeatedly (this is usually handled by the Process + classes defined in multiprocess.processes). + + + + + """ handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process ## an object proxy belongs to @@ -55,19 +74,25 @@ class RemoteEventHandler(object): def processRequests(self): """Process all pending requests from the pipe, return - after no more events are immediately available. (non-blocking)""" + after no more events are immediately available. (non-blocking) + Returns the number of events processed. + """ if self.exited: raise ExitError() + numProcessed = 0 while self.conn.poll(): try: self.handleRequest() + numProcessed += 1 except ExitError: self.exited = True raise except: print "Error in process %s" % self.name sys.excepthook(*sys.exc_info()) + + return numProcessed def handleRequest(self): """Handle a single request from the remote process. @@ -175,6 +200,7 @@ class RemoteEventHandler(object): self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result)) def replyError(self, reqId, *exc): + print "error:", self.name, reqId, exc[1] excStr = traceback.format_exception(*exc) try: self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr)) @@ -282,7 +308,9 @@ class RemoteEventHandler(object): try: optStr = pickle.dumps(opts) except: - print "Error pickling:", opts + print "==== Error pickling this object: ====" + print opts + print "=======================================" raise request = (request, reqId, optStr) @@ -381,8 +409,8 @@ class RemoteEventHandler(object): def transfer(self, obj, **kwds): """ - Transfer an object to the remote host (the object must be picklable) and return - a proxy for the new remote object. + Transfer an object by value to the remote host (the object must be picklable) + and return a proxy for the new remote object. """ return self.send(request='transfer', opts=dict(obj=obj), **kwds) @@ -395,7 +423,12 @@ class RemoteEventHandler(object): class Request: - ## used internally for tracking asynchronous requests and returning results + """ + Request objects are returned when calling an ObjectProxy in asynchronous mode + or if a synchronous call has timed out. Use hasResult() to ask whether + the result of the call has been returned yet. Use result() to get + the returned value. + """ def __init__(self, process, reqId, description=None, timeout=10): self.proc = process self.description = description @@ -405,10 +438,13 @@ class Request: self.timeout = timeout def result(self, block=True, timeout=None): - """Return the result for this request. + """ + Return the result for this request. + If block is True, wait until the result has arrived or *timeout* seconds passes. - If the timeout is reached, raise an exception. (use timeout=None to disable) - If block is False, raises an exception if the result has not arrived yet.""" + If the timeout is reached, raise NoResultError. (use timeout=None to disable) + If block is False, raise NoResultError immediately if the result has not arrived yet. + """ if self.gotResult: return self._result @@ -434,16 +470,24 @@ class Request: def hasResult(self): """Returns True if the result for this request has arrived.""" try: - #print "check result", self.description self.result(block=False) except NoResultError: - #print " -> not yet" pass return self.gotResult class LocalObjectProxy(object): - """Used for wrapping local objects to ensure that they are send by proxy to a remote host.""" + """ + Used for wrapping local objects to ensure that they are send by proxy to a remote host. + Note that 'proxy' is just a shorter alias for LocalObjectProxy. + + For example:: + + data = [1,2,3,4,5] + remotePlot.plot(data) ## by default, lists are pickled and sent by value + remotePlot.plot(proxy(data)) ## force the object to be sent by proxy + + """ nextProxyId = 0 proxiedObjects = {} ## maps {proxyId: object} @@ -501,7 +545,44 @@ class ObjectProxy(object): attributes on existing proxy objects. For the most part, this object can be used exactly as if it - were a local object. + were a local object:: + + rsys = proc._import('sys') # returns proxy to sys module on remote process + rsys.stdout # proxy to remote sys.stdout + rsys.stdout.write # proxy to remote sys.stdout.write + rsys.stdout.write('hello') # calls sys.stdout.write('hello') on remote machine + # and returns the result (None) + + When calling a proxy to a remote function, the call can be made synchronous + (result of call is returned immediately), asynchronous (result is returned later), + or return can be disabled entirely:: + + ros = proc._import('os') + + ## synchronous call; result is returned immediately + pid = ros.getpid() + + ## asynchronous call + request = ros.getpid(_callSync='async') + while not request.hasResult(): + time.sleep(0.01) + pid = request.result() + + ## disable return when we know it isn't needed + rsys.stdout.write('hello', _callSync='off') + + Additionally, values returned from a remote function call are automatically + returned either by value (must be picklable) or by proxy. + This behavior can be forced:: + + rnp = proc._import('numpy') + arrProxy = rnp.array([1,2,3,4], _returnType='proxy') + arrValue = rnp.array([1,2,3,4], _returnType='value') + + The default callSync and returnType behaviors (as well as others) can be set + for each proxy individually using ObjectProxy._setProxyOptions() or globally using + proc.setProxyOptions(). + """ def __init__(self, processId, proxyId, typeStr='', parent=None): object.__init__(self) @@ -574,6 +655,13 @@ class ObjectProxy(object): """ self._proxyOptions.update(kwds) + def _getValue(self): + """ + Return the value of the proxied object + (the remote object must be picklable) + """ + return self._handler.getObjValue(self) + def _getProxyOption(self, opt): val = self._proxyOptions[opt] if val is None: @@ -591,20 +679,31 @@ class ObjectProxy(object): return "" % (self._processId, self._proxyId, self._typeStr) - def __getattr__(self, attr): - #if '_processId' not in self.__dict__: - #raise Exception("ObjectProxy has no processId") - #proc = Process._processes[self._processId] - deferred = self._getProxyOption('deferGetattr') - if deferred is True: + def __getattr__(self, attr, **kwds): + """ + Calls __getattr__ on the remote object and returns the attribute + by value or by proxy depending on the options set (see + ObjectProxy._setProxyOptions and RemoteEventHandler.setProxyOptions) + + If the option 'deferGetattr' is True for this proxy, then a new proxy object + is returned _without_ asking the remote object whether the named attribute exists. + This can save time when making multiple chained attribute requests, + but may also defer a possible AttributeError until later, making + them more difficult to debug. + """ + opts = self._getProxyOptions() + for k in opts: + if '_'+k in kwds: + opts[k] = kwds.pop('_'+k) + if opts['deferGetattr'] is True: return self._deferredAttr(attr) else: - opts = self._getProxyOptions() + #opts = self._getProxyOptions() return self._handler.getObjAttr(self, attr, **opts) - + def _deferredAttr(self, attr): return DeferredObjectProxy(self, attr) - + def __call__(self, *args, **kwds): """ Attempts to call the proxied object from the remote process. @@ -613,44 +712,34 @@ class ObjectProxy(object): _callSync 'off', 'sync', or 'async' _returnType 'value', 'proxy', or 'auto' + If the remote call raises an exception on the remote process, + it will be re-raised on the local process. + """ - #opts = {} - #callSync = kwds.pop('_callSync', self.) - #if callSync is not None: - #opts['callSync'] = callSync - #returnType = kwds.pop('_returnType', self._defaultReturnValue) - #if returnType is not None: - #opts['returnType'] = returnType opts = self._getProxyOptions() for k in opts: if '_'+k in kwds: opts[k] = kwds.pop('_'+k) - #print "call", opts return self._handler.callObj(obj=self, args=args, kwds=kwds, **opts) - def _getValue(self): - ## this just gives us an easy way to change the behavior of the special methods - #proc = Process._processes[self._processId] - return self._handler.getObjValue(self) - ## Explicitly proxy special methods. Is there a better way to do this?? def _getSpecialAttr(self, attr): - #return self.__getattr__(attr) + ## this just gives us an easy way to change the behavior of the special methods return self._deferredAttr(attr) def __getitem__(self, *args): return self._getSpecialAttr('__getitem__')(*args) def __setitem__(self, *args): - return self._getSpecialAttr('__setitem__')(*args) + return self._getSpecialAttr('__setitem__')(*args, _callSync='off') def __setattr__(self, *args): - return self._getSpecialAttr('__setattr__')(*args) + return self._getSpecialAttr('__setattr__')(*args, _callSync='off') def __str__(self, *args): - return self._getSpecialAttr('__str__')(*args, _returnType=True) + return self._getSpecialAttr('__str__')(*args, _returnType='value') def __len__(self, *args): return self._getSpecialAttr('__len__')(*args) @@ -670,6 +759,21 @@ class ObjectProxy(object): def __pow__(self, *args): return self._getSpecialAttr('__pow__')(*args) + def __iadd__(self, *args): + return self._getSpecialAttr('__iadd__')(*args, _callSync='off') + + def __isub__(self, *args): + return self._getSpecialAttr('__isub__')(*args, _callSync='off') + + def __idiv__(self, *args): + return self._getSpecialAttr('__idiv__')(*args, _callSync='off') + + def __imul__(self, *args): + return self._getSpecialAttr('__imul__')(*args, _callSync='off') + + def __ipow__(self, *args): + return self._getSpecialAttr('__ipow__')(*args, _callSync='off') + def __rshift__(self, *args): return self._getSpecialAttr('__rshift__')(*args) @@ -679,6 +783,15 @@ class ObjectProxy(object): def __floordiv__(self, *args): return self._getSpecialAttr('__pow__')(*args) + def __irshift__(self, *args): + return self._getSpecialAttr('__rshift__')(*args, _callSync='off') + + def __ilshift__(self, *args): + return self._getSpecialAttr('__lshift__')(*args, _callSync='off') + + def __ifloordiv__(self, *args): + return self._getSpecialAttr('__pow__')(*args, _callSync='off') + def __eq__(self, *args): return self._getSpecialAttr('__eq__')(*args) @@ -704,7 +817,16 @@ class ObjectProxy(object): return self._getSpecialAttr('__or__')(*args) def __xor__(self, *args): - return self._getSpecialAttr('__or__')(*args) + return self._getSpecialAttr('__xor__')(*args) + + def __iand__(self, *args): + return self._getSpecialAttr('__iand__')(*args, _callSync='off') + + def __ior__(self, *args): + return self._getSpecialAttr('__ior__')(*args, _callSync='off') + + def __ixor__(self, *args): + return self._getSpecialAttr('__ixor__')(*args, _callSync='off') def __mod__(self, *args): return self._getSpecialAttr('__mod__')(*args) @@ -746,6 +868,37 @@ class ObjectProxy(object): return self._getSpecialAttr('__rmod__')(*args) class DeferredObjectProxy(ObjectProxy): + """ + This class represents an attribute (or sub-attribute) of a proxied object. + It is used to speed up attribute requests. Take the following scenario:: + + rsys = proc._import('sys') + rsys.stdout.write('hello') + + For this simple example, a total of 4 synchronous requests are made to + the remote process: + + 1) import sys + 2) getattr(sys, 'stdout') + 3) getattr(stdout, 'write') + 4) write('hello') + + This takes a lot longer than running the equivalent code locally. To + speed things up, we can 'defer' the two attribute lookups so they are + only carried out when neccessary:: + + rsys = proc._import('sys') + rsys._setProxyOptions(deferGetattr=True) + rsys.stdout.write('hello') + + This example only makes two requests to the remote process; the two + attribute lookups immediately return DeferredObjectProxy instances + immediately without contacting the remote process. When the call + to write() is made, all attribute requests are processed at the same time. + + Note that if the attributes requested do not exist on the remote object, + making the call to write() will raise an AttributeError. + """ def __init__(self, parentProxy, attribute): ## can't set attributes directly because setattr is overridden. for k in ['_processId', '_typeStr', '_proxyId', '_handler']: @@ -756,4 +909,10 @@ class DeferredObjectProxy(ObjectProxy): def __repr__(self): return ObjectProxy.__repr__(self) + '.' + '.'.join(self._attributes) - \ No newline at end of file + + def _undefer(self): + """ + Return a non-deferred ObjectProxy referencing the same object + """ + return self._parent.__getattr__(self._attributes[-1], _deferGetattr=False) + diff --git a/widgets/RemoteGraphicsView.py b/widgets/RemoteGraphicsView.py new file mode 100644 index 00000000..6e96a2b0 --- /dev/null +++ b/widgets/RemoteGraphicsView.py @@ -0,0 +1,70 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.multiprocess as mp +import pyqtgraph as pg +import numpy as np +import ctypes, os + +__all__ = ['RemoteGraphicsView'] + +class RemoteGraphicsView(QtGui.QWidget): + def __init__(self, parent=None, *args, **kwds): + self._img = None + self._imgReq = None + QtGui.QWidget.__init__(self) + self._proc = mp.QtProcess() + self.pg = self._proc._import('pyqtgraph') + rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') + self._view = rpgRemote.Renderer(*args, **kwds) + self._view._setProxyOptions(deferGetattr=True) + self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) + + def scene(self): + return self._view.scene() + + def resizeEvent(self, ev): + ret = QtGui.QWidget.resizeEvent(self, ev) + self._view.resize(self.size(), _callSync='off') + return ret + + def remoteSceneChanged(self, data): + self._img = pg.makeQImage(data, alpha=True) + self.update() + + def paintEvent(self, ev): + if self._img is None: + return + p = QtGui.QPainter(self) + p.drawImage(self.rect(), self._img, self.rect()) + p.end() + +class Renderer(pg.GraphicsView): + + sceneRendered = QtCore.Signal(object) + + def __init__(self, *args, **kwds): + pg.GraphicsView.__init__(self, *args, **kwds) + self.scene().changed.connect(self.update) + self.img = None + self.renderTimer = QtCore.QTimer() + self.renderTimer.timeout.connect(self.renderView) + self.renderTimer.start(16) + + def update(self): + self.img = None + return pg.GraphicsView.update(self) + + def resize(self, size): + pg.GraphicsView.resize(self, size) + self.update() + + def renderView(self): + if self.img is None: + self.img = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self.img.fill(0xffffffff) + p = QtGui.QPainter(self.img) + self.render(p, self.viewRect(), self.rect()) + p.end() + self.data = np.fromstring(ctypes.string_at(int(self.img.bits()), self.img.byteCount()), dtype=np.ubyte).reshape(self.height(), self.width(),4).transpose(1,0,2) + #self.data = ctypes.string_at(int(self.img.bits()), self.img.byteCount()) + self.sceneRendered.emit(self.data) + From 5b6f77be58d79a2c18a125a36362939e777b00d9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 21 Jun 2012 22:02:19 -0400 Subject: [PATCH 04/24] Flowchart fixes: better job managing / saving / restoring 'muli' state of terminals --- flowchart/Flowchart.py | 12 ++++--- flowchart/Node.py | 60 ++++++++-------------------------- flowchart/Terminal.py | 15 +++++++-- flowchart/library/Data.py | 16 ++++----- flowchart/library/functions.py | 2 +- 5 files changed, 40 insertions(+), 65 deletions(-) diff --git a/flowchart/Flowchart.py b/flowchart/Flowchart.py index 18f1f211..35f848fd 100644 --- a/flowchart/Flowchart.py +++ b/flowchart/Flowchart.py @@ -71,7 +71,8 @@ class Flowchart(Node): if terminals is None: terminals = {} self.filePath = filePath - Node.__init__(self, name) ## create node without terminals; we'll add these later + Node.__init__(self, name, allowAddInput=True, allowAddOutput=True) ## create node without terminals; we'll add these later + self.inputWasSet = False ## flag allows detection of changes in the absence of input change. self._nodes = {} @@ -457,7 +458,7 @@ class Flowchart(Node): state = Node.saveState(self) state['nodes'] = [] state['connects'] = [] - state['terminals'] = self.saveTerminals() + #state['terminals'] = self.saveTerminals() for name, node in self._nodes.items(): cls = type(node) @@ -470,7 +471,7 @@ class Flowchart(Node): conn = self.listConnections() for a, b in conn: state['connects'].append((a.node().name(), a.name(), b.node().name(), b.name())) - + state['inputNode'] = self.inputNode.saveState() state['outputNode'] = self.outputNode.saveState() @@ -486,7 +487,8 @@ class Flowchart(Node): nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0])) for n in nodes: if n['name'] in self._nodes: - self._nodes[n['name']].moveBy(*n['pos']) + #self._nodes[n['name']].graphicsItem().moveBy(*n['pos']) + self._nodes[n['name']].restoreState(n['state']) continue try: node = self.createNode(n['class'], name=n['name']) @@ -498,7 +500,7 @@ class Flowchart(Node): self.inputNode.restoreState(state.get('inputNode', {})) self.outputNode.restoreState(state.get('outputNode', {})) - self.restoreTerminals(state['terminals']) + #self.restoreTerminals(state['terminals']) for n1, t1, n2, t2 in state['connects']: try: self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2]) diff --git a/flowchart/Node.py b/flowchart/Node.py index e3dce61d..b79941db 100644 --- a/flowchart/Node.py +++ b/flowchart/Node.py @@ -1,16 +1,13 @@ # -*- coding: utf-8 -*- from pyqtgraph.Qt import QtCore, QtGui -#from PySide import QtCore, QtGui from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject import pyqtgraph.functions as fn from .Terminal import * from collections import OrderedDict from pyqtgraph.debug import * import numpy as np -#from pyqtgraph.ObjectWorkaround import QObjectWorkaround from .eq import * -#TETRACYCLINE = True def strDict(d): return dict([(str(k), v) for k, v in d.items()]) @@ -32,8 +29,8 @@ class Node(QtCore.QObject): self.bypassButton = None ## this will be set by the flowchart ctrl widget.. self._graphicsItem = None self.terminals = OrderedDict() - self._inputs = {} - self._outputs = {} + self._inputs = OrderedDict() + self._outputs = OrderedDict() self._allowAddInput = allowAddInput ## flags to allow the user to add/remove terminals self._allowAddOutput = allowAddOutput self._allowRemove = allowRemove @@ -85,24 +82,16 @@ class Node(QtCore.QObject): def terminalRenamed(self, term, oldName): """Called after a terminal has been renamed""" newName = term.name() - #print "node", self, "handling rename..", newName, oldName for d in [self.terminals, self._inputs, self._outputs]: if oldName not in d: continue - #print " got one" d[newName] = d[oldName] del d[oldName] self.graphicsItem().updateTerminals() - #self.emit(QtCore.SIGNAL('terminalRenamed'), term, oldName) self.sigTerminalRenamed.emit(term, oldName) def addTerminal(self, name, **opts): - #print "Node.addTerminal called. name:", name, "opts:", opts - #global TETRACYCLINE - #print "TETRACYCLINE: ", TETRACYCLINE - #if TETRACYCLINE: - #print "Creating Terminal..." name = self.nextTerminalName(name) term = Terminal(self, name, **opts) self.terminals[name] = term @@ -278,12 +267,20 @@ class Node(QtCore.QObject): def saveState(self): pos = self.graphicsItem().pos() - return {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()} + state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()} + termsEditable = self._allowAddInput | self._allowAddOutput + for term in self._inputs.values() + self._outputs.values(): + termsEditable |= term._renamable | term._removable | term._multiable + if termsEditable: + state['terminals'] = self.saveTerminals() + return state def restoreState(self, state): pos = state.get('pos', (0,0)) self.graphicsItem().setPos(*pos) self.bypass(state.get('bypass', False)) + if 'terminals' in state: + self.restoreTerminals(state['terminals']) def saveTerminals(self): terms = OrderedDict() @@ -309,8 +306,8 @@ class Node(QtCore.QObject): for t in self.terminals.values(): t.close() self.terminals = OrderedDict() - self._inputs = {} - self._outputs = {} + self._inputs = OrderedDict() + self._outputs = OrderedDict() def close(self): """Cleans up after the node--removes terminals, graphicsItem, widget""" @@ -493,10 +490,6 @@ class NodeGraphicsItem(GraphicsObject): self.hovered = False self.update() - #def mouseReleaseEvent(self, ev): - #ret = QtGui.QGraphicsItem.mouseReleaseEvent(self, ev) - #return ret - def keyPressEvent(self, ev): if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: ev.accept() @@ -513,13 +506,8 @@ class NodeGraphicsItem(GraphicsObject): return GraphicsObject.itemChange(self, change, val) - #def contextMenuEvent(self, ev): - #ev.accept() - #self.menu.popup(ev.screenPos()) - def getMenu(self): return self.menu - def getContextMenus(self, event): return [self.menu] @@ -548,25 +536,3 @@ class NodeGraphicsItem(GraphicsObject): def addOutputFromMenu(self): ## called when add output is clicked in context menu self.node.addOutput(renamable=True, removable=True, multiable=False) - #def menuTriggered(self, action): - ##print "node.menuTriggered called. action:", action - #act = str(action.text()) - #if act == "Add input": - #self.node.addInput() - #self.updateActionMenu() - #elif act == "Add output": - #self.node.addOutput() - #self.updateActionMenu() - #elif act == "Remove node": - #self.node.close() - #else: ## only other option is to remove a terminal - #self.node.removeTerminal(act) - #self.terminalMenu.removeAction(action) - - #def updateActionMenu(self): - #for t in self.node.terminals: - #if t not in [str(a.text()) for a in self.terminalMenu.actions()]: - #self.terminalMenu.addAction(t) - #for a in self.terminalMenu.actions(): - #if str(a.text()) not in self.node.terminals: - #self.terminalMenu.removeAction(a) diff --git a/flowchart/Terminal.py b/flowchart/Terminal.py index 77f5b72c..f5181b08 100644 --- a/flowchart/Terminal.py +++ b/flowchart/Terminal.py @@ -45,7 +45,7 @@ class Terminal: self._value = {} ## dictionary of terminal:value pairs. else: self._value = None - + self.valueOk = None self.recolor() @@ -70,6 +70,8 @@ class Terminal: return self._value = val else: + if not isinstance(self._value, dict): + self._value = {} if val is not None: self._value.update(val) @@ -132,9 +134,14 @@ class Terminal: def isMultiValue(self): return self._multi - def setMultiValue(self, b): + def setMultiValue(self, multi): """Set whether this is a multi-value terminal.""" - self._multi = b + self._multi = multi + if not multi and len(self.inputTerminals()) > 1: + self.disconnectAll() + + for term in self.inputTerminals(): + self.inputChanged(term) def isOutput(self): return self._io == 'out' @@ -407,6 +414,8 @@ class TerminalGraphicsItem(GraphicsObject): multiAct = QtGui.QAction("Multi-value", self.menu) multiAct.setCheckable(True) multiAct.setChecked(self.term.isMultiValue()) + multiAct.setEnabled(self.term.isMultiable()) + multiAct.triggered.connect(self.toggleMulti) self.menu.addAction(multiAct) self.menu.multiAct = multiAct diff --git a/flowchart/library/Data.py b/flowchart/library/Data.py index 289cbba6..a7e08f8d 100644 --- a/flowchart/library/Data.py +++ b/flowchart/library/Data.py @@ -240,7 +240,7 @@ class EvalNode(Node): def saveState(self): state = Node.saveState(self) state['text'] = str(self.text.toPlainText()) - state['terminals'] = self.saveTerminals() + #state['terminals'] = self.saveTerminals() return state def restoreState(self, state): @@ -282,7 +282,7 @@ class ColumnJoinNode(Node): def addInput(self): #print "ColumnJoinNode.addInput called." - term = Node.addInput(self, 'input', renamable=True) + term = Node.addInput(self, 'input', renamable=True, removable=True) #print "Node.addInput returned. term:", term item = QtGui.QTreeWidgetItem([term.name()]) item.term = term @@ -322,16 +322,14 @@ class ColumnJoinNode(Node): def restoreState(self, state): Node.restoreState(self, state) - inputs = [inp.name() for inp in self.inputs()] + inputs = self.inputs() + order = [name for name in state['order'] if name in inputs] for name in inputs: - if name not in state['order']: - self.removeTerminal(name) - for name in state['order']: - if name not in inputs: - Node.addInput(self, name, renamable=True) + if name not in order: + order.append(name) self.tree.clear() - for name in state['order']: + for name in order: term = self[name] item = QtGui.QTreeWidgetItem([name]) item.term = term diff --git a/flowchart/library/functions.py b/flowchart/library/functions.py index 2b4e3881..646210e8 100644 --- a/flowchart/library/functions.py +++ b/flowchart/library/functions.py @@ -153,7 +153,7 @@ def denoise(data, radius=2, threshold=4): r2 = radius * 2 d1 = data.view(np.ndarray) - d2 = data[radius:] - data[:-radius] #a derivative + d2 = d1[radius:] - d1[:-radius] #a derivative #d3 = data[r2:] - data[:-r2] #d4 = d2 - d3 stdev = d2.std() From 96202aed3eea723ef754d7bf6643bae8817ce306 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 22 Jun 2012 22:10:37 -0400 Subject: [PATCH 05/24] Basic functionality in RemoteGraphicsView is working. --- examples/RemoteGraphicsView.py | 7 +- multiprocess/remoteproxy.py | 18 +++-- widgets/RemoteGraphicsView.py | 118 ++++++++++++++++++++++++++++++--- 3 files changed, 126 insertions(+), 17 deletions(-) diff --git a/examples/RemoteGraphicsView.py b/examples/RemoteGraphicsView.py index 8a3346bf..1ca5ad38 100644 --- a/examples/RemoteGraphicsView.py +++ b/examples/RemoteGraphicsView.py @@ -7,10 +7,9 @@ app = pg.mkQApp() v = pg.RemoteGraphicsView() v.show() -QtGui = v.pg.QtGui -rect = QtGui.QGraphicsRectItem(0,0,10,10) -rect.setPen(QtGui.QPen(QtGui.QColor(255,255,0))) -v.scene().addItem(rect) +plt = v.pg.PlotItem() +v.setCentralItem(plt) +plt.plot([1,4,2,3,6,2,3,4,2,3], pen='g') diff --git a/multiprocess/remoteproxy.py b/multiprocess/remoteproxy.py index 23e2fa8b..c15c5b9d 100644 --- a/multiprocess/remoteproxy.py +++ b/multiprocess/remoteproxy.py @@ -511,24 +511,31 @@ class LocalObjectProxy(object): del cls.proxiedObjects[pid] #print "release:", cls.proxiedObjects - def __init__(self, obj): + def __init__(self, obj, **opts): + """ + Create a 'local' proxy object that, when sent to a remote host, + will appear as a normal ObjectProxy to *obj*. + Any extra keyword arguments are passed to proxy._setProxyOptions() + on the remote side. + """ self.processId = os.getpid() #self.objectId = id(obj) self.typeStr = repr(obj) #self.handler = handler self.obj = obj + self.opts = opts def __reduce__(self): ## a proxy is being pickled and sent to a remote process. ## every time this happens, a new proxy will be generated in the remote process, ## so we keep a new ID so we can track when each is released. pid = LocalObjectProxy.registerObject(self.obj) - return (unpickleObjectProxy, (self.processId, pid, self.typeStr)) + return (unpickleObjectProxy, (self.processId, pid, self.typeStr, None, self.opts)) ## alias proxy = LocalObjectProxy -def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None): +def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None, opts=None): if processId == os.getpid(): obj = LocalObjectProxy.lookupProxyId(proxyId) if attributes is not None: @@ -536,7 +543,10 @@ def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None): obj = getattr(obj, attr) return obj else: - return ObjectProxy(processId, proxyId=proxyId, typeStr=typeStr) + proxy = ObjectProxy(processId, proxyId=proxyId, typeStr=typeStr) + if opts is not None: + proxy._setProxyOptions(**opts) + return proxy class ObjectProxy(object): """ diff --git a/widgets/RemoteGraphicsView.py b/widgets/RemoteGraphicsView.py index 6e96a2b0..eda14025 100644 --- a/widgets/RemoteGraphicsView.py +++ b/widgets/RemoteGraphicsView.py @@ -2,11 +2,18 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph.multiprocess as mp import pyqtgraph as pg import numpy as np -import ctypes, os +import mmap, tempfile, ctypes, atexit __all__ = ['RemoteGraphicsView'] class RemoteGraphicsView(QtGui.QWidget): + """ + Replacement for GraphicsView that does all scene management and rendering on a remote process, + while displaying on the local widget. + + GraphicsItems must be created by proxy to the remote process. + + """ def __init__(self, parent=None, *args, **kwds): self._img = None self._imgReq = None @@ -16,10 +23,16 @@ class RemoteGraphicsView(QtGui.QWidget): rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') self._view = rpgRemote.Renderer(*args, **kwds) self._view._setProxyOptions(deferGetattr=True) + self.setFocusPolicy(self._view.focusPolicy()) + + shmFileName = self._view.shmFileName() + self.shmFile = open(shmFileName, 'r') + self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ) + self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) - def scene(self): - return self._view.scene() + for method in ['scene', 'setCentralItem']: + setattr(self, method, getattr(self._view, method)) def resizeEvent(self, ev): ret = QtGui.QWidget.resizeEvent(self, ev) @@ -27,21 +40,63 @@ class RemoteGraphicsView(QtGui.QWidget): return ret def remoteSceneChanged(self, data): - self._img = pg.makeQImage(data, alpha=True) + w, h, size = data + if self.shm.size != size: + self.shm.close() + self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) + self.shm.seek(0) + self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32) self.update() def paintEvent(self, ev): if self._img is None: return p = QtGui.QPainter(self) - p.drawImage(self.rect(), self._img, self.rect()) + p.drawImage(self.rect(), self._img, QtCore.QRect(0, 0, self._img.width(), self._img.height())) p.end() + + def mousePressEvent(self, ev): + self._view.mousePressEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + ev.accept() + return QtGui.QWidget.mousePressEvent(self, ev) + def mouseReleaseEvent(self, ev): + self._view.mouseReleaseEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + ev.accept() + return QtGui.QWidget.mouseReleaseEvent(self, ev) + + def mouseMoveEvent(self, ev): + self._view.mouseMoveEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + ev.accept() + return QtGui.QWidget.mouseMoveEvent(self, ev) + + def wheelEvent(self, ev): + self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), ev.orientation(), _callSync='off') + ev.accept() + return QtGui.QWidget.wheelEvent(self, ev) + + def keyEvent(self, ev): + if self._view.keyEvent(ev.type(), int(ev.modifiers()), text, autorep, count): + ev.accept() + return QtGui.QWidget.keyEvent(self, ev) + + + class Renderer(pg.GraphicsView): sceneRendered = QtCore.Signal(object) def __init__(self, *args, **kwds): + ## Create shared memory for rendered image + #fd = os.open('/tmp/mmaptest', os.O_CREAT | os.O_TRUNC | os.O_RDWR) + #os.write(fd, '\x00' * mmap.PAGESIZE) + self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') + self.shmFile.write('\x00' * mmap.PAGESIZE) + #fh.flush() + fd = self.shmFile.fileno() + self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) + atexit.register(self.close) + pg.GraphicsView.__init__(self, *args, **kwds) self.scene().changed.connect(self.update) self.img = None @@ -49,22 +104,67 @@ class Renderer(pg.GraphicsView): self.renderTimer.timeout.connect(self.renderView) self.renderTimer.start(16) + def close(self): + self.shm.close() + self.shmFile.close() + + def shmFileName(self): + return self.shmFile.name + def update(self): self.img = None return pg.GraphicsView.update(self) def resize(self, size): + oldSize = self.size() pg.GraphicsView.resize(self, size) + self.resizeEvent(QtGui.QResizeEvent(size, oldSize)) self.update() def renderView(self): if self.img is None: - self.img = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + ## make sure shm is large enough and get its address + size = self.width() * self.height() * 4 + if size > self.shm.size(): + self.shm.resize(size) + address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) + + ## render the scene directly to shared memory + self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32) self.img.fill(0xffffffff) p = QtGui.QPainter(self.img) self.render(p, self.viewRect(), self.rect()) p.end() - self.data = np.fromstring(ctypes.string_at(int(self.img.bits()), self.img.byteCount()), dtype=np.ubyte).reshape(self.height(), self.width(),4).transpose(1,0,2) - #self.data = ctypes.string_at(int(self.img.bits()), self.img.byteCount()) - self.sceneRendered.emit(self.data) + self.sceneRendered.emit((self.width(), self.height(), self.shm.size())) + def mousePressEvent(self, typ, pos, gpos, btn, btns, mods): + typ = QtCore.QEvent.Type(typ) + btns = QtCore.Qt.MouseButtons(btns) + mods = QtCore.Qt.KeyboardModifiers(mods) + return pg.GraphicsView.mousePressEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + + def mouseMoveEvent(self, typ, pos, gpos, btn, btns, mods): + typ = QtCore.QEvent.Type(typ) + btns = QtCore.Qt.MouseButtons(btns) + mods = QtCore.Qt.KeyboardModifiers(mods) + return pg.GraphicsView.mouseMoveEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + + def mouseReleaseEvent(self, typ, pos, gpos, btn, btns, mods): + typ = QtCore.QEvent.Type(typ) + btns = QtCore.Qt.MouseButtons(btns) + mods = QtCore.Qt.KeyboardModifiers(mods) + return pg.GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + + def wheelEvent(self, pos, gpos, d, btns, mods, ori): + btns = QtCore.Qt.MouseButtons(btns) + mods = QtCore.Qt.KeyboardModifiers(mods) + return pg.GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori)) + + def keyEvent(self, typ, mods, text, autorep, count): + typ = QtCore.QEvent.Type(typ) + mods = QtCore.Qt.KeyboardModifiers(mods) + pg.GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count)) + return ev.accepted() + + + \ No newline at end of file From f99ed791bc4ae39c21a0c6d12f6595641114c258 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 29 Jun 2012 12:36:31 -0400 Subject: [PATCH 06/24] Added LayoutWidget --- widgets/LayoutWidget.py | 96 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 widgets/LayoutWidget.py diff --git a/widgets/LayoutWidget.py b/widgets/LayoutWidget.py new file mode 100644 index 00000000..bcf88199 --- /dev/null +++ b/widgets/LayoutWidget.py @@ -0,0 +1,96 @@ +from pyqtgraph.Qt import QtGui, QtCore + +__all__ = ['LayoutWidget'] +class LayoutWidget(QtGui.QWidget): + """ + Convenience class used for laying out QWidgets in a grid. + (It's just a little less effort to use than QGridLayout) + """ + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + self.layout = QtGui.QGridLayout() + self.setLayout(self.layout) + self.items = {} + self.rows = {} + self.currentRow = 0 + self.currentCol = 0 + + def nextRow(self): + """Advance to next row for automatic widget placement""" + self.currentRow += 1 + self.currentCol = 0 + + def nextColumn(self, colspan=1): + """Advance to next column, while returning the current column number + (generally only for internal use--called by addWidget)""" + self.currentCol += colspan + return self.currentCol-colspan + + def nextCol(self, *args, **kargs): + """Alias of nextColumn""" + return self.nextColumn(*args, **kargs) + + + def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create a QLabel with *text* and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to QLabel(). + Returns the created widget. + """ + text = QtGui.QLabel(text, **kargs) + self.addItem(text, row, col, rowspan, colspan) + return text + + def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create an empty LayoutWidget and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`LayoutWidget.__init__ ` + Returns the created widget. + """ + layout = LayoutWidget(**kargs) + self.addItem(layout, row, col, rowspan, colspan) + return layout + + def addWidget(self, item, row=None, col=None, rowspan=1, colspan=1): + """ + Add a widget to the layout and place it in the next available cell (or in the cell specified). + """ + if row is None: + row = self.currentRow + if col is None: + col = self.nextCol(colspan) + + if row not in self.rows: + self.rows[row] = {} + self.rows[row][col] = item + self.items[item] = (row, col) + + self.layout.addWidget(item, row, col, rowspan, colspan) + + def getWidget(self, row, col): + """Return the widget in (*row*, *col*)""" + return self.row[row][col] + + #def itemIndex(self, item): + #for i in range(self.layout.count()): + #if self.layout.itemAt(i).graphicsItem() is item: + #return i + #raise Exception("Could not determine index of item " + str(item)) + + #def removeItem(self, item): + #"""Remove *item* from the layout.""" + #ind = self.itemIndex(item) + #self.layout.removeAt(ind) + #self.scene().removeItem(item) + #r,c = self.items[item] + #del self.items[item] + #del self.rows[r][c] + #self.update() + + #def clear(self): + #items = [] + #for i in list(self.items.keys()): + #self.removeItem(i) + + From debe847f9fe005e959a38ea1e624ddd51a322675 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 29 Jun 2012 12:37:48 -0400 Subject: [PATCH 07/24] ConsoleWidget: Added methods for toggling exception catching --- console/Console.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/console/Console.py b/console/Console.py index 066a4073..27e660be 100644 --- a/console/Console.py +++ b/console/Console.py @@ -22,7 +22,7 @@ class ConsoleWidget(QtGui.QWidget): be baffling and frustrating to users since it would appear the program has frozen. - some terminals (eg windows cmd.exe) have notoriously unfriendly interfaces - ability to add extra features like exception stack introspection - - ability to have multiple interactive prompts for remotely generated processes + - ability to have multiple interactive prompts, including for spawned sub-processes """ def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None): @@ -70,8 +70,8 @@ class ConsoleWidget(QtGui.QWidget): self.ui.historyList.itemDoubleClicked.connect(self.cmdDblClicked) self.ui.exceptionBtn.toggled.connect(self.ui.exceptionGroup.setVisible) - self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllToggled) - self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextToggled) + self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllExceptions) + self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextException) self.ui.clearExceptionBtn.clicked.connect(self.clearExceptionClicked) self.ui.exceptionStackList.itemClicked.connect(self.stackItemClicked) self.ui.exceptionStackList.itemDoubleClicked.connect(self.stackItemDblClicked) @@ -227,15 +227,25 @@ class ConsoleWidget(QtGui.QWidget): def flush(self): pass - def catchAllToggled(self, b): - if b: + def catchAllExceptions(self, catch=True): + """ + If True, the console will catch all unhandled exceptions and display the stack + trace. Each exception caught clears the last. + """ + self.ui.catchAllExceptionsBtn.setChecked(catch) + if catch: self.ui.catchNextExceptionBtn.setChecked(False) exceptionHandling.register(self.allExceptionsHandler) else: exceptionHandling.unregister(self.allExceptionsHandler) - def catchNextToggled(self, b): - if b: + def catchNextException(self, catch=True): + """ + If True, the console will catch the next unhandled exception and display the stack + trace. + """ + self.ui.catchNextExceptionBtn.setChecked(catch) + if catch: self.ui.catchAllExceptionsBtn.setChecked(False) exceptionHandling.register(self.nextExceptionHandler) else: @@ -252,11 +262,15 @@ class ConsoleWidget(QtGui.QWidget): pass def stackItemDblClicked(self, item): - global EDITOR + editor = self.editor + if editor is None: + editor = pg.getConfigOption('editorCommand') + if editor is None: + return tb = self.currentFrame() lineNum = tb.tb_lineno fileName = tb.tb_frame.f_code.co_filename - subprocess.Popen(EDITOR.format(fileName=fileName, lineNum=lineNum), shell=True) + subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True) def allExceptionsHandler(self, *args): From ad7b5f0aad55c5fca68ff6620cf4719de968dd89 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 29 Jun 2012 14:39:27 -0400 Subject: [PATCH 08/24] - Default foreground / background colors can now be set using pyqtgraph.setConfigOption() - Added pyqtgraph.systemInfo() for bug reporting - GraphicsLayout does a better job of avoiding occupied cells when using automatic placement - Fixed sizing issues with LabelItem - Updated GraphicsLayout example --- Qt.py | 27 ++++++---- __init__.py | 22 ++++++-- examples/GraphicsLayout.py | 68 +++++++++++++++++------- graphicsItems/AxisItem.py | 60 +++++++++++++++------ graphicsItems/GradientEditorItem.py | 8 +-- graphicsItems/GraphicsLayout.py | 49 +++++++++++------ graphicsItems/HistogramLUTItem.py | 2 +- graphicsItems/LabelItem.py | 71 +++++++++++++++++-------- graphicsItems/ViewBox/ViewBox.py | 21 +++++++- widgets/GradientWidget.py | 4 +- widgets/GraphicsView.py | 82 +++++++++++++++++++++-------- widgets/HistogramLUTWidget.py | 2 +- widgets/RemoteGraphicsView.py | 20 +++---- 13 files changed, 306 insertions(+), 130 deletions(-) diff --git a/Qt.py b/Qt.py index 89bf00d6..349721ab 100644 --- a/Qt.py +++ b/Qt.py @@ -1,15 +1,20 @@ ## Do all Qt imports from here to allow easier PyQt / PySide compatibility -#from PySide import QtGui, QtCore, QtOpenGL, QtSvg -from PyQt4 import QtGui, QtCore -try: - from PyQt4 import QtSvg -except ImportError: - pass -try: - from PyQt4 import QtOpenGL -except ImportError: - pass +USE_PYSIDE = False ## If False, import PyQt4. If True, import PySide + +if USE_PYSIDE: + from PySide import QtGui, QtCore, QtOpenGL, QtSvg + VERSION_INFO = 'PySide ' + PySide.__version__ +else: + from PyQt4 import QtGui, QtCore + try: + from PyQt4 import QtSvg + except ImportError: + pass + try: + from PyQt4 import QtOpenGL + except ImportError: + pass -if not hasattr(QtCore, 'Signal'): QtCore.Signal = QtCore.pyqtSignal + VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR diff --git a/__init__.py b/__init__.py index 51a0fe5c..039188c1 100644 --- a/__init__.py +++ b/__init__.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- +REVISION = '621' + ### import all the goodies and add some helper functions for easy CLI use ## 'Qt' is a local module; it is intended mainly to cover up the differences ## between PyQt4 and PySide. -from .Qt import QtGui +from .Qt import QtGui ## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause) #if QtGui.QApplication.instance() is None: @@ -30,13 +32,15 @@ else: useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. CONFIG_OPTIONS = { - 'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. + 'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. 'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox - 'foregroundColor': (200,200,200), - 'backgroundColor': (0,0,0), + 'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc. + 'background': (0, 0, 0), ## default background for GraphicsWidget 'antialias': False, + 'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets } + def setConfigOption(opt, value): CONFIG_OPTIONS[opt] = value @@ -44,6 +48,16 @@ def getConfigOption(opt): return CONFIG_OPTIONS[opt] +def systemInfo(): + print "sys.platform:", sys.platform + print "sys.version:", sys.version + from .Qt import VERSION_INFO + print "qt bindings:", VERSION_INFO + print "pyqtgraph:", REVISION + print "config:" + import pprint + pprint.pprint(CONFIG_OPTIONS) + ## Rename orphaned .pyc files. This is *probably* safe :) def renamePyc(startDir): diff --git a/examples/GraphicsLayout.py b/examples/GraphicsLayout.py index 0602dd87..c8b4160f 100644 --- a/examples/GraphicsLayout.py +++ b/examples/GraphicsLayout.py @@ -4,41 +4,73 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg -import user +import numpy as np app = QtGui.QApplication([]) view = pg.GraphicsView() -l = pg.GraphicsLayout(border=pg.mkPen(0, 0, 255)) +l = pg.GraphicsLayout(border=(100,100,100)) view.setCentralItem(l) view.show() +view.resize(800,600) + +## Title at top +text = """ +This example demonstrates the use of GraphicsLayout to arrange items in a grid.
+The items added to the layout must be subclasses of QGraphicsWidget (this includes
+PlotItem, ViewBox, LabelItem, and GrphicsLayout itself). +""" +l.addLabel(text, col=1, colspan=4) +l.nextRow() + +## Put vertical label on left side +l.addLabel('Long Vertical Label', angle=-90, rowspan=3) ## Add 3 plots into the first row (automatic position) -p1 = l.addPlot() -p2 = l.addPlot() -p3 = l.addPlot() +p1 = l.addPlot(title="Plot 1") +p2 = l.addPlot(title="Plot 2") +vb = l.addViewBox(lockAspect=True) +img = pg.ImageItem(np.random.normal(size=(100,100))) +vb.addItem(img) +vb.autoRange() -## Add a viewbox into the second row (automatic position) + +## Add a sub-layout into the second row (automatic position) +## The added item should avoid the first column, which is already filled l.nextRow() -vb = l.addViewBox(colspan=3) +l2 = l.addLayout(colspan=3, border=(50,0,0)) +l2.setContentsMargins(10, 10, 10, 10) +l2.addLabel("Sub-layout: this layout demonstrates the use of shared axes and axis labels", colspan=3) +l2.nextRow() +l2.addLabel('Vertical Axis Label', angle=-90, rowspan=2) +p21 = l2.addPlot() +p22 = l2.addPlot() +l2.nextRow() +p23 = l2.addPlot() +p24 = l2.addPlot() +l2.nextRow() +l2.addLabel("HorizontalAxisLabel", col=1, colspan=2) + +## hide axes on some plots +p21.hideAxis('bottom') +p22.hideAxis('bottom') +p22.hideAxis('left') +p24.hideAxis('left') +p21.hideButtons() +p22.hideButtons() +p23.hideButtons() +p24.hideButtons() + ## Add 2 more plots into the third row (manual position) -p4 = l.addPlot(row=2, col=0) -p5 = l.addPlot(row=2, col=1, colspan=2) +p4 = l.addPlot(row=3, col=1) +p5 = l.addPlot(row=3, col=2, colspan=2) - - -## show some content +## show some content in the plots p1.plot([1,3,2,4,3,5]) p2.plot([1,3,2,4,3,5]) -p3.plot([1,3,2,4,3,5]) p4.plot([1,3,2,4,3,5]) p5.plot([1,3,2,4,3,5]) -b = QtGui.QGraphicsRectItem(0, 0, 1, 1) -b.setPen(pg.mkPen(255,255,0)) -vb.addItem(b) -vb.setRange(QtCore.QRectF(-1, -1, 3, 3)) - ## Start Qt event loop unless running in interactive mode. diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index ce2cb503..d6615748 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -4,6 +4,7 @@ from pyqtgraph.Point import Point import pyqtgraph.debug as debug import weakref import pyqtgraph.functions as fn +import pyqtgraph as pg from .GraphicsWidget import GraphicsWidget __all__ = ['AxisItem'] @@ -65,8 +66,6 @@ class AxisItem(GraphicsWidget): self.setRange(0, 1) - if pen is None: - pen = QtGui.QPen(QtGui.QColor(100, 100, 100)) self.setPen(pen) self._linkedView = None @@ -189,8 +188,18 @@ class AxisItem(GraphicsWidget): self.setMaximumWidth(w) self.setMinimumWidth(w) + def pen(self): + if self._pen is None: + return fn.mkPen(pg.getConfigOption('foreground')) + return self._pen + def setPen(self, pen): - self.pen = pen + """ + Set the pen used for drawing text, axes, ticks, and grid lines. + if pen == None, the default will be used (see :func:`setConfigOption + `) + """ + self._pen = pen self.picture = None self.update() @@ -370,8 +379,6 @@ class AxisItem(GraphicsWidget): """ minVal, maxVal = sorted((minVal, maxVal)) - if self.logMode: - return self.logTickValues(minVal, maxVal, size) ticks = [] tickLevels = self.tickSpacing(minVal, maxVal, size) @@ -391,18 +398,33 @@ class AxisItem(GraphicsWidget): values = filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) allValues = np.concatenate([allValues, values]) ticks.append((spacing, values)) + + if self.logMode: + return self.logTickValues(minVal, maxVal, size, ticks) + return ticks - def logTickValues(self, minVal, maxVal, size): - v1 = int(np.floor(minVal)) - v2 = int(np.ceil(maxVal)) - major = list(range(v1+1, v2)) + def logTickValues(self, minVal, maxVal, size, stdTicks): - minor = [] - for v in range(v1, v2): - minor.extend(v + np.log10(np.arange(1, 10))) - minor = [x for x in minor if x>minVal and x= 1.0: + ticks.append((spacing, t)) + + if len(ticks) < 3: + v1 = int(np.floor(minVal)) + v2 = int(np.ceil(maxVal)) + #major = list(range(v1+1, v2)) + + minor = [] + for v in range(v1, v2): + minor.extend(v + np.log10(np.arange(1, 10))) + minor = [x for x in minor if x>minVal and x` Returns the created item. + + To create a vertical label, use *angle*=-90 """ text = LabelItem(text, **kargs) self.addItem(text, row, col, rowspan, colspan) @@ -89,18 +99,24 @@ class GraphicsLayout(GraphicsWidget): if row is None: row = self.currentRow if col is None: - col = self.nextCol(colspan) + col = self.currentCol - if row not in self.rows: - self.rows[row] = {} - self.rows[row][col] = item - self.items[item] = (row, col) + self.items[item] = [] + for i in range(rowspan): + for j in range(colspan): + row2 = row + i + col2 = col + j + if row2 not in self.rows: + self.rows[row2] = {} + self.rows[row2][col2] = item + self.items[item].append((row2, col2)) self.layout.addItem(item, row, col, rowspan, colspan) + self.nextColumn() def getItem(self, row, col): - """Return the item in (*row*, *col*)""" - return self.row[row][col] + """Return the item in (*row*, *col*). If the cell is empty, return None.""" + return self.rows.get(row, {}).get(col, None) def boundingRect(self): return self.rect() @@ -124,9 +140,10 @@ class GraphicsLayout(GraphicsWidget): ind = self.itemIndex(item) self.layout.removeAt(ind) self.scene().removeItem(item) - r,c = self.items[item] + + for r,c in self.items[item]: + del self.rows[r][c] del self.items[item] - del self.rows[r][c] self.update() def clear(self): diff --git a/graphicsItems/HistogramLUTItem.py b/graphicsItems/HistogramLUTItem.py index 56730c3e..5a3b63d6 100644 --- a/graphicsItems/HistogramLUTItem.py +++ b/graphicsItems/HistogramLUTItem.py @@ -50,7 +50,7 @@ class HistogramLUTItem(GraphicsWidget): self.layout.setSpacing(0) self.vb = ViewBox() self.vb.setMaximumWidth(152) - self.vb.setMinimumWidth(52) + self.vb.setMinimumWidth(45) self.vb.setMouseEnabled(x=False, y=True) self.gradient = GradientEditorItem() self.gradient.setOrientation('right') diff --git a/graphicsItems/LabelItem.py b/graphicsItems/LabelItem.py index 5ebbe739..17301fb3 100644 --- a/graphicsItems/LabelItem.py +++ b/graphicsItems/LabelItem.py @@ -1,5 +1,6 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph.functions as fn +import pyqtgraph as pg from .GraphicsWidget import GraphicsWidget @@ -18,14 +19,13 @@ class LabelItem(GraphicsWidget): GraphicsWidget.__init__(self, parent) self.item = QtGui.QGraphicsTextItem(self) self.opts = { - 'color': 'CCC', + 'color': None, 'justify': 'center' } self.opts.update(args) - self.sizeHint = {} + self._sizeHint = {} self.setText(text) self.setAngle(angle) - def setAttr(self, attr, value): """Set default text properties. See setText() for accepted parameters.""" @@ -44,15 +44,17 @@ class LabelItem(GraphicsWidget): ==================== ============================== """ self.text = text - opts = self.opts.copy() + opts = self.opts for k in args: opts[k] = args[k] optlist = [] - if 'color' in opts: - if isinstance(opts['color'], QtGui.QColor): - opts['color'] = fn.colorStr(opts['color'])[:6] - optlist.append('color: #' + opts['color']) + + color = self.opts['color'] + if color is None: + color = pg.getConfigOption('foreground') + color = fn.mkColor(color) + optlist.append('color: #' + fn.colorStr(color)[:6]) if 'size' in opts: optlist.append('font-size: ' + opts['size']) if 'bold' in opts and opts['bold'] in [True, False]: @@ -64,7 +66,7 @@ class LabelItem(GraphicsWidget): self.item.setHtml(full) self.updateMin() self.resizeEvent(None) - self.update() + self.updateGeometry() def resizeEvent(self, ev): #c1 = self.boundingRect().center() @@ -72,16 +74,35 @@ class LabelItem(GraphicsWidget): #dif = c1 - c2 #self.item.moveBy(dif.x(), dif.y()) #print c1, c2, dif, self.item.pos() + self.item.setPos(0,0) + bounds = self.itemRect() + left = self.mapFromItem(self.item, QtCore.QPointF(0,0)) - self.mapFromItem(self.item, QtCore.QPointF(1,0)) + rect = self.rect() + if self.opts['justify'] == 'left': - self.item.setPos(0,0) + if left.x() != 0: + bounds.moveLeft(rect.left()) + if left.y() < 0: + bounds.moveTop(rect.top()) + elif left.y() > 0: + bounds.moveBottom(rect.bottom()) + elif self.opts['justify'] == 'center': - bounds = self.item.mapRectToParent(self.item.boundingRect()) - self.item.setPos(self.width()/2. - bounds.width()/2., 0) + bounds.moveCenter(rect.center()) + #bounds = self.itemRect() + #self.item.setPos(self.width()/2. - bounds.width()/2., 0) elif self.opts['justify'] == 'right': - bounds = self.item.mapRectToParent(self.item.boundingRect()) - self.item.setPos(self.width() - bounds.width(), 0) - #if self.width() > 0: - #self.item.setTextWidth(self.width()) + if left.x() != 0: + bounds.moveRight(rect.right()) + if left.y() < 0: + bounds.moveBottom(rect.bottom()) + elif left.y() > 0: + bounds.moveTop(rect.top()) + #bounds = self.itemRect() + #self.item.setPos(self.width() - bounds.width(), 0) + + self.item.setPos(bounds.topLeft() - self.itemRect().topLeft()) + self.updateMin() def setAngle(self, angle): self.angle = angle @@ -89,27 +110,31 @@ class LabelItem(GraphicsWidget): self.item.rotate(angle) self.updateMin() + def updateMin(self): - bounds = self.item.mapRectToParent(self.item.boundingRect()) + bounds = self.itemRect() self.setMinimumWidth(bounds.width()) self.setMinimumHeight(bounds.height()) - self.sizeHint = { + self._sizeHint = { QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2), QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? } - - self.update() + self.updateGeometry() def sizeHint(self, hint, constraint): - if hint not in self.sizeHint: + if hint not in self._sizeHint: return QtCore.QSizeF(0, 0) - return QtCore.QSizeF(*self.sizeHint[hint]) + return QtCore.QSizeF(*self._sizeHint[hint]) + + def itemRect(self): + return self.item.mapRectToParent(self.item.boundingRect()) #def paint(self, p, *args): #p.setPen(fn.mkPen('r')) #p.drawRect(self.rect()) - #p.drawRect(self.item.boundingRect()) + #p.setPen(fn.mkPen('g')) + #p.drawRect(self.itemRect()) diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index eecc9bdd..45c0fe01 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -105,6 +105,8 @@ class ViewBox(GraphicsWidget): 'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode, 'enableMenu': enableMenu, 'wheelScaleFactor': -1.0 / 8.0, + + 'background': None, } @@ -118,11 +120,17 @@ class ViewBox(GraphicsWidget): self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses ## childGroup is required so that ViewBox has local coordinates similar to device coordinates. - ## this is a workaround for a Qt + OpenGL but that causes improper clipping + ## this is a workaround for a Qt + OpenGL bug that causes improper clipping ## https://bugreports.qt.nokia.com/browse/QTBUG-23723 self.childGroup = ChildGroup(self) self.childGroup.sigItemsChanged.connect(self.itemsChanged) + self.background = QtGui.QGraphicsRectItem(self.rect()) + self.background.setParentItem(self) + self.background.setZValue(-1e6) + self.background.setPen(fn.mkPen(None)) + self.updateBackground() + #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan # this also enables capture of keyPressEvents. @@ -286,6 +294,7 @@ class ViewBox(GraphicsWidget): #self.updateAutoRange() self.updateMatrix() self.sigStateChanged.emit(self) + self.background.setRect(self.rect()) #self.linkedXChanged() #self.linkedYChanged() @@ -1155,6 +1164,16 @@ class ViewBox(GraphicsWidget): #self.scene().render(p) #p.end() + def updateBackground(self): + bg = self.state['background'] + #print self, bg + if bg is None: + self.background.hide() + else: + self.background.show() + self.background.setBrush(fn.mkBrush(bg)) + + def updateViewLists(self): def cmpViews(a, b): wins = 100 * cmp(a.window() is self.window(), b.window() is self.window()) diff --git a/widgets/GradientWidget.py b/widgets/GradientWidget.py index 287a2635..35c6852d 100644 --- a/widgets/GradientWidget.py +++ b/widgets/GradientWidget.py @@ -15,7 +15,7 @@ class GradientWidget(GraphicsView): def __init__(self, parent=None, orientation='bottom', *args, **kargs): GraphicsView.__init__(self, parent, useOpenGL=False, background=None) - self.maxDim = 27 + self.maxDim = 31 kargs['tickPen'] = 'k' self.item = GradientEditorItem(*args, **kargs) self.item.sigGradientChanged.connect(self.sigGradientChanged) @@ -24,7 +24,7 @@ class GradientWidget(GraphicsView): self.setCacheMode(self.CacheNone) self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) - self.setBackgroundRole(QtGui.QPalette.NoRole) + #self.setBackgroundRole(QtGui.QPalette.NoRole) #self.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.NoBrush)) #self.setAutoFillBackground(False) #self.setAttribute(QtCore.Qt.WA_PaintOnScreen, False) diff --git a/widgets/GraphicsView.py b/widgets/GraphicsView.py index 83a7f91b..b3d2863f 100644 --- a/widgets/GraphicsView.py +++ b/widgets/GraphicsView.py @@ -6,6 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg try: from pyqtgraph.Qt import QtOpenGL @@ -13,12 +14,8 @@ try: except ImportError: HAVE_OPENGL = False -#from numpy import vstack -#import time from pyqtgraph.Point import Point -#from vector import * import sys, os -#import debug from .FileDialog import FileDialog from pyqtgraph.GraphicsScene import GraphicsScene import numpy as np @@ -29,6 +26,20 @@ import pyqtgraph __all__ = ['GraphicsView'] class GraphicsView(QtGui.QGraphicsView): + """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the + viewed coordinate range. Also automatically creates a GraphicsScene and a central QGraphicsWidget + that is automatically scaled to the full view geometry. + + This widget is the basis for :class:`PlotWidget `, + :class:`GraphicsLayoutWidget `, and the view widget in + :class:`ImageView `. + + By default, the view coordinate system matches the widget's pixel coordinates and + automatically updates when the view is resized. This can be overridden by setting + autoPixelRange=False. The exact visible range can be set with setRange(). + + The view can be panned using the middle mouse button and scaled using the right mouse button if + enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality).""" sigRangeChanged = QtCore.Signal(object, object) sigMouseReleased = QtCore.Signal(object) @@ -37,17 +48,25 @@ class GraphicsView(QtGui.QGraphicsView): sigScaleChanged = QtCore.Signal(object) lastFileDir = None - def __init__(self, parent=None, useOpenGL=None, background='k'): - """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the - viewed coordinate range. Also automatically creates a QGraphicsScene and a central QGraphicsWidget - that is automatically scaled to the full view geometry. + def __init__(self, parent=None, useOpenGL=None, background='default'): + """ + ============ ============================================================ + Arguments: + parent Optional parent widget + useOpenGL If True, the GraphicsView will use OpenGL to do all of its + rendering. This can improve performance on some systems, + but may also introduce bugs (the combination of + QGraphicsView and QGLWidget is still an 'experimental' + feature of Qt) + background Set the background color of the GraphicsView. Accepts any + single argument accepted by + :func:`mkColor `. By + default, the background color is determined using the + 'backgroundColor' configuration option (see + :func:`setConfigOption `. + ============ ============================================================ + """ - By default, the view coordinate system matches the widget's pixel coordinates and - automatically updates when the view is resized. This can be overridden by setting - autoPixelRange=False. The exact visible range can be set with setRange(). - - The view can be panned using the middle mouse button and scaled using the right mouse button if - enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality).""" self.closed = False QtGui.QGraphicsView.__init__(self, parent) @@ -62,9 +81,7 @@ class GraphicsView(QtGui.QGraphicsView): ## This might help, but it's probably dangerous in the general case.. #self.setOptimizationFlag(self.DontSavePainterState, True) - if background is not None: - brush = fn.mkBrush(background) - self.setBackgroundBrush(brush) + self.setBackground(background) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setFrameShape(QtGui.QFrame.NoFrame) @@ -98,7 +115,22 @@ class GraphicsView(QtGui.QGraphicsView): self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) self.clickAccepted = False - + def setBackground(self, background): + """ + Set the background color of the GraphicsView. + To use the defaults specified py pyqtgraph.setConfigOption, use background='default'. + To make the background transparent, use background=None. + """ + self._background = background + if background == 'default': + background = pyqtgraph.getConfigOption('background') + if background is None: + self.setBackgroundRole(QtGui.QPalette.NoRole) + else: + brush = fn.mkBrush(background) + self.setBackgroundBrush(brush) + + def close(self): self.centralWidget = None self.scene().clear() @@ -126,7 +158,8 @@ class GraphicsView(QtGui.QGraphicsView): return self.setCentralWidget(item) def setCentralWidget(self, item): - """Sets a QGraphicsWidget to automatically fill the entire view.""" + """Sets a QGraphicsWidget to automatically fill the entire view (the item will be automatically + resize whenever the GraphicsView is resized).""" if self.centralWidget is not None: self.scene().removeItem(self.centralWidget) self.centralWidget = item @@ -152,15 +185,18 @@ class GraphicsView(QtGui.QGraphicsView): return if self.autoPixelRange: self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) - GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False) + GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way. self.updateMatrix() def updateMatrix(self, propagate=True): self.setSceneRect(self.range) - if self.aspectLocked: - self.fitInView(self.range, QtCore.Qt.KeepAspectRatio) + if self.autoPixelRange: + self.resetTransform() else: - self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) + if self.aspectLocked: + self.fitInView(self.range, QtCore.Qt.KeepAspectRatio) + else: + self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) self.sigRangeChanged.emit(self, self.range) diff --git a/widgets/HistogramLUTWidget.py b/widgets/HistogramLUTWidget.py index 5d6f3d44..bc041595 100644 --- a/widgets/HistogramLUTWidget.py +++ b/widgets/HistogramLUTWidget.py @@ -18,7 +18,7 @@ class HistogramLUTWidget(GraphicsView): self.item = HistogramLUTItem(*args, **kargs) self.setCentralItem(self.item) self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) - self.setMinimumWidth(92) + self.setMinimumWidth(95) def sizeHint(self): diff --git a/widgets/RemoteGraphicsView.py b/widgets/RemoteGraphicsView.py index eda14025..5a389616 100644 --- a/widgets/RemoteGraphicsView.py +++ b/widgets/RemoteGraphicsView.py @@ -1,6 +1,7 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph.multiprocess as mp import pyqtgraph as pg +from .GraphicsView import GraphicsView import numpy as np import mmap, tempfile, ctypes, atexit @@ -20,6 +21,7 @@ class RemoteGraphicsView(QtGui.QWidget): QtGui.QWidget.__init__(self) self._proc = mp.QtProcess() self.pg = self._proc._import('pyqtgraph') + self.pg.setConfigOptions(self.pg.CONFIG_OPTIONS) rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') self._view = rpgRemote.Renderer(*args, **kwds) self._view._setProxyOptions(deferGetattr=True) @@ -82,7 +84,7 @@ class RemoteGraphicsView(QtGui.QWidget): -class Renderer(pg.GraphicsView): +class Renderer(GraphicsView): sceneRendered = QtCore.Signal(object) @@ -97,7 +99,7 @@ class Renderer(pg.GraphicsView): self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) atexit.register(self.close) - pg.GraphicsView.__init__(self, *args, **kwds) + GraphicsView.__init__(self, *args, **kwds) self.scene().changed.connect(self.update) self.img = None self.renderTimer = QtCore.QTimer() @@ -113,11 +115,11 @@ class Renderer(pg.GraphicsView): def update(self): self.img = None - return pg.GraphicsView.update(self) + return GraphicsView.update(self) def resize(self, size): oldSize = self.size() - pg.GraphicsView.resize(self, size) + GraphicsView.resize(self, size) self.resizeEvent(QtGui.QResizeEvent(size, oldSize)) self.update() @@ -141,29 +143,29 @@ class Renderer(pg.GraphicsView): typ = QtCore.QEvent.Type(typ) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) - return pg.GraphicsView.mousePressEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + return GraphicsView.mousePressEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) def mouseMoveEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) - return pg.GraphicsView.mouseMoveEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + return GraphicsView.mouseMoveEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) def mouseReleaseEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) - return pg.GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + return GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) def wheelEvent(self, pos, gpos, d, btns, mods, ori): btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) - return pg.GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori)) + return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori)) def keyEvent(self, typ, mods, text, autorep, count): typ = QtCore.QEvent.Type(typ) mods = QtCore.Qt.KeyboardModifiers(mods) - pg.GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count)) + GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count)) return ev.accepted() From 09537416716eff68191d35f02a85d8a7cfc44104 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 29 Jun 2012 14:53:32 -0400 Subject: [PATCH 09/24] Updated systemInfo() to read from .bzr if needed --- __init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 039188c1..756e46e8 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -REVISION = '621' +REVISION = None ### import all the goodies and add some helper functions for easy CLI use @@ -11,7 +11,7 @@ from .Qt import QtGui #if QtGui.QApplication.instance() is None: #app = QtGui.QApplication([]) -import sys +import os, sys ## check python version if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] != 7): @@ -53,6 +53,13 @@ def systemInfo(): print "sys.version:", sys.version from .Qt import VERSION_INFO print "qt bindings:", VERSION_INFO + + global REVISION + if REVISION is None: ## this code was probably checked out from bzr; look up the last-revision file + lastRevFile = os.path.join(os.path.dirname(__file__), '.bzr', 'branch', 'last-revision') + if os.path.exists(lastRevFile): + REVISION = open(lastRevFile, 'r').read().strip() + print "pyqtgraph:", REVISION print "config:" import pprint From 5eacefb9265ff34d211fb01a9012c7e569c18c71 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 29 Jun 2012 15:08:14 -0400 Subject: [PATCH 10/24] Fixed pyside import error --- Qt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Qt.py b/Qt.py index 349721ab..ff3becdd 100644 --- a/Qt.py +++ b/Qt.py @@ -1,9 +1,12 @@ ## Do all Qt imports from here to allow easier PyQt / PySide compatibility USE_PYSIDE = False ## If False, import PyQt4. If True, import PySide + ## Note that when switching between PyQt and PySide, all template + ## files (*.ui) must be rebuilt for the target library. if USE_PYSIDE: from PySide import QtGui, QtCore, QtOpenGL, QtSvg + import PySide VERSION_INFO = 'PySide ' + PySide.__version__ else: from PyQt4 import QtGui, QtCore From 3d71a1f555a34422c46d46849fa6d1608eb2ec9b Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 30 Jun 2012 23:30:35 -0400 Subject: [PATCH 11/24] Documentation updates minor code edits --- console/template.py | 2 +- console/template.ui | 2 +- documentation/source/how_to_use.rst | 9 ++++++- documentation/source/style.rst | 37 +++++++++++++++++++++----- documentation/source/widgets/index.rst | 2 ++ graphicsItems/ViewBox/ViewBox.py | 1 - multiprocess/processes.py | 10 ++++++- 7 files changed, 52 insertions(+), 11 deletions(-) diff --git a/console/template.py b/console/template.py index f7790e4a..c8e5ebbc 100644 --- a/console/template.py +++ b/console/template.py @@ -91,7 +91,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Console", "Console", None, QtGui.QApplication.UnicodeUTF8)) self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/console/template.ui b/console/template.ui index e6fc22b0..ed734b52 100644 --- a/console/template.ui +++ b/console/template.ui @@ -11,7 +11,7 @@ - Form + Console diff --git a/documentation/source/how_to_use.rst b/documentation/source/how_to_use.rst index 76c2d72b..ef938104 100644 --- a/documentation/source/how_to_use.rst +++ b/documentation/source/how_to_use.rst @@ -43,5 +43,12 @@ While I consider this approach somewhat lazy, it is often the case that 'lazy' i Embedding widgets inside PyQt applications ------------------------------------------ -For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget `, :class:`ImageView `, :class:`GraphicsLayoutWidget `, and :class:`GraphicsView `. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality. +For the serious application developer, all of the functionality in pyqtgraph is available via :ref:`widgets ` that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget `, :class:`ImageView `, :class:`GraphicsLayoutWidget `, and :class:`GraphicsView `. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality: + +#. In Designer, create a QGraphicsView widget ("Graphics View" under the "Display Widgets" category). +#. Right-click on the QGraphicsView and select "Promote To...". +#. Under "Promoted class name", enter the class name you wish to use ("PlotWidget", "GraphicsLayoutWidget", etc). +#. Under "Header file", enter "pyqtgraph". +#. Click "Add", then click "Promote". +See the designer documentation for more information on promoting widgets. diff --git a/documentation/source/style.rst b/documentation/source/style.rst index ae6233bd..593e6a32 100644 --- a/documentation/source/style.rst +++ b/documentation/source/style.rst @@ -1,22 +1,47 @@ Line, Fill, and Color ===================== -Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color. +Qt relies on its QColor, QPen and QBrush classes for specifying line and fill styles for all of its drawing. +Internally, pyqtgraph uses the same system but also allows many shorthand methods of specifying +the same style options. -For these function arguments, the following values may be used: +Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color. +For most of these function arguments, the following values may be used: * single-character string representing color (b, g, r, c, m, y, k, w) * (r, g, b) or (r, g, b, a) tuple * single greyscale value (0.0 - 1.0) -* (index, maximum) tuple for automatically iterating through colors (see functions.intColor) +* (index, maximum) tuple for automatically iterating through colors (see :func:`intColor `) * QColor * QPen / QBrush where appropriate -Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes:: +Notably, more complex pens and brushes can be easily built using the +:func:`mkPen() ` / :func:`mkBrush() ` functions or with Qt's QPen and QBrush classes:: mkPen('y', width=3, style=QtCore.Qt.DashLine) ## Make a dashed yellow line 2px wide mkPen(0.5) ## solid grey line 1px wide mkPen(color=(200, 200, 255), style=QtCore.Qt.DotLine) ## Dotted pale-blue line -See the Qt documentation for 'QPen' and 'PenStyle' for more options. -Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt's QColor class +See the Qt documentation for 'QPen' and 'PenStyle' for more line-style options and 'QBrush' for more fill options. +Colors can also be built using :func:`mkColor() `, +:func:`intColor() `, :func:`hsvColor() `, or Qt's QColor class. + + +Default Background and Foreground Colors +---------------------------------------- + +By default, pyqtgraph uses a black background for its plots and grey for axes, text, and plot lines. +These defaults can be changed using pyqtgraph.setConfigOption():: + + import pyqtgraph as pg + + ## Switch to using white background and black foreground + pg.setConfigOption('background', 'w') + pg.setConfigOption('foreground', 'k') + + ## The following plot has inverted colors + pg.plot([1,4,2,3,5]) + +(Note that this must be set *before* creating any widgets) + + diff --git a/documentation/source/widgets/index.rst b/documentation/source/widgets/index.rst index 1beaf1ec..bfcf34ee 100644 --- a/documentation/source/widgets/index.rst +++ b/documentation/source/widgets/index.rst @@ -1,3 +1,5 @@ +.. _api_widgets: + Pyqtgraph's Widgets =================== diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 45c0fe01..398b1e8f 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -1166,7 +1166,6 @@ class ViewBox(GraphicsWidget): def updateBackground(self): bg = self.state['background'] - #print self, bg if bg is None: self.background.hide() else: diff --git a/multiprocess/processes.py b/multiprocess/processes.py index c4356a80..c14b8b0a 100644 --- a/multiprocess/processes.py +++ b/multiprocess/processes.py @@ -130,7 +130,7 @@ class ForkedProcess(RemoteEventHandler): """ - def __init__(self, name=None, target=0, preProxy=None): + def __init__(self, name=None, target=0, preProxy=None, randomReseed=True): """ When initializing, an optional target may be given. If no target is specified, self.eventLoop will be used. @@ -141,6 +141,9 @@ class ForkedProcess(RemoteEventHandler): in the remote process (but do not need to be sent explicitly since they are available immediately before the call to fork(). Proxies will be availabe as self.proxies[name]. + + If randomReseed is True, the built-in random and numpy.random generators + will be reseeded in the child process. """ self.hasJoined = False if target == 0: @@ -188,6 +191,11 @@ class ForkedProcess(RemoteEventHandler): atexit._exithandlers = [] atexit.register(lambda: os._exit(0)) + if randomReseed: + if 'numpy.random' in sys.modules: + sys.modules['numpy.random'].seed(os.getpid() ^ int(time.time()*10000%10000)) + if 'random' in sys.modules: + sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000)) RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid()) From 73e94f543cb84ee43063613bd90510878976081f Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 30 Jun 2012 23:32:26 -0400 Subject: [PATCH 12/24] Parallelize now reseeds random number generators after fork() Bugfix -- AxisItem enforces tick boundaries more strictly --- graphicsItems/AxisItem.py | 21 +++++++++++++++++++-- multiprocess/parallelizer.py | 20 +++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index d6615748..27589636 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -537,6 +537,10 @@ class AxisItem(GraphicsWidget): else: xScale = bounds.width() / dif offset = self.range[0] * xScale + + xRange = [x * xScale - offset for x in self.range] + xMin = min(xRange) + xMax = max(xRange) prof.mark('init') @@ -545,6 +549,7 @@ class AxisItem(GraphicsWidget): ## draw ticks ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) ## draw three different intervals, long ticks first + for i in range(len(tickLevels)): tickPositions.append([]) ticks = tickLevels[i][1] @@ -557,7 +562,13 @@ class AxisItem(GraphicsWidget): lineAlpha = self.grid for v in ticks: + ## determine actual position to draw this tick x = (v * xScale) - offset + if x < xMin or x > xMax: ## last check to make sure no out-of-bounds ticks are drawn + tickPositions[i].append(None) + continue + tickPositions[i].append(x) + p1 = [x, x] p2 = [x, x] p1[axis] = tickStart @@ -570,7 +581,6 @@ class AxisItem(GraphicsWidget): tickPen.setColor(color) p.setPen(tickPen) p.drawLine(Point(p1), Point(p2)) - tickPositions[i].append(x) prof.mark('draw ticks') ## Draw text until there is no more room (or no more text) @@ -585,10 +595,15 @@ class AxisItem(GraphicsWidget): if len(strings) == 0: continue + + ## ignore strings belonging to ticks that were previously ignored + for j in range(len(strings)): + if tickPositions[i][j] is None: + strings[j] = None if i > 0: ## always draw top level ## measure all text, make sure there's enough room - textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings]) + textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None]) if axis == 0: textSize = np.sum([r.height() for r in textRects]) else: @@ -603,6 +618,8 @@ class AxisItem(GraphicsWidget): #strings = self.tickStrings(values, self.scale, spacing) for j in range(len(strings)): vstr = strings[j] + if vstr is None:## this tick was ignored because it is out of bounds + continue x = tickPositions[i][j] textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) height = textRect.height() diff --git a/multiprocess/parallelizer.py b/multiprocess/parallelizer.py index 66f01938..eda82d31 100644 --- a/multiprocess/parallelizer.py +++ b/multiprocess/parallelizer.py @@ -37,7 +37,7 @@ class Parallelize: since it is automatically sent via pipe back to the parent process. """ - def __init__(self, tasks, workers=None, block=True, progressDialog=None, **kwds): + def __init__(self, tasks, workers=None, block=True, progressDialog=None, randomReseed=True, **kwds): """ =============== =================================================================== Arguments: @@ -47,6 +47,9 @@ class Parallelize: system progressDialog optional dict of arguments for ProgressDialog to update while tasks are processed + randomReseed If True, each forked process will reseed its random number generator + to ensure independent results. Works with the built-in random + and numpy.random. kwds objects to be shared by proxy with child processes (they will appear as attributes of the tasker) =============== =================================================================== @@ -68,6 +71,7 @@ class Parallelize: workers = 1 self.workers = workers self.tasks = list(tasks) + self.reseed = randomReseed self.kwds = kwds.copy() self.kwds['_taskStarted'] = self._taskStarted @@ -112,7 +116,7 @@ class Parallelize: ## fork and assign tasks to each worker for i in range(workers): - proc = ForkedProcess(target=None, preProxy=self.kwds) + proc = ForkedProcess(target=None, preProxy=self.kwds, randomReseed=self.reseed) if not proc.isParent: self.proc = proc return Tasker(proc, chunks[i], proc.forkedProxies) @@ -150,7 +154,17 @@ class Parallelize: #print "remove:", [ch.childPid for ch in rem] for ch in rem: activeChilds.remove(ch) - os.waitpid(ch.childPid, 0) + while True: + try: + os.waitpid(ch.childPid, 0) + break + except OSError as ex: + if ex.errno == 4: ## If we get this error, just try again + continue + #print "Ignored system call interruption" + else: + raise + #print [ch.childPid for ch in activeChilds] if self.showProgress and self.progressDlg.wasCanceled(): From 6d01aa2b090bd3aeead894df66085383a16b71bd Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 3 Jul 2012 14:44:07 -0400 Subject: [PATCH 13/24] Bugfixes: - workaround for PySide exit crash - fixed alpha of major/minor grid lines --- graphicsItems/AxisItem.py | 2 +- graphicsItems/PlotItem/plotConfigTemplate.py | 2 +- graphicsItems/PlotItem/plotConfigTemplate.ui | 2 +- widgets/GraphicsView.py | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 27589636..351b4ed5 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -559,7 +559,7 @@ class AxisItem(GraphicsWidget): lineAlpha = 255 / (i+1) if self.grid is not False: - lineAlpha = self.grid + lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.) for v in ticks: ## determine actual position to draw this tick diff --git a/graphicsItems/PlotItem/plotConfigTemplate.py b/graphicsItems/PlotItem/plotConfigTemplate.py index 9a107e9d..0c620839 100644 --- a/graphicsItems/PlotItem/plotConfigTemplate.py +++ b/graphicsItems/PlotItem/plotConfigTemplate.py @@ -116,7 +116,7 @@ class Ui_Form(object): self.gridLayout_2.addWidget(self.yGridCheck, 1, 0, 1, 2) self.gridAlphaSlider = QtGui.QSlider(self.gridGroup) self.gridAlphaSlider.setMaximum(255) - self.gridAlphaSlider.setProperty("value", 70) + self.gridAlphaSlider.setProperty("value", 128) self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal) self.gridAlphaSlider.setObjectName(_fromUtf8("gridAlphaSlider")) self.gridLayout_2.addWidget(self.gridAlphaSlider, 2, 1, 1, 1) diff --git a/graphicsItems/PlotItem/plotConfigTemplate.ui b/graphicsItems/PlotItem/plotConfigTemplate.ui index a1807fd6..516ec721 100644 --- a/graphicsItems/PlotItem/plotConfigTemplate.ui +++ b/graphicsItems/PlotItem/plotConfigTemplate.ui @@ -221,7 +221,7 @@ 255 - 70 + 128 Qt::Horizontal diff --git a/widgets/GraphicsView.py b/widgets/GraphicsView.py index b3d2863f..6f7da132 100644 --- a/widgets/GraphicsView.py +++ b/widgets/GraphicsView.py @@ -104,6 +104,11 @@ class GraphicsView(QtGui.QGraphicsView): self.sceneObj = GraphicsScene() self.setScene(self.sceneObj) + ## Workaround for PySide crash + ## This ensures that the scene will outlive the view. + if pyqtgraph.Qt.USE_PYSIDE: + self.sceneObj._view_ref_workaround = self + ## by default we set up a central widget with a grid layout. ## this can be replaced if needed. self.centralWidget = None From b1dbec848c04f295711c62fd14cd5a5d1ab76515 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 8 Jul 2012 21:33:35 -0400 Subject: [PATCH 14/24] Added checks for OpenGL version and 3D texture size limits Added script for reporting GL version info --- opengl/GLViewWidget.py | 31 +++++++++++++++++++++++++++---- opengl/glInfo.py | 16 ++++++++++++++++ opengl/items/GLVolumeItem.py | 5 +++++ 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 opengl/glInfo.py diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py index c68e8726..6f636e0d 100644 --- a/opengl/GLViewWidget.py +++ b/opengl/GLViewWidget.py @@ -29,12 +29,18 @@ class GLViewWidget(QtOpenGL.QGLWidget): self.keysPressed = {} self.keyTimer = QtCore.QTimer() self.keyTimer.timeout.connect(self.evalKeyState) + + self.makeCurrent() def addItem(self, item): self.items.append(item) if hasattr(item, 'initializeGL'): self.makeCurrent() - item.initializeGL() + try: + item.initializeGL() + except: + self.checkOpenGLVersion('Error while adding item %s to GLViewWidget.' % str(item)) + item._setView(self) #print "set view", item, self, item.view() self.update() @@ -100,9 +106,15 @@ class GLViewWidget(QtOpenGL.QGLWidget): glPushAttrib(GL_ALL_ATTRIB_BITS) i.paint() except: - import sys - sys.excepthook(*sys.exc_info()) - print("Error while drawing item", i) + import pyqtgraph.debug + pyqtgraph.debug.printExc() + msg = "Error while drawing item %s." % str(item) + ver = glGetString(GL_VERSION).split()[0] + if int(ver.split('.')[0]) < 2: + print(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) + else: + print(msg) + finally: glPopAttrib(GL_ALL_ATTRIB_BITS) else: @@ -237,4 +249,15 @@ class GLViewWidget(QtOpenGL.QGLWidget): else: self.keyTimer.stop() + def checkOpenGLVersion(self, msg): + ## Only to be called from within exception handler. + ver = glGetString(GL_VERSION).split()[0] + if int(ver.split('.')[0]) < 2: + import pyqtgraph.debug + pyqtgraph.debug.printExc() + raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) + else: + raise + + \ No newline at end of file diff --git a/opengl/glInfo.py b/opengl/glInfo.py new file mode 100644 index 00000000..95f59630 --- /dev/null +++ b/opengl/glInfo.py @@ -0,0 +1,16 @@ +from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL +from OpenGL.GL import * +app = QtGui.QApplication([]) + +class GLTest(QtOpenGL.QGLWidget): + def __init__(self): + QtOpenGL.QGLWidget.__init__(self) + self.makeCurrent() + print "GL version:", glGetString(GL_VERSION) + print "MAX_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_TEXTURE_SIZE) + print "MAX_3D_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE) + print "Extensions:", glGetString(GL_EXTENSIONS) + +GLTest() + + diff --git a/opengl/items/GLVolumeItem.py b/opengl/items/GLVolumeItem.py index a16dedb4..9981f4ba 100644 --- a/opengl/items/GLVolumeItem.py +++ b/opengl/items/GLVolumeItem.py @@ -43,6 +43,11 @@ class GLVolumeItem(GLGraphicsItem): glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) shape = self.data.shape + ## Test texture dimensions first + glTexImage3D(GL_PROXY_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, None) + if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_3D, 0, GL_TEXTURE_WIDTH) == 0: + raise Exception("OpenGL failed to create 3D texture (%dx%dx%d); too large for this hardware." % shape[:3]) + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data.transpose((2,1,0,3))) glDisable(GL_TEXTURE_3D) From f178919beed0fd84cfb1415516efb9af8ddbd3ec Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 9 Jul 2012 08:36:59 -0400 Subject: [PATCH 15/24] Python3 compatibility updates --- SRTTransform.py | 2 +- SRTTransform3D.py | 30 +++++++++++++++--------------- Vector.py | 2 +- console/CmdInput.py | 4 ++-- console/Console.py | 2 +- console/__init__.py | 2 +- console/template.py | 2 +- console/template.ui | 2 +- exceptionHandling.py | 8 ++++---- graphicsItems/ScatterPlotItem.py | 4 ++-- multiprocess/__init__.py | 6 +++--- parametertree/__main__.py | 2 +- 12 files changed, 33 insertions(+), 33 deletions(-) diff --git a/SRTTransform.py b/SRTTransform.py index d55d72cf..ebd66062 100644 --- a/SRTTransform.py +++ b/SRTTransform.py @@ -76,7 +76,7 @@ class SRTTransform(QtGui.QTransform): m = pg.SRTTransform3D(m) angle, axis = m.getRotation() if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1): - print angle, axis + print("angle: %s axis: %s" % (str(angle), str(axis))) raise Exception("Can only convert 4x4 matrix to 3x3 if rotation is around Z-axis.") self._state = { 'pos': Point(m.getTranslation()), diff --git a/SRTTransform3D.py b/SRTTransform3D.py index 8b093008..00e2b842 100644 --- a/SRTTransform3D.py +++ b/SRTTransform3D.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from Qt import QtCore, QtGui -from Vector import Vector -from SRTTransform import SRTTransform +from .Qt import QtCore, QtGui +from .Vector import Vector +from .SRTTransform import SRTTransform import pyqtgraph as pg import numpy as np import scipy.linalg @@ -136,15 +136,15 @@ class SRTTransform3D(QtGui.QMatrix4x4): try: evals, evecs = scipy.linalg.eig(r) except: - print "Rotation matrix:", r - print "Scale:", scale - print "Original matrix:", m + print("Rotation matrix: %s" % str(r)) + print("Scale: %s" % str(scale)) + print("Original matrix: %s" % str(m)) raise eigIndex = np.argwhere(np.abs(evals-1) < 1e-7) if len(eigIndex) < 1: - print "eigenvalues:", evals - print "eigenvectors:", evecs - print "index:", eigIndex, evals-1 + print("eigenvalues: %s" % str(evals)) + print("eigenvectors: %s" % str(evecs)) + print("index: %s, %s" % (str(eigIndex), str(evals-1))) raise Exception("Could not determine rotation axis.") axis = evecs[eigIndex[0,0]].real axis /= ((axis**2).sum())**0.5 @@ -259,23 +259,23 @@ if __name__ == '__main__': tr3 = QtGui.QTransform() tr3.translate(20, 0) tr3.rotate(45) - print "QTransform -> Transform:", SRTTransform(tr3) + print("QTransform -> Transform: %s" % str(SRTTransform(tr3))) - print "tr1:", tr1 + print("tr1: %s" % str(tr1)) tr2.translate(20, 0) tr2.rotate(45) - print "tr2:", tr2 + print("tr2: %s" % str(tr2)) dt = tr2/tr1 - print "tr2 / tr1 = ", dt + print("tr2 / tr1 = %s" % str(dt)) - print "tr2 * tr1 = ", tr2*tr1 + print("tr2 * tr1 = %s" % str(tr2*tr1)) tr4 = SRTTransform() tr4.scale(-1, 1) tr4.rotate(30) - print "tr1 * tr4 = ", tr1*tr4 + print("tr1 * tr4 = %s" % str(tr1*tr4)) w1 = widgets.TestROI((19,19), (22, 22), invertible=True) #w2 = widgets.TestROI((0,0), (150, 150)) diff --git a/Vector.py b/Vector.py index 3b0258ca..79da3162 100644 --- a/Vector.py +++ b/Vector.py @@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from Qt import QtGui, QtCore +from .Qt import QtGui, QtCore import numpy as np class Vector(QtGui.QVector3D): diff --git a/console/CmdInput.py b/console/CmdInput.py index 70017289..8db52e72 100644 --- a/console/CmdInput.py +++ b/console/CmdInput.py @@ -25,10 +25,10 @@ class CmdInput(QtGui.QLineEdit): self.execCmd() else: QtGui.QLineEdit.keyPressEvent(self, ev) - self.history[0] = unicode(self.text()) + self.history[0] = asUnicode(self.text()) def execCmd(self): - cmd = unicode(self.text()) + cmd = asUnicode(self.text()) if len(self.history) == 1 or cmd != self.history[1]: self.history.insert(1, cmd) #self.lastCmd = cmd diff --git a/console/Console.py b/console/Console.py index 27e660be..342af9ce 100644 --- a/console/Console.py +++ b/console/Console.py @@ -2,7 +2,7 @@ from pyqtgraph.Qt import QtCore, QtGui import sys, re, os, time, traceback, subprocess import pyqtgraph as pg -import template +from . import template import pyqtgraph.exceptionHandling as exceptionHandling import pickle diff --git a/console/__init__.py b/console/__init__.py index 7288b8d7..16436abd 100644 --- a/console/__init__.py +++ b/console/__init__.py @@ -1 +1 @@ -from Console import ConsoleWidget \ No newline at end of file +from .Console import ConsoleWidget \ No newline at end of file diff --git a/console/template.py b/console/template.py index c8e5ebbc..42e9e6ed 100644 --- a/console/template.py +++ b/console/template.py @@ -101,4 +101,4 @@ class Ui_Form(object): self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8)) -from CmdInput import CmdInput +from .CmdInput import CmdInput diff --git a/console/template.ui b/console/template.ui index ed734b52..42adfaad 100644 --- a/console/template.ui +++ b/console/template.ui @@ -153,7 +153,7 @@ CmdInput QLineEdit -
CmdInput
+
.CmdInput
diff --git a/exceptionHandling.py b/exceptionHandling.py index 0f94e34a..71d538c6 100644 --- a/exceptionHandling.py +++ b/exceptionHandling.py @@ -51,16 +51,16 @@ class ExceptionHandler: def __call__(self, *args): ## call original exception handler first (prints exception) global original_excepthook, callbacks, clear_tracebacks - print "=====", time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time())), "=====" + print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time())))) ret = original_excepthook(*args) for cb in callbacks: try: cb(*args) except: - print " --------------------------------------------------------------" - print " Error occurred during exception callback", cb - print " --------------------------------------------------------------" + print(" --------------------------------------------------------------") + print(" Error occurred during exception callback %s" % str(cb)) + print(" --------------------------------------------------------------") traceback.print_exception(*sys.exc_info()) diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index d1ad2a68..3b224cf0 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -79,7 +79,7 @@ class ScatterPlotItem(GraphicsObject): prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True) GraphicsObject.__init__(self) self.setFlag(self.ItemHasNoContents, True) - self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('item', object), ('data', object)]) + self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('item', object), ('data', object)]) self.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots @@ -559,7 +559,7 @@ class SpotItem(GraphicsItem): If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. """ symbol = self._data['symbol'] - if symbol == '': + if symbol is None: symbol = self._plot.opts['symbol'] try: n = int(symbol) diff --git a/multiprocess/__init__.py b/multiprocess/__init__.py index 7dcf61a5..843b42a3 100644 --- a/multiprocess/__init__.py +++ b/multiprocess/__init__.py @@ -19,6 +19,6 @@ TODO: (RemoteGraphicsView class) """ -from processes import * -from parallelizer import Parallelize, CanceledError -from remoteproxy import proxy \ No newline at end of file +from .processes import * +from .parallelizer import Parallelize, CanceledError +from .remoteproxy import proxy \ No newline at end of file diff --git a/parametertree/__main__.py b/parametertree/__main__.py index c1b3ad09..95bc9b70 100644 --- a/parametertree/__main__.py +++ b/parametertree/__main__.py @@ -6,7 +6,7 @@ md = os.path.abspath(os.path.dirname(__file__)) sys.path.append(os.path.join(md, '..', '..')) from pyqtgraph.Qt import QtCore, QtGui -import collections, user +import collections app = QtGui.QApplication([]) import pyqtgraph.parametertree.parameterTypes as pTypes from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType From 662b319d7b707597b89e11b1cfee99e8b062f649 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 9 Jul 2012 08:38:30 -0400 Subject: [PATCH 16/24] - PlotItem can now be constructed with customized ViewBox and AxisItems - Text spacing fix for AxisItem --- GraphicsScene/GraphicsScene.py | 7 +- __init__.py | 12 +- examples/__main__.py | 3 +- examples/customPlot.py | 52 +++ graphicsItems/AxisItem.py | 28 +- graphicsItems/PlotItem/PlotItem.py | 583 ++++------------------------- graphicsItems/ViewBox/ViewBox.py | 11 +- widgets/PlotWidget.py | 2 + 8 files changed, 168 insertions(+), 530 deletions(-) create mode 100644 examples/customPlot.py diff --git a/GraphicsScene/GraphicsScene.py b/GraphicsScene/GraphicsScene.py index 73867059..d4dd8534 100644 --- a/GraphicsScene/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -505,18 +505,19 @@ class GraphicsScene(QtGui.QGraphicsScene): menusToAdd = [] while item is not self: item = item.parentItem() - + if item is None: item = self if not hasattr(item, "getContextMenus"): continue - subMenus = item.getContextMenus(event) + if subMenus is None: + continue if type(subMenus) is not list: ## so that some items (like FlowchartViewBox) can return multiple menus subMenus = [subMenus] - + for sm in subMenus: menusToAdd.append(sm) diff --git a/__init__.py b/__init__.py index 756e46e8..b31f15d0 100644 --- a/__init__.py +++ b/__init__.py @@ -49,10 +49,10 @@ def getConfigOption(opt): def systemInfo(): - print "sys.platform:", sys.platform - print "sys.version:", sys.version + print("sys.platform: %s" % sys.platform) + print("sys.version: %s" % sys.version) from .Qt import VERSION_INFO - print "qt bindings:", VERSION_INFO + print("qt bindings: %s" % VERSION_INFO) global REVISION if REVISION is None: ## this code was probably checked out from bzr; look up the last-revision file @@ -60,8 +60,8 @@ def systemInfo(): if os.path.exists(lastRevFile): REVISION = open(lastRevFile, 'r').read().strip() - print "pyqtgraph:", REVISION - print "config:" + print("pyqtgraph: %s" % REVISION) + print("config:") import pprint pprint.pprint(CONFIG_OPTIONS) @@ -126,7 +126,7 @@ def importAll(path, excludes=()): globals()[k] = getattr(mod, k) importAll('graphicsItems') -importAll('widgets', excludes=['MatplotlibWidget']) +importAll('widgets', excludes=['MatplotlibWidget', 'RemoteGraphicsView']) from .imageview import * from .WidgetGroup import * diff --git a/examples/__main__.py b/examples/__main__.py index 0881fcea..39c51b7f 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -16,6 +16,7 @@ examples = OrderedDict([ ('Video speed test', 'VideoSpeedTest.py'), ('Plot speed test', 'PlotSpeedTest.py'), ('Data Slicing', 'DataSlicing.py'), + ('Plot Customization', 'customPlot.py'), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), @@ -45,7 +46,7 @@ examples = OrderedDict([ #('VerticalLabel', '../widgets/VerticalLabel.py'), ('JoystickButton', 'JoystickButton.py'), ])), - + ('GraphicsScene', 'GraphicsScene.py'), ('Flowcharts', 'Flowchart.py'), #('Canvas', '../canvas'), diff --git a/examples/customPlot.py b/examples/customPlot.py new file mode 100644 index 00000000..86ed4829 --- /dev/null +++ b/examples/customPlot.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +## +## This example demonstrates the creation of a plot with a customized +## AxisItem and ViewBox. +## + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +import time + +class DateAxis(pg.AxisItem): + def tickStrings(self, values, scale, spacing): + return [time.strftime('%b %Y', time.localtime(x)) for x in values] + +class CustomViewBox(pg.ViewBox): + def __init__(self, *args, **kwds): + pg.ViewBox.__init__(self, *args, **kwds) + self.setMouseMode(self.RectMode) + + ## reimplement right-click to zoom out + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + self.autoRange() + + def mouseDragEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + ev.ignore() + else: + pg.ViewBox.mouseDragEvent(self, ev) + + +app = pg.mkQApp() + +axis = DateAxis(orientation='bottom') +vb = CustomViewBox() + +pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with custom axis and ViewBox
Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom") +dates = np.arange(8) * (3600*24*356) +pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o') +pw.show() + +r = pg.PolyLineROI([(0,0), (10, 10)]) +pw.addItem(r) + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 351b4ed5..bcf6ce5c 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -363,6 +363,29 @@ class AxisItem(GraphicsWidget): (intervals[minorIndex], 0) ] + ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection + ### Determine major/minor tick spacings which flank the optimal spacing. + #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit + #minorIndex = 0 + #while intervals[minorIndex+1] <= optimalSpacing: + #minorIndex += 1 + + ### make sure we never see 5 and 2 at the same time + #intIndexes = [ + #[0,1,3], + #[0,2,3], + #[2,3,4], + #[3,4,6], + #[3,5,6], + #][minorIndex] + + #return [ + #(intervals[intIndexes[2]], 0), + #(intervals[intIndexes[1]], 0), + #(intervals[intIndexes[0]], 0) + #] + + def tickValues(self, minVal, maxVal, size): """ @@ -395,7 +418,7 @@ class AxisItem(GraphicsWidget): ## remove any ticks that were present in higher levels ## we assume here that if the difference between a tick value and a previously seen tick value ## is less than spacing/100, then they are 'equal' and we can ignore the new tick. - values = filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) + values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) ) allValues = np.concatenate([allValues, values]) ticks.append((spacing, values)) @@ -601,9 +624,9 @@ class AxisItem(GraphicsWidget): if tickPositions[i][j] is None: strings[j] = None + textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None]) if i > 0: ## always draw top level ## measure all text, make sure there's enough room - textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None]) if axis == 0: textSize = np.sum([r.height() for r in textRects]) else: @@ -613,7 +636,6 @@ class AxisItem(GraphicsWidget): textFillRatio = float(textSize) / lengthInPixels if textFillRatio > 0.7: break - #spacing, values = tickLevels[best] #strings = self.tickStrings(values, self.scale, spacing) for j in range(len(strings)): diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index f027a434..dfd0ad65 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -33,17 +33,12 @@ from .. AxisItem import AxisItem from .. LabelItem import LabelItem from .. GraphicsWidget import GraphicsWidget from .. ButtonItem import ButtonItem +#from .. GraphicsLayout import GraphicsLayout from pyqtgraph.WidgetGroup import WidgetGroup import collections __all__ = ['PlotItem'] -#try: - #from WidgetGroup import * - #HAVE_WIDGETGROUP = True -#except: - #HAVE_WIDGETGROUP = False - try: from metaarray import * HAVE_METAARRAY = True @@ -99,26 +94,28 @@ class PlotItem(GraphicsWidget): lastFileDir = None managers = {} - def __init__(self, parent=None, name=None, labels=None, title=None, **kargs): + def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): """ Create a new PlotItem. All arguments are optional. Any extra keyword arguments are passed to PlotItem.plot(). - ============= ========================================================================================== + ============== ========================================================================================== **Arguments** - *title* Title to display at the top of the item. Html is allowed. - *labels* A dictionary specifying the axis labels to display:: + *title* Title to display at the top of the item. Html is allowed. + *labels* A dictionary specifying the axis labels to display:: - {'left': (args), 'bottom': (args), ...} + {'left': (args), 'bottom': (args), ...} - The name of each axis and the corresponding arguments are passed to - :func:`PlotItem.setLabel() ` - Optionally, PlotItem my also be initialized with the keyword arguments left, - right, top, or bottom to achieve the same effect. - *name* Registers a name for this view so that others may link to it - ============= ========================================================================================== - - + The name of each axis and the corresponding arguments are passed to + :func:`PlotItem.setLabel() ` + Optionally, PlotItem my also be initialized with the keyword arguments left, + right, top, or bottom to achieve the same effect. + *name* Registers a name for this view so that others may link to it + *viewBox* If specified, the PlotItem will be constructed with this as its ViewBox. + *axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items + for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top') + and the values must be instances of AxisItem (or at least compatible with AxisItem). + ============== ========================================================================================== """ GraphicsWidget.__init__(self, parent) @@ -127,8 +124,6 @@ class PlotItem(GraphicsWidget): ## Set up control buttons path = os.path.dirname(__file__) - #self.ctrlBtn = ButtonItem(os.path.join(path, 'ctrl.png'), 14, self) - #self.ctrlBtn.clicked.connect(self.ctrlBtnClicked) self.autoImageFile = os.path.join(path, 'auto.png') self.lockImageFile = os.path.join(path, 'lock.png') self.autoBtn = ButtonItem(self.autoImageFile, 14, self) @@ -141,32 +136,33 @@ class PlotItem(GraphicsWidget): self.layout.setHorizontalSpacing(0) self.layout.setVerticalSpacing(0) - self.vb = ViewBox(name=name) + if viewBox is None: + viewBox = ViewBox() + self.vb = viewBox + self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus + + if name is not None: + self.vb.register(name) self.vb.sigRangeChanged.connect(self.sigRangeChanged) self.vb.sigXRangeChanged.connect(self.sigXRangeChanged) self.vb.sigYRangeChanged.connect(self.sigYRangeChanged) - #self.vb.sigRangeChangedManually.connect(self.enableManualScale) - #self.vb.sigRangeChanged.connect(self.viewRangeChanged) self.layout.addItem(self.vb, 2, 1) self.alpha = 1.0 self.autoAlpha = True self.spectrumMode = False - #self.autoScale = [True, True] - - ## Create and place scale items - self.scales = { - 'top': {'item': AxisItem(orientation='top', linkView=self.vb), 'pos': (1, 1)}, - 'bottom': {'item': AxisItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)}, - 'left': {'item': AxisItem(orientation='left', linkView=self.vb), 'pos': (2, 0)}, - 'right': {'item': AxisItem(orientation='right', linkView=self.vb), 'pos': (2, 2)} - } - for k in self.scales: - item = self.scales[k]['item'] - self.layout.addItem(item, *self.scales[k]['pos']) - item.setZValue(-1000) - item.setFlag(item.ItemNegativeZStacksBehindParent) + ## Create and place axis items + if axisItems is None: + axisItems = {} + self.axes = {} + for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): + axis = axisItems.get(k, AxisItem(orientation=k)) + axis.linkToView(self.vb) + self.axes[k] = {'item': axis, 'pos': pos} + self.layout.addItem(axis, *pos) + axis.setZValue(-1000) + axis.setFlag(axis.ItemNegativeZStacksBehindParent) self.titleLabel = LabelItem('', size='11pt') self.layout.addItem(self.titleLabel, 0, 1) @@ -193,7 +189,6 @@ class PlotItem(GraphicsWidget): 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', - 'setMenuEnabled', 'menuEnabled', 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. setattr(self, m, getattr(self.vb, m)) @@ -233,45 +228,12 @@ class PlotItem(GraphicsWidget): self.subMenus.append(sm) self.ctrlMenu.addMenu(sm) - ## exporting is handled by GraphicsScene now - #exportOpts = collections.OrderedDict([ - #('SVG - Full Plot', self.saveSvgClicked), - #('SVG - Curves Only', self.saveSvgCurvesClicked), - #('Image', self.saveImgClicked), - #('CSV', self.saveCsvClicked), - #]) - - #self.vb.menu.setExportMethods(exportOpts) - - - #if HAVE_WIDGETGROUP: self.stateGroup = WidgetGroup() for name, w in menuItems: self.stateGroup.autoAdd(w) self.fileDialog = None - #self.xLinkPlot = None - #self.yLinkPlot = None - #self.linksBlocked = False - - #self.setAcceptHoverEvents(True) - - ## Connect control widgets - #c.xMinText.editingFinished.connect(self.setManualXScale) - #c.xMaxText.editingFinished.connect(self.setManualXScale) - #c.yMinText.editingFinished.connect(self.setManualYScale) - #c.yMaxText.editingFinished.connect(self.setManualYScale) - - #c.xManualRadio.clicked.connect(lambda: self.updateXScale()) - #c.yManualRadio.clicked.connect(lambda: self.updateYScale()) - - #c.xAutoRadio.clicked.connect(self.updateXScale) - #c.yAutoRadio.clicked.connect(self.updateYScale) - - #c.xAutoPercentSpin.valueChanged.connect(self.replot) - #c.yAutoPercentSpin.valueChanged.connect(self.replot) - c.alphaGroup.toggled.connect(self.updateAlpha) c.alphaSlider.valueChanged.connect(self.updateAlpha) c.autoAlphaCheck.toggled.connect(self.updateAlpha) @@ -283,13 +245,6 @@ class PlotItem(GraphicsWidget): c.fftCheck.toggled.connect(self.updateSpectrumMode) c.logXCheck.toggled.connect(self.updateLogMode) c.logYCheck.toggled.connect(self.updateLogMode) - #c.saveSvgBtn.clicked.connect(self.saveSvgClicked) - #c.saveSvgCurvesBtn.clicked.connect(self.saveSvgCurvesClicked) - #c.saveImgBtn.clicked.connect(self.saveImgClicked) - #c.saveCsvBtn.clicked.connect(self.saveCsvClicked) - - #self.ctrl.xLinkCombo.currentIndexChanged.connect(self.xLinkComboChanged) - #self.ctrl.yLinkCombo.currentIndexChanged.connect(self.yLinkComboChanged) c.downsampleSpin.valueChanged.connect(self.updateDownsampling) @@ -298,24 +253,15 @@ class PlotItem(GraphicsWidget): self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - #c.xMouseCheck.toggled.connect(self.mouseCheckChanged) - #c.yMouseCheck.toggled.connect(self.mouseCheckChanged) - - #self.xLinkPlot = None - #self.yLinkPlot = None - #self.linksBlocked = False - self.manager = None self.hideAxis('right') self.hideAxis('top') self.showAxis('left') self.showAxis('bottom') - #if name is not None: - #self.registerPlot(name) if labels is None: labels = {} - for label in list(self.scales.keys()): + for label in list(self.axes.keys()): if label in kargs: labels[label] = kargs[label] del kargs[label] @@ -330,15 +276,16 @@ class PlotItem(GraphicsWidget): if len(kargs) > 0: self.plot(**kargs) - #self.enableAutoRange() def implements(self, interface=None): return interface in ['ViewBoxWrapper'] def getViewBox(self): - """Return the ViewBox within.""" + """Return the :class:`ViewBox ` contained within.""" return self.vb + + def setLogMode(self, x, y): """ Set log scaling for x and y axes. @@ -399,11 +346,11 @@ class PlotItem(GraphicsWidget): #self.autoBtn.setParent(None) #self.autoBtn = None - for k in self.scales: - i = self.scales[k]['item'] + for k in self.axes: + i = self.axes[k]['item'] i.close() - self.scales = None + self.axes = None self.scene().removeItem(self.vb) self.vb = None @@ -431,47 +378,6 @@ class PlotItem(GraphicsWidget): def registerPlot(self, name): ## for backward compatibility self.vb.register(name) - #self.name = name - #win = str(self.window()) - ##print "register", name, win - #if win not in PlotItem.managers: - #PlotItem.managers[win] = PlotWidgetManager() - #self.manager = PlotItem.managers[win] - #self.manager.addWidget(self, name) - ##QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList) - #self.manager.sigWidgetListChanged.connect(self.updatePlotList) - #self.updatePlotList() - - #def updatePlotList(self): - #"""Update the list of all plotWidgets in the "link" combos""" - ##print "update plot list", self - #try: - #for sc in [self.ctrl.xLinkCombo, self.ctrl.yLinkCombo]: - #current = unicode(sc.currentText()) - #sc.blockSignals(True) - #try: - #sc.clear() - #sc.addItem("") - #if self.manager is not None: - #for w in self.manager.listWidgets(): - ##print w - #if w == self.name: - #continue - #sc.addItem(w) - #if w == current: - #sc.setCurrentIndex(sc.count()-1) - #finally: - #sc.blockSignals(False) - #if unicode(sc.currentText()) != current: - #sc.currentItemChanged.emit() - #except: - #import gc - #refs= gc.get_referrers(self) - #print " error during update of", self - #print " Referrers are:", refs - #raise - - def updateGrid(self, *args): alpha = self.ctrl.gridAlphaSlider.value() @@ -492,91 +398,6 @@ class PlotItem(GraphicsWidget): return wr - - - - #def viewRangeChanged(self, vb, range): - ##self.emit(QtCore.SIGNAL('viewChanged'), *args) - #self.sigRangeChanged.emit(self, range) - - #def blockLink(self, b): - #self.linksBlocked = b - - #def xLinkComboChanged(self): - #self.setXLink(str(self.ctrl.xLinkCombo.currentText())) - - #def yLinkComboChanged(self): - #self.setYLink(str(self.ctrl.yLinkCombo.currentText())) - - #def setXLink(self, plot=None): - #"""Link this plot's X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" - #if isinstance(plot, basestring): - #if self.manager is None: - #return - #if self.xLinkPlot is not None: - #self.manager.unlinkX(self, self.xLinkPlot) - #plot = self.manager.getWidget(plot) - #if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): - #plot = plot.getPlotItem() - #self.xLinkPlot = plot - #if plot is not None: - #self.setManualXScale() - #self.manager.linkX(self, plot) - - - - #def setYLink(self, plot=None): - #"""Link this plot's Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" - #if isinstance(plot, basestring): - #if self.manager is None: - #return - #if self.yLinkPlot is not None: - #self.manager.unlinkY(self, self.yLinkPlot) - #plot = self.manager.getWidget(plot) - #if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): - #plot = plot.getPlotItem() - #self.yLinkPlot = plot - #if plot is not None: - #self.setManualYScale() - #self.manager.linkY(self, plot) - - #def linkXChanged(self, plot): - #"""Called when a linked plot has changed its X scale""" - ##print "update from", plot - #if self.linksBlocked: - #return - #pr = plot.vb.viewRect() - #pg = plot.viewGeometry() - #if pg is None: - ##print " return early" - #return - #sg = self.viewGeometry() - #upp = float(pr.width()) / pg.width() - #x1 = pr.left() + (sg.x()-pg.x()) * upp - #x2 = x1 + sg.width() * upp - #plot.blockLink(True) - #self.setManualXScale() - #self.setXRange(x1, x2, padding=0) - #plot.blockLink(False) - #self.replot() - - #def linkYChanged(self, plot): - #"""Called when a linked plot has changed its Y scale""" - #if self.linksBlocked: - #return - #pr = plot.vb.viewRect() - #pg = plot.vb.boundingRect() - #sg = self.vb.boundingRect() - #upp = float(pr.height()) / pg.height() - #y1 = pr.bottom() + (sg.y()-pg.y()) * upp - #y2 = y1 + sg.height() * upp - #plot.blockLink(True) - #self.setManualYScale() - #self.setYRange(y1, y2, padding=0) - #plot.blockLink(False) - #self.replot() - - def avgToggled(self, b): if b: self.recomputeAverages() @@ -650,50 +471,6 @@ class PlotItem(GraphicsWidget): else: plot.setData(x, y) - - #def mouseCheckChanged(self): - #state = [self.ctrl.xMouseCheck.isChecked(), self.ctrl.yMouseCheck.isChecked()] - #self.vb.setMouseEnabled(*state) - - #def xRangeChanged(self, _, range): - #if any(np.isnan(range)) or any(np.isinf(range)): - #raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) - #self.ctrl.xMinText.setText('%0.5g' % range[0]) - #self.ctrl.xMaxText.setText('%0.5g' % range[1]) - - ### automatically change unit scale - #maxVal = max(abs(range[0]), abs(range[1])) - #(scale, prefix) = fn.siScale(maxVal) - ##for l in ['top', 'bottom']: - ##if self.getLabel(l).isVisible(): - ##self.setLabel(l, unitPrefix=prefix) - ##self.getScale(l).setScale(scale) - ##else: - ##self.setLabel(l, unitPrefix='') - ##self.getScale(l).setScale(1.0) - - ##self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) - #self.sigXRangeChanged.emit(self, range) - - #def yRangeChanged(self, _, range): - #if any(np.isnan(range)) or any(np.isinf(range)): - #raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) - #self.ctrl.yMinText.setText('%0.5g' % range[0]) - #self.ctrl.yMaxText.setText('%0.5g' % range[1]) - - ### automatically change unit scale - #maxVal = max(abs(range[0]), abs(range[1])) - #(scale, prefix) = fn.siScale(maxVal) - ##for l in ['left', 'right']: - ##if self.getLabel(l).isVisible(): - ##self.setLabel(l, unitPrefix=prefix) - ##self.getScale(l).setScale(scale) - ##else: - ##self.setLabel(l, unitPrefix='') - ##self.getScale(l).setScale(1.0) - ##self.emit(QtCore.SIGNAL('yRangeChanged'), self, range) - #self.sigYRangeChanged.emit(self, range) - def autoBtnClicked(self): if self.autoBtn.mode == 'auto': self.enableAutoRange() @@ -706,72 +483,6 @@ class PlotItem(GraphicsWidget): """ print("Warning: enableAutoScale is deprecated. Use enableAutoRange(axis, enable) instead.") self.vb.enableAutoRange(self.vb.XYAxes) - #self.ctrl.xAutoRadio.setChecked(True) - #self.ctrl.yAutoRadio.setChecked(True) - - #self.autoBtn.setImageFile(self.lockImageFile) - #self.autoBtn.mode = 'lock' - #self.updateXScale() - #self.updateYScale() - #self.replot() - - #def updateXScale(self): - #"""Set plot to autoscale or not depending on state of radio buttons""" - #if self.ctrl.xManualRadio.isChecked(): - #self.setManualXScale() - #else: - #self.setAutoXScale() - #self.replot() - - #def updateYScale(self, b=False): - #"""Set plot to autoscale or not depending on state of radio buttons""" - #if self.ctrl.yManualRadio.isChecked(): - #self.setManualYScale() - #else: - #self.setAutoYScale() - #self.replot() - - #def enableManualScale(self, v=[True, True]): - #if v[0]: - #self.autoScale[0] = False - #self.ctrl.xManualRadio.setChecked(True) - ##self.setManualXScale() - #if v[1]: - #self.autoScale[1] = False - #self.ctrl.yManualRadio.setChecked(True) - ##self.setManualYScale() - ##self.autoBtn.enable() - #self.autoBtn.setImageFile(self.autoImageFile) - #self.autoBtn.mode = 'auto' - ##self.replot() - - #def setManualXScale(self): - #self.autoScale[0] = False - #x1 = float(self.ctrl.xMinText.text()) - #x2 = float(self.ctrl.xMaxText.text()) - #self.ctrl.xManualRadio.setChecked(True) - #self.setXRange(x1, x2, padding=0) - #self.autoBtn.show() - ##self.replot() - - #def setManualYScale(self): - #self.autoScale[1] = False - #y1 = float(self.ctrl.yMinText.text()) - #y2 = float(self.ctrl.yMaxText.text()) - #self.ctrl.yManualRadio.setChecked(True) - #self.setYRange(y1, y2, padding=0) - #self.autoBtn.show() - ##self.replot() - - #def setAutoXScale(self): - #self.autoScale[0] = True - #self.ctrl.xAutoRadio.setChecked(True) - ##self.replot() - - #def setAutoYScale(self): - #self.autoScale[1] = True - #self.ctrl.yAutoRadio.setChecked(True) - ##self.replot() def addItem(self, item, *args, **kargs): """ @@ -867,17 +578,6 @@ class PlotItem(GraphicsWidget): """ - - #if y is not None: - #data = y - #if data2 is not None: - #x = data - #data = data2 - #if decimate is not None and decimate > 1: - #data = data[::decimate] - #if x is not None: - #x = x[::decimate] - ## print 'plot with decimate = %d' % (decimate) clear = kargs.get('clear', False) params = kargs.get('params', None) @@ -888,23 +588,7 @@ class PlotItem(GraphicsWidget): if params is None: params = {} - #if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): - #curve = self._plotMetaArray(data, x=x, **kargs) - #elif isinstance(data, np.ndarray): - #curve = self._plotArray(data, x=x, **kargs) - #elif isinstance(data, list): - #if x is not None: - #x = np.array(x) - #curve = self._plotArray(np.array(data), x=x, **kargs) - #elif data is None: - #curve = PlotCurveItem(**kargs) - #else: - #raise Exception('Not sure how to plot object of type %s' % type(data)) - - #print data, curve self.addItem(item, params=params) - #if pen is not None: - #curve.setPen(fn.mkPen(pen)) return item @@ -922,80 +606,34 @@ class PlotItem(GraphicsWidget): del kargs['size'] return self.plot(*args, **kargs) - #sp = ScatterPlotItem(*args, **kargs) - #self.addItem(sp) - #return sp - - - - #def plotChanged(self, curve=None): - ## Recompute auto range if needed - #args = {} - #for ax in [0, 1]: - #print "range", ax - #if self.autoScale[ax]: - #percentScale = [self.ctrl.xAutoPercentSpin.value(), self.ctrl.yAutoPercentSpin.value()][ax] * 0.01 - #mn = None - #mx = None - #for c in self.curves + [c[1] for c in self.avgCurves.values()] + self.dataItems: - #if not c.isVisible(): - #continue - #cmn, cmx = c.getRange(ax, percentScale) - ##print " ", c, cmn, cmx - #if mn is None or cmn < mn: - #mn = cmn - #if mx is None or cmx > mx: - #mx = cmx - #if mn is None or mx is None or any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])): - #continue - #if mn == mx: - #mn -= 1 - #mx += 1 - #if ax == 0: - #args['xRange'] = [mn, mx] - #else: - #args['yRange'] = [mn, mx] - - #if len(args) > 0: - ##print args - #self.setRange(**args) def replot(self): - #self.plotChanged() self.update() def updateParamList(self): self.ctrl.avgParamList.clear() ## Check to see that each parameter for each curve is present in the list - #print "\nUpdate param list", self - #print "paramList:", self.paramList for c in self.curves: - #print " curve:", c for p in list(self.itemMeta.get(c, {}).keys()): - #print " param:", p if type(p) is tuple: p = '.'.join(p) ## If the parameter is not in the list, add it. matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly) - #print " matches:", matches if len(matches) == 0: i = QtGui.QListWidgetItem(p) if p in self.paramList and self.paramList[p] is True: - #print " set checked" i.setCheckState(QtCore.Qt.Checked) else: - #print " set unchecked" i.setCheckState(QtCore.Qt.Unchecked) self.ctrl.avgParamList.addItem(i) else: i = matches[0] self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) - #print "paramList:", self.paramList - ## This is bullshit. + ## Qt's SVG-writing capabilities are pretty terrible. def writeSvgCurves(self, fileName=None): if fileName is None: self.fileDialog = FileDialog() @@ -1190,18 +828,12 @@ class PlotItem(GraphicsWidget): def saveState(self): - #if not HAVE_WIDGETGROUP: - #raise Exception("State save/restore requires WidgetGroup class.") state = self.stateGroup.state() state['paramList'] = self.paramList.copy() state['view'] = self.vb.getState() - #print "\nSAVE %s:\n" % str(self.name), state - #print "Saving state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup'])) return state def restoreState(self, state): - #if not HAVE_WIDGETGROUP: - #raise Exception("State save/restore requires WidgetGroup class.") if 'paramList' in state: self.paramList = state['paramList'].copy() @@ -1218,8 +850,6 @@ class PlotItem(GraphicsWidget): state['yGridCheck'] = state['gridGroup'] self.stateGroup.setState(state) - #self.updateXScale() - #self.updateYScale() self.updateParamList() if 'view' not in state: @@ -1232,13 +862,6 @@ class PlotItem(GraphicsWidget): } self.vb.setState(state['view']) - - #print "\nRESTORE %s:\n" % str(self.name), state - #print "Restoring state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup'])) - #avg = self.ctrl.averageGroup.isChecked() - #if avg != state['averageGroup']: - #print " WARNING: avgGroup is %s, should be %s" % (str(avg), str(state['averageGroup'])) - def widgetGroupInterface(self): return (None, PlotItem.saveState, PlotItem.restoreState) @@ -1269,8 +892,6 @@ class PlotItem(GraphicsWidget): for c in self.curves: c.setDownsampling(ds) self.recomputeAverages() - #for c in self.avgCurves.values(): - #c[1].setDownsampling(ds) def downsampleMode(self): @@ -1306,8 +927,6 @@ class PlotItem(GraphicsWidget): (alpha, auto) = self.alphaState() for c in self.curves: c.setAlpha(alpha**2, auto) - - #self.replot(autoRange=False) def alphaState(self): enabled = self.ctrl.alphaGroup.isChecked() @@ -1330,9 +949,6 @@ class PlotItem(GraphicsWidget): mode = False return mode - #def wheelEvent(self, ev): - ## disables default panning the whole scene by mousewheel - #ev.accept() def resizeEvent(self, ev): if self.autoBtn is None: ## already closed down @@ -1340,29 +956,42 @@ class PlotItem(GraphicsWidget): btnRect = self.mapRectFromItem(self.autoBtn, self.autoBtn.boundingRect()) y = self.size().height() - btnRect.height() self.autoBtn.setPos(0, y) - - #def hoverMoveEvent(self, ev): - #self.mousePos = ev.pos() - #self.mouseScreenPos = ev.screenPos() - - - #def ctrlBtnClicked(self): - #self.ctrlMenu.popup(self.mouseScreenPos) - + + def getMenu(self): return self.ctrlMenu def getContextMenus(self, event): ## called when another item is displaying its context menu; we get to add extras to the end of the menu. - return self.ctrlMenu - + if self.menuEnabled(): + return self.ctrlMenu + else: + return None + + def setMenuEnabled(self, enableMenu=True, enableViewBoxMenu='same'): + """ + Enable or disable the context menu for this PlotItem. + By default, the ViewBox's context menu will also be affected. + (use enableViewBoxMenu=None to leave the ViewBox unchanged) + """ + self._menuEnabled = enableMenu + if enableViewBoxMenu is None: + return + if enableViewBoxMenu is 'same': + enableViewBoxMenu = enableMenu + self.vb.setMenuEnabled(enableViewBoxMenu) + + def menuEnabled(self): + return self._menuEnabled + + def getLabel(self, key): pass def _checkScaleKey(self, key): - if key not in self.scales: - raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.scales.keys())))) + if key not in self.axes: + raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.axes.keys())))) def getScale(self, key): return self.getAxis(key) @@ -1371,7 +1000,7 @@ class PlotItem(GraphicsWidget): """Return the specified AxisItem. *name* should be 'left', 'bottom', 'top', or 'right'.""" self._checkScaleKey(name) - return self.scales[name]['item'] + return self.axes[name]['item'] def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args): """ @@ -1417,7 +1046,7 @@ class PlotItem(GraphicsWidget): axis must be one of 'left', 'bottom', 'right', or 'top' """ s = self.getScale(axis) - p = self.scales[axis]['pos'] + p = self.axes[axis]['pos'] if show: s.show() else: @@ -1454,7 +1083,6 @@ class PlotItem(GraphicsWidget): ## create curve try: xv = arr.xvals(0) - #print 'xvals:', xv except: if x is None: xv = np.arange(arr.shape[0]) @@ -1474,17 +1102,6 @@ class PlotItem(GraphicsWidget): return c - #def saveSvgClicked(self): - #self.writeSvg() - - #def saveSvgCurvesClicked(self): - #self.writeSvgCurves() - - #def saveImgClicked(self): - #self.writeImage() - - #def saveCsvClicked(self): - #self.writeCsv() def setExportMode(self, export, opts): if export: @@ -1492,63 +1109,3 @@ class PlotItem(GraphicsWidget): else: self.autoBtn.show() - -#class PlotWidgetManager(QtCore.QObject): - - #sigWidgetListChanged = QtCore.Signal(object) - - #"""Used for managing communication between PlotWidgets""" - #def __init__(self): - #QtCore.QObject.__init__(self) - #self.widgets = weakref.WeakValueDictionary() # Don't keep PlotWidgets around just because they are listed here - - #def addWidget(self, w, name): - #self.widgets[name] = w - ##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) - #self.sigWidgetListChanged.emit(self.widgets.keys()) - - #def removeWidget(self, name): - #if name in self.widgets: - #del self.widgets[name] - ##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) - #self.sigWidgetListChanged.emit(self.widgets.keys()) - #else: - #print "plot %s not managed" % name - - - #def listWidgets(self): - #return self.widgets.keys() - - #def getWidget(self, name): - #if name not in self.widgets: - #return None - #else: - #return self.widgets[name] - - #def linkX(self, p1, p2): - ##QtCore.QObject.connect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) - #p1.sigXRangeChanged.connect(p2.linkXChanged) - ##QtCore.QObject.connect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) - #p2.sigXRangeChanged.connect(p1.linkXChanged) - #p1.linkXChanged(p2) - ##p2.setManualXScale() - - #def unlinkX(self, p1, p2): - ##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) - #p1.sigXRangeChanged.disconnect(p2.linkXChanged) - ##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) - #p2.sigXRangeChanged.disconnect(p1.linkXChanged) - - #def linkY(self, p1, p2): - ##QtCore.QObject.connect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) - #p1.sigYRangeChanged.connect(p2.linkYChanged) - ##QtCore.QObject.connect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) - #p2.sigYRangeChanged.connect(p1.linkYChanged) - #p1.linkYChanged(p2) - ##p2.setManualYScale() - - #def unlinkY(self, p1, p2): - ##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) - #p1.sigYRangeChanged.disconnect(p2.linkYChanged) - ##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) - #p2.sigYRangeChanged.disconnect(p1.linkYChanged) diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 398b1e8f..694455e5 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -62,7 +62,7 @@ class ViewBox(GraphicsWidget): NamedViews = weakref.WeakValueDictionary() # name: ViewBox AllViews = weakref.WeakKeyDictionary() # ViewBox: None - def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu = True, name=None): + def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None): """ ============= ============================================================= **Arguments** @@ -136,7 +136,7 @@ class ViewBox(GraphicsWidget): ## Make scale box that is shown when dragging on the view self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) - self.rbScaleBox.setPen(fn.mkPen((255,0,0), width=1)) + self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100)) self.rbScaleBox.hide() self.addItem(self.rbScaleBox) @@ -358,7 +358,7 @@ class ViewBox(GraphicsWidget): changes[1] = yRange 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))) changed = [False, False] @@ -863,7 +863,10 @@ class ViewBox(GraphicsWidget): return self._menuCopy def getContextMenus(self, event): - return self.menu.subMenus() + if self.menuEnabled(): + return self.menu.subMenus() + else: + return None #return [self.getMenu(event)] diff --git a/widgets/PlotWidget.py b/widgets/PlotWidget.py index 82e4701d..1fa07f2a 100644 --- a/widgets/PlotWidget.py +++ b/widgets/PlotWidget.py @@ -41,6 +41,8 @@ class PlotWidget(GraphicsView): other methods, use :func:`getPlotItem `. """ def __init__(self, parent=None, **kargs): + """When initializing PlotWidget, all keyword arguments except *parent* are passed + to :func:`PlotItem.__init__() `.""" GraphicsView.__init__(self, parent) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.enableMouse(False) From 2213dea9d852cdedb227d665ef1705de2199ddf8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 9 Jul 2012 14:41:10 -0400 Subject: [PATCH 17/24] Bugfixes - AxisItem.setScale(1) works properly to disable auto-value-scaling - OpenGL fixes (stack overflow when drawing items, improper call to glPopAttrib) --- graphicsItems/AxisItem.py | 16 ++++++++-------- opengl/GLViewWidget.py | 16 +++++++++------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index bcf6ce5c..08639706 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -205,16 +205,14 @@ class AxisItem(GraphicsWidget): def setScale(self, scale=None): """ - Set the value scaling for this axis. - The scaling value 1) multiplies the values displayed along the axis - and 2) changes the way units are displayed in the label. + Set the value scaling for this axis. Values on the axis are multiplied + by this scale factor before being displayed as text. By default, + this scaling value is automatically determined based on the visible range + and the axis units are updated to reflect the chosen scale factor. For example: If the axis spans values from -0.1 to 0.1 and has units set to 'V' then a scale of 1000 would cause the axis to display values -100 to 100 and the units would appear as 'mV' - - If scale is None, then it will be determined automatically based on the current - range displayed by the axis. """ if scale is None: #if self.drawLabel: ## If there is a label, then we are free to rescale the values @@ -228,8 +226,10 @@ class AxisItem(GraphicsWidget): self.setLabel(unitPrefix=prefix) else: scale = 1.0 - - + else: + self.setLabel(unitPrefix='') + self.autoScale = False + if scale != self.scale: self.scale = scale self.setLabel() diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py index 6f636e0d..e0465793 100644 --- a/opengl/GLViewWidget.py +++ b/opengl/GLViewWidget.py @@ -116,16 +116,18 @@ class GLViewWidget(QtOpenGL.QGLWidget): print(msg) finally: - glPopAttrib(GL_ALL_ATTRIB_BITS) + glPopAttrib() else: glMatrixMode(GL_MODELVIEW) glPushMatrix() - tr = i.transform() - a = np.array(tr.copyDataTo()).reshape((4,4)) - glMultMatrixf(a.transpose()) - self.drawItemTree(i) - glMatrixMode(GL_MODELVIEW) - glPopMatrix() + try: + tr = i.transform() + a = np.array(tr.copyDataTo()).reshape((4,4)) + glMultMatrixf(a.transpose()) + self.drawItemTree(i) + finally: + glMatrixMode(GL_MODELVIEW) + glPopMatrix() def cameraPosition(self): From 4384944952a919ef7f9a79e039fab92e60dfc4f6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 9 Jul 2012 17:14:41 -0400 Subject: [PATCH 18/24] Cleaned up parametertree example --- examples/__main__.py | 2 +- examples/parametertree.py | 160 ++++++++++++++++++++++++++++++++ parametertree/Parameter.py | 7 +- parametertree/__main__.py | 132 -------------------------- parametertree/parameterTypes.py | 1 + 5 files changed, 168 insertions(+), 134 deletions(-) create mode 100644 examples/parametertree.py delete mode 100644 parametertree/__main__.py diff --git a/examples/__main__.py b/examples/__main__.py index 39c51b7f..84a175e6 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -11,7 +11,7 @@ examples = OrderedDict([ ('Command-line usage', 'CLIexample.py'), ('Basic Plotting', 'Plotting.py'), ('ImageView', 'ImageView.py'), - ('ParameterTree', '../parametertree'), + ('ParameterTree', 'parametertree.py'), ('Crosshair / Mouse interaction', 'crosshair.py'), ('Video speed test', 'VideoSpeedTest.py'), ('Plot speed test', 'PlotSpeedTest.py'), diff --git a/examples/parametertree.py b/examples/parametertree.py new file mode 100644 index 00000000..c4aca740 --- /dev/null +++ b/examples/parametertree.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +""" +This example demonstrates the use of pyqtgraph's parametertree system. This provides +a simple way to generate user interfaces that control sets of parameters. The example +demonstrates a variety of different parameter types (int, float, list, etc.) +as well as some customized parameter types + +""" + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui + + +import collections +app = QtGui.QApplication([]) +import pyqtgraph.parametertree.parameterTypes as pTypes +from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType + + +## test subclassing parameters +## This parameter automatically generates two child parameters which are always reciprocals of each other +class ComplexParameter(pTypes.GroupParameter): + def __init__(self, **opts): + opts['type'] = 'bool' + opts['value'] = True + pTypes.GroupParameter.__init__(self, **opts) + + self.addChild({'name': 'A = 1/B', 'type': 'float', 'value': 7, 'suffix': 'Hz', 'siPrefix': True}) + self.addChild({'name': 'B = 1/A', 'type': 'float', 'value': 1/7., 'suffix': 's', 'siPrefix': True}) + self.a = self.param('A = 1/B') + self.b = self.param('B = 1/A') + self.a.sigValueChanged.connect(self.aChanged) + self.b.sigValueChanged.connect(self.bChanged) + + def aChanged(self): + self.b.setValue(1.0 / self.a.value(), blockSignal=self.bChanged) + + def bChanged(self): + self.a.setValue(1.0 / self.b.value(), blockSignal=self.aChanged) + + +## test add/remove +## this group includes a menu allowing the user to add new parameters into its child list +class ScalableGroup(pTypes.GroupParameter): + def __init__(self, **opts): + opts['type'] = 'group' + opts['addText'] = "Add" + opts['addList'] = ['str', 'float', 'int'] + pTypes.GroupParameter.__init__(self, **opts) + + def addNew(self, typ): + val = { + 'str': '', + 'float': 0.0, + 'int': 0 + }[typ] + self.addChild(dict(name="ScalableParam %d" % (len(self.childs)+1), type=typ, value=val, removable=True, renamable=True)) + + +## test column spanning (widget sub-item that spans all columns) +class TextParameterItem(pTypes.WidgetParameterItem): + def __init__(self, param, depth): + pTypes.WidgetParameterItem.__init__(self, param, depth) + self.subItem = QtGui.QTreeWidgetItem() + self.addChild(self.subItem) + + def treeWidgetChanged(self): + self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) + self.treeWidget().setItemWidget(self.subItem, 0, self.textBox) + self.setExpanded(True) + + def makeWidget(self): + self.textBox = QtGui.QTextEdit() + self.textBox.setMaximumHeight(100) + self.textBox.value = lambda: str(self.textBox.toPlainText()) + self.textBox.setValue = self.textBox.setPlainText + self.textBox.sigChanged = self.textBox.textChanged + return self.textBox + +class TextParameter(Parameter): + type = 'text' + itemClass = TextParameterItem + +registerParameterType('text', TextParameter) + + + + +params = [ + {'name': 'Basic parameter data types', 'type': 'group', 'children': [ + {'name': 'Integer', 'type': 'int', 'value': 10}, + {'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1}, + {'name': 'String', 'type': 'str', 'value': "hi"}, + {'name': 'List', 'type': 'list', 'values': [1,2,3], 'value': 2}, + {'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2}, + {'name': 'Boolean', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"}, + {'name': 'Color', 'type': 'color', 'value': "FF0", 'tip': "This is a color button"}, + {'name': 'Subgroup', 'type': 'group', 'children': [ + {'name': 'Sub-param 1', 'type': 'int', 'value': 10}, + {'name': 'Sub-param 2', 'type': 'float', 'value': 1.2e6}, + ]}, + ]}, + {'name': 'Numerical Parameter Options', 'type': 'group', 'children': [ + {'name': 'Units + SI prefix', 'type': 'float', 'value': 1.2e-6, 'step': 1e-6, 'siPrefix': True, 'suffix': 'V'}, + {'name': 'Limits (min=7;max=15)', 'type': 'int', 'value': 11, 'limits': (7, 15), 'default': -6}, + {'name': 'DEC stepping', 'type': 'float', 'value': 1.2e6, 'dec': True, 'step': 1, 'siPrefix': True, 'suffix': 'Hz'}, + + ]}, + {'name': 'Extra Parameter Options', 'type': 'group', 'children': [ + {'name': 'Read-only', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'readonly': True}, + {'name': 'Renamable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'renamable': True}, + {'name': 'Removable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'removable': True}, + ]}, + ComplexParameter(name='Custom parameter group (reciprocal values)'), + ScalableGroup(name="Expandable Parameter Group", children=[ + {'name': 'ScalableParam 1', 'type': 'str', 'value': "default param 1"}, + {'name': 'ScalableParam 2', 'type': 'str', 'value': "default param 2"}, + ]), + {'name': 'Custom parameter class (text box)', 'type': 'text', 'value': 'Some text...'}, +] + +## Create tree of Parameter objects +p = Parameter(name='params', type='group', children=params) + +## If anything changes in the tree, print a message +def change(param, changes): + print("tree changes:") + for param, change, data in changes: + path = p.childPath(param) + if path is not None: + childName = '.'.join(path) + else: + childName = param.name() + print(' parameter: %s'% childName) + print(' change: %s'% change) + print(' data: %s'% str(data)) + print(' ----------') + +p.sigTreeStateChanged.connect(change) + +## Create two ParameterTree widgets, both accessing the same data +t = ParameterTree() +t.setParameters(p, showTop=False) +t.show() +t.resize(400,600) +t2 = ParameterTree() +t2.setParameters(p, showTop=False) +t2.show() +t2.resize(400,600) + + + + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/parametertree/Parameter.py b/parametertree/Parameter.py index 1c92457e..ebf9be58 100644 --- a/parametertree/Parameter.py +++ b/parametertree/Parameter.py @@ -131,11 +131,16 @@ class Parameter(QtCore.QObject): return name def childPath(self, child): - """Return the path of parameter names from self to child.""" + """ + Return the path of parameter names from self to child. + If child is not a (grand)child of self, return None. + """ path = [] while child is not self: path.insert(0, child.name()) child = child.parent() + if child is None: + return None return path def setValue(self, value, blockSignal=None): diff --git a/parametertree/__main__.py b/parametertree/__main__.py deleted file mode 100644 index 95bc9b70..00000000 --- a/parametertree/__main__.py +++ /dev/null @@ -1,132 +0,0 @@ -## tests for ParameterTree - -## make sure pyqtgraph is in path -import sys,os -md = os.path.abspath(os.path.dirname(__file__)) -sys.path.append(os.path.join(md, '..', '..')) - -from pyqtgraph.Qt import QtCore, QtGui -import collections -app = QtGui.QApplication([]) -import pyqtgraph.parametertree.parameterTypes as pTypes -from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType - - -## test subclassing parameters -## This parameter automatically generates two child parameters which are always reciprocals of each other -class ComplexParameter(Parameter): - def __init__(self, **opts): - opts['type'] = 'bool' - opts['value'] = True - Parameter.__init__(self, **opts) - - self.addChild({'name': 'A = 1/B', 'type': 'float', 'value': 7, 'suffix': 'Hz', 'siPrefix': True}) - self.addChild({'name': 'B = 1/A', 'type': 'float', 'value': 1/7., 'suffix': 's', 'siPrefix': True}) - self.a = self.param('A = 1/B') - self.b = self.param('B = 1/A') - self.a.sigValueChanged.connect(self.aChanged) - self.b.sigValueChanged.connect(self.bChanged) - - def aChanged(self): - self.b.setValue(1.0 / self.a.value(), blockSignal=self.bChanged) - - def bChanged(self): - self.a.setValue(1.0 / self.b.value(), blockSignal=self.aChanged) - - -## test add/remove -## this group includes a menu allowing the user to add new parameters into its child list -class ScalableGroup(pTypes.GroupParameter): - def __init__(self, **opts): - opts['type'] = 'group' - opts['addText'] = "Add" - opts['addList'] = ['str', 'float', 'int'] - pTypes.GroupParameter.__init__(self, **opts) - - def addNew(self, typ): - val = { - 'str': '', - 'float': 0.0, - 'int': 0 - }[typ] - self.addChild(dict(name="ScalableParam %d" % (len(self.childs)+1), type=typ, value=val, removable=True, renamable=True)) - - -## test column spanning (widget sub-item that spans all columns) -class TextParameterItem(pTypes.WidgetParameterItem): - def __init__(self, param, depth): - pTypes.WidgetParameterItem.__init__(self, param, depth) - self.subItem = QtGui.QTreeWidgetItem() - self.addChild(self.subItem) - - def treeWidgetChanged(self): - self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) - self.treeWidget().setItemWidget(self.subItem, 0, self.textBox) - self.setExpanded(True) - - def makeWidget(self): - self.textBox = QtGui.QTextEdit() - self.textBox.setMaximumHeight(100) - self.textBox.value = lambda: str(self.textBox.toPlainText()) - self.textBox.setValue = self.textBox.setPlainText - self.textBox.sigChanged = self.textBox.textChanged - return self.textBox - -class TextParameter(Parameter): - type = 'text' - itemClass = TextParameterItem - -registerParameterType('text', TextParameter) - - - - -params = [ - {'name': 'Group 0', 'type': 'group', 'children': [ - {'name': 'Param 1', 'type': 'int', 'value': 10}, - {'name': 'Param 2', 'type': 'float', 'value': 10}, - ]}, - {'name': 'Group 1', 'type': 'group', 'children': [ - {'name': 'Param 1.1', 'type': 'float', 'value': 1.2e-6, 'dec': True, 'siPrefix': True, 'suffix': 'V'}, - {'name': 'Param 1.2', 'type': 'float', 'value': 1.2e6, 'dec': True, 'siPrefix': True, 'suffix': 'Hz'}, - {'name': 'Group 1.3', 'type': 'group', 'children': [ - {'name': 'Param 1.3.1', 'type': 'int', 'value': 11, 'limits': (-7, 15), 'default': -6}, - {'name': 'Param 1.3.2', 'type': 'float', 'value': 1.2e6, 'dec': True, 'siPrefix': True, 'suffix': 'Hz', 'readonly': True}, - ]}, - {'name': 'Param 1.4', 'type': 'str', 'value': "hi"}, - {'name': 'Param 1.5', 'type': 'list', 'values': [1,2,3], 'value': 2}, - {'name': 'Param 1.6', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2}, - ComplexParameter(name='ComplexParam'), - ScalableGroup(name="ScalableGroup", children=[ - {'name': 'ScalableParam 1', 'type': 'str', 'value': "hi"}, - {'name': 'ScalableParam 2', 'type': 'str', 'value': "hi"}, - - ]) - ]}, - {'name': 'Param 5', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"}, - {'name': 'Param 6', 'type': 'color', 'value': "FF0", 'tip': "This is a color button. It cam be renamed.", 'renamable': True}, - {'name': 'TextParam', 'type': 'text', 'value': 'Some text...'}, -] - -#p = pTypes.ParameterSet("params", params) -p = Parameter(name='params', type='group', children=params) -def change(param, changes): - print("tree changes:") - for param, change, data in changes: - print(" [" + '.'.join(p.childPath(param))+ "] ", change, data) - -p.sigTreeStateChanged.connect(change) - - -t = ParameterTree() -t.setParameters(p, showTop=False) -t.show() -t.resize(400,600) -t2 = ParameterTree() -t2.setParameters(p, showTop=False) -t2.show() -t2.resize(400,600) - -import sys -if sys.flags.interactive == 0: - app.exec_() diff --git a/parametertree/parameterTypes.py b/parametertree/parameterTypes.py index a9fbd413..039c0493 100644 --- a/parametertree/parameterTypes.py +++ b/parametertree/parameterTypes.py @@ -348,6 +348,7 @@ class GroupParameterItem(ParameterItem): def treeWidgetChanged(self): ParameterItem.treeWidgetChanged(self) + self.treeWidget().setFirstItemColumnSpanned(self, True) if self.addItem is not None: self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox) self.treeWidget().setFirstItemColumnSpanned(self.addItem, True) From f81e94061fefb9dfff146b4ff345ee370e965251 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 10 Jul 2012 16:30:03 -0400 Subject: [PATCH 19/24] added dockarea example --- dockarea/DockArea.py | 1 + dockarea/__main__.py | 83 ------------------------------- examples/__main__.py | 5 +- examples/dockarea.py | 116 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 85 deletions(-) delete mode 100644 dockarea/__main__.py create mode 100644 examples/dockarea.py diff --git a/dockarea/DockArea.py b/dockarea/DockArea.py index 819726c3..49dd95ff 100644 --- a/dockarea/DockArea.py +++ b/dockarea/DockArea.py @@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtCore, QtGui from .Container import * from .DockDrop import * +from .Dock import Dock import pyqtgraph.debug as debug import weakref diff --git a/dockarea/__main__.py b/dockarea/__main__.py deleted file mode 100644 index ae794dd7..00000000 --- a/dockarea/__main__.py +++ /dev/null @@ -1,83 +0,0 @@ -import sys - -## Make sure pyqtgraph is importable -p = os.path.dirname(os.path.abspath(__file__)) -p = os.path.join(p, '..', '..') -sys.path.insert(0, p) - -from pyqtgraph.Qt import QtCore, QtGui - -from .DockArea import * -from .Dock import * - -app = QtGui.QApplication([]) -win = QtGui.QMainWindow() -area = DockArea() -win.setCentralWidget(area) -win.resize(800,800) -from .Dock import Dock -d1 = Dock("Dock1", size=(200,200)) -d2 = Dock("Dock2", size=(100,100)) -d3 = Dock("Dock3", size=(1,1)) -d4 = Dock("Dock4", size=(50,50)) -d5 = Dock("Dock5", size=(100,100)) -d6 = Dock("Dock6", size=(300,300)) -area.addDock(d1, 'left') -area.addDock(d2, 'right') -area.addDock(d3, 'bottom') -area.addDock(d4, 'right') -area.addDock(d5, 'left', d1) -area.addDock(d6, 'top', d4) - -area.moveDock(d6, 'above', d4) -d3.hideTitleBar() - -print("===build complete====") - -for d in [d1, d2, d3, d4, d5]: - w = QtGui.QWidget() - l = QtGui.QVBoxLayout() - w.setLayout(l) - btns = [] - for i in range(4): - btns.append(QtGui.QPushButton("%s Button %d"%(d.name(), i))) - l.addWidget(btns[-1]) - d.w = (w, l, btns) - d.addWidget(w) - - - -import pyqtgraph as pg -p = pg.PlotWidget() -d6.addWidget(p) - -print("===widgets added===") - - -#s = area.saveState() - - -#print "\n\n-------restore----------\n\n" -#area.restoreState(s) -s = None -def save(): - global s - s = area.saveState() - -def load(): - global s - area.restoreState(s) - - -#d6.container().setCurrentIndex(0) -#d2.label.setTabPos(40) - -#win2 = QtGui.QMainWindow() -#area2 = DockArea() -#win2.setCentralWidget(area2) -#win2.resize(800,800) - - -win.show() -#win2.show() - diff --git a/examples/__main__.py b/examples/__main__.py index 84a175e6..68b77a47 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -17,6 +17,7 @@ examples = OrderedDict([ ('Plot speed test', 'PlotSpeedTest.py'), ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), + ('Dock widgets', 'dockarea.py'), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), @@ -67,9 +68,9 @@ class ExampleLoader(QtGui.QMainWindow): self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples) self.ui.exampleTree.expandAll() - self.resize(900,500) + self.resize(1000,500) self.show() - self.ui.splitter.setSizes([150,750]) + self.ui.splitter.setSizes([250,750]) self.ui.loadBtn.clicked.connect(self.loadFile) self.ui.exampleTree.currentItemChanged.connect(self.showFile) self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile) diff --git a/examples/dockarea.py b/examples/dockarea.py new file mode 100644 index 00000000..c47ed390 --- /dev/null +++ b/examples/dockarea.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +""" +This example demonstrates the use of pyqtgraph's dock widget system. + +The dockarea system allows the design of user interfaces which can be rearranged by +the user at runtime. Docks can be moved, resized, stacked, and torn out of the main +window. This is similar in principle to the docking system built into Qt, but +offers a more deterministic dock placement API (in Qt it is very difficult to +programatically generate complex dock arrangements). Additionally, Qt's docks are +designed to be used as small panels around the outer edge of a window. Pyqtgraph's +docks were created with the notion that the entire window (or any portion of it) +would consist of dockable components. + +""" + + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.console +import numpy as np + +from pyqtgraph.dockarea import * + +app = QtGui.QApplication([]) +win = QtGui.QMainWindow() +area = DockArea() +win.setCentralWidget(area) +win.resize(1000,500) + +## Create docks, place them into the window one at a time. +## Note that size arguments are only a suggestion; docks will still have to +## fill the entire dock area and obey the limits of their internal widgets. +d1 = Dock("Dock1", size=(1, 1)) ## give this dock the minimum possible size +d2 = Dock("Dock2 - Console", size=(500,300)) +d3 = Dock("Dock3", size=(500,400)) +d4 = Dock("Dock4 (tabbed) - Plot", size=(500,200)) +d5 = Dock("Dock5 - Image", size=(500,200)) +d6 = Dock("Dock6 (tabbed) - Plot", size=(500,200)) +area.addDock(d1, 'left') ## place d1 at left edge of dock area (it will fill the whole space since there are no other docks yet) +area.addDock(d2, 'right') ## place d2 at right edge of dock area +area.addDock(d3, 'bottom', d1)## place d3 at bottom edge of d1 +area.addDock(d4, 'right') ## place d4 at right edge of dock area +area.addDock(d5, 'left', d1) ## place d5 at left edge of d1 +area.addDock(d6, 'top', d4) ## place d5 at top edge of d4 + +## Test ability to move docks programatically after they have been placed +area.moveDock(d4, 'top', d2) ## move d4 to top edge of d2 +area.moveDock(d6, 'above', d4) ## move d6 to stack on top of d4 +area.moveDock(d5, 'top', d2) ## move d5 to top edge of d2 + + +## Add widgets into each dock + +## first dock gets save/restore buttons +w1 = pg.LayoutWidget() +label = QtGui.QLabel(""" -- DockArea Example -- +This window has 6 Dock widgets in it. Each dock can be dragged +by its title bar to occupy a different space within the window +but note that one dock has its title bar hidden). Additionally, +the borders between docks may be dragged to resize. Docks that are dragged on top +of one another are stacked in a tabbed layout. Double-click a dock title +bar to place it in its own window. +""") +saveBtn = QtGui.QPushButton('Save dock state') +restoreBtn = QtGui.QPushButton('Restore dock state') +restoreBtn.setEnabled(False) +w1.addWidget(label, row=0, col=0) +w1.addWidget(saveBtn, row=1, col=0) +w1.addWidget(restoreBtn, row=2, col=0) +d1.addWidget(w1) +state = None +def save(): + global state + state = area.saveState() + restoreBtn.setEnabled(True) +def load(): + global state + area.restoreState(state) +saveBtn.clicked.connect(save) +restoreBtn.clicked.connect(load) + + +w2 = pg.console.ConsoleWidget() +d2.addWidget(w2) + +## Hide title bar on dock 3 +d3.hideTitleBar() +w3 = pg.PlotWidget(title="Plot inside dock with no title bar") +w3.plot(np.random.normal(size=100)) +d3.addWidget(w3) + +w4 = pg.PlotWidget(title="Dock 4 plot") +w4.plot(np.random.normal(size=100)) +d4.addWidget(w4) + +w5 = pg.ImageView() +w5.setImage(np.random.normal(size=(100,100))) +d5.addWidget(w5) + +w6 = pg.PlotWidget(title="Dock 6 plot") +w6.plot(np.random.normal(size=100)) +d6.addWidget(w6) + + + +win.show() + + + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() From a41d330c29c11afec7efde3ebbe6c5b7675d74a0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 12 Jul 2012 15:35:58 -0400 Subject: [PATCH 20/24] Bugfixes: - Fixed some floating-point precision issues. (Added a workaround for QTransform.inverted() bug) - No longer putting asUnicode inside __builtin__ since this causes problems in some rare circumstances (pyshell, lazy import recipe) - Minor docstring updates --- GraphicsScene/GraphicsScene.py | 1 + WidgetGroup.py | 1 + __init__.py | 3 ++- configfile.py | 1 + console/CmdInput.py | 1 + functions.py | 17 +++++++++++++ graphicsItems/AxisItem.py | 1 + graphicsItems/GradientEditorItem.py | 1 + graphicsItems/GraphicsItem.py | 38 +++++++++++++++++++++------- graphicsItems/GridItem.py | 5 ++-- graphicsItems/PlotItem/PlotItem.py | 2 ++ graphicsItems/ROI.py | 4 +-- graphicsItems/TextItem.py | 3 ++- graphicsItems/ViewBox/ViewBox.py | 14 +++++----- graphicsItems/ViewBox/ViewBoxMenu.py | 1 + parametertree/parameterTypes.py | 1 + python2_3.py | 13 +++++----- widgets/SpinBox.py | 1 + widgets/TableWidget.py | 1 + 19 files changed, 82 insertions(+), 27 deletions(-) diff --git a/GraphicsScene/GraphicsScene.py b/GraphicsScene/GraphicsScene.py index d4dd8534..03436a97 100644 --- a/GraphicsScene/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.python2_3 import sortList #try: #from PyQt4 import QtOpenGL #HAVE_OPENGL = True diff --git a/WidgetGroup.py b/WidgetGroup.py index 6e183b16..29541454 100644 --- a/WidgetGroup.py +++ b/WidgetGroup.py @@ -10,6 +10,7 @@ of a large group of widgets. from .Qt import QtCore, QtGui import weakref, inspect +from .python2_3 import asUnicode __all__ = ['WidgetGroup'] diff --git a/__init__.py b/__init__.py index b31f15d0..1a689168 100644 --- a/__init__.py +++ b/__init__.py @@ -14,7 +14,8 @@ from .Qt import QtGui import os, sys ## check python version -if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] != 7): +## Allow anything >= 2.7 +if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] < 7): raise Exception("Pyqtgraph requires Python version 2.7 (this is %d.%d)" % (sys.version_info[0], sys.version_info[1])) ## helpers for 2/3 compatibility diff --git a/configfile.py b/configfile.py index c4448ff0..d413ea56 100644 --- a/configfile.py +++ b/configfile.py @@ -13,6 +13,7 @@ import re, os, sys from collections import OrderedDict GLOBAL_PATH = None # so not thread safe. from . import units +from .python2_3 import asUnicode class ParseError(Exception): def __init__(self, message, lineNum, line, fileName=None): diff --git a/console/CmdInput.py b/console/CmdInput.py index 8db52e72..3e9730d6 100644 --- a/console/CmdInput.py +++ b/console/CmdInput.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.python2_3 import asUnicode class CmdInput(QtGui.QLineEdit): diff --git a/functions.py b/functions.py index 3391b080..4fd629c8 100644 --- a/functions.py +++ b/functions.py @@ -5,6 +5,7 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ +from .python2_3 import asUnicode Colors = { 'b': (0,0,255,255), 'g': (0,255,0,255), @@ -1275,3 +1276,19 @@ def isosurface(data, level): return facets + + +def invertQTransform(tr): + """Return a QTransform that is the inverse of *tr*. + Rasises an exception if tr is not invertible. + + Note that this function is preferred over QTransform.inverted() due to + bugs in that method. (specifically, Qt has floating-point precision issues + when determining whether a matrix is invertible) + """ + #return tr.inverted()[0] + arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]]) + inv = scipy.linalg.inv(arr) + return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1]) + + \ No newline at end of file diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 08639706..d594132d 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import asUnicode import numpy as np from pyqtgraph.Point import Point import pyqtgraph.debug as debug diff --git a/graphicsItems/GradientEditorItem.py b/graphicsItems/GradientEditorItem.py index e56638aa..8e488a28 100644 --- a/graphicsItems/GradientEditorItem.py +++ b/graphicsItems/GradientEditorItem.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import sortList import pyqtgraph.functions as fn from .GraphicsObject import GraphicsObject from .GraphicsWidget import GraphicsWidget diff --git a/graphicsItems/GraphicsItem.py b/graphicsItems/GraphicsItem.py index 67fef456..bd181f8d 100644 --- a/graphicsItems/GraphicsItem.py +++ b/graphicsItems/GraphicsItem.py @@ -1,6 +1,7 @@ from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.GraphicsScene import GraphicsScene from pyqtgraph.Point import Point +import pyqtgraph.functions as fn import weakref class GraphicsItem(object): @@ -149,16 +150,32 @@ class GraphicsItem(object): """Return vectors in local coordinates representing the width and height of a view pixel. If direction is specified, then return vectors parallel and orthogonal to it. - Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed).""" + Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed) + or if pixel size is below floating-point precision limit. + """ dt = self.deviceTransform() if dt is None: return None, None if direction is None: - direction = Point(1, 0) + direction = Point(1, 0) + if direction.manhattanLength() == 0: + raise Exception("Cannot compute pixel length for 0-length vector.") + ## attempt to re-scale direction vector to fit within the precision of the coordinate system + if direction.x() == 0: + r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22())) + elif direction.y() == 0: + r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21())) + else: + r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5 + direction = direction * r + viewDir = Point(dt.map(direction) - dt.map(Point(0,0))) + if viewDir.manhattanLength() == 0: + return None, None ## pixel size cannot be represented on this scale + orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space try: @@ -168,7 +185,7 @@ class GraphicsItem(object): raise Exception("Invalid direction %s" %direction) - dti = dt.inverted()[0] + dti = fn.invertQTransform(dt) return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0))) #vt = self.deviceTransform() @@ -194,23 +211,26 @@ class GraphicsItem(object): def pixelSize(self): + ## deprecated v = self.pixelVectors() if v == (None, None): return None, None return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5 def pixelWidth(self): + ## deprecated vt = self.deviceTransform() if vt is None: return 0 - vt = vt.inverted()[0] + vt = fn.invertQTransform(vt) return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length() def pixelHeight(self): + ## deprecated vt = self.deviceTransform() if vt is None: return 0 - vt = vt.inverted()[0] + vt = fn.invertQTransform(vt) return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length() @@ -232,7 +252,7 @@ class GraphicsItem(object): vt = self.deviceTransform() if vt is None: return None - vt = vt.inverted()[0] + vt = fn.invertQTransform(vt) return vt.map(obj) def mapRectToDevice(self, rect): @@ -253,7 +273,7 @@ class GraphicsItem(object): vt = self.deviceTransform() if vt is None: return None - vt = vt.inverted()[0] + vt = fn.invertQTransform(vt) return vt.mapRect(rect) def mapToView(self, obj): @@ -272,14 +292,14 @@ class GraphicsItem(object): vt = self.viewTransform() if vt is None: return None - vt = vt.inverted()[0] + vt = fn.invertQTransform(vt) return vt.map(obj) def mapRectFromView(self, obj): vt = self.viewTransform() if vt is None: return None - vt = vt.inverted()[0] + vt = fn.invertQTransform(vt) return vt.mapRect(obj) def pos(self): diff --git a/graphicsItems/GridItem.py b/graphicsItems/GridItem.py index d3f093f9..29b0aa2c 100644 --- a/graphicsItems/GridItem.py +++ b/graphicsItems/GridItem.py @@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtGui, QtCore from .UIGraphicsItem import * import numpy as np from pyqtgraph.Point import Point +import pyqtgraph.functions as fn __all__ = ['GridItem'] class GridItem(UIGraphicsItem): @@ -47,7 +48,7 @@ class GridItem(UIGraphicsItem): p = QtGui.QPainter() p.begin(self.picture) - dt = self.viewTransform().inverted()[0] + dt = fn.invertQTransform(self.viewTransform()) vr = self.getViewWidget().rect() unit = self.pixelWidth(), self.pixelHeight() dim = [vr.width(), vr.height()] @@ -112,7 +113,7 @@ class GridItem(UIGraphicsItem): texts.append((QtCore.QPointF(x, y), "%g"%p1[ax])) tr = self.deviceTransform() #tr.scale(1.5, 1.5) - p.setWorldTransform(tr.inverted()[0]) + p.setWorldTransform(fn.invertQTransform(tr)) for t in texts: x = tr.map(t[0]) + Point(0.5, 0.5) p.drawText(x, t[1]) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index dfd0ad65..cc72dab9 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -1053,6 +1053,7 @@ class PlotItem(GraphicsWidget): s.hide() def hideAxis(self, axis): + """Hide one of the PlotItem's axes. ('left', 'bottom', 'right', or 'top')""" self.showAxis(axis, False) def showScale(self, *args, **kargs): @@ -1060,6 +1061,7 @@ class PlotItem(GraphicsWidget): return self.showAxis(*args, **kargs) def hideButtons(self): + """Causes auto-scale button ('A' in lower-left corner) to be hidden for this PlotItem""" #self.ctrlBtn.hide() self.autoBtn.hide() diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index 5d099490..7606160a 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -800,7 +800,7 @@ class ROI(GraphicsObject): #print " dshape", dShape ## Determine transform that maps ROI bounding box to image coordinates - tr = self.sceneTransform() * img.sceneTransform().inverted()[0] + tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform()) ## Modify transform to scale from image coords to data coords #m = QtGui.QTransform() @@ -1251,7 +1251,7 @@ class Handle(UIGraphicsItem): v = dt.map(QtCore.QPointF(1, 0)) - dt.map(QtCore.QPointF(0, 0)) va = np.arctan2(v.y(), v.x()) - dti = dt.inverted()[0] + dti = fn.invertQTransform(dt) devPos = dt.map(QtCore.QPointF(0,0)) tr = QtGui.QTransform() tr.translate(devPos.x(), devPos.y()) diff --git a/graphicsItems/TextItem.py b/graphicsItems/TextItem.py index b7d0f3b7..a16e6841 100644 --- a/graphicsItems/TextItem.py +++ b/graphicsItems/TextItem.py @@ -1,6 +1,7 @@ from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg from .UIGraphicsItem import * +import pyqtgraph.functions as fn class TextItem(UIGraphicsItem): """ @@ -87,7 +88,7 @@ class TextItem(UIGraphicsItem): if br is None: return self.prepareGeometryChange() - self._bounds = self.deviceTransform().inverted()[0].mapRect(br) + self._bounds = fn.invertQTransform(self.deviceTransform()).mapRect(br) #print self._bounds def boundingRect(self): diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 694455e5..138573a0 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import sortList import numpy as np from pyqtgraph.Point import Point import pyqtgraph.functions as fn @@ -451,10 +452,8 @@ class ViewBox(GraphicsWidget): center = Point(vr.center()) else: center = Point(center) - tl = center + (vr.topLeft()-center) * scale br = center + (vr.bottomRight()-center) * scale - self.setRange(QtCore.QRectF(tl, br), padding=0) def translateBy(self, t): @@ -764,7 +763,7 @@ class ViewBox(GraphicsWidget): def mapToView(self, obj): """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox""" - m = self.childTransform().inverted()[0] + m = fn.invertQTransform(self.childTransform()) return m.map(obj) def mapFromView(self, obj): @@ -830,7 +829,7 @@ class ViewBox(GraphicsWidget): mask[axis] = mv s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor - center = Point(self.childGroup.transform().inverted()[0].map(ev.pos())) + center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) #center = ev.pos() self.scaleBy(s, center) @@ -913,8 +912,11 @@ class ViewBox(GraphicsWidget): dif = np.array([dif.x(), dif.y()]) dif[0] *= -1 s = ((mask * 0.02) + 1) ** dif - center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton))) - #center = Point(ev.buttonDownPos(QtCore.Qt.RightButton)) + + tr = self.childGroup.transform() + tr = fn.invertQTransform(tr) + + center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) self.scaleBy(s, center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) diff --git a/graphicsItems/ViewBox/ViewBoxMenu.py b/graphicsItems/ViewBox/ViewBoxMenu.py index dde6b129..4c6f54a1 100644 --- a/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/graphicsItems/ViewBox/ViewBoxMenu.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.python2_3 import asUnicode from pyqtgraph.WidgetGroup import WidgetGroup from .axisCtrlTemplate import Ui_Form as AxisCtrlTemplate import weakref diff --git a/parametertree/parameterTypes.py b/parametertree/parameterTypes.py index 039c0493..30f714c1 100644 --- a/parametertree/parameterTypes.py +++ b/parametertree/parameterTypes.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.python2_3 import asUnicode from .Parameter import Parameter, registerParameterType from .ParameterItem import ParameterItem from pyqtgraph.widgets.SpinBox import SpinBox diff --git a/python2_3.py b/python2_3.py index 51696a90..c295dbfc 100644 --- a/python2_3.py +++ b/python2_3.py @@ -42,8 +42,9 @@ def sortList(l, cmpFunc): if sys.version_info[0] == 3: import builtins builtins.basestring = str - builtins.asUnicode = asUnicode - builtins.sortList = sortList + #builtins.asUnicode = asUnicode + #builtins.sortList = sortList + basestring = str def cmp(a,b): if a>b: return 1 @@ -52,7 +53,7 @@ if sys.version_info[0] == 3: else: return 0 builtins.cmp = cmp -else: - import __builtin__ - __builtin__.asUnicode = asUnicode - __builtin__.sortList = sortList +#else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe + #import __builtin__ + #__builtin__.asUnicode = asUnicode + #__builtin__.sortList = sortList diff --git a/widgets/SpinBox.py b/widgets/SpinBox.py index 3e772237..93722c2a 100644 --- a/widgets/SpinBox.py +++ b/widgets/SpinBox.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import asUnicode from pyqtgraph.SignalProxy import SignalProxy import pyqtgraph.functions as fn diff --git a/widgets/TableWidget.py b/widgets/TableWidget.py index c58b5cb0..dc4f875b 100644 --- a/widgets/TableWidget.py +++ b/widgets/TableWidget.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import asUnicode import numpy as np try: From c686395ebed336047eb008641a5964a6ecfdf05d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 2 Aug 2012 22:46:08 -0400 Subject: [PATCH 21/24] ImageView fix: display correct coordinates in ROI plot for scaled, single-frame images Minor documentation updates --- documentation/source/conf.py | 1 + functions.py | 55 +++++++++++++++---------- graphicsItems/ROI.py | 80 +++++++++++++++++++++++++----------- imageview/ImageView.py | 10 +++-- 4 files changed, 97 insertions(+), 49 deletions(-) diff --git a/documentation/source/conf.py b/documentation/source/conf.py index 8145ba4a..2fd718e4 100644 --- a/documentation/source/conf.py +++ b/documentation/source/conf.py @@ -18,6 +18,7 @@ import sys, os # documentation root, use os.path.abspath to make it absolute, like shown here. path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(path, '..', '..', '..')) +print sys.path # -- General configuration ----------------------------------------------------- diff --git a/functions.py b/functions.py index 4fd629c8..7b108dbd 100644 --- a/functions.py +++ b/functions.py @@ -358,28 +358,36 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0) -def affineSlice(data, shape, origin, vectors, axes, **kargs): +def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs): """ Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. - The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this). - For a graphical interface to this function, see :func:`ROI.getArrayRegion` + For a graphical interface to this function, see :func:`ROI.getArrayRegion ` + ============== ==================================================================================================== Arguments: + *data* (ndarray) the original dataset + *shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape)) + *origin* the location in the original dataset that will become the origin of the sliced data. + *vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same + length as *axes*. If the vectors are not unit length, the result will be scaled relative to the + original data. If the vectors are not orthogonal, the result will be sheared relative to the + original data. + *axes* The axes in the original dataset which correspond to the slice *vectors* + *order* The order of spline interpolation. Default is 1 (linear). See scipy.ndimage.map_coordinates + for more information. + *returnCoords* If True, return a tuple (result, coords) where coords is the array of coordinates used to select + values from the original dataset. + *All extra keyword arguments are passed to scipy.ndimage.map_coordinates.* + -------------------------------------------------------------------------------------------------------------------- + ============== ==================================================================================================== - | *data* (ndarray): the original dataset - | *shape*: the shape of the slice to take (Note the return value may have more dimensions than len(shape)) - | *origin*: the location in the original dataset that will become the origin in the sliced data. - | *vectors*: list of unit vectors which point in the direction of the slice axes + Note the following must be true: - * each vector must have the same length as *axes* - * If the vectors are not unit length, the result will be scaled. - * If the vectors are not orthogonal, the result will be sheared. - - *axes*: the axes in the original dataset which correspond to the slice *vectors* - - All extra keyword arguments are passed to scipy.ndimage.map_coordinates + | len(shape) == len(vectors) + | len(origin) == len(axes) == len(vectors[i]) Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes @@ -392,10 +400,6 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) - Note the following must be true: - - | len(shape) == len(vectors) - | len(origin) == len(axes) == len(vectors[0]) """ # sanity check @@ -437,7 +441,7 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): for inds in np.ndindex(*extraShape): ind = (Ellipsis,) + inds #print data[ind].shape, x.shape, output[ind].shape, output.shape - output[ind] = scipy.ndimage.map_coordinates(data[ind], x, **kargs) + output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) tr = list(range(output.ndim)) trb = [] @@ -448,9 +452,18 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): tr2 = tuple(trb+tr) ## Untranspose array before returning - return output.transpose(tr2) - + output = output.transpose(tr2) + if returnCoords: + return (output, x) + else: + return output +def transformToArray(tr): + """ + Given a QTransform, return a 3x3 numpy array. + """ + return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]]) + def solve3DTransform(points1, points2): """ Find a 3D transformation matrix that maps points1 onto points2 diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index 7606160a..68fb65ed 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -832,35 +832,34 @@ class ROI(GraphicsObject): else: return bounds, tr - - def getArrayRegion(self, data, img, axes=(0,1)): - """Use the position of this ROI relative to an imageItem to pull a slice from an array.""" + def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): + """Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. + This method uses :func:`affineSlice ` to generate + the slice from *data* and uses :func:`getAffineSliceParams ` to determine the parameters to + pass to :func:`affineSlice `. - shape = self.state['size'] + If *returnMappedCoords* is True, then the method returns a tuple (result, coords) + such that coords is the set of coordinates used to interpolate values from the original + data, mapped into the parent coordinate system of the image. This is useful, when slicing + data from images that have been transformed, for determining the location of each value + in the sliced data. - origin = self.mapToItem(img, QtCore.QPointF(0, 0)) - - ## vx and vy point in the directions of the slice axes, but must be scaled properly - vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin - vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin - - lvx = np.sqrt(vx.x()**2 + vx.y()**2) - lvy = np.sqrt(vy.x()**2 + vy.y()**2) - pxLen = img.width() / float(data.shape[axes[0]]) - sx = pxLen / lvx - sy = pxLen / lvy - - vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) - shape = self.state['size'] - shape = [abs(shape[0]/sx), abs(shape[1]/sy)] - - origin = (origin.x(), origin.y()) - - #print "shape", shape, "vectors", vectors, "origin", origin - - return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, order=1) + All extra keyword arguments are passed to :func:`affineSlice `. + """ + shape, vectors, origin = self.getAffineSliceParams(data, img, axes) + if not returnMappedCoords: + return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + else: + kwds['returnCoords'] = True + result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + tr = fn.transformToArray(img.transform())[:,:2].reshape((3, 2) + (1,)*(coords.ndim-1)) + coords = coords[np.newaxis, ...] + mapped = (tr*coords).sum(axis=0) + return result, mapped + + ### transpose data so x and y are the first 2 axes #trAx = range(0, data.ndim) #trAx.remove(axes[0]) @@ -959,6 +958,37 @@ class ROI(GraphicsObject): ### Untranspose array before returning #return arr5.transpose(tr2) + def getAffineSliceParams(self, data, img, axes=(0.1)): + """ + Returns the parameters needed to use :func:`affineSlice ` to + extract a subset of *data* using this ROI and *img* to specify the subset. + + See :func:`getArrayRegion ` for more information. + """ + + shape = self.state['size'] + + origin = self.mapToItem(img, QtCore.QPointF(0, 0)) + + ## vx and vy point in the directions of the slice axes, but must be scaled properly + vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin + vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin + + lvx = np.sqrt(vx.x()**2 + vx.y()**2) + lvy = np.sqrt(vy.x()**2 + vy.y()**2) + pxLen = img.width() / float(data.shape[axes[0]]) + #img.width is number of pixels or width of item? + #need pxWidth and pxHeight instead of pxLen ? + sx = pxLen / lvx + sy = pxLen / lvy + + vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) + shape = self.state['size'] + shape = [abs(shape[0]/sx), abs(shape[1]/sy)] + + origin = (origin.x(), origin.y()) + return shape, vectors, origin + def getGlobalTransform(self, relativeTo=None): """Return global transformation (rotation angle+translation) required to move from relative state to current state. If relative state isn't specified, diff --git a/imageview/ImageView.py b/imageview/ImageView.py index 41a43639..ce0a420f 100644 --- a/imageview/ImageView.py +++ b/imageview/ImageView.py @@ -38,7 +38,7 @@ from pyqtgraph.SignalProxy import SignalProxy class PlotROI(ROI): def __init__(self, size): - ROI.__init__(self, pos=[0,0], size=size, scaleSnap=True, translateSnap=True) + ROI.__init__(self, pos=[0,0], size=size) #, scaleSnap=True, translateSnap=True) self.addScaleHandle([1, 1], [0, 0]) self.addRotateHandle([0, 0], [0.5, 0.5]) @@ -531,14 +531,18 @@ class ImageView(QtGui.QWidget): axes = (1, 2) else: return - data = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes) + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) if data is not None: while data.ndim > 1: data = data.mean(axis=1) if image.ndim == 3: self.roiCurve.setData(y=data, x=self.tVals) else: - self.roiCurve.setData(y=data, x=list(range(len(data)))) + while coords.ndim > 2: + coords = coords[:,:,0] + coords = coords - coords[:,0,np.newaxis] + xvals = (coords**2).sum(axis=0) ** 0.5 + self.roiCurve.setData(y=data, x=xvals) #self.ui.roiPlot.replot() From e4e3a636f35a73675a347095d86b16060d67ffac Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 3 Aug 2012 00:08:05 -0400 Subject: [PATCH 22/24] Added console example to menu --- examples/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/__main__.py b/examples/__main__.py index 68b77a47..12197b3d 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -18,6 +18,7 @@ examples = OrderedDict([ ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), ('Dock widgets', 'dockarea.py'), + ('Console', 'ConsoleWidget.py'), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), From b88f0a22a2fe8d9123eb029b5b738bf64863deae Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 3 Aug 2012 00:29:05 -0400 Subject: [PATCH 23/24] ImageView can now be initialized with custom view and image objects --- graphicsItems/PlotItem/PlotItem.py | 3 ++- imageview/ImageView.py | 25 +++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index cc72dab9..b2c1dbf4 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -73,6 +73,7 @@ class PlotItem(GraphicsWidget): :func:`enableAutoRange `, :func:`disableAutoRange `, :func:`setAspectLocked `, + :func:`invertY `, :func:`register `, :func:`unregister ` @@ -188,7 +189,7 @@ class PlotItem(GraphicsWidget): for m in [ 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', - 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', + 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. setattr(self, m, getattr(self.vb, m)) diff --git a/imageview/ImageView.py b/imageview/ImageView.py index ce0a420f..65c5f0f9 100644 --- a/imageview/ImageView.py +++ b/imageview/ImageView.py @@ -67,7 +67,12 @@ class ImageView(QtGui.QWidget): sigTimeChanged = QtCore.Signal(object, object) sigProcessingChanged = QtCore.Signal(object) - def __init__(self, parent=None, name="ImageView", *args): + def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args): + """ + By default, this class creates an :class:`ImageItem ` to display image data + and a :class:`ViewBox ` to contain the ImageItem. Custom items may be given instead + by specifying the *view* and/or *imageItem* arguments. + """ QtGui.QWidget.__init__(self, parent, *args) self.levelMax = 4096 self.levelMin = 0 @@ -89,7 +94,10 @@ class ImageView(QtGui.QWidget): #self.ui.graphicsView.setAspectLocked(True) #self.ui.graphicsView.invertY() #self.ui.graphicsView.enableMouse() - self.view = ViewBox() + if view is None: + self.view = ViewBox() + else: + self.view = view self.ui.graphicsView.setCentralItem(self.view) self.view.setAspectLocked(True) self.view.invertY() @@ -101,7 +109,10 @@ class ImageView(QtGui.QWidget): #self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255)) #self.ui.gradientWidget.setOrientation('right') - self.imageItem = ImageItem() + if imageItem is None: + self.imageItem = ImageItem() + else: + self.imageItem = imageItem self.view.addItem(self.imageItem) self.currentIndex = 0 @@ -668,4 +679,10 @@ class ImageView(QtGui.QWidget): #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0]) ##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() - \ No newline at end of file + def getView(self): + """Return the ViewBox (or other compatible object) which displays the ImageItem""" + return self.view + + def getImageItem(self): + """Return the ImageItem for this ImageView.""" + return self.imageItem \ No newline at end of file From 9fa590d03b2a536d2c1b9bf9253f07b97d7d7c90 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 3 Aug 2012 00:41:41 -0400 Subject: [PATCH 24/24] Added accessors for ImageView PlotWidget and histogram --- imageview/ImageView.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/imageview/ImageView.py b/imageview/ImageView.py index 65c5f0f9..53eb6809 100644 --- a/imageview/ImageView.py +++ b/imageview/ImageView.py @@ -685,4 +685,12 @@ class ImageView(QtGui.QWidget): def getImageItem(self): """Return the ImageItem for this ImageView.""" - return self.imageItem \ No newline at end of file + return self.imageItem + + def getRoiPlot(self): + """Return the ROI PlotWidget for this ImageView""" + return self.ui.roiPlot + + def getHistogramWidget(self): + """Return the HistogramLUTWidget for this ImageView""" + return self.ui.histogram