428 lines
16 KiB
Python
428 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
GraphicsView.py - Extension of QGraphicsView
|
|
Copyright 2010 Luke Campagnola
|
|
Distributed under MIT/X11 license. See license.txt for more information.
|
|
"""
|
|
|
|
from ..Qt import QtCore, QtGui, QT_LIB
|
|
|
|
try:
|
|
from ..Qt import QtOpenGL
|
|
HAVE_OPENGL = True
|
|
except ImportError:
|
|
HAVE_OPENGL = False
|
|
|
|
from ..Point import Point
|
|
import sys, os
|
|
import warnings
|
|
from .FileDialog import FileDialog
|
|
from ..GraphicsScene import GraphicsScene
|
|
import numpy as np
|
|
from .. import functions as fn
|
|
from .. import debug as debug
|
|
from .. import getConfigOption
|
|
|
|
__all__ = ['GraphicsView']
|
|
|
|
class GraphicsView(QtGui.QGraphicsView):
|
|
"""Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the
|
|
viewed coordinate range. Also automatically creates a GraphicsScene and a central QGraphicsWidget
|
|
that is automatically scaled to the full view geometry.
|
|
|
|
This widget is the basis for :class:`PlotWidget <pyqtgraph.PlotWidget>`,
|
|
:class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and the view widget in
|
|
:class:`ImageView <pyqtgraph.ImageView>`.
|
|
|
|
By default, the view coordinate system matches the widget's pixel coordinates and
|
|
automatically updates when the view is resized. This can be overridden by setting
|
|
autoPixelRange=False. The exact visible range can be set with setRange().
|
|
|
|
The view can be panned using the middle mouse button and scaled using the right mouse button if
|
|
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
|
|
|
|
sigDeviceRangeChanged = QtCore.Signal(object, object)
|
|
sigDeviceTransformChanged = QtCore.Signal(object)
|
|
sigMouseReleased = QtCore.Signal(object)
|
|
sigSceneMouseMoved = QtCore.Signal(object)
|
|
#sigRegionChanged = QtCore.Signal(object)
|
|
sigScaleChanged = QtCore.Signal(object)
|
|
lastFileDir = None
|
|
|
|
def __init__(self, parent=None, useOpenGL=None, background='default'):
|
|
"""
|
|
============== ============================================================
|
|
**Arguments:**
|
|
parent Optional parent widget
|
|
useOpenGL If True, the GraphicsView will use OpenGL to do all of its
|
|
rendering. This can improve performance on some systems,
|
|
but may also introduce bugs (the combination of
|
|
QGraphicsView and QGLWidget is still an 'experimental'
|
|
feature of Qt)
|
|
background Set the background color of the GraphicsView. Accepts any
|
|
single argument accepted by
|
|
:func:`mkColor <pyqtgraph.mkColor>`. By
|
|
default, the background color is determined using the
|
|
'backgroundColor' configuration option (see
|
|
:func:`setConfigOptions <pyqtgraph.setConfigOptions>`).
|
|
============== ============================================================
|
|
"""
|
|
|
|
self.closed = False
|
|
|
|
QtGui.QGraphicsView.__init__(self, parent)
|
|
|
|
# This connects a cleanup function to QApplication.aboutToQuit. It is
|
|
# called from here because we have no good way to react when the
|
|
# QApplication is created by the user.
|
|
# See pyqtgraph.__init__.py
|
|
from .. import _connectCleanup
|
|
_connectCleanup()
|
|
|
|
if useOpenGL is None:
|
|
useOpenGL = getConfigOption('useOpenGL')
|
|
|
|
self.useOpenGL(useOpenGL)
|
|
|
|
self.setCacheMode(self.CacheBackground)
|
|
|
|
## This might help, but it's probably dangerous in the general case..
|
|
#self.setOptimizationFlag(self.DontSavePainterState, True)
|
|
|
|
self.setBackgroundRole(QtGui.QPalette.NoRole)
|
|
self.setBackground(background)
|
|
|
|
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
|
self.setFrameShape(QtGui.QFrame.NoFrame)
|
|
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
|
|
self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
|
|
self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate)
|
|
|
|
|
|
self.lockedViewports = []
|
|
self.lastMousePos = None
|
|
self.setMouseTracking(True)
|
|
self.aspectLocked = False
|
|
self.range = QtCore.QRectF(0, 0, 1, 1)
|
|
self.autoPixelRange = True
|
|
self.currentItem = None
|
|
self.clearMouse()
|
|
self.updateMatrix()
|
|
# GraphicsScene must have parent or expect crashes!
|
|
self.sceneObj = GraphicsScene(parent=self)
|
|
self.setScene(self.sceneObj)
|
|
|
|
## Workaround for PySide crash
|
|
## This ensures that the scene will outlive the view.
|
|
if QT_LIB == 'PySide':
|
|
self.sceneObj._view_ref_workaround = self
|
|
|
|
## by default we set up a central widget with a grid layout.
|
|
## this can be replaced if needed.
|
|
self.centralWidget = None
|
|
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
|
|
|
|
# Set a transparent background QPalette!
|
|
palette = self.palette()
|
|
palette.setColor(QtGui.QPalette.Window, QtCore.Qt.transparent)
|
|
self.setPalette(palette)
|
|
|
|
def setAntialiasing(self, aa):
|
|
"""Enable or disable default antialiasing.
|
|
Note that this will only affect items that do not specify their own antialiasing options."""
|
|
if aa:
|
|
self.setRenderHints(self.renderHints() | QtGui.QPainter.Antialiasing)
|
|
else:
|
|
self.setRenderHints(self.renderHints() & ~QtGui.QPainter.Antialiasing)
|
|
|
|
def setBackground(self, background):
|
|
"""
|
|
Set the background color of the GraphicsView.
|
|
To use the defaults specified py pyqtgraph.setConfigOption, use background='default'.
|
|
To make the background transparent, use background=None.
|
|
"""
|
|
self._background = background
|
|
if background == 'default':
|
|
background = getConfigOption('background')
|
|
brush = fn.mkBrush(background)
|
|
self.setBackgroundBrush(brush)
|
|
|
|
def paintEvent(self, ev):
|
|
self.scene().prepareForPaint()
|
|
return QtGui.QGraphicsView.paintEvent(self, ev)
|
|
|
|
def render(self, *args, **kwds):
|
|
self.scene().prepareForPaint()
|
|
return QtGui.QGraphicsView.render(self, *args, **kwds)
|
|
|
|
|
|
def close(self):
|
|
self.centralWidget = None
|
|
self.scene().clear()
|
|
self.currentItem = None
|
|
self.sceneObj = None
|
|
self.closed = True
|
|
self.setViewport(None)
|
|
super(GraphicsView, self).close()
|
|
|
|
def useOpenGL(self, b=True):
|
|
if b:
|
|
if not HAVE_OPENGL:
|
|
raise Exception("Requested to use OpenGL with QGraphicsView, but QtOpenGL module is not available.")
|
|
v = QtOpenGL.QGLWidget()
|
|
else:
|
|
v = QtGui.QWidget()
|
|
|
|
self.setViewport(v)
|
|
|
|
def keyPressEvent(self, ev):
|
|
self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene
|
|
## (view likes to eat arrow key events)
|
|
|
|
|
|
def setCentralItem(self, item):
|
|
return self.setCentralWidget(item)
|
|
|
|
def setCentralWidget(self, item):
|
|
"""Sets a QGraphicsWidget to automatically fill the entire view (the item will be automatically
|
|
resize whenever the GraphicsView is resized)."""
|
|
if self.centralWidget is not None:
|
|
self.scene().removeItem(self.centralWidget)
|
|
self.centralWidget = item
|
|
if item is not None:
|
|
self.sceneObj.addItem(item)
|
|
self.resizeEvent(None)
|
|
|
|
def addItem(self, *args):
|
|
return self.scene().addItem(*args)
|
|
|
|
def removeItem(self, *args):
|
|
return self.scene().removeItem(*args)
|
|
|
|
def enableMouse(self, b=True):
|
|
self.mouseEnabled = b
|
|
self.autoPixelRange = (not b)
|
|
|
|
def clearMouse(self):
|
|
self.mouseTrail = []
|
|
self.lastButtonReleased = None
|
|
|
|
def resizeEvent(self, ev):
|
|
if self.closed:
|
|
return
|
|
if self.autoPixelRange:
|
|
self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height())
|
|
GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way.
|
|
self.updateMatrix()
|
|
|
|
def updateMatrix(self, propagate=True):
|
|
self.setSceneRect(self.range)
|
|
if self.autoPixelRange:
|
|
self.resetTransform()
|
|
else:
|
|
if self.aspectLocked:
|
|
self.fitInView(self.range, QtCore.Qt.KeepAspectRatio)
|
|
else:
|
|
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
|
|
|
if propagate:
|
|
for v in self.lockedViewports:
|
|
v.setXRange(self.range, padding=0)
|
|
|
|
self.sigDeviceRangeChanged.emit(self, self.range)
|
|
self.sigDeviceTransformChanged.emit(self)
|
|
|
|
def viewRect(self):
|
|
"""Return the boundaries of the view in scene coordinates"""
|
|
## easier to just return self.range ?
|
|
r = QtCore.QRectF(self.rect())
|
|
return self.viewportTransform().inverted()[0].mapRect(r)
|
|
|
|
def visibleRange(self):
|
|
## for backward compatibility
|
|
return self.viewRect()
|
|
|
|
def translate(self, dx, dy):
|
|
self.range.adjust(dx, dy, dx, dy)
|
|
self.updateMatrix()
|
|
|
|
def scale(self, sx, sy, center=None):
|
|
scale = [sx, sy]
|
|
if self.aspectLocked:
|
|
scale[0] = scale[1]
|
|
|
|
if self.scaleCenter:
|
|
center = None
|
|
if center is None:
|
|
center = self.range.center()
|
|
|
|
w = self.range.width() / scale[0]
|
|
h = self.range.height() / scale[1]
|
|
self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h)
|
|
|
|
self.updateMatrix()
|
|
self.sigScaleChanged.emit(self)
|
|
|
|
def setRange(self, newRect=None, padding=0.05, lockAspect=None, propagate=True, disableAutoPixel=True):
|
|
if disableAutoPixel:
|
|
self.autoPixelRange=False
|
|
if newRect is None:
|
|
newRect = self.visibleRange()
|
|
padding = 0
|
|
|
|
padding = Point(padding)
|
|
newRect = QtCore.QRectF(newRect)
|
|
pw = newRect.width() * padding[0]
|
|
ph = newRect.height() * padding[1]
|
|
newRect = newRect.adjusted(-pw, -ph, pw, ph)
|
|
scaleChanged = False
|
|
if self.range.width() != newRect.width() or self.range.height() != newRect.height():
|
|
scaleChanged = True
|
|
self.range = newRect
|
|
#print "New Range:", self.range
|
|
if self.centralWidget is not None:
|
|
self.centralWidget.setGeometry(self.range)
|
|
self.updateMatrix(propagate)
|
|
if scaleChanged:
|
|
self.sigScaleChanged.emit(self)
|
|
|
|
def scaleToImage(self, image):
|
|
"""Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase."""
|
|
pxSize = image.pixelSize()
|
|
image.setPxMode(True)
|
|
try:
|
|
self.sigScaleChanged.disconnect(image.setScaledMode)
|
|
except (TypeError, RuntimeError):
|
|
pass
|
|
tl = image.sceneBoundingRect().topLeft()
|
|
w = self.size().width() * pxSize[0]
|
|
h = self.size().height() * pxSize[1]
|
|
range = QtCore.QRectF(tl.x(), tl.y(), w, h)
|
|
GraphicsView.setRange(self, range, padding=0)
|
|
self.sigScaleChanged.connect(image.setScaledMode)
|
|
|
|
|
|
|
|
def lockXRange(self, v1):
|
|
if not v1 in self.lockedViewports:
|
|
self.lockedViewports.append(v1)
|
|
|
|
def setXRange(self, r, padding=0.05):
|
|
r1 = QtCore.QRectF(self.range)
|
|
r1.setLeft(r.left())
|
|
r1.setRight(r.right())
|
|
GraphicsView.setRange(self, r1, padding=[padding, 0], propagate=False)
|
|
|
|
def setYRange(self, r, padding=0.05):
|
|
r1 = QtCore.QRectF(self.range)
|
|
r1.setTop(r.top())
|
|
r1.setBottom(r.bottom())
|
|
GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False)
|
|
|
|
def wheelEvent(self, ev):
|
|
QtGui.QGraphicsView.wheelEvent(self, ev)
|
|
if not self.mouseEnabled:
|
|
return
|
|
|
|
delta = 0
|
|
if QT_LIB in ['PyQt4', 'PySide']:
|
|
delta = ev.delta()
|
|
else:
|
|
delta = ev.angleDelta().x()
|
|
if delta == 0:
|
|
delta = ev.angleDelta().y()
|
|
|
|
sc = 1.001 ** delta
|
|
#self.scale *= sc
|
|
#self.updateMatrix()
|
|
self.scale(sc, sc)
|
|
|
|
def setAspectLocked(self, s):
|
|
self.aspectLocked = s
|
|
|
|
def leaveEvent(self, ev):
|
|
self.scene().leaveEvent(ev) ## inform scene when mouse leaves
|
|
|
|
def mousePressEvent(self, ev):
|
|
QtGui.QGraphicsView.mousePressEvent(self, ev)
|
|
|
|
|
|
if not self.mouseEnabled:
|
|
return
|
|
self.lastMousePos = Point(ev.pos())
|
|
self.mousePressPos = ev.pos()
|
|
self.clickAccepted = ev.isAccepted()
|
|
if not self.clickAccepted:
|
|
self.scene().clearSelection()
|
|
return ## Everything below disabled for now..
|
|
|
|
def mouseReleaseEvent(self, ev):
|
|
QtGui.QGraphicsView.mouseReleaseEvent(self, ev)
|
|
if not self.mouseEnabled:
|
|
return
|
|
self.sigMouseReleased.emit(ev)
|
|
self.lastButtonReleased = ev.button()
|
|
return ## Everything below disabled for now..
|
|
|
|
def mouseMoveEvent(self, ev):
|
|
if self.lastMousePos is None:
|
|
self.lastMousePos = Point(ev.pos())
|
|
delta = Point(ev.pos() - self.lastMousePos.toQPoint())
|
|
self.lastMousePos = Point(ev.pos())
|
|
|
|
QtGui.QGraphicsView.mouseMoveEvent(self, ev)
|
|
if not self.mouseEnabled:
|
|
return
|
|
self.sigSceneMouseMoved.emit(self.mapToScene(ev.pos()))
|
|
|
|
if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it.
|
|
return
|
|
|
|
if ev.buttons() == QtCore.Qt.RightButton:
|
|
delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50))
|
|
scale = 1.01 ** delta
|
|
self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos))
|
|
self.sigDeviceRangeChanged.emit(self, self.range)
|
|
|
|
elif ev.buttons() in [QtCore.Qt.MiddleButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button.
|
|
px = self.pixelSize()
|
|
tr = -delta * px
|
|
|
|
self.translate(tr[0], tr[1])
|
|
self.sigDeviceRangeChanged.emit(self, self.range)
|
|
|
|
def pixelSize(self):
|
|
"""Return vector with the length and width of one view pixel in scene coordinates"""
|
|
p0 = Point(0,0)
|
|
p1 = Point(1,1)
|
|
tr = self.transform().inverted()[0]
|
|
p01 = tr.map(p0)
|
|
p11 = tr.map(p1)
|
|
return Point(p11 - p01)
|
|
|
|
def dragEnterEvent(self, ev):
|
|
ev.ignore() ## not sure why, but for some reason this class likes to consume drag events
|
|
|
|
def _del(self):
|
|
try:
|
|
if self.parentWidget() is None and self.isVisible():
|
|
msg = "Visible window deleted. To prevent this, store a reference to the window object."
|
|
try:
|
|
warnings.warn(msg, RuntimeWarning, stacklevel=2)
|
|
except TypeError:
|
|
# warnings module not available during interpreter shutdown
|
|
pass
|
|
except RuntimeError:
|
|
pass
|
|
|
|
if sys.version_info[0] == 3 and sys.version_info[1] >= 4:
|
|
GraphicsView.__del__ = GraphicsView._del
|