From 724debf2d48b328eb51b89964bed01040fc53c15 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 29 May 2012 23:22:00 -0400 Subject: [PATCH] Changed the way GraphicsItem.pixelVectors and pixelLength work. The values returned are more useful now, but this introduces a minor API change. --- graphicsItems/GraphicsItem.py | 82 +++++++++++++++++++++++++---------- graphicsItems/InfiniteLine.py | 33 +++++--------- 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/graphicsItems/GraphicsItem.py b/graphicsItems/GraphicsItem.py index 838fb76d..8139cf05 100644 --- a/graphicsItems/GraphicsItem.py +++ b/graphicsItems/GraphicsItem.py @@ -9,6 +9,10 @@ class GraphicsItem(object): Abstract class providing useful methods to GraphicsObject and GraphicsWidget. (This is required because we cannot have multiple inheritance with QObject subclasses.) + + A note about Qt's GraphicsView framework: + + The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task. """ def __init__(self, register=True): self._viewWidget = None @@ -31,10 +35,10 @@ class GraphicsItem(object): return None self._viewWidget = weakref.ref(self.scene().views()[0]) return self._viewWidget() - + def forgetViewWidget(self): self._viewWidget = None - + def getViewBox(self): """ Return the first ViewBox or GraphicsView which bounds this item's visible space. @@ -72,7 +76,15 @@ class GraphicsItem(object): if view is None: return None viewportTransform = view.viewportTransform() - return QtGui.QGraphicsObject.deviceTransform(self, viewportTransform) + dt = QtGui.QGraphicsObject.deviceTransform(self, viewportTransform) + + #xmag = abs(dt.m11())+abs(dt.m12()) + #ymag = abs(dt.m21())+abs(dt.m22()) + #if xmag * ymag == 0: + if dt.determinant() == 0: ## occurs when deviceTransform is invalid because widget has not been displayed + return None + else: + return dt def viewTransform(self): """Return the transform that maps from local coordinates to the item's ViewBox coordinates @@ -123,35 +135,59 @@ class GraphicsItem(object): - def pixelVectors(self): - """Return vectors in local coordinates representing the width and height of a view pixel.""" - vt = self.deviceTransform() - if vt is None: - return None - vt = vt.inverted()[0] - orig = vt.map(QtCore.QPointF(0, 0)) - return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig + + def pixelVectors(self, direction=None): + """Return vectors in local coordinates representing the width and height of a view pixel. + If direction is specified, then return vectors parallel and orthogonal to it. - def pixelLength(self, direction): - """ - Return the length of one pixel in the direction indicated (in local coordinates) - If the result would be infinite (this happens if the device transform is not properly configured yet), - then return None instead. - """ + Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed).""" + dt = self.deviceTransform() if dt is None: - return None + return None, None + + if direction is None: + direction = Point(1, 0) + viewDir = Point(dt.map(direction) - dt.map(Point(0,0))) - try: - norm = viewDir.norm() - except ZeroDivisionError: - return None + orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space + + try: + normView = viewDir.norm() ## direction of one pixel orthogonal to line + normOrtho = orthoDir.norm() + except: + raise Exception("Invalid direction %s" %direction) + + dti = dt.inverted()[0] - return Point(dti.map(norm)-dti.map(Point(0,0))).length() + return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0))) + + #vt = self.deviceTransform() + #if vt is None: + #return None + #vt = vt.inverted()[0] + #orig = vt.map(QtCore.QPointF(0, 0)) + #return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig + + def pixelLength(self, direction, ortho=False): + """Return the length of one pixel in the direction indicated (in local coordinates) + If ortho=True, then return the length of one pixel orthogonal to the direction indicated. + + Return None if pixel size is not yet defined (usually because the item has not yet been displayed). + """ + normV, orthoV = self.pixelVectors(direction) + if normV == None or orthoV == None: + return None + if ortho: + return orthoV.length() + return normV.length() + def pixelSize(self): v = self.pixelVectors() + if v == (None, None): + return None, None return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5 def pixelWidth(self): diff --git a/graphicsItems/InfiniteLine.py b/graphicsItems/InfiniteLine.py index 692c242f..4f0df863 100644 --- a/graphicsItems/InfiniteLine.py +++ b/graphicsItems/InfiniteLine.py @@ -1,15 +1,15 @@ from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Point import Point -from .UIGraphicsItem import UIGraphicsItem +from .GraphicsObject import GraphicsObject import pyqtgraph.functions as fn import numpy as np import weakref __all__ = ['InfiniteLine'] -class InfiniteLine(UIGraphicsItem): +class InfiniteLine(GraphicsObject): """ - **Bases:** :class:`UIGraphicsItem ` + **Bases:** :class:`GraphicsObject ` Displays a line of infinite length. This line may be dragged to indicate a position in data coordinates. @@ -42,7 +42,7 @@ class InfiniteLine(UIGraphicsItem): ============= ================================================================== """ - UIGraphicsItem.__init__(self) + GraphicsObject.__init__(self) if bounds is None: ## allowed value boundaries for orthogonal lines self.maxRange = [None, None] @@ -121,7 +121,7 @@ class InfiniteLine(UIGraphicsItem): if self.p != newPos: self.p = newPos - UIGraphicsItem.setPos(self, Point(self.p)) + GraphicsObject.setPos(self, Point(self.p)) self.update() self.sigPositionChanged.emit(self) @@ -161,31 +161,18 @@ class InfiniteLine(UIGraphicsItem): #return GraphicsObject.itemChange(self, change, val) def boundingRect(self): - br = UIGraphicsItem.boundingRect(self) - + #br = UIGraphicsItem.boundingRect(self) + br = self.viewRect() ## add a 4-pixel radius around the line for mouse interaction. - #print "line bounds:", self, br - dt = self.deviceTransform() - if dt is None: - return QtCore.QRectF() - lineDir = Point(dt.map(Point(1, 0)) - dt.map(Point(0,0))) ## direction of line in pixel-space - orthoDir = Point(lineDir[1], -lineDir[0]) ## orthogonal to line in pixel-space - try: - norm = orthoDir.norm() ## direction of one pixel orthogonal to line - except ZeroDivisionError: - return br - - dti = dt.inverted()[0] - px = Point(dti.map(norm)-dti.map(Point(0,0))) ## orthogonal pixel mapped back to item coords - px = px[1] ## project to y-direction - + px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line + if px is None: + px = 0 br.setBottom(-px*4) br.setTop(px*4) return br.normalized() def paint(self, p, *args): - UIGraphicsItem.paint(self, p, *args) br = self.boundingRect() p.setPen(self.currentPen) p.drawLine(Point(br.right(), 0), Point(br.left(), 0))