1# -*- coding: utf-8 -*- 2""" 3GraphicsView.py - Extension of QGraphicsView 4Copyright 2010 Luke Campagnola 5Distributed under MIT/X11 license. See license.txt for more information. 6""" 7 8from ..Qt import QtCore, QtGui, QtWidgets, QT_LIB 9from ..Point import Point 10from ..GraphicsScene import GraphicsScene 11from .. import functions as fn 12from .. import getConfigOption 13 14__all__ = ['GraphicsView'] 15 16 17class GraphicsView(QtGui.QGraphicsView): 18 """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the 19 viewed coordinate range. Also automatically creates a GraphicsScene and a central QGraphicsWidget 20 that is automatically scaled to the full view geometry. 21 22 This widget is the basis for :class:`PlotWidget <pyqtgraph.PlotWidget>`, 23 :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and the view widget in 24 :class:`ImageView <pyqtgraph.ImageView>`. 25 26 By default, the view coordinate system matches the widget's pixel coordinates and 27 automatically updates when the view is resized. This can be overridden by setting 28 autoPixelRange=False. The exact visible range can be set with setRange(). 29 30 The view can be panned using the middle mouse button and scaled using the right mouse button if 31 enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality).""" 32 33 sigDeviceRangeChanged = QtCore.Signal(object, object) 34 sigDeviceTransformChanged = QtCore.Signal(object) 35 sigMouseReleased = QtCore.Signal(object) 36 sigSceneMouseMoved = QtCore.Signal(object) 37 #sigRegionChanged = QtCore.Signal(object) 38 sigScaleChanged = QtCore.Signal(object) 39 lastFileDir = None 40 41 def __init__(self, parent=None, useOpenGL=None, background='default'): 42 """ 43 ============== ============================================================ 44 **Arguments:** 45 parent Optional parent widget 46 useOpenGL If True, the GraphicsView will use OpenGL to do all of its 47 rendering. This can improve performance on some systems, 48 but may also introduce bugs (the combination of 49 QGraphicsView and QOpenGLWidget is still an 'experimental' 50 feature of Qt) 51 background Set the background color of the GraphicsView. Accepts any 52 single argument accepted by 53 :func:`mkColor <pyqtgraph.mkColor>`. By 54 default, the background color is determined using the 55 'backgroundColor' configuration option (see 56 :func:`setConfigOptions <pyqtgraph.setConfigOptions>`). 57 ============== ============================================================ 58 """ 59 60 self.closed = False 61 62 QtGui.QGraphicsView.__init__(self, parent) 63 64 # This connects a cleanup function to QApplication.aboutToQuit. It is 65 # called from here because we have no good way to react when the 66 # QApplication is created by the user. 67 # See pyqtgraph.__init__.py 68 from .. import _connectCleanup 69 _connectCleanup() 70 71 if useOpenGL is None: 72 useOpenGL = getConfigOption('useOpenGL') 73 74 self.useOpenGL(useOpenGL) 75 self.setCacheMode(self.CacheModeFlag.CacheBackground) 76 77 ## This might help, but it's probably dangerous in the general case.. 78 #self.setOptimizationFlag(self.DontSavePainterState, True) 79 80 self.setBackgroundRole(QtGui.QPalette.ColorRole.NoRole) 81 self.setBackground(background) 82 83 self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus) 84 self.setFrameShape(QtGui.QFrame.Shape.NoFrame) 85 self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 86 self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 87 self.setTransformationAnchor(QtGui.QGraphicsView.ViewportAnchor.NoAnchor) 88 self.setResizeAnchor(QtGui.QGraphicsView.ViewportAnchor.AnchorViewCenter) 89 self.setViewportUpdateMode(QtGui.QGraphicsView.ViewportUpdateMode.MinimalViewportUpdate) 90 91 92 self.lockedViewports = [] 93 self.lastMousePos = None 94 self.setMouseTracking(True) 95 self.aspectLocked = False 96 self.range = QtCore.QRectF(0, 0, 1, 1) 97 self.autoPixelRange = True 98 self.currentItem = None 99 self.clearMouse() 100 self.updateMatrix() 101 # GraphicsScene must have parent or expect crashes! 102 self.sceneObj = GraphicsScene(parent=self) 103 self.setScene(self.sceneObj) 104 105 ## Workaround for PySide crash 106 ## This ensures that the scene will outlive the view. 107 if QT_LIB == 'PySide': 108 self.sceneObj._view_ref_workaround = self 109 110 ## by default we set up a central widget with a grid layout. 111 ## this can be replaced if needed. 112 self.centralWidget = None 113 self.setCentralItem(QtGui.QGraphicsWidget()) 114 self.centralLayout = QtGui.QGraphicsGridLayout() 115 self.centralWidget.setLayout(self.centralLayout) 116 117 self.mouseEnabled = False 118 self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) 119 self.clickAccepted = False 120 121 def setAntialiasing(self, aa): 122 """Enable or disable default antialiasing. 123 Note that this will only affect items that do not specify their own antialiasing options.""" 124 if aa: 125 self.setRenderHints(self.renderHints() | QtGui.QPainter.RenderHint.Antialiasing) 126 else: 127 self.setRenderHints(self.renderHints() & ~QtGui.QPainter.RenderHint.Antialiasing) 128 129 def setBackground(self, background): 130 """ 131 Set the background color of the GraphicsView. 132 To use the defaults specified py pyqtgraph.setConfigOption, use background='default'. 133 To make the background transparent, use background=None. 134 """ 135 self._background = background 136 if background == 'default': 137 background = getConfigOption('background') 138 brush = fn.mkBrush(background) 139 self.setBackgroundBrush(brush) 140 141 def paintEvent(self, ev): 142 self.scene().prepareForPaint() 143 return super().paintEvent(ev) 144 145 def render(self, *args, **kwds): 146 self.scene().prepareForPaint() 147 return super().render(*args, **kwds) 148 149 150 def close(self): 151 self.centralWidget = None 152 self.scene().clear() 153 self.currentItem = None 154 self.sceneObj = None 155 self.closed = True 156 self.setViewport(None) 157 super(GraphicsView, self).close() 158 159 def useOpenGL(self, b=True): 160 if b: 161 HAVE_OPENGL = hasattr(QtWidgets, 'QOpenGLWidget') 162 if not HAVE_OPENGL: 163 raise Exception("Requested to use OpenGL with QGraphicsView, but QOpenGLWidget is not available.") 164 165 v = QtWidgets.QOpenGLWidget() 166 else: 167 v = QtGui.QWidget() 168 169 self.setViewport(v) 170 171 def keyPressEvent(self, ev): 172 self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene 173 ## (view likes to eat arrow key events) 174 175 176 def setCentralItem(self, item): 177 return self.setCentralWidget(item) 178 179 def setCentralWidget(self, item): 180 """Sets a QGraphicsWidget to automatically fill the entire view (the item will be automatically 181 resize whenever the GraphicsView is resized).""" 182 if self.centralWidget is not None: 183 self.scene().removeItem(self.centralWidget) 184 self.centralWidget = item 185 if item is not None: 186 self.sceneObj.addItem(item) 187 self.resizeEvent(None) 188 189 def addItem(self, *args): 190 return self.scene().addItem(*args) 191 192 def removeItem(self, *args): 193 return self.scene().removeItem(*args) 194 195 def enableMouse(self, b=True): 196 self.mouseEnabled = b 197 self.autoPixelRange = (not b) 198 199 def clearMouse(self): 200 self.mouseTrail = [] 201 self.lastButtonReleased = None 202 203 def resizeEvent(self, ev): 204 if self.closed: 205 return 206 if self.autoPixelRange: 207 self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) 208 GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way. 209 self.updateMatrix() 210 211 def updateMatrix(self, propagate=True): 212 self.setSceneRect(self.range) 213 if self.autoPixelRange: 214 self.resetTransform() 215 else: 216 if self.aspectLocked: 217 self.fitInView(self.range, QtCore.Qt.AspectRatioMode.KeepAspectRatio) 218 else: 219 self.fitInView(self.range, QtCore.Qt.AspectRatioMode.IgnoreAspectRatio) 220 221 if propagate: 222 for v in self.lockedViewports: 223 v.setXRange(self.range, padding=0) 224 225 self.sigDeviceRangeChanged.emit(self, self.range) 226 self.sigDeviceTransformChanged.emit(self) 227 228 def viewRect(self): 229 """Return the boundaries of the view in scene coordinates""" 230 ## easier to just return self.range ? 231 r = QtCore.QRectF(self.rect()) 232 return self.viewportTransform().inverted()[0].mapRect(r) 233 234 def visibleRange(self): 235 ## for backward compatibility 236 return self.viewRect() 237 238 def translate(self, dx, dy): 239 self.range.adjust(dx, dy, dx, dy) 240 self.updateMatrix() 241 242 def scale(self, sx, sy, center=None): 243 scale = [sx, sy] 244 if self.aspectLocked: 245 scale[0] = scale[1] 246 247 if self.scaleCenter: 248 center = None 249 if center is None: 250 center = self.range.center() 251 252 w = self.range.width() / scale[0] 253 h = self.range.height() / scale[1] 254 self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h) 255 256 self.updateMatrix() 257 self.sigScaleChanged.emit(self) 258 259 def setRange(self, newRect=None, padding=0.05, lockAspect=None, propagate=True, disableAutoPixel=True): 260 if disableAutoPixel: 261 self.autoPixelRange=False 262 if newRect is None: 263 newRect = self.visibleRange() 264 padding = 0 265 266 padding = Point(padding) 267 newRect = QtCore.QRectF(newRect) 268 pw = newRect.width() * padding[0] 269 ph = newRect.height() * padding[1] 270 newRect = newRect.adjusted(-pw, -ph, pw, ph) 271 scaleChanged = False 272 if self.range.width() != newRect.width() or self.range.height() != newRect.height(): 273 scaleChanged = True 274 self.range = newRect 275 #print "New Range:", self.range 276 if self.centralWidget is not None: 277 self.centralWidget.setGeometry(self.range) 278 self.updateMatrix(propagate) 279 if scaleChanged: 280 self.sigScaleChanged.emit(self) 281 282 def scaleToImage(self, image): 283 """Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase.""" 284 pxSize = image.pixelSize() 285 image.setPxMode(True) 286 try: 287 self.sigScaleChanged.disconnect(image.setScaledMode) 288 except (TypeError, RuntimeError): 289 pass 290 tl = image.sceneBoundingRect().topLeft() 291 w = self.size().width() * pxSize[0] 292 h = self.size().height() * pxSize[1] 293 range = QtCore.QRectF(tl.x(), tl.y(), w, h) 294 GraphicsView.setRange(self, range, padding=0) 295 self.sigScaleChanged.connect(image.setScaledMode) 296 297 298 299 def lockXRange(self, v1): 300 if not v1 in self.lockedViewports: 301 self.lockedViewports.append(v1) 302 303 def setXRange(self, r, padding=0.05): 304 r1 = QtCore.QRectF(self.range) 305 r1.setLeft(r.left()) 306 r1.setRight(r.right()) 307 GraphicsView.setRange(self, r1, padding=[padding, 0], propagate=False) 308 309 def setYRange(self, r, padding=0.05): 310 r1 = QtCore.QRectF(self.range) 311 r1.setTop(r.top()) 312 r1.setBottom(r.bottom()) 313 GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False) 314 315 def wheelEvent(self, ev): 316 super().wheelEvent(ev) 317 if not self.mouseEnabled: 318 return 319 delta = ev.angleDelta().x() 320 if delta == 0: 321 delta = ev.angleDelta().y() 322 323 sc = 1.001 ** delta 324 #self.scale *= sc 325 #self.updateMatrix() 326 self.scale(sc, sc) 327 328 def setAspectLocked(self, s): 329 self.aspectLocked = s 330 331 def leaveEvent(self, ev): 332 self.scene().leaveEvent(ev) ## inform scene when mouse leaves 333 334 def mousePressEvent(self, ev): 335 super().mousePressEvent(ev) 336 337 338 if not self.mouseEnabled: 339 return 340 lpos = ev.position() if hasattr(ev, 'position') else ev.localPos() 341 self.lastMousePos = lpos 342 self.mousePressPos = lpos 343 self.clickAccepted = ev.isAccepted() 344 if not self.clickAccepted: 345 self.scene().clearSelection() 346 return ## Everything below disabled for now.. 347 348 def mouseReleaseEvent(self, ev): 349 super().mouseReleaseEvent(ev) 350 if not self.mouseEnabled: 351 return 352 self.sigMouseReleased.emit(ev) 353 self.lastButtonReleased = ev.button() 354 return ## Everything below disabled for now.. 355 356 def mouseMoveEvent(self, ev): 357 lpos = ev.position() if hasattr(ev, 'position') else ev.localPos() 358 if self.lastMousePos is None: 359 self.lastMousePos = lpos 360 delta = Point(lpos - self.lastMousePos) 361 self.lastMousePos = lpos 362 363 super().mouseMoveEvent(ev) 364 if not self.mouseEnabled: 365 return 366 self.sigSceneMouseMoved.emit(self.mapToScene(lpos)) 367 368 if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it. 369 return 370 371 if ev.buttons() == QtCore.Qt.MouseButton.RightButton: 372 delta = Point(fn.clip_scalar(delta[0], -50, 50), fn.clip_scalar(-delta[1], -50, 50)) 373 scale = 1.01 ** delta 374 self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) 375 self.sigDeviceRangeChanged.emit(self, self.range) 376 377 elif ev.buttons() in [QtCore.Qt.MouseButton.MiddleButton, QtCore.Qt.MouseButton.LeftButton]: ## Allow panning by left or mid button. 378 px = self.pixelSize() 379 tr = -delta * px 380 381 self.translate(tr[0], tr[1]) 382 self.sigDeviceRangeChanged.emit(self, self.range) 383 384 def pixelSize(self): 385 """Return vector with the length and width of one view pixel in scene coordinates""" 386 p0 = Point(0,0) 387 p1 = Point(1,1) 388 tr = self.transform().inverted()[0] 389 p01 = tr.map(p0) 390 p11 = tr.map(p1) 391 return Point(p11 - p01) 392 393 def dragEnterEvent(self, ev): 394 ev.ignore() ## not sure why, but for some reason this class likes to consume drag events 395