From c55392965f0ddb03da2143d752ecc41ad265d234 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 7 Feb 2011 19:40:38 -0500 Subject: [PATCH 1/2] Merge from ACQ4: - Lots of bug fixes - API change in PlotItem.plot(...) - Started replacing QObjectWorkaround with QWidget - Made plotCurveItems clickable - Added curve-following arrows --- GraphicsView.py | 22 ++- ImageView.py | 20 ++- ObjectWorkaround.py | 30 +++- PlotItem.py | 111 ++++--------- PlotWidget.py | 5 +- Point.py | 6 +- __init__.py | 37 +++++ examples/test_PlotWidget.py | 2 +- examples/test_ROItypes.py | 4 +- functions.py | 104 ++++++++++-- graphicsItems.py | 323 +++++++++++++++++++++++++++++++----- graphicsWindows.py | 131 ++++++++++++--- widgets.py | 171 +++++++++++++++++-- 13 files changed, 789 insertions(+), 177 deletions(-) diff --git a/GraphicsView.py b/GraphicsView.py index 852988fd..edfc2d86 100644 --- a/GraphicsView.py +++ b/GraphicsView.py @@ -66,8 +66,14 @@ class GraphicsView(QtGui.QGraphicsView): self.updateMatrix() self.sceneObj = QtGui.QGraphicsScene() self.setScene(self.sceneObj) + + ## by default we set up a central widget with a grid layout. + ## this can be replaced if needed. self.centralWidget = None self.setCentralItem(QtGui.QGraphicsWidget()) + self.centralLayout = QtGui.QGraphicsGridLayout() + self.centralWidget.setLayout(self.centralLayout) + self.mouseEnabled = False self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) self.clickAccepted = False @@ -85,6 +91,7 @@ class GraphicsView(QtGui.QGraphicsView): ev.ignore() def setCentralItem(self, item): + """Sets a QGraphicsWidget to automatically fill the entire view.""" if self.centralWidget is not None: self.scene().removeItem(self.centralWidget) self.centralWidget = item @@ -305,21 +312,24 @@ class GraphicsView(QtGui.QGraphicsView): #self.currentItem = None def mouseMoveEvent(self, ev): + #if self.lastMousePos is None: + #self.lastMousePos = Point(ev.pos()) + #delta = Point(ev.pos()) - self.lastMousePos + #if abs(delta[0]) > 100 or abs(delta[1]) > 100: ## Weird bug generating extra events.. + #return + #self.lastMousePos = Point(ev.pos()) + #print "move", delta QtGui.QGraphicsView.mouseMoveEvent(self, ev) if not self.mouseEnabled: return self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos())) #print "moved. Grabber:", self.scene().mouseGrabberItem() - if self.lastMousePos is None: - self.lastMousePos = Point(ev.pos()) if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it. return - delta = Point(ev.pos()) - self.lastMousePos - self.lastMousePos = Point(ev.pos()) if ev.buttons() == QtCore.Qt.RightButton: delta = Point(clip(delta[0], -50, 50), clip(-delta[1], -50, 50)) @@ -377,6 +387,10 @@ class GraphicsView(QtGui.QGraphicsView): 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): diff --git a/ImageView.py b/ImageView.py index b1448644..c93731a4 100644 --- a/ImageView.py +++ b/ImageView.py @@ -106,12 +106,12 @@ class ImageView(QtGui.QWidget): setattr(self, fn, getattr(self.ui.graphicsView, fn)) #QtCore.QObject.connect(self.ui.timeSlider, QtCore.SIGNAL('valueChanged(int)'), self.timeChanged) - self.timeLine.connect(QtCore.SIGNAL('positionChanged'), self.timeLineChanged) + self.timeLine.connect(self.timeLine, QtCore.SIGNAL('positionChanged'), self.timeLineChanged) #QtCore.QObject.connect(self.ui.whiteSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage) #QtCore.QObject.connect(self.ui.blackSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage) QtCore.QObject.connect(self.ui.gradientWidget, QtCore.SIGNAL('gradientChanged'), self.updateImage) QtCore.QObject.connect(self.ui.roiBtn, QtCore.SIGNAL('clicked()'), self.roiClicked) - self.roi.connect(QtCore.SIGNAL('regionChanged'), self.roiChanged) + self.roi.connect(self.roi, QtCore.SIGNAL('regionChanged'), self.roiChanged) QtCore.QObject.connect(self.ui.normBtn, QtCore.SIGNAL('toggled(bool)'), self.normToggled) QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) QtCore.QObject.connect(self.ui.normSubtractRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) @@ -124,7 +124,7 @@ class ImageView(QtGui.QWidget): ##QtCore.QObject.connect(self.ui.normStartSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) #QtCore.QObject.connect(self.ui.normStopSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) self.normProxy = proxyConnect(self.normRgn, QtCore.SIGNAL('regionChanged'), self.updateNorm) - self.normRoi.connect(QtCore.SIGNAL('regionChangeFinished'), self.updateNorm) + self.normRoi.connect(self.normRoi, QtCore.SIGNAL('regionChangeFinished'), self.updateNorm) self.ui.roiPlot.registerPlot(self.name + '_ROI') @@ -347,7 +347,19 @@ class ImageView(QtGui.QWidget): self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} elif img.ndim == 4: self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} - + else: + raise Exception("Can not interpret image with dimensions %s" % (str(img))) + elif isinstance(axes, dict): + self.axes = axes.copy() + elif isinstance(axes, list) or isinstance(axes, tuple): + self.axes = {} + for i in range(len(axes)): + self.axes[axes[i]] = i + else: + raise Exception("Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes))) + + for x in ['t', 'x', 'y', 'c']: + self.axes[x] = self.axes.get(x, None) self.imageDisp = None if autoLevels: diff --git a/ObjectWorkaround.py b/ObjectWorkaround.py index 67982dd4..7573b64f 100644 --- a/ObjectWorkaround.py +++ b/ObjectWorkaround.py @@ -1,9 +1,17 @@ # -*- coding: utf-8 -*- from PyQt4 import QtGui, QtCore +"""For circumventing PyQt's lack of multiple inheritance (just until PySide becomes stable)""" + + +class Obj(QtCore.QObject): + def event(self, ev): + self.emit(QtCore.SIGNAL('event'), ev) + return QtCore.QObject.event(self, ev) class QObjectWorkaround: def __init__(self): - self._qObj_ = QtCore.QObject() + self._qObj_ = Obj() + self.connect(QtCore.SIGNAL('event'), self.event) def connect(self, *args): if args[0] is self: return QtCore.QObject.connect(self._qObj_, *args[1:]) @@ -15,8 +23,20 @@ class QObjectWorkaround: return QtCore.QObject.emit(self._qObj_, *args) def blockSignals(self, b): return self._qObj_.blockSignals(b) + def setProperty(self, prop, val): + return self._qObj_.setProperty(prop, val) + def property(self, prop): + return self._qObj_.property(prop) + def event(self, ev): + pass -class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround): - def __init__(self, *args): - QtGui.QGraphicsItem.__init__(self, *args) - QObjectWorkaround.__init__(self) +#class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround): + #def __init__(self, *args): + #QtGui.QGraphicsItem.__init__(self, *args) + #QObjectWorkaround.__init__(self) + +class QGraphicsObject(QtGui.QGraphicsWidget): + def shape(self): + return QtGui.QGraphicsItem.shape(self) + +#QGraphicsObject = QtGui.QGraphicsObject \ No newline at end of file diff --git a/PlotItem.py b/PlotItem.py index 0cbc9459..286e621e 100644 --- a/PlotItem.py +++ b/PlotItem.py @@ -20,6 +20,7 @@ This class is very heavily featured: from graphicsItems import * from plotConfigTemplate import * from PyQt4 import QtGui, QtCore, QtSvg +from functions import * #from ObjectWorkaround import * #tryWorkaround(QtCore, QtGui) import weakref @@ -43,7 +44,7 @@ class PlotItem(QtGui.QGraphicsWidget): lastFileDir = None managers = {} - def __init__(self, parent=None, name=None): + def __init__(self, parent=None, name=None, labels=None, **kargs): QtGui.QGraphicsWidget.__init__(self, parent) ## Set up control buttons @@ -226,6 +227,15 @@ class PlotItem(QtGui.QGraphicsWidget): if name is not None: self.registerPlot(name) + if labels is not None: + for k in labels: + if isinstance(labels[k], basestring): + labels[k] = (labels[k],) + self.setLabel(k, *labels[k]) + + if len(kargs) > 0: + self.plot(**kargs) + def __del__(self): if self.manager is not None: @@ -274,8 +284,8 @@ class PlotItem(QtGui.QGraphicsWidget): v = self.scene().views()[0] b = self.vb.mapRectToScene(self.vb.boundingRect()) wr = v.mapFromScene(b).boundingRect() - pos = v.pos() - wr.adjust(v.x(), v.y(), v.x(), v.y()) + pos = v.mapToGlobal(v.pos()) + wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) return wr @@ -324,6 +334,7 @@ class PlotItem(QtGui.QGraphicsWidget): 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 @@ -337,11 +348,13 @@ class PlotItem(QtGui.QGraphicsWidget): 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() @@ -351,6 +364,7 @@ class PlotItem(QtGui.QGraphicsWidget): 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() @@ -554,7 +568,7 @@ class PlotItem(QtGui.QGraphicsWidget): self.curves.remove(item) self.updateDecimation() self.updateParamList() - item.connect(QtCore.SIGNAL('plotChanged'), self.plotChanged) + item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) def clear(self): for i in self.items[:]: @@ -567,7 +581,18 @@ class PlotItem(QtGui.QGraphicsWidget): self.avgCurves = {} - def plot(self, data=None, x=None, clear=False, params=None, pen=None): + def plot(self, data=None, data2=None, x=None, y=None, clear=False, params=None, pen=None): + """Add a new plot curve. Data may be specified a few ways: + plot(yVals) # x vals will be integers + plot(xVals, yVals) + plot(y=yVals, x=xVals) + """ + if y is not None: + data = y + if data2 is not None: + x = data + data = data2 + if clear: self.clear() if params is None: @@ -588,7 +613,7 @@ class PlotItem(QtGui.QGraphicsWidget): #print data, curve self.addCurve(curve, params) if pen is not None: - curve.setPen(pen) + curve.setPen(mkPen(pen)) return curve @@ -619,7 +644,7 @@ class PlotItem(QtGui.QGraphicsWidget): if self.ctrl.averageGroup.isChecked(): self.addAvgCurve(c) - c.connect(QtCore.SIGNAL('plotChanged'), self.plotChanged) + c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) self.plotChanged() def plotChanged(self, curve=None): @@ -643,6 +668,7 @@ class PlotItem(QtGui.QGraphicsWidget): mn -= 1 mx += 1 self.setRange(ax, mn, mx) + #print "Auto range:", ax, mn, mx def replot(self): self.plotChanged() @@ -876,25 +902,6 @@ class PlotItem(QtGui.QGraphicsWidget): mode = False return mode - - - #def mousePressEvent(self, ev): - #self.mousePos = array([ev.pos().x(), ev.pos().y()]) - #self.pressPos = self.mousePos.copy() - #QtGui.QGraphicsWidget.mousePressEvent(self, ev) - ## NOTE: we will only receive move/release events if we run ev.accept() - #print 'press' - - #def mouseReleaseEvent(self, ev): - #pos = array([ev.pos().x(), ev.pos().y()]) - #print 'release' - #if sum(abs(self.pressPos - pos)) < 3: ## Detect click - #if ev.button() == QtCore.Qt.RightButton: - #print 'popup' - #self.ctrlMenu.popup(self.mapToGlobal(ev.pos())) - #self.mousePos = pos - #QtGui.QGraphicsWidget.mouseReleaseEvent(self, ev) - def resizeEvent(self, ev): self.ctrlBtn.move(0, self.size().height() - self.ctrlBtn.size().height()) self.autoBtn.move(self.ctrlBtn.width(), self.size().height() - self.autoBtn.size().height()) @@ -906,14 +913,8 @@ class PlotItem(QtGui.QGraphicsWidget): def ctrlBtnClicked(self): self.ctrlMenu.popup(self.mouseScreenPos) - #def _checkLabelKey(self, key): - #if key not in self.labels: - #raise Exception("Label '%s' not found. Labels are: %s" % (key, str(self.labels.keys()))) - def getLabel(self, key): pass - #self._checkLabelKey(key) - #return self.labels[key]['item'] def _checkScaleKey(self, key): if key not in self.scales: @@ -925,43 +926,9 @@ class PlotItem(QtGui.QGraphicsWidget): def setLabel(self, key, text=None, units=None, unitPrefix=None, **args): self.getScale(key).setLabel(text=text, units=units, unitPrefix=unitPrefix, **args) - #if text is not None: - #self.labels[key]['text'] = text - #if units != None: - #self.labels[key]['units'] = units - #if unitPrefix != None: - #self.labels[key]['unitPrefix'] = unitPrefix - - #text = self.labels[key]['text'] - #units = self.labels[key]['units'] - #unitPrefix = self.labels[key]['unitPrefix'] - - #if text is not '' or units is not '': - #l = self.getLabel(key) - #l.setText("%s (%s%s)" % (text, unitPrefix, units), **args) - #self.showLabel(key) - def showLabel(self, key, show=True): self.getScale(key).showLabel(show) - #l = self.getLabel(key) - #p = self.labels[key]['pos'] - #if show: - #l.show() - #if key in ['left', 'right']: - #self.layout.setColumnFixedWidth(p[1], l.size().width()) - #l.setMaximumWidth(20) - #else: - #self.layout.setRowFixedHeight(p[0], l.size().height()) - #l.setMaximumHeight(20) - #else: - #l.hide() - #if key in ['left', 'right']: - #self.layout.setColumnFixedWidth(p[1], 0) - #l.setMaximumWidth(0) - #else: - #self.layout.setRowFixedHeight(p[0], 0) - #l.setMaximumHeight(0) def setTitle(self, title=None, **args): if title is None: @@ -979,20 +946,8 @@ class PlotItem(QtGui.QGraphicsWidget): p = self.scales[key]['pos'] if show: s.show() - #if key in ['left', 'right']: - #self.layout.setColumnFixedWidth(p[1], s.maximumWidth()) - ##s.setMaximumWidth(40) - #else: - #self.layout.setRowFixedHeight(p[0], s.maximumHeight()) - #s.setMaximumHeight(20) else: s.hide() - #if key in ['left', 'right']: - #self.layout.setColumnFixedWidth(p[1], 0) - ##s.setMaximumWidth(0) - #else: - #self.layout.setRowFixedHeight(p[0], 0) - #s.setMaximumHeight(0) def _plotArray(self, arr, x=None): if arr.ndim != 1: diff --git a/PlotWidget.py b/PlotWidget.py index 81429af2..de4797d5 100644 --- a/PlotWidget.py +++ b/PlotWidget.py @@ -51,4 +51,7 @@ class PlotWidget(GraphicsView): return self.plotItem.restoreState(state) def getPlotItem(self): - return self.plotItem \ No newline at end of file + return self.plotItem + + + \ No newline at end of file diff --git a/Point.py b/Point.py index c03b4c06..1027bc5d 100644 --- a/Point.py +++ b/Point.py @@ -92,20 +92,24 @@ class Point(QtCore.QPointF): return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1])) def length(self): + """Returns the vector length of this Point.""" return (self[0]**2 + self[1]**2) ** 0.5 def angle(self, a): + """Returns the angle between this vector and the vector a.""" n1 = self.length() n2 = a.length() if n1 == 0. or n2 == 0.: return None - ang = acos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) + ## Probably this should be done with arctan2 instead.. + ang = acos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) ### in radians c = self.cross(a) if c > 0: ang *= -1. return ang def dot(self, a): + """Returns the dot product of a and this Point.""" a = Point(a) return self[0]*a[0] + self[1]*a[1] diff --git a/__init__.py b/__init__.py index e69de29b..f15464ec 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +### import all the goodies and add some helper functions for easy CLI use + +from functions import * +from graphicsItems import * +from graphicsWindows import * +#import PlotWidget +#import ImageView +from PyQt4 import QtGui + +plots = [] +images = [] +QAPP = None + +def plot(*args, **kargs): + mkQApp() + if 'title' in kargs: + w = PlotWindow(title=kargs['title']) + del kargs['title'] + else: + w = PlotWindow() + w.plot(*args, **kargs) + plots.append(w) + w.show() + return w + +def show(*args, **kargs): + mkQApp() + w = ImageWindow(*args, **kargs) + images.append(w) + w.show() + return w + +def mkQApp(): + if QtGui.QApplication.instance() is None: + global QAPP + QAPP = QtGui.QApplication([]) \ No newline at end of file diff --git a/examples/test_PlotWidget.py b/examples/test_PlotWidget.py index 80e4a9c4..8c98ca22 100755 --- a/examples/test_PlotWidget.py +++ b/examples/test_PlotWidget.py @@ -88,6 +88,6 @@ t.start(50) for i in range(0, 5): for j in range(0, 3): yd, xd = rand(10000) - pw2.plot(yd*(j+1), xd, params={'iter': i, 'val': j}) + pw2.plot(y=yd*(j+1), x=xd, params={'iter': i, 'val': j}) #app.exec_() diff --git a/examples/test_ROItypes.py b/examples/test_ROItypes.py index c35a698d..eb9fe7a8 100755 --- a/examples/test_ROItypes.py +++ b/examples/test_ROItypes.py @@ -4,7 +4,8 @@ import sys, os sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) -from scipy import zeros +from numpy import random +from scipy import zeros, ones from pyqtgraph.graphicsWindows import * from pyqtgraph.graphicsItems import * from pyqtgraph.widgets import * @@ -84,6 +85,7 @@ rois.append(MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=mkPen(2))) rois.append(EllipseROI([110, 10], [30, 20], pen=mkPen(3))) rois.append(CircleROI([110, 50], [20, 20], pen=mkPen(4))) rois.append(PolygonROI([[2,0], [2.1,0], [2,.1]], pen=mkPen(5))) +#rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0))) for r in rois: s.addItem(r) c = pi1.plot(pen=r.pen) diff --git a/functions.py b/functions.py index aeb74194..f5227146 100644 --- a/functions.py +++ b/functions.py @@ -5,6 +5,18 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ +colorAbbrev = { + 'b': (0,0,255,255), + 'g': (0,255,0,255), + 'r': (255,0,0,255), + 'c': (0,255,255,255), + 'm': (255,0,255,255), + 'y': (255,255,0,255), + 'k': (0,0,0,255), + 'w': (255,255,255,255), +} + + from PyQt4 import QtGui from numpy import clip, floor, log @@ -26,11 +38,25 @@ def siScale(x, minVal=1e-25): p = .001**m return (p, pref) +def mkBrush(color): + return QtGui.QBrush(mkColor(color)) - -def mkPen(color=None, width=1, style=None, cosmetic=True, hsv=None, ): +def mkPen(arg=None, color=None, width=1, style=None, cosmetic=True, hsv=None, ): + """Convenience function for making pens. Examples: + mkPen(color) + mkPen(color, width=2) + mkPen(cosmetic=False, width=4.5, color='r') + mkPen({'color': "FF0", width: 2}) + """ + if isinstance(arg, dict): + return mkPen(**arg) + elif arg is not None: + if isinstance(arg, QtGui.QPen): + return arg + color = arg + if color is None: - color = [255, 255, 255] + color = mkColor(200, 200, 200) if hsv is not None: color = hsvColor(*hsv) else: @@ -48,20 +74,64 @@ def hsvColor(h, s=1.0, v=1.0, a=1.0): return c def mkColor(*args): - """make a QColor from a variety of argument types""" + """make a QColor from a variety of argument types + accepted types are: + r, g, b, [a] + (r, g, b, [a]) + float (greyscale, 0.0-1.0) + int (uses intColor) + (int, hues) (uses intColor) + QColor + "c" (see colorAbbrev dictionary) + "RGB" (strings may optionally begin with "#") + "RGBA" + "RRGGBB" + "RRGGBBAA" + """ err = 'Not sure how to make a color from "%s"' % str(args) if len(args) == 1: if isinstance(args[0], QtGui.QColor): return QtGui.QColor(args[0]) + elif isinstance(args[0], float): + r = g = b = int(args[0] * 255) + a = 255 + elif isinstance(args[0], basestring): + c = args[0] + if c[0] == '#': + c = c[1:] + if len(c) == 1: + (r, g, b, a) = colorAbbrev[c] + if len(c) == 3: + r = int(c[0]*2, 16) + g = int(c[1]*2, 16) + b = int(c[2]*2, 16) + a = 255 + elif len(c) == 4: + r = int(c[0]*2, 16) + g = int(c[1]*2, 16) + b = int(c[2]*2, 16) + a = int(c[3]*2, 16) + elif len(c) == 6: + r = int(c[0:2], 16) + g = int(c[2:4], 16) + b = int(c[4:6], 16) + a = 255 + elif len(c) == 8: + r = int(c[0:2], 16) + g = int(c[2:4], 16) + b = int(c[4:6], 16) + a = int(c[6:8], 16) elif hasattr(args[0], '__len__'): if len(args[0]) == 3: (r, g, b) = args[0] a = 255 elif len(args[0]) == 4: (r, g, b, a) = args[0] + elif len(args[0]) == 2: + return intColor(*args[0]) else: raise Exception(err) - if type(args[0]) == int: + elif type(args[0]) == int: return intColor(args[0]) else: raise Exception(err) @@ -74,19 +144,27 @@ def mkColor(*args): raise Exception(err) return QtGui.QColor(r, g, b, a) +def colorTuple(c): + return (c.red(), c.blue(), c.green(), c.alpha()) + def colorStr(c): """Generate a hex string code from a QColor""" - return ('%02x'*4) % (c.red(), c.blue(), c.green(), c.alpha()) + return ('%02x'*4) % colorTuple(c) -def intColor(ind, colors=9, values=3, maxValue=255, minValue=150, sat=255): - """Creates a QColor from a single index. Useful for stepping through a predefined list of colors.""" - colors = int(colors) +def intColor(index, hues=9, values=3, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255): + """Creates a QColor from a single index. Useful for stepping through a predefined list of colors. + - The argument "index" determines which color from the set will be returned + - All other arguments determine what the set of predefined colors will be + + Colors are chosen by cycling across hues while varying the value (brightness). By default, there + are 9 hues and 3 values for a total of 27 different colors. """ + hues = int(hues) values = int(values) - ind = int(ind) % (colors * values) - indh = ind % colors - indv = ind / colors + ind = int(index) % (hues * values) + indh = ind % hues + indv = ind / hues v = minValue + indv * ((maxValue-minValue) / (values-1)) - h = (indh * 360) / colors + h = minHue + (indh * (maxHue-minHue)) / hues c = QtGui.QColor() c.setHsv(h, sat, v) diff --git a/graphicsItems.py b/graphicsItems.py index 299b9cf8..a5ac37f6 100644 --- a/graphicsItems.py +++ b/graphicsItems.py @@ -9,6 +9,8 @@ Provides ImageItem, PlotCurveItem, and ViewBox, amongst others. from PyQt4 import QtGui, QtCore +if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal from ObjectWorkaround import * #tryWorkaround(QtCore, QtGui) #from numpy import * @@ -178,7 +180,7 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): else: useWeave = False - def __init__(self, image=None, copy=True, parent=None, *args): + def __init__(self, image=None, copy=True, parent=None, border=None, *args): QObjectWorkaround.__init__(self) self.qimage = QtGui.QImage() self.pixmap = None @@ -189,6 +191,9 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): self.image = None self.clipLevel = None self.drawKernel = None + if border is not None: + border = mkPen(border) + self.border = border QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) @@ -248,9 +253,9 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): else: gotNewData = True if copy: - self.image = image.copy() + self.image = image.view(np.ndarray).copy() else: - self.image = image + self.image = image.view(np.ndarray) #print " image max:", self.image.max(), "min:", self.image.min() # Determine scale factors @@ -381,22 +386,36 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): def setDrawKernel(self, kernel=None): self.drawKernel = kernel - + def paint(self, p, *args): + + #QtGui.QGraphicsPixmapItem.paint(self, p, *args) + if self.pixmap is None: + return + + p.drawPixmap(self.boundingRect(), self.pixmap, QtCore.QRectF(0, 0, self.pixmap.width(), self.pixmap.height())) + if self.border is not None: + p.setPen(self.border) + p.drawRect(self.boundingRect()) class PlotCurveItem(GraphicsObject): """Class representing a single plot curve.""" - def __init__(self, y=None, x=None, copy=False, pen=None, shadow=None, parent=None, color=None): + + sigClicked = QtCore.Signal(object) + + def __init__(self, y=None, x=None, copy=False, pen=None, shadow=None, parent=None, color=None, clickable=False): GraphicsObject.__init__(self, parent) + #GraphicsWidget.__init__(self, parent) self.free() #self.dispPath = None if pen is None: if color is None: - pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) + self.setPen((200,200,200)) else: - pen = QtGui.QPen(color) - self.pen = pen + self.setPen(color) + else: + self.setPen(pen) self.shadow = shadow if y is not None: @@ -414,8 +433,13 @@ class PlotCurveItem(GraphicsObject): 'alphaMode': False } + self.setClickable(clickable) #self.fps = None + def setClickable(self, s): + self.clickable = s + + def getData(self): if self.xData is None: return (None, None) @@ -497,7 +521,7 @@ class PlotCurveItem(GraphicsObject): return self.metaData def setPen(self, pen): - self.pen = pen + self.pen = mkPen(pen) self.update() def setColor(self, color): @@ -547,6 +571,13 @@ class PlotCurveItem(GraphicsObject): x = np.array(x) if not isinstance(data, np.ndarray) or data.ndim > 2: raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape)) + if x == None: + if 'complex' in str(data.dtype): + raise Exception("Can not plot complex data types.") + else: + if 'complex' in str(data.dtype)+str(x.dtype): + raise Exception("Can not plot complex data types.") + if data.ndim == 2: ### If data is 2D array, then assume x and y values are in first two columns or rows. if x is not None: raise Exception("Plot data may be 2D only if no x argument is supplied.") @@ -573,7 +604,7 @@ class PlotCurveItem(GraphicsObject): self.xData = x if x is None: - self.xData = arange(0, self.yData.shape[0]) + self.xData = np.arange(0, self.yData.shape[0]) if self.xData.shape != self.yData.shape: raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) @@ -691,10 +722,181 @@ class PlotCurveItem(GraphicsObject): self.path = None #del self.xData, self.yData, self.xDisp, self.yDisp, self.path - -class ScatterPlotItem(QtGui.QGraphicsItem): - def __init__(self, spots=None, pxMode=True, pen=None, brush=None, size=5): + def mousePressEvent(self, ev): + #GraphicsObject.mousePressEvent(self, ev) + if not self.clickable: + ev.ignore() + if ev.button() != QtCore.Qt.LeftButton: + ev.ignore() + self.mousePressPos = ev.pos() + self.mouseMoved = False + + def mouseMoveEvent(self, ev): + #GraphicsObject.mouseMoveEvent(self, ev) + self.mouseMoved = True + print "move" + + def mouseReleaseEvent(self, ev): + #GraphicsObject.mouseReleaseEvent(self, ev) + if not self.mouseMoved: + self.sigClicked.emit(self) + + +class CurvePoint(QtGui.QGraphicsItem, QObjectWorkaround): + """A GraphicsItem that sets its location to a point on a PlotCurveItem. + The position along the curve is a property, and thus can be easily animated.""" + + def __init__(self, curve, index=0, pos=None): + """Position can be set either as an index referring to the sample number or + the position 0.0 - 1.0""" + QtGui.QGraphicsItem.__init__(self) + QObjectWorkaround.__init__(self) + self.curve = None + self.setProperty('position', 0.0) + self.setProperty('index', 0) + + if hasattr(self, 'ItemHasNoContents'): + self.setFlags(self.flags() | self.ItemHasNoContents) + + self.curve = curve + self.setParentItem(curve) + if pos is not None: + self.setPos(pos) + else: + self.setIndex(index) + + def setPos(self, pos): + self.setProperty('position', pos) + + def setIndex(self, index): + self.setProperty('index', index) + + def event(self, ev): + if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve is None: + return + + if ev.propertyName() == 'index': + index = self.property('index').toInt()[0] + elif ev.propertyName() == 'position': + index = None + else: + return + + (x, y) = self.curve.getData() + if index is None: + #print self.property('position').toDouble()[0], self.property('position').typeName() + index = (len(x)-1) * clip(self.property('position').toDouble()[0], 0.0, 1.0) + + if index != int(index): + i1 = int(index) + i2 = clip(i1+1, 0, len(x)-1) + s2 = index-i1 + s1 = 1.0-s2 + newPos = (x[i1]*s1+x[i2]*s2, y[i1]*s1+y[i2]*s2) + else: + index = int(index) + i1 = clip(index-1, 0, len(x)-1) + i2 = clip(index+1, 0, len(x)-1) + newPos = (x[index], y[index]) + + p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1])) + p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2])) + ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) + self.resetTransform() + self.rotate(180+ ang * 180 / np.pi) + QtGui.QGraphicsItem.setPos(self, *newPos) + + def boundingRect(self): + return QtCore.QRectF() + + def paint(self, *args): + pass + + def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1): + anim = QtCore.QPropertyAnimation(self._qObj_, prop) + anim.setDuration(duration) + anim.setStartValue(start) + anim.setEndValue(end) + anim.setLoopCount(loop) + return anim + + + +class ArrowItem(QtGui.QGraphicsPolygonItem): + def __init__(self, **opts): + QtGui.QGraphicsPolygonItem.__init__(self) + defOpts = { + 'style': 'tri', + 'pxMode': True, + 'size': 20, + 'angle': -150, + 'pos': (0,0), + 'width': 8, + 'tipAngle': 25, + 'baseAngle': 90, + 'pen': (200,200,200), + 'brush': (50,50,200), + } + defOpts.update(opts) + + self.setStyle(**defOpts) + + self.setPen(mkPen(defOpts['pen'])) + self.setBrush(mkBrush(defOpts['brush'])) + + self.rotate(self.opts['angle']) + self.moveBy(*self.opts['pos']) + + def setStyle(self, **opts): + self.opts = opts + + if opts['style'] == 'tri': + points = [ + QtCore.QPointF(0,0), + QtCore.QPointF(opts['size'],-opts['width']/2.), + QtCore.QPointF(opts['size'],opts['width']/2.), + ] + poly = QtGui.QPolygonF(points) + + else: + raise Exception("Unrecognized arrow style '%s'" % opts['style']) + + self.setPolygon(poly) + + if opts['pxMode']: + self.setFlags(self.flags() | self.ItemIgnoresTransformations) + else: + self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + QtGui.QGraphicsPolygonItem.paint(self, p, *args) + +class CurveArrow(CurvePoint): + """Provides an arrow that points to any specific sample on a PlotCurveItem. + Provides properties that can be animated.""" + + def __init__(self, curve, index=0, pos=None, **opts): + CurvePoint.__init__(self, curve, index=index, pos=pos) + if opts.get('pxMode', True): + opts['pxMode'] = False + self.setFlags(self.flags() | self.ItemIgnoresTransformations) + opts['angle'] = 0 + self.arrow = ArrowItem(**opts) + self.arrow.setParentItem(self) + + def setStyle(**opts): + return self.arrow.setStyle(**opts) + + + +class ScatterPlotItem(QtGui.QGraphicsWidget): + + sigPointClicked = QtCore.Signal(object) + + def __init__(self, spots=None, pxMode=True, pen=None, brush=None, size=5): + QtGui.QGraphicsWidget.__init__(self) self.spots = [] self.range = [[0,0], [0,0]] @@ -740,7 +942,8 @@ class ScatterPlotItem(QtGui.QGraphicsItem): brush = s.get('brush', self.brush) pen = s.get('pen', self.pen) pen.setCosmetic(True) - item = self.mkSpot(pos, size, self.pxMode, brush, pen) + data = s.get('data', None) + item = self.mkSpot(pos, size, self.pxMode, brush, pen, data) self.spots.append(item) if xmn is None: xmn = pos[0]-size @@ -755,10 +958,11 @@ class ScatterPlotItem(QtGui.QGraphicsItem): self.range = [[xmn, xmx], [ymn, ymx]] - def mkSpot(self, pos, size, pxMode, brush, pen): - item = SpotItem(size, pxMode, brush, pen) + def mkSpot(self, pos, size, pxMode, brush, pen, data): + item = SpotItem(size, pxMode, brush, pen, data) item.setParentItem(self) item.setPos(pos) + item.sigClicked.connect(self.pointClicked) return item def boundingRect(self): @@ -769,11 +973,17 @@ class ScatterPlotItem(QtGui.QGraphicsItem): def paint(self, p, *args): pass + def pointClicked(self, point): + self.sigPointClicked.emit(point) + def points(self): + return self.spots[:] -class SpotItem(QtGui.QGraphicsItem): - def __init__(self, size, pxMode, brush, pen): - QtGui.QGraphicsItem.__init__(self) +class SpotItem(QtGui.QGraphicsWidget): + sigClicked = QtCore.Signal(object) + + def __init__(self, size, pxMode, brush, pen, data): + QtGui.QGraphicsWidget.__init__(self) if pxMode: self.setFlags(self.flags() | self.ItemIgnoresTransformations) #self.setCacheMode(self.DeviceCoordinateCache) ## causes crash on linux @@ -782,6 +992,15 @@ class SpotItem(QtGui.QGraphicsItem): self.path = QtGui.QPainterPath() s2 = size/2. self.path.addEllipse(QtCore.QRectF(-s2, -s2, size, size)) + self.data = data + + def setBrush(self, brush): + self.brush = mkBrush(brush) + self.update() + + def setPen(self, pen): + self.pen = mkPen(pen) + self.update() def boundingRect(self): return self.path.boundingRect() @@ -794,8 +1013,26 @@ class SpotItem(QtGui.QGraphicsItem): p.setBrush(self.brush) p.drawPath(self.path) + def mousePressEvent(self, ev): + QtGui.QGraphicsItem.mousePressEvent(self, ev) + if ev.button() == QtCore.Qt.LeftButton: + self.mouseMoved = False + ev.accept() + else: + ev.ignore() + + def mouseMoveEvent(self, ev): + QtGui.QGraphicsItem.mouseMoveEvent(self, ev) + self.mouseMoved = True + pass + + def mouseReleaseEvent(self, ev): + QtGui.QGraphicsItem.mouseReleaseEvent(self, ev) + if not self.mouseMoved: + self.sigClicked.emit(self) + class ROIPlotItem(PlotCurveItem): @@ -806,7 +1043,7 @@ class ROIPlotItem(PlotCurveItem): self.axes = axes self.xVals = xVals PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color) - roi.connect(QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) + roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) #self.roiChangedEvent() def getRoiData(self): @@ -1361,7 +1598,7 @@ class ViewBox(QtGui.QGraphicsWidget): #self.picture = None self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) - self.drawFrame = True + self.drawFrame = False self.mouseEnabled = [True, True] @@ -1493,6 +1730,7 @@ class ViewBox(QtGui.QGraphicsWidget): def mouseMoveEvent(self, ev): + QtGui.QGraphicsWidget.mouseMoveEvent(self, ev) pos = np.array([ev.pos().x(), ev.pos().y()]) dif = pos - self.mousePos dif *= -1 @@ -1523,11 +1761,14 @@ class ViewBox(QtGui.QGraphicsWidget): ev.ignore() def mousePressEvent(self, ev): + QtGui.QGraphicsWidget.mousePressEvent(self, ev) + self.mousePos = np.array([ev.pos().x(), ev.pos().y()]) self.pressPos = self.mousePos.copy() ev.accept() def mouseReleaseEvent(self, ev): + QtGui.QGraphicsWidget.mouseReleaseEvent(self, ev) pos = np.array([ev.pos().x(), ev.pos().y()]) #if sum(abs(self.pressPos - pos)) < 3: ## Detect click #if ev.button() == QtCore.Qt.RightButton: @@ -1619,14 +1860,12 @@ class InfiniteLine(GraphicsObject): self.maxRange = [None, None] else: self.maxRange = bounds - self.movable = movable + self.setMovable(movable) self.view = weakref.ref(view) self.p = [0, 0] self.setAngle(angle) self.setPos(pos) - if movable: - self.setAcceptHoverEvents(True) self.hasMoved = False @@ -1639,7 +1878,12 @@ class InfiniteLine(GraphicsObject): #for p in self.getBoundingParents(): #QtCore.QObject.connect(p, QtCore.SIGNAL('viewChanged'), self.updateLine) QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateLine) + + def setMovable(self, m): + self.movable = m + self.setAcceptHoverEvents(m) + def setBounds(self, bounds): self.maxRange = bounds self.setValue(self.value()) @@ -1825,7 +2069,6 @@ class LinearRegionItem(GraphicsObject): self.rect.setParentItem(self) self.bounds = QtCore.QRectF() self.view = weakref.ref(view) - self.setBrush = self.rect.setBrush self.brush = self.rect.brush @@ -1841,18 +2084,23 @@ class LinearRegionItem(GraphicsObject): for l in self.lines: l.setParentItem(self) - l.connect(QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished) - l.connect(QtCore.SIGNAL('positionChanged'), self.lineMoved) + l.connect(l, QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished) + l.connect(l, QtCore.SIGNAL('positionChanged'), self.lineMoved) if brush is None: brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) self.setBrush(brush) + self.setMovable(movable) def setBounds(self, bounds): for l in self.lines: l.setBounds(bounds) - + def setMovable(self, m): + for l in self.lines: + l.setMovable(m) + self.movable = m + def boundingRect(self): return self.rect.boundingRect() @@ -1878,6 +2126,9 @@ class LinearRegionItem(GraphicsObject): self.rect.setRect(vb) def mousePressEvent(self, ev): + if not self.movable: + ev.ignore() + return for l in self.lines: l.mousePressEvent(ev) ## pass event to both lines so they move together #if self.movable and ev.button() == QtCore.Qt.LeftButton: @@ -1892,6 +2143,8 @@ class LinearRegionItem(GraphicsObject): def mouseMoveEvent(self, ev): #print "move", ev.pos() + if not self.movable: + return self.lines[0].blockSignals(True) # only want to update once for l in self.lines: l.mouseMoveEvent(ev) @@ -1919,7 +2172,7 @@ class VTickGroup(QtGui.QGraphicsPathItem): if xvals is None: xvals = [] if pen is None: - pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) + pen = (200, 200, 200) self.ticks = [] self.xvals = [] if view is None: @@ -1932,14 +2185,9 @@ class VTickGroup(QtGui.QGraphicsPathItem): self.setXVals(xvals) self.valid = False - - #def setPen(self, pen=None): - #if pen is None: - #pen = self.pen - #self.pen = pen - #for t in self.ticks: - #t.setPen(pen) - ##self.update() + def setPen(self, pen): + pen = mkPen(pen) + QtGui.QGraphicsPathItem.setPen(self, pen) def setXVals(self, vals): self.xvals = vals @@ -2065,7 +2313,6 @@ class GridItem(UIGraphicsItem): x = ul[1] ul[1] = br[1] br[1] = x - for i in range(2, -1, -1): ## Draw three different scales of grid dist = br-ul diff --git a/graphicsWindows.py b/graphicsWindows.py index 55eba8e8..4f51f052 100644 --- a/graphicsWindows.py +++ b/graphicsWindows.py @@ -9,30 +9,119 @@ from PyQt4 import QtCore, QtGui from PlotWidget import * from ImageView import * QAPP = None -class PlotWindow(QtGui.QMainWindow): - def __init__(self, title=None): - if QtGui.QApplication.instance() is None: - global QAPP - QAPP = QtGui.QApplication([]) - QtGui.QMainWindow.__init__(self) - self.cw = PlotWidget() - self.setCentralWidget(self.cw) - for m in ['plot', 'autoRange', 'addItem', 'removeItem', 'setLabel', 'clear']: - setattr(self, m, getattr(self.cw, m)) - if title is not None: - self.setWindowTitle(title) - self.show() -class ImageWindow(QtGui.QMainWindow): - def __init__(self, title=None): - if QtGui.QApplication.instance() is None: - global QAPP - QAPP = QtGui.QApplication([]) +def mkQApp(): + if QtGui.QApplication.instance() is None: + global QAPP + QAPP = QtGui.QApplication([]) + +class GraphicsLayoutWidget(GraphicsView): + def __init__(self): + GraphicsView.__init__(self) + self.items = {} + self.currentRow = 0 + self.currentCol = 0 + + def nextRow(self): + """Advance to next row for automatic item placement""" + self.currentRow += 1 + self.currentCol = 0 + + def nextCol(self, colspan=1): + """Advance to next column, while returning the current column number + (generally only for internal use)""" + self.currentCol += colspan + return self.currentCol-colspan + + def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + plot = PlotItem(**kargs) + self.addItem(plot, row, col, rowspan, colspan) + return plot + + def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): + if row not in self.items: + self.items[row] = {} + self.items[row][col] = item + + if row is None: + row = self.currentRow + if col is None: + col = self.nextCol(colspan) + self.centralLayout.addItem(item, row, col, rowspan, colspan) + + def getItem(self, row, col): + return self.items[row][col] + + +class GraphicsWindow(GraphicsLayoutWidget): + def __init__(self, title=None, size=(800,600)): + mkQApp() + self.win = QtGui.QMainWindow() + GraphicsLayoutWidget.__init__(self) + self.win.setCentralWidget(self) + self.win.resize(*size) + if title is not None: + self.win.setWindowTitle(title) + self.win.show() + + +class TabWindow(QtGui.QMainWindow): + def __init__(self, title=None, size=(800,600)): + mkQApp() QtGui.QMainWindow.__init__(self) - self.cw = ImageView() + self.resize(*size) + self.cw = QtGui.QTabWidget() self.setCentralWidget(self.cw) - for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']: - setattr(self, m, getattr(self.cw, m)) if title is not None: self.setWindowTitle(title) self.show() + + def __getattr__(self, attr): + if hasattr(self.cw, attr): + return getattr(self.cw, attr) + else: + raise NameError(attr) + + +#class PlotWindow(QtGui.QMainWindow): + #def __init__(self, title=None, **kargs): + #mkQApp() + #QtGui.QMainWindow.__init__(self) + #self.cw = PlotWidget(**kargs) + #self.setCentralWidget(self.cw) + #for m in ['plot', 'autoRange', 'addItem', 'removeItem', 'setLabel', 'clear', 'viewRect']: + #setattr(self, m, getattr(self.cw, m)) + #if title is not None: + #self.setWindowTitle(title) + #self.show() + + +class PlotWindow(PlotWidget): + def __init__(self, title=None, **kargs): + mkQApp() + self.win = QtGui.QMainWindow() + PlotWidget.__init__(self, **kargs) + self.win.setCentralWidget(self) + for m in ['resize']: + setattr(self, m, getattr(self.win, m)) + if title is not None: + self.win.setWindowTitle(title) + self.win.show() + + +class ImageWindow(ImageView): + def __init__(self, *args, **kargs): + mkQApp() + self.win = QtGui.QMainWindow() + if 'title' in kargs: + self.win.setWindowTitle(kargs['title']) + del kargs['title'] + ImageView.__init__(self, self.win) + if len(args) > 0 or len(kargs) > 0: + self.setImage(*args, **kargs) + self.win.setCentralWidget(self) + for m in ['resize']: + setattr(self, m, getattr(self.win, m)) + #for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']: + #setattr(self, m, getattr(self.cw, m)) + self.win.show() diff --git a/widgets.py b/widgets.py index e4f92581..6fa7ad95 100644 --- a/widgets.py +++ b/widgets.py @@ -144,6 +144,11 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): raise Exception("Scale/rotate handles must have either the same x or y coordinate as their center point.") return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}) + def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None): + pos = Point(pos) + center = Point(center) + return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}) + def addHandle(self, info): if not info.has_key('item') or info['item'] is None: #print "BEFORE ADD CHILD:", self.childItems() @@ -262,14 +267,20 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier()): #print "movePoint() called." + ## pos is the new position of the handle in scene coords, as requested by the handle. + newState = self.stateCopy() h = self.handles[pt] #p0 = self.mapToScene(h['item'].pos()) + ## p0 is current (before move) position of handle in scene coords p0 = self.mapToScene(h['pos'] * self.state['size']) p1 = Point(pos) + + ## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why. p0 = self.mapSceneToParent(p0) p1 = self.mapSceneToParent(p1) + ## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1) if h.has_key('center'): c = h['center'] cs = c * self.state['size'] @@ -296,15 +307,18 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): #cs = c * self.state['size'] #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs + ## If a handle and its center have the same x or y value, we can't scale across that axis. if h['center'][0] == h['pos'][0]: lp1[0] = 0 if h['center'][1] == h['pos'][1]: lp1[1] = 0 + ## snap if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize + ## determine scale factors and new size of ROI hs = h['pos'] - c if hs[0] == 0: hs[0] = 1 @@ -312,6 +326,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): hs[1] = 1 newSize = lp1 / hs + ## Perform some corrections and limit checks if newSize[0] == 0: newSize[0] = newState['size'][0] if newSize[1] == 0: @@ -324,10 +339,12 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): if self.aspectLocked: newSize[0] = newSize[1] + ## Move ROI so the center point occupies the same scene location after the scale s0 = c * self.state['size'] s1 = c * newSize cc = self.mapToParent(s0 - s1) - self.mapToParent(Point(0, 0)) + ## update state, do more boundary checks newState['size'] = newSize newState['pos'] = newState['pos'] + cc if self.maxBounds is not None: @@ -339,30 +356,31 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): self.prepareGeometryChange() self.state = newState + ## move handles to their new locations self.updateHandles() - elif h['type'] == 'r': - #newState = self.stateCopy() - #c = h['center'] - #cs = c * self.state['size'] - #p0 = Point(h['item'].pos()) - cs - #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs + elif h['type'] in ['r', 'rf']: + ## If the handle is directly over its center point, we can't compute an angle. if lp1.length() == 0 or lp0.length() == 0: return + ## determine new rotation angle, constrained if necessary ang = newState['angle'] + lp0.angle(lp1) - if ang is None: + if ang is None: ## this should never happen.. return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): ang = round(ang / (np.pi/12.)) * (np.pi/12.) - + ## create rotation transform tr = QtGui.QTransform() tr.rotate(-ang * 180. / np.pi) + ## mvoe ROI so that center point remains stationary after rotate cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) newState['angle'] = ang newState['pos'] = newState['pos'] + cc + + ## check boundaries, update if self.maxBounds is not None: r = self.stateRect(newState) if not self.maxBounds.contains(r): @@ -370,6 +388,45 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): self.setTransform(tr) self.setPos(newState['pos'], update=False) self.state = newState + + ## If this is a free-rotate handle, its distance from the center may change. + + if h['type'] == 'rf': + h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle + + + #elif h['type'] == 'rf': + ### If the handle is directly over its center point, we can't compute an angle. + #if lp1.length() == 0 or lp0.length() == 0: + #return + + ### determine new rotation angle, constrained if necessary + #pos = Point(pos) + #ang = newState['angle'] + lp0.angle(lp1) + #if ang is None: + ##h['item'].setPos(self.mapFromScene(Point(pos[0], 0.0))) ## changes ROI coordinates of handle + #h['item'].setPos(self.mapFromScene(pos)) + #return + #if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): + #ang = round(ang / (np.pi/12.)) * (np.pi/12.) + + + #tr = QtGui.QTransform() + #tr.rotate(-ang * 180. / np.pi) + + #cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) + #newState['angle'] = ang + #newState['pos'] = newState['pos'] + cc + #if self.maxBounds is not None: + #r = self.stateRect(newState) + #if not self.maxBounds.contains(r): + #return + #self.setTransform(tr) + #self.setPos(newState['pos'], update=False) + #self.state = newState + + #h['item'].setPos(self.mapFromScene(pos)) ## changes ROI coordinates of handle + ##self.emit(QtCore.SIGNAL('regionChanged'), self) elif h['type'] == 'sr': #newState = self.stateCopy() @@ -419,6 +476,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): self.handleChange() def handleChange(self): + """The state of the ROI has changed; redraw if needed.""" #print "handleChange() called." changed = False #print "self.lastState:", self.lastState @@ -446,7 +504,8 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): self.state['pos'] = self.state['pos'] + c - c1 self.setPos(self.state['pos']) self.updateHandles() - + + def translate(self, *args, **kargs): """accepts either (x, y, snap) or ([x,y], snap) as arguments""" if 'snap' not in kargs: @@ -687,6 +746,9 @@ class Handle(QtGui.QGraphicsItem): elif typ == 'sr': self.sides = 12 self.startAng = 0 + elif typ == 'rf': + self.sides = 12 + self.startAng = 0 else: self.sides = 4 self.startAng = np.pi/4 @@ -723,6 +785,7 @@ class Handle(QtGui.QGraphicsItem): if not r[0].checkPointMove(r[1], pos, modifiers): return #print "point moved; inform %d ROIs" % len(self.roi) + # A handle can be used by multiple ROIs; tell each to update its handle position for r in self.roi: r[0].movePoint(r[1], pos, modifiers) @@ -915,6 +978,7 @@ class PolygonROI(ROI): #ROI.__init__(self, positions[0]) for p in positions: self.addFreeHandle(p) + self.setZValue(1000) def listPoints(self): return [p['item'].pos() for p in self.handles] @@ -922,6 +986,8 @@ class PolygonROI(ROI): def movePoint(self, *args, **kargs): ROI.movePoint(self, *args, **kargs) self.prepareGeometryChange() + for h in self.handles: + h['pos'] = h['item'].pos() def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) @@ -934,7 +1000,7 @@ class PolygonROI(ROI): def boundingRect(self): r = QtCore.QRectF() for h in self.handles: - r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() + r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs return r def shape(self): @@ -943,4 +1009,89 @@ class PolygonROI(ROI): for i in range(len(self.handles)): p.lineTo(self.handles[i]['item'].pos()) return p + + def stateCopy(self): + sc = {} + sc['pos'] = Point(self.state['pos']) + sc['size'] = Point(self.state['size']) + sc['angle'] = self.state['angle'] + #sc['handles'] = self.handles + return sc + +class SpiralROI(ROI): + def __init__(self, pos=None, size=None, **args): + if size == None: + size = [100e-6,100e-6] + if pos == None: + pos = [0,0] + ROI.__init__(self, pos, size, **args) + self.translateSnap = False + self.addFreeHandle([0.25,0], name='a') + self.addRotateFreeHandle([1,0], [0,0], name='r') + #self.getRadius() + #QtCore.connect(self, QtCore.SIGNAL('regionChanged'), self. + + + def getRadius(self): + radius = Point(self.handles[1]['item'].pos()).length() + #r2 = radius[1] + #r3 = r2[0] + return radius + + def boundingRect(self): + r = self.getRadius() + return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r) + #return self.bounds + + def movePoint(self, *args, **kargs): + ROI.movePoint(self, *args, **kargs) + self.prepareGeometryChange() + for h in self.handles: + h['pos'] = h['item'].pos()/self.state['size'][0] + + def handleChange(self): + ROI.handleChange(self) + if len(self.handles) > 1: + self.path = QtGui.QPainterPath() + h0 = Point(self.handles[0]['item'].pos()).length() + a = h0/(2.0*np.pi) + theta = 30.0*(2.0*np.pi)/360.0 + self.path.moveTo(QtCore.QPointF(a*theta*cos(theta), a*theta*sin(theta))) + x0 = a*theta*cos(theta) + y0 = a*theta*sin(theta) + radius = self.getRadius() + theta += 20.0*(2.0*np.pi)/360.0 + i = 0 + while Point(x0, y0).length() < radius and i < 1000: + x1 = a*theta*cos(theta) + y1 = a*theta*sin(theta) + self.path.lineTo(QtCore.QPointF(x1,y1)) + theta += 20.0*(2.0*np.pi)/360.0 + x0 = x1 + y0 = y1 + i += 1 + + + return self.path + + + def shape(self): + p = QtGui.QPainterPath() + p.addEllipse(self.boundingRect()) + return p + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + #path = self.shape() + p.setPen(self.pen) + p.drawPath(self.path) + p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) + p.drawPath(self.shape()) + p.setPen(QtGui.QPen(QtGui.QColor(0,0,255))) + p.drawRect(self.boundingRect()) + + + + + \ No newline at end of file From 397a1c8a66b0a36428277739a416ea21a61375d7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 9 Feb 2011 22:44:09 -0500 Subject: [PATCH 2/2] bugfixes --- GraphicsView.py | 18 +++----- PlotItem.py | 2 +- examples/test_PlotWidget.py | 3 +- graphicsItems.py | 21 ++------- widgets.py | 88 +++++++++++++++++++++++-------------- 5 files changed, 66 insertions(+), 66 deletions(-) diff --git a/GraphicsView.py b/GraphicsView.py index dda96506..ac582b65 100644 --- a/GraphicsView.py +++ b/GraphicsView.py @@ -226,14 +226,15 @@ class GraphicsView(QtGui.QGraphicsView): def wheelEvent(self, ev): - QtGui.QGraphicsView.wheelEvent(self, ev) if not self.mouseEnabled: return + QtGui.QGraphicsView.wheelEvent(self, ev) sc = 1.001 ** ev.delta() #self.scale *= sc #self.updateMatrix() self.scale(sc, sc) + def setAspectLocked(self, s): self.aspectLocked = s @@ -311,13 +312,11 @@ class GraphicsView(QtGui.QGraphicsView): #self.currentItem = None def mouseMoveEvent(self, ev): - #if self.lastMousePos is None: - #self.lastMousePos = Point(ev.pos()) - #delta = Point(ev.pos()) - self.lastMousePos - #if abs(delta[0]) > 100 or abs(delta[1]) > 100: ## Weird bug generating extra events.. - #return - #self.lastMousePos = Point(ev.pos()) - #print "move", delta + if self.lastMousePos is None: + self.lastMousePos = Point(ev.pos()) + delta = Point(ev.pos()) - self.lastMousePos + self.lastMousePos = Point(ev.pos()) + QtGui.QGraphicsView.mouseMoveEvent(self, ev) if not self.mouseEnabled: return @@ -327,8 +326,6 @@ class GraphicsView(QtGui.QGraphicsView): if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it. return - - if ev.buttons() == QtCore.Qt.RightButton: delta = Point(clip(delta[0], -50, 50), clip(-delta[1], -50, 50)) @@ -338,7 +335,6 @@ class GraphicsView(QtGui.QGraphicsView): self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) - elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. tr = -delta / self.currentScale diff --git a/PlotItem.py b/PlotItem.py index 336e659d..286e621e 100644 --- a/PlotItem.py +++ b/PlotItem.py @@ -953,7 +953,7 @@ class PlotItem(QtGui.QGraphicsWidget): if arr.ndim != 1: raise Exception("Array must be 1D to plot (shape is %s)" % arr.shape) if x is None: - x = np.arange(arr.shape[0]) + x = arange(arr.shape[0]) if x.ndim != 1: raise Exception("X array must be 1D to plot (shape is %s)" % x.shape) c = PlotCurveItem(arr, x=x) diff --git a/examples/test_PlotWidget.py b/examples/test_PlotWidget.py index 76dab924..8c98ca22 100755 --- a/examples/test_PlotWidget.py +++ b/examples/test_PlotWidget.py @@ -5,7 +5,6 @@ import sys, os sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) from scipy import random -from numpy import array, arange from PyQt4 import QtGui, QtCore from pyqtgraph.PlotWidget import * from pyqtgraph.graphicsItems import * @@ -91,4 +90,4 @@ for i in range(0, 5): yd, xd = rand(10000) pw2.plot(y=yd*(j+1), x=xd, params={'iter': i, 'val': j}) -app.exec_() +#app.exec_() diff --git a/graphicsItems.py b/graphicsItems.py index bd6bff35..a5ac37f6 100644 --- a/graphicsItems.py +++ b/graphicsItems.py @@ -1458,10 +1458,8 @@ class ScaleItem(QtGui.QGraphicsWidget): else: xs = bounds.width() / dif - tickPositions = set() # remembers positions of previously drawn ticks ## draw ticks and text - ## draw three different intervals, long ticks first - for i in reversed([i1, i1+1, i1+2]): + for i in [i1, i1+1, i1+2]: ## draw three different intervals if i > len(intervals): continue ## spacing for this interval @@ -1505,11 +1503,7 @@ class ScaleItem(QtGui.QGraphicsWidget): if p1[1-axis] < 0: continue p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100, a))) - # draw tick only if there is none - tickPos = p1[1-axis] - if tickPos not in tickPositions: - p.drawLine(Point(p1), Point(p2)) - tickPositions.add(tickPos) + p.drawLine(Point(p1), Point(p2)) if i == textLevel: if abs(v) < .001 or abs(v) >= 10000: vstr = "%g" % (v * self.scale) @@ -1734,16 +1728,7 @@ class ViewBox(QtGui.QGraphicsWidget): #self.replot(autoRange=False) #self.updateMatrix() - def wheelEvent(self, ev): - mask = np.array(self.mouseEnabled, dtype=np.float) - degree = ev.delta() / 8.0; - dif = np.array([degree, degree]) - s = ((mask * 0.02) + 1) ** dif - center = Point(self.childGroup.transform().inverted()[0].map(ev.pos())) - self.scaleBy(s, center) - self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) - ev.accept() - + def mouseMoveEvent(self, ev): QtGui.QGraphicsWidget.mouseMoveEvent(self, ev) pos = np.array([ev.pos().x(), ev.pos().y()]) diff --git a/widgets.py b/widgets.py index 6fa7ad95..14e04ae5 100644 --- a/widgets.py +++ b/widgets.py @@ -53,6 +53,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): self.setPos(pos) self.rotate(-angle * 180. / np.pi) self.setZValue(10) + self.isMoving = False self.handleSize = 5 self.invertible = invertible @@ -208,15 +209,23 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): if ev.button() == QtCore.Qt.LeftButton: self.setSelected(True) if self.translatable: + self.isMoving = True + self.preMoveState = self.getState() self.cursorOffset = self.scenePos() - ev.scenePos() self.emit(QtCore.SIGNAL('regionChangeStarted'), self) ev.accept() + elif ev.button() == QtCore.Qt.RightButton: + if self.isMoving: + ev.accept() + self.cancelMove() + else: + ev.ignore() else: ev.ignore() def mouseMoveEvent(self, ev): #print "mouse move", ev.pos() - if self.translatable: + if self.translatable and self.isMoving and ev.buttons() == QtCore.Qt.LeftButton: snap = None if self.translateSnap or (ev.modifiers() & QtCore.Qt.ControlModifier): snap = Point(self.snapSize, self.snapSize) @@ -226,18 +235,25 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): def mouseReleaseEvent(self, ev): if self.translatable: + self.isMoving = False self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - + def cancelMove(self): + self.isMoving = False + self.setState(self.preMoveState) def pointPressEvent(self, pt, ev): #print "press" + self.isMoving = True + self.preMoveState = self.getState() + self.emit(QtCore.SIGNAL('regionChangeStarted'), self) #self.pressPos = self.mapFromScene(ev.scenePos()) #self.pressHandlePos = self.handles[pt]['item'].pos() def pointReleaseEvent(self, pt, ev): #print "release" + self.isMoving = False self.emit(QtCore.SIGNAL('regionChangeFinished'), self) def stateCopy(self): @@ -718,6 +734,16 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): class Handle(QtGui.QGraphicsItem): + + types = { ## defines number of sides, start angle for each handle type + 't': (4, np.pi/4), + 'f': (4, np.pi/4), + 's': (4, 0), + 'r': (12, 0), + 'sr': (12, 0), + 'rf': (12, 0), + } + def __init__(self, radius, typ=None, pen=QtGui.QPen(QtGui.QColor(200, 200, 220)), parent=None): #print " create item with parent", parent self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) @@ -731,27 +757,8 @@ class Handle(QtGui.QGraphicsItem): self.pen = pen self.pen.setWidth(0) self.pen.setCosmetic(True) - if typ == 't': - self.sides = 4 - self.startAng = np.pi/4 - elif typ == 'f': - self.sides = 4 - self.startAng = np.pi/4 - elif typ == 's': - self.sides = 4 - self.startAng = 0 - elif typ == 'r': - self.sides = 12 - self.startAng = 0 - elif typ == 'sr': - self.sides = 12 - self.startAng = 0 - elif typ == 'rf': - self.sides = 12 - self.startAng = 0 - else: - self.sides = 4 - self.startAng = np.pi/4 + self.isMoving = False + self.sides, self.startAng = self.types[typ] def connectROI(self, roi, i): self.roi.append((roi, i)) @@ -761,24 +768,37 @@ class Handle(QtGui.QGraphicsItem): def mousePressEvent(self, ev): #print "handle press" - if ev.button() != QtCore.Qt.LeftButton: + if ev.button() == QtCore.Qt.LeftButton: + self.isMoving = True + self.cursorOffset = self.scenePos() - ev.scenePos() + for r in self.roi: + r[0].pointPressEvent(r[1], ev) + #print " accepted." + ev.accept() + elif ev.button() == QtCore.Qt.RightButton: + if self.isMoving: + self.isMoving = False ## prevents any further motion + for r in self.roi: + r[0].cancelMove() + ev.accept() + else: + ev.ignore() + else: ev.ignore() - return - self.cursorOffset = self.scenePos() - ev.scenePos() - for r in self.roi: - r[0].pointPressEvent(r[1], ev) - #print " accepted." - ev.accept() + def mouseReleaseEvent(self, ev): #print "release" - for r in self.roi: - r[0].pointReleaseEvent(r[1], ev) + if ev.button() == QtCore.Qt.LeftButton: + self.isMoving = False + for r in self.roi: + r[0].pointReleaseEvent(r[1], ev) def mouseMoveEvent(self, ev): #print "handle mouseMove", ev.pos() - pos = ev.scenePos() + self.cursorOffset - self.movePoint(pos, ev.modifiers()) + if self.isMoving and ev.buttons() == QtCore.Qt.LeftButton: + pos = ev.scenePos() + self.cursorOffset + self.movePoint(pos, ev.modifiers()) def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier()): for r in self.roi: