Changed the way GraphicsItem.pixelVectors and pixelLength work.

The values returned are more useful now, but this introduces a minor API change.
This commit is contained in:
Luke Campagnola 2012-05-29 23:22:00 -04:00
parent 5f94cebdaf
commit 724debf2d4
2 changed files with 69 additions and 46 deletions

View File

@ -9,6 +9,10 @@ class GraphicsItem(object):
Abstract class providing useful methods to GraphicsObject and GraphicsWidget. Abstract class providing useful methods to GraphicsObject and GraphicsWidget.
(This is required because we cannot have multiple inheritance with QObject subclasses.) (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): def __init__(self, register=True):
self._viewWidget = None self._viewWidget = None
@ -31,10 +35,10 @@ class GraphicsItem(object):
return None return None
self._viewWidget = weakref.ref(self.scene().views()[0]) self._viewWidget = weakref.ref(self.scene().views()[0])
return self._viewWidget() return self._viewWidget()
def forgetViewWidget(self): def forgetViewWidget(self):
self._viewWidget = None self._viewWidget = None
def getViewBox(self): def getViewBox(self):
""" """
Return the first ViewBox or GraphicsView which bounds this item's visible space. Return the first ViewBox or GraphicsView which bounds this item's visible space.
@ -72,7 +76,15 @@ class GraphicsItem(object):
if view is None: if view is None:
return None return None
viewportTransform = view.viewportTransform() 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): def viewTransform(self):
"""Return the transform that maps from local coordinates to the item's ViewBox coordinates """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.""" def pixelVectors(self, direction=None):
vt = self.deviceTransform() """Return vectors in local coordinates representing the width and height of a view pixel.
if vt is None: If direction is specified, then return vectors parallel and orthogonal to it.
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): Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed)."""
"""
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.
"""
dt = self.deviceTransform() dt = self.deviceTransform()
if dt is None: 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))) viewDir = Point(dt.map(direction) - dt.map(Point(0,0)))
try: orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
norm = viewDir.norm()
except ZeroDivisionError: try:
return None normView = viewDir.norm() ## direction of one pixel orthogonal to line
normOrtho = orthoDir.norm()
except:
raise Exception("Invalid direction %s" %direction)
dti = dt.inverted()[0] 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): def pixelSize(self):
v = self.pixelVectors() 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 return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5
def pixelWidth(self): def pixelWidth(self):

View File

@ -1,15 +1,15 @@
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point from pyqtgraph.Point import Point
from .UIGraphicsItem import UIGraphicsItem from .GraphicsObject import GraphicsObject
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
import numpy as np import numpy as np
import weakref import weakref
__all__ = ['InfiniteLine'] __all__ = ['InfiniteLine']
class InfiniteLine(UIGraphicsItem): class InfiniteLine(GraphicsObject):
""" """
**Bases:** :class:`UIGraphicsItem <pyqtgraph.UIGraphicsItem>` **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
Displays a line of infinite length. Displays a line of infinite length.
This line may be dragged to indicate a position in data coordinates. 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 if bounds is None: ## allowed value boundaries for orthogonal lines
self.maxRange = [None, None] self.maxRange = [None, None]
@ -121,7 +121,7 @@ class InfiniteLine(UIGraphicsItem):
if self.p != newPos: if self.p != newPos:
self.p = newPos self.p = newPos
UIGraphicsItem.setPos(self, Point(self.p)) GraphicsObject.setPos(self, Point(self.p))
self.update() self.update()
self.sigPositionChanged.emit(self) self.sigPositionChanged.emit(self)
@ -161,31 +161,18 @@ class InfiniteLine(UIGraphicsItem):
#return GraphicsObject.itemChange(self, change, val) #return GraphicsObject.itemChange(self, change, val)
def boundingRect(self): 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. ## add a 4-pixel radius around the line for mouse interaction.
#print "line bounds:", self, br px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
dt = self.deviceTransform() if px is None:
if dt is None: px = 0
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
br.setBottom(-px*4) br.setBottom(-px*4)
br.setTop(px*4) br.setTop(px*4)
return br.normalized() return br.normalized()
def paint(self, p, *args): def paint(self, p, *args):
UIGraphicsItem.paint(self, p, *args)
br = self.boundingRect() br = self.boundingRect()
p.setPen(self.currentPen) p.setPen(self.currentPen)
p.drawLine(Point(br.right(), 0), Point(br.left(), 0)) p.drawLine(Point(br.right(), 0), Point(br.left(), 0))