sync with lp:~luke-campagnola/pyqtgraph/main

This commit is contained in:
Ingo B. 2011-02-16 18:11:23 +01:00
commit 3b820c2972
12 changed files with 804 additions and 132 deletions

View File

@ -66,8 +66,14 @@ class GraphicsView(QtGui.QGraphicsView):
self.updateMatrix() self.updateMatrix()
self.sceneObj = QtGui.QGraphicsScene() self.sceneObj = QtGui.QGraphicsScene()
self.setScene(self.sceneObj) 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.centralWidget = None
self.setCentralItem(QtGui.QGraphicsWidget()) self.setCentralItem(QtGui.QGraphicsWidget())
self.centralLayout = QtGui.QGraphicsGridLayout()
self.centralWidget.setLayout(self.centralLayout)
self.mouseEnabled = False self.mouseEnabled = False
self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False)
self.clickAccepted = False self.clickAccepted = False
@ -85,6 +91,7 @@ class GraphicsView(QtGui.QGraphicsView):
ev.ignore() ev.ignore()
def setCentralItem(self, item): def setCentralItem(self, item):
"""Sets a QGraphicsWidget to automatically fill the entire view."""
if self.centralWidget is not None: if self.centralWidget is not None:
self.scene().removeItem(self.centralWidget) self.scene().removeItem(self.centralWidget)
self.centralWidget = item self.centralWidget = item
@ -304,22 +311,21 @@ class GraphicsView(QtGui.QGraphicsView):
#self.currentItem = None #self.currentItem = None
def mouseMoveEvent(self, ev): def mouseMoveEvent(self, ev):
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) QtGui.QGraphicsView.mouseMoveEvent(self, ev)
if not self.mouseEnabled: if not self.mouseEnabled:
return return
self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos())) self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos()))
#print "moved. Grabber:", self.scene().mouseGrabberItem() #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. if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it.
return return
delta = Point(ev.pos()) - self.lastMousePos
self.lastMousePos = Point(ev.pos())
if ev.buttons() == QtCore.Qt.RightButton: if ev.buttons() == QtCore.Qt.RightButton:
delta = Point(clip(delta[0], -50, 50), clip(-delta[1], -50, 50)) delta = Point(clip(delta[0], -50, 50), clip(-delta[1], -50, 50))
scale = 1.01 ** delta scale = 1.01 ** delta
@ -328,7 +334,6 @@ class GraphicsView(QtGui.QGraphicsView):
self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos))
self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) 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. elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button.
tr = -delta / self.currentScale tr = -delta / self.currentScale
@ -376,6 +381,10 @@ class GraphicsView(QtGui.QGraphicsView):
self.render(painter) self.render(painter)
painter.end() 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): #def getFreehandLine(self):

View File

@ -106,12 +106,12 @@ class ImageView(QtGui.QWidget):
setattr(self, fn, getattr(self.ui.graphicsView, fn)) setattr(self, fn, getattr(self.ui.graphicsView, fn))
#QtCore.QObject.connect(self.ui.timeSlider, QtCore.SIGNAL('valueChanged(int)'), self.timeChanged) #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.whiteSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage)
#QtCore.QObject.connect(self.ui.blackSlider, 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.gradientWidget, QtCore.SIGNAL('gradientChanged'), self.updateImage)
QtCore.QObject.connect(self.ui.roiBtn, QtCore.SIGNAL('clicked()'), self.roiClicked) 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.normBtn, QtCore.SIGNAL('toggled(bool)'), self.normToggled)
QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm)
QtCore.QObject.connect(self.ui.normSubtractRadio, 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.normStartSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm)
#QtCore.QObject.connect(self.ui.normStopSlider, 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.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') 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} self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None}
elif img.ndim == 4: elif img.ndim == 4:
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} 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 self.imageDisp = None
if autoLevels: if autoLevels:

View File

@ -1,9 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from PyQt4 import QtGui, QtCore 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: class QObjectWorkaround:
def __init__(self): def __init__(self):
self._qObj_ = QtCore.QObject() self._qObj_ = Obj()
self.connect(QtCore.SIGNAL('event'), self.event)
def connect(self, *args): def connect(self, *args):
if args[0] is self: if args[0] is self:
return QtCore.QObject.connect(self._qObj_, *args[1:]) return QtCore.QObject.connect(self._qObj_, *args[1:])
@ -15,8 +23,20 @@ class QObjectWorkaround:
return QtCore.QObject.emit(self._qObj_, *args) return QtCore.QObject.emit(self._qObj_, *args)
def blockSignals(self, b): def blockSignals(self, b):
return self._qObj_.blockSignals(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): #class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround):
def __init__(self, *args): #def __init__(self, *args):
QtGui.QGraphicsItem.__init__(self, *args) #QtGui.QGraphicsItem.__init__(self, *args)
QObjectWorkaround.__init__(self) #QObjectWorkaround.__init__(self)
class QGraphicsObject(QtGui.QGraphicsWidget):
def shape(self):
return QtGui.QGraphicsItem.shape(self)
#QGraphicsObject = QtGui.QGraphicsObject

View File

@ -52,3 +52,6 @@ class PlotWidget(GraphicsView):
def getPlotItem(self): def getPlotItem(self):
return self.plotItem return self.plotItem

View File

@ -92,20 +92,24 @@ class Point(QtCore.QPointF):
return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1])) return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1]))
def length(self): def length(self):
"""Returns the vector length of this Point."""
return (self[0]**2 + self[1]**2) ** 0.5 return (self[0]**2 + self[1]**2) ** 0.5
def angle(self, a): def angle(self, a):
"""Returns the angle between this vector and the vector a."""
n1 = self.length() n1 = self.length()
n2 = a.length() n2 = a.length()
if n1 == 0. or n2 == 0.: if n1 == 0. or n2 == 0.:
return None 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) c = self.cross(a)
if c > 0: if c > 0:
ang *= -1. ang *= -1.
return ang return ang
def dot(self, a): def dot(self, a):
"""Returns the dot product of a and this Point."""
a = Point(a) a = Point(a)
return self[0]*a[0] + self[1]*a[1] return self[0]*a[0] + self[1]*a[1]

View File

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

View File

@ -89,6 +89,6 @@ t.start(50)
for i in range(0, 5): for i in range(0, 5):
for j in range(0, 3): for j in range(0, 3):
yd, xd = rand(10000) 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_() app.exec_()

View File

@ -4,7 +4,8 @@
import sys, os import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) 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.graphicsWindows import *
from pyqtgraph.graphicsItems import * from pyqtgraph.graphicsItems import *
from pyqtgraph.widgets 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(EllipseROI([110, 10], [30, 20], pen=mkPen(3)))
rois.append(CircleROI([110, 50], [20, 20], pen=mkPen(4))) 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(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: for r in rois:
s.addItem(r) s.addItem(r)
c = pi1.plot(pen=r.pen) c = pi1.plot(pen=r.pen)

View File

@ -5,6 +5,18 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation. 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 PyQt4 import QtGui
from numpy import clip, floor, log from numpy import clip, floor, log
@ -26,11 +38,25 @@ def siScale(x, minVal=1e-25):
p = .001**m p = .001**m
return (p, pref) return (p, pref)
def mkBrush(color):
return QtGui.QBrush(mkColor(color))
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
def mkPen(color=None, width=1, style=None, cosmetic=True, hsv=None, ):
if color is None: if color is None:
color = [255, 255, 255] color = mkColor(200, 200, 200)
if hsv is not None: if hsv is not None:
color = hsvColor(*hsv) color = hsvColor(*hsv)
else: else:
@ -48,20 +74,64 @@ def hsvColor(h, s=1.0, v=1.0, a=1.0):
return c return c
def mkColor(*args): 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) err = 'Not sure how to make a color from "%s"' % str(args)
if len(args) == 1: if len(args) == 1:
if isinstance(args[0], QtGui.QColor): if isinstance(args[0], QtGui.QColor):
return QtGui.QColor(args[0]) 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__'): elif hasattr(args[0], '__len__'):
if len(args[0]) == 3: if len(args[0]) == 3:
(r, g, b) = args[0] (r, g, b) = args[0]
a = 255 a = 255
elif len(args[0]) == 4: elif len(args[0]) == 4:
(r, g, b, a) = args[0] (r, g, b, a) = args[0]
elif len(args[0]) == 2:
return intColor(*args[0])
else: else:
raise Exception(err) raise Exception(err)
if type(args[0]) == int: elif type(args[0]) == int:
return intColor(args[0]) return intColor(args[0])
else: else:
raise Exception(err) raise Exception(err)
@ -74,19 +144,27 @@ def mkColor(*args):
raise Exception(err) raise Exception(err)
return QtGui.QColor(r, g, b, a) return QtGui.QColor(r, g, b, a)
def colorTuple(c):
return (c.red(), c.blue(), c.green(), c.alpha())
def colorStr(c): def colorStr(c):
"""Generate a hex string code from a QColor""" """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): 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.""" """Creates a QColor from a single index. Useful for stepping through a predefined list of colors.
colors = int(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) values = int(values)
ind = int(ind) % (colors * values) ind = int(index) % (hues * values)
indh = ind % colors indh = ind % hues
indv = ind / colors indv = ind / hues
v = minValue + indv * ((maxValue-minValue) / (values-1)) v = minValue + indv * ((maxValue-minValue) / (values-1))
h = (indh * 360) / colors h = minHue + (indh * (maxHue-minHue)) / hues
c = QtGui.QColor() c = QtGui.QColor()
c.setHsv(h, sat, v) c.setHsv(h, sat, v)

View File

@ -9,6 +9,8 @@ Provides ImageItem, PlotCurveItem, and ViewBox, amongst others.
from PyQt4 import QtGui, QtCore from PyQt4 import QtGui, QtCore
if not hasattr(QtCore, 'Signal'):
QtCore.Signal = QtCore.pyqtSignal
from ObjectWorkaround import * from ObjectWorkaround import *
#tryWorkaround(QtCore, QtGui) #tryWorkaround(QtCore, QtGui)
#from numpy import * #from numpy import *
@ -178,7 +180,7 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround):
else: else:
useWeave = False 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) QObjectWorkaround.__init__(self)
self.qimage = QtGui.QImage() self.qimage = QtGui.QImage()
self.pixmap = None self.pixmap = None
@ -189,6 +191,9 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround):
self.image = None self.image = None
self.clipLevel = None self.clipLevel = None
self.drawKernel = None self.drawKernel = None
if border is not None:
border = mkPen(border)
self.border = border
QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) QtGui.QGraphicsPixmapItem.__init__(self, parent, *args)
#self.pixmapItem = QtGui.QGraphicsPixmapItem(self) #self.pixmapItem = QtGui.QGraphicsPixmapItem(self)
@ -248,9 +253,9 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround):
else: else:
gotNewData = True gotNewData = True
if copy: if copy:
self.image = image.copy() self.image = image.view(np.ndarray).copy()
else: else:
self.image = image self.image = image.view(np.ndarray)
#print " image max:", self.image.max(), "min:", self.image.min() #print " image max:", self.image.max(), "min:", self.image.min()
# Determine scale factors # Determine scale factors
@ -381,22 +386,36 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround):
def setDrawKernel(self, kernel=None): def setDrawKernel(self, kernel=None):
self.drawKernel = kernel 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 PlotCurveItem(GraphicsObject):
"""Class representing a single plot curve.""" """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) GraphicsObject.__init__(self, parent)
#GraphicsWidget.__init__(self, parent)
self.free() self.free()
#self.dispPath = None #self.dispPath = None
if pen is None: if pen is None:
if color is None: if color is None:
pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) self.setPen((200,200,200))
else: else:
pen = QtGui.QPen(color) self.setPen(color)
self.pen = pen else:
self.setPen(pen)
self.shadow = shadow self.shadow = shadow
if y is not None: if y is not None:
@ -414,8 +433,13 @@ class PlotCurveItem(GraphicsObject):
'alphaMode': False 'alphaMode': False
} }
self.setClickable(clickable)
#self.fps = None #self.fps = None
def setClickable(self, s):
self.clickable = s
def getData(self): def getData(self):
if self.xData is None: if self.xData is None:
return (None, None) return (None, None)
@ -497,7 +521,7 @@ class PlotCurveItem(GraphicsObject):
return self.metaData return self.metaData
def setPen(self, pen): def setPen(self, pen):
self.pen = pen self.pen = mkPen(pen)
self.update() self.update()
def setColor(self, color): def setColor(self, color):
@ -547,6 +571,13 @@ class PlotCurveItem(GraphicsObject):
x = np.array(x) x = np.array(x)
if not isinstance(data, np.ndarray) or data.ndim > 2: 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)) 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 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: if x is not None:
raise Exception("Plot data may be 2D only if no x argument is supplied.") raise Exception("Plot data may be 2D only if no x argument is supplied.")
@ -691,10 +722,181 @@ class PlotCurveItem(GraphicsObject):
self.path = None self.path = None
#del self.xData, self.yData, self.xDisp, self.yDisp, self.path #del self.xData, self.yData, self.xDisp, self.yDisp, self.path
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"""
class ScatterPlotItem(QtGui.QGraphicsItem):
def __init__(self, spots=None, pxMode=True, pen=None, brush=None, size=5):
QtGui.QGraphicsItem.__init__(self) 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.spots = []
self.range = [[0,0], [0,0]] self.range = [[0,0], [0,0]]
@ -740,7 +942,8 @@ class ScatterPlotItem(QtGui.QGraphicsItem):
brush = s.get('brush', self.brush) brush = s.get('brush', self.brush)
pen = s.get('pen', self.pen) pen = s.get('pen', self.pen)
pen.setCosmetic(True) 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) self.spots.append(item)
if xmn is None: if xmn is None:
xmn = pos[0]-size xmn = pos[0]-size
@ -755,10 +958,11 @@ class ScatterPlotItem(QtGui.QGraphicsItem):
self.range = [[xmn, xmx], [ymn, ymx]] self.range = [[xmn, xmx], [ymn, ymx]]
def mkSpot(self, pos, size, pxMode, brush, pen): def mkSpot(self, pos, size, pxMode, brush, pen, data):
item = SpotItem(size, pxMode, brush, pen) item = SpotItem(size, pxMode, brush, pen, data)
item.setParentItem(self) item.setParentItem(self)
item.setPos(pos) item.setPos(pos)
item.sigClicked.connect(self.pointClicked)
return item return item
def boundingRect(self): def boundingRect(self):
@ -769,11 +973,17 @@ class ScatterPlotItem(QtGui.QGraphicsItem):
def paint(self, p, *args): def paint(self, p, *args):
pass pass
def pointClicked(self, point):
self.sigPointClicked.emit(point)
def points(self):
return self.spots[:]
class SpotItem(QtGui.QGraphicsItem): class SpotItem(QtGui.QGraphicsWidget):
def __init__(self, size, pxMode, brush, pen): sigClicked = QtCore.Signal(object)
QtGui.QGraphicsItem.__init__(self)
def __init__(self, size, pxMode, brush, pen, data):
QtGui.QGraphicsWidget.__init__(self)
if pxMode: if pxMode:
self.setFlags(self.flags() | self.ItemIgnoresTransformations) self.setFlags(self.flags() | self.ItemIgnoresTransformations)
#self.setCacheMode(self.DeviceCoordinateCache) ## causes crash on linux #self.setCacheMode(self.DeviceCoordinateCache) ## causes crash on linux
@ -782,6 +992,15 @@ class SpotItem(QtGui.QGraphicsItem):
self.path = QtGui.QPainterPath() self.path = QtGui.QPainterPath()
s2 = size/2. s2 = size/2.
self.path.addEllipse(QtCore.QRectF(-s2, -s2, size, size)) 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): def boundingRect(self):
return self.path.boundingRect() return self.path.boundingRect()
@ -794,9 +1013,27 @@ class SpotItem(QtGui.QGraphicsItem):
p.setBrush(self.brush) p.setBrush(self.brush)
p.drawPath(self.path) 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): class ROIPlotItem(PlotCurveItem):
def __init__(self, roi, data, img, axes=(0,1), xVals=None, color=None): def __init__(self, roi, data, img, axes=(0,1), xVals=None, color=None):
@ -806,7 +1043,7 @@ class ROIPlotItem(PlotCurveItem):
self.axes = axes self.axes = axes
self.xVals = xVals self.xVals = xVals
PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color) 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() #self.roiChangedEvent()
def getRoiData(self): def getRoiData(self):
@ -1371,7 +1608,7 @@ class ViewBox(QtGui.QGraphicsWidget):
#self.picture = None #self.picture = None
self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
self.drawFrame = True self.drawFrame = False
self.mouseEnabled = [True, True] self.mouseEnabled = [True, True]
@ -1519,6 +1756,7 @@ class ViewBox(QtGui.QGraphicsWidget):
ev.accept() ev.accept()
def mouseMoveEvent(self, ev): def mouseMoveEvent(self, ev):
QtGui.QGraphicsWidget.mouseMoveEvent(self, ev)
pos = np.array([ev.pos().x(), ev.pos().y()]) pos = np.array([ev.pos().x(), ev.pos().y()])
dif = pos - self.mousePos dif = pos - self.mousePos
dif *= -1 dif *= -1
@ -1549,11 +1787,14 @@ class ViewBox(QtGui.QGraphicsWidget):
ev.ignore() ev.ignore()
def mousePressEvent(self, ev): def mousePressEvent(self, ev):
QtGui.QGraphicsWidget.mousePressEvent(self, ev)
self.mousePos = np.array([ev.pos().x(), ev.pos().y()]) self.mousePos = np.array([ev.pos().x(), ev.pos().y()])
self.pressPos = self.mousePos.copy() self.pressPos = self.mousePos.copy()
ev.accept() ev.accept()
def mouseReleaseEvent(self, ev): def mouseReleaseEvent(self, ev):
QtGui.QGraphicsWidget.mouseReleaseEvent(self, ev)
pos = np.array([ev.pos().x(), ev.pos().y()]) pos = np.array([ev.pos().x(), ev.pos().y()])
#if sum(abs(self.pressPos - pos)) < 3: ## Detect click #if sum(abs(self.pressPos - pos)) < 3: ## Detect click
#if ev.button() == QtCore.Qt.RightButton: #if ev.button() == QtCore.Qt.RightButton:
@ -1645,14 +1886,12 @@ class InfiniteLine(GraphicsObject):
self.maxRange = [None, None] self.maxRange = [None, None]
else: else:
self.maxRange = bounds self.maxRange = bounds
self.movable = movable self.setMovable(movable)
self.view = weakref.ref(view) self.view = weakref.ref(view)
self.p = [0, 0] self.p = [0, 0]
self.setAngle(angle) self.setAngle(angle)
self.setPos(pos) self.setPos(pos)
if movable:
self.setAcceptHoverEvents(True)
self.hasMoved = False self.hasMoved = False
@ -1666,6 +1905,11 @@ class InfiniteLine(GraphicsObject):
#QtCore.QObject.connect(p, QtCore.SIGNAL('viewChanged'), self.updateLine) #QtCore.QObject.connect(p, QtCore.SIGNAL('viewChanged'), self.updateLine)
QtCore.QObject.connect(self.view(), 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): def setBounds(self, bounds):
self.maxRange = bounds self.maxRange = bounds
self.setValue(self.value()) self.setValue(self.value())
@ -1851,7 +2095,6 @@ class LinearRegionItem(GraphicsObject):
self.rect.setParentItem(self) self.rect.setParentItem(self)
self.bounds = QtCore.QRectF() self.bounds = QtCore.QRectF()
self.view = weakref.ref(view) self.view = weakref.ref(view)
self.setBrush = self.rect.setBrush self.setBrush = self.rect.setBrush
self.brush = self.rect.brush self.brush = self.rect.brush
@ -1867,17 +2110,22 @@ class LinearRegionItem(GraphicsObject):
for l in self.lines: for l in self.lines:
l.setParentItem(self) l.setParentItem(self)
l.connect(QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished) l.connect(l, QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished)
l.connect(QtCore.SIGNAL('positionChanged'), self.lineMoved) l.connect(l, QtCore.SIGNAL('positionChanged'), self.lineMoved)
if brush is None: if brush is None:
brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50))
self.setBrush(brush) self.setBrush(brush)
self.setMovable(movable)
def setBounds(self, bounds): def setBounds(self, bounds):
for l in self.lines: for l in self.lines:
l.setBounds(bounds) l.setBounds(bounds)
def setMovable(self, m):
for l in self.lines:
l.setMovable(m)
self.movable = m
def boundingRect(self): def boundingRect(self):
return self.rect.boundingRect() return self.rect.boundingRect()
@ -1904,6 +2152,9 @@ class LinearRegionItem(GraphicsObject):
self.rect.setRect(vb) self.rect.setRect(vb)
def mousePressEvent(self, ev): def mousePressEvent(self, ev):
if not self.movable:
ev.ignore()
return
for l in self.lines: for l in self.lines:
l.mousePressEvent(ev) ## pass event to both lines so they move together l.mousePressEvent(ev) ## pass event to both lines so they move together
#if self.movable and ev.button() == QtCore.Qt.LeftButton: #if self.movable and ev.button() == QtCore.Qt.LeftButton:
@ -1918,6 +2169,8 @@ class LinearRegionItem(GraphicsObject):
def mouseMoveEvent(self, ev): def mouseMoveEvent(self, ev):
#print "move", ev.pos() #print "move", ev.pos()
if not self.movable:
return
self.lines[0].blockSignals(True) # only want to update once self.lines[0].blockSignals(True) # only want to update once
for l in self.lines: for l in self.lines:
l.mouseMoveEvent(ev) l.mouseMoveEvent(ev)
@ -1945,7 +2198,7 @@ class VTickGroup(QtGui.QGraphicsPathItem):
if xvals is None: if xvals is None:
xvals = [] xvals = []
if pen is None: if pen is None:
pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) pen = (200, 200, 200)
self.ticks = [] self.ticks = []
self.xvals = [] self.xvals = []
if view is None: if view is None:
@ -1958,14 +2211,9 @@ class VTickGroup(QtGui.QGraphicsPathItem):
self.setXVals(xvals) self.setXVals(xvals)
self.valid = False self.valid = False
def setPen(self, pen):
#def setPen(self, pen=None): pen = mkPen(pen)
#if pen is None: QtGui.QGraphicsPathItem.setPen(self, pen)
#pen = self.pen
#self.pen = pen
#for t in self.ticks:
#t.setPen(pen)
##self.update()
def setXVals(self, vals): def setXVals(self, vals):
self.xvals = vals self.xvals = vals
@ -2091,7 +2339,6 @@ class GridItem(UIGraphicsItem):
x = ul[1] x = ul[1]
ul[1] = br[1] ul[1] = br[1]
br[1] = x br[1] = x
for i in range(2, -1, -1): ## Draw three different scales of grid for i in range(2, -1, -1): ## Draw three different scales of grid
dist = br-ul dist = br-ul

View File

@ -9,30 +9,119 @@ from PyQt4 import QtCore, QtGui
from PlotWidget import * from PlotWidget import *
from ImageView import * from ImageView import *
QAPP = None QAPP = None
class PlotWindow(QtGui.QMainWindow):
def __init__(self, title=None): def mkQApp():
if QtGui.QApplication.instance() is None: if QtGui.QApplication.instance() is None:
global QAPP global QAPP
QAPP = QtGui.QApplication([]) 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) QtGui.QMainWindow.__init__(self)
self.cw = PlotWidget() self.resize(*size)
self.cw = QtGui.QTabWidget()
self.setCentralWidget(self.cw) 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: if title is not None:
self.setWindowTitle(title) self.setWindowTitle(title)
self.show() self.show()
class ImageWindow(QtGui.QMainWindow): def __getattr__(self, attr):
def __init__(self, title=None): if hasattr(self.cw, attr):
if QtGui.QApplication.instance() is None: return getattr(self.cw, attr)
global QAPP else:
QAPP = QtGui.QApplication([]) raise NameError(attr)
QtGui.QMainWindow.__init__(self)
self.cw = ImageView()
self.setCentralWidget(self.cw) #class PlotWindow(QtGui.QMainWindow):
for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']: #def __init__(self, title=None, **kargs):
setattr(self, m, getattr(self.cw, m)) #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: if title is not None:
self.setWindowTitle(title) self.win.setWindowTitle(title)
self.show() 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()

View File

@ -53,6 +53,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
self.setPos(pos) self.setPos(pos)
self.rotate(-angle * 180. / np.pi) self.rotate(-angle * 180. / np.pi)
self.setZValue(10) self.setZValue(10)
self.isMoving = False
self.handleSize = 5 self.handleSize = 5
self.invertible = invertible self.invertible = invertible
@ -144,6 +145,11 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
raise Exception("Scale/rotate handles must have either the same x or y coordinate as their center point.") 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}) 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): def addHandle(self, info):
if not info.has_key('item') or info['item'] is None: if not info.has_key('item') or info['item'] is None:
#print "BEFORE ADD CHILD:", self.childItems() #print "BEFORE ADD CHILD:", self.childItems()
@ -203,15 +209,23 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
if ev.button() == QtCore.Qt.LeftButton: if ev.button() == QtCore.Qt.LeftButton:
self.setSelected(True) self.setSelected(True)
if self.translatable: if self.translatable:
self.isMoving = True
self.preMoveState = self.getState()
self.cursorOffset = self.scenePos() - ev.scenePos() self.cursorOffset = self.scenePos() - ev.scenePos()
self.emit(QtCore.SIGNAL('regionChangeStarted'), self) self.emit(QtCore.SIGNAL('regionChangeStarted'), self)
ev.accept() ev.accept()
elif ev.button() == QtCore.Qt.RightButton:
if self.isMoving:
ev.accept()
self.cancelMove()
else:
ev.ignore()
else: else:
ev.ignore() ev.ignore()
def mouseMoveEvent(self, ev): def mouseMoveEvent(self, ev):
#print "mouse move", ev.pos() #print "mouse move", ev.pos()
if self.translatable: if self.translatable and self.isMoving and ev.buttons() == QtCore.Qt.LeftButton:
snap = None snap = None
if self.translateSnap or (ev.modifiers() & QtCore.Qt.ControlModifier): if self.translateSnap or (ev.modifiers() & QtCore.Qt.ControlModifier):
snap = Point(self.snapSize, self.snapSize) snap = Point(self.snapSize, self.snapSize)
@ -221,18 +235,25 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
def mouseReleaseEvent(self, ev): def mouseReleaseEvent(self, ev):
if self.translatable: if self.translatable:
self.isMoving = False
self.emit(QtCore.SIGNAL('regionChangeFinished'), self) self.emit(QtCore.SIGNAL('regionChangeFinished'), self)
def cancelMove(self):
self.isMoving = False
self.setState(self.preMoveState)
def pointPressEvent(self, pt, ev): def pointPressEvent(self, pt, ev):
#print "press" #print "press"
self.isMoving = True
self.preMoveState = self.getState()
self.emit(QtCore.SIGNAL('regionChangeStarted'), self) self.emit(QtCore.SIGNAL('regionChangeStarted'), self)
#self.pressPos = self.mapFromScene(ev.scenePos()) #self.pressPos = self.mapFromScene(ev.scenePos())
#self.pressHandlePos = self.handles[pt]['item'].pos() #self.pressHandlePos = self.handles[pt]['item'].pos()
def pointReleaseEvent(self, pt, ev): def pointReleaseEvent(self, pt, ev):
#print "release" #print "release"
self.isMoving = False
self.emit(QtCore.SIGNAL('regionChangeFinished'), self) self.emit(QtCore.SIGNAL('regionChangeFinished'), self)
def stateCopy(self): def stateCopy(self):
@ -262,14 +283,20 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier()): def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier()):
#print "movePoint() called." #print "movePoint() called."
## pos is the new position of the handle in scene coords, as requested by the handle.
newState = self.stateCopy() newState = self.stateCopy()
h = self.handles[pt] h = self.handles[pt]
#p0 = self.mapToScene(h['item'].pos()) #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']) p0 = self.mapToScene(h['pos'] * self.state['size'])
p1 = Point(pos) 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) p0 = self.mapSceneToParent(p0)
p1 = self.mapSceneToParent(p1) 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'): if h.has_key('center'):
c = h['center'] c = h['center']
cs = c * self.state['size'] cs = c * self.state['size']
@ -296,15 +323,18 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
#cs = c * self.state['size'] #cs = c * self.state['size']
#p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs #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]: if h['center'][0] == h['pos'][0]:
lp1[0] = 0 lp1[0] = 0
if h['center'][1] == h['pos'][1]: if h['center'][1] == h['pos'][1]:
lp1[1] = 0 lp1[1] = 0
## snap
if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier):
lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize
lp1[1] = round(lp1[1] / 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 hs = h['pos'] - c
if hs[0] == 0: if hs[0] == 0:
hs[0] = 1 hs[0] = 1
@ -312,6 +342,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
hs[1] = 1 hs[1] = 1
newSize = lp1 / hs newSize = lp1 / hs
## Perform some corrections and limit checks
if newSize[0] == 0: if newSize[0] == 0:
newSize[0] = newState['size'][0] newSize[0] = newState['size'][0]
if newSize[1] == 0: if newSize[1] == 0:
@ -324,10 +355,12 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
if self.aspectLocked: if self.aspectLocked:
newSize[0] = newSize[1] newSize[0] = newSize[1]
## Move ROI so the center point occupies the same scene location after the scale
s0 = c * self.state['size'] s0 = c * self.state['size']
s1 = c * newSize s1 = c * newSize
cc = self.mapToParent(s0 - s1) - self.mapToParent(Point(0, 0)) cc = self.mapToParent(s0 - s1) - self.mapToParent(Point(0, 0))
## update state, do more boundary checks
newState['size'] = newSize newState['size'] = newSize
newState['pos'] = newState['pos'] + cc newState['pos'] = newState['pos'] + cc
if self.maxBounds is not None: if self.maxBounds is not None:
@ -339,30 +372,31 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
self.prepareGeometryChange() self.prepareGeometryChange()
self.state = newState self.state = newState
## move handles to their new locations
self.updateHandles() self.updateHandles()
elif h['type'] == 'r': elif h['type'] in ['r', 'rf']:
#newState = self.stateCopy() ## If the handle is directly over its center point, we can't compute an angle.
#c = h['center']
#cs = c * self.state['size']
#p0 = Point(h['item'].pos()) - cs
#p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs
if lp1.length() == 0 or lp0.length() == 0: if lp1.length() == 0 or lp0.length() == 0:
return return
## determine new rotation angle, constrained if necessary
ang = newState['angle'] + lp0.angle(lp1) ang = newState['angle'] + lp0.angle(lp1)
if ang is None: if ang is None: ## this should never happen..
return return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
ang = round(ang / (np.pi/12.)) * (np.pi/12.) ang = round(ang / (np.pi/12.)) * (np.pi/12.)
## create rotation transform
tr = QtGui.QTransform() tr = QtGui.QTransform()
tr.rotate(-ang * 180. / np.pi) 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']) cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos'])
newState['angle'] = ang newState['angle'] = ang
newState['pos'] = newState['pos'] + cc newState['pos'] = newState['pos'] + cc
## check boundaries, update
if self.maxBounds is not None: if self.maxBounds is not None:
r = self.stateRect(newState) r = self.stateRect(newState)
if not self.maxBounds.contains(r): if not self.maxBounds.contains(r):
@ -371,6 +405,45 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
self.setPos(newState['pos'], update=False) self.setPos(newState['pos'], update=False)
self.state = newState 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': elif h['type'] == 'sr':
#newState = self.stateCopy() #newState = self.stateCopy()
if h['center'][0] == h['pos'][0]: if h['center'][0] == h['pos'][0]:
@ -419,6 +492,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
self.handleChange() self.handleChange()
def handleChange(self): def handleChange(self):
"""The state of the ROI has changed; redraw if needed."""
#print "handleChange() called." #print "handleChange() called."
changed = False changed = False
#print "self.lastState:", self.lastState #print "self.lastState:", self.lastState
@ -447,6 +521,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
self.setPos(self.state['pos']) self.setPos(self.state['pos'])
self.updateHandles() self.updateHandles()
def translate(self, *args, **kargs): def translate(self, *args, **kargs):
"""accepts either (x, y, snap) or ([x,y], snap) as arguments""" """accepts either (x, y, snap) or ([x,y], snap) as arguments"""
if 'snap' not in kargs: if 'snap' not in kargs:
@ -659,6 +734,16 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
class Handle(QtGui.QGraphicsItem): 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): def __init__(self, radius, typ=None, pen=QtGui.QPen(QtGui.QColor(200, 200, 220)), parent=None):
#print " create item with parent", parent #print " create item with parent", parent
self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10)
@ -672,24 +757,8 @@ class Handle(QtGui.QGraphicsItem):
self.pen = pen self.pen = pen
self.pen.setWidth(0) self.pen.setWidth(0)
self.pen.setCosmetic(True) self.pen.setCosmetic(True)
if typ == 't': self.isMoving = False
self.sides = 4 self.sides, self.startAng = self.types[typ]
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
else:
self.sides = 4
self.startAng = np.pi/4
def connectROI(self, roi, i): def connectROI(self, roi, i):
self.roi.append((roi, i)) self.roi.append((roi, i))
@ -699,30 +768,44 @@ class Handle(QtGui.QGraphicsItem):
def mousePressEvent(self, ev): def mousePressEvent(self, ev):
#print "handle press" #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() 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): def mouseReleaseEvent(self, ev):
#print "release" #print "release"
for r in self.roi: if ev.button() == QtCore.Qt.LeftButton:
r[0].pointReleaseEvent(r[1], ev) self.isMoving = False
for r in self.roi:
r[0].pointReleaseEvent(r[1], ev)
def mouseMoveEvent(self, ev): def mouseMoveEvent(self, ev):
#print "handle mouseMove", ev.pos() #print "handle mouseMove", ev.pos()
pos = ev.scenePos() + self.cursorOffset if self.isMoving and ev.buttons() == QtCore.Qt.LeftButton:
self.movePoint(pos, ev.modifiers()) pos = ev.scenePos() + self.cursorOffset
self.movePoint(pos, ev.modifiers())
def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier()): def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier()):
for r in self.roi: for r in self.roi:
if not r[0].checkPointMove(r[1], pos, modifiers): if not r[0].checkPointMove(r[1], pos, modifiers):
return return
#print "point moved; inform %d ROIs" % len(self.roi) #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: for r in self.roi:
r[0].movePoint(r[1], pos, modifiers) r[0].movePoint(r[1], pos, modifiers)
@ -915,6 +998,7 @@ class PolygonROI(ROI):
#ROI.__init__(self, positions[0]) #ROI.__init__(self, positions[0])
for p in positions: for p in positions:
self.addFreeHandle(p) self.addFreeHandle(p)
self.setZValue(1000)
def listPoints(self): def listPoints(self):
return [p['item'].pos() for p in self.handles] return [p['item'].pos() for p in self.handles]
@ -922,6 +1006,8 @@ class PolygonROI(ROI):
def movePoint(self, *args, **kargs): def movePoint(self, *args, **kargs):
ROI.movePoint(self, *args, **kargs) ROI.movePoint(self, *args, **kargs)
self.prepareGeometryChange() self.prepareGeometryChange()
for h in self.handles:
h['pos'] = h['item'].pos()
def paint(self, p, *args): def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.Antialiasing)
@ -934,7 +1020,7 @@ class PolygonROI(ROI):
def boundingRect(self): def boundingRect(self):
r = QtCore.QRectF() r = QtCore.QRectF()
for h in self.handles: 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 return r
def shape(self): def shape(self):
@ -944,3 +1030,88 @@ class PolygonROI(ROI):
p.lineTo(self.handles[i]['item'].pos()) p.lineTo(self.handles[i]['item'].pos())
return p 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())