1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
"""Common useful `QGraphicsView` classes that can be composed to achieve
desired functionality."""
from itertools import repeat
import numpy as np
from AnyQt.QtWidgets import QGraphicsView
from AnyQt.QtGui import QTransform
from AnyQt.QtCore import Qt
class ZoomableGraphicsView(QGraphicsView):
"""Zoomable graphics view.
Composable graphics view that adds zoom functionality.
It also handles automatic resizing of content whenever the window is
resized.
Right click will reset the zoom to a factor where the entire scene is
visible.
Parameters
----------
scene : QGraphicsScene
padding : int or tuple, optional
Specify the padding around the drawn widgets. Can be an int, or tuple,
the tuple can contain either 2 or 4 elements.
Notes
-----
.. note:: This view will NOT consume the wheel event, so it would be wise
to use this component in conjuction with the `PreventDefaultWheelEvent`
in most cases.
.. note:: This view does however consume the right mouse click event.
"""
def __init__(self, scene, padding=(0, 0), **kwargs):
self.zoom = 1
self.scale_factor = 1 / 16
# zoomout limit prevents the zoom factor to become negative, which
# results in the canvas being flipped over the x axis
self.__zoomout_limit_reached = False
# Does the view need to recalculate the initial scale factor
self.__needs_to_recalculate_initial = True
self.__initial_zoom = -1
self.__central_widget = None
self.__set_padding(padding)
super().__init__(scene, **kwargs)
def resizeEvent(self, event):
super().resizeEvent(event)
self.__needs_to_recalculate_initial = True
def wheelEvent(self, event):
self.__handle_zoom(event.angleDelta().y())
super().wheelEvent(event)
def mousePressEvent(self, event):
# right click resets the zoom factor
if event.button() == Qt.RightButton:
self.reset_zoom()
super().mousePressEvent(event)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Plus:
self.__handle_zoom(1)
elif event.key() == Qt.Key_Minus:
self.__handle_zoom(-1)
super().keyPressEvent(event)
def __set_padding(self, padding):
# Allow for multiple formats of padding for convenience
if isinstance(padding, int):
padding = tuple(repeat(padding, 4))
elif isinstance(padding, list) or isinstance(padding, tuple):
if len(padding) == 2:
padding = tuple(padding * 2)
else:
padding = 0, 0, 0, 0
left, top, right, bottom = padding
self.__padding = -left, -top, right, bottom
def __handle_zoom(self, direction):
"""Handle zoom event, direction is positive if zooming in, otherwise
negative."""
if self.__zooming_in(direction):
self.__reset_zoomout_limit()
if self.__zoomout_limit_reached and self.__zooming_out(direction):
return
self.zoom += np.sign(direction) * self.scale_factor
if self.zoom <= 0:
self.__zoomout_limit_reached = True
self.zoom += self.scale_factor
else:
self.setTransformationAnchor(self.AnchorUnderMouse)
self.setTransform(QTransform().scale(self.zoom, self.zoom))
@staticmethod
def __zooming_out(direction):
return direction < 0
def __zooming_in(self, event):
return not self.__zooming_out(event)
def __reset_zoomout_limit(self):
self.__zoomout_limit_reached = False
def set_central_widget(self, widget):
"""Set the central widget in the view.
This means that the initial zoom will fit the central widget, and may
cut out any other widgets.
Parameters
----------
widget : QGraphicsWidget
"""
self.__central_widget = widget
def central_widget_rect(self):
"""Get the bounding box of the central widget.
If a central widget and padding are set, this method calculates the
rect containing both of them. This is useful because if the padding was
added directly onto the widget, the padding would be rescaled as well.
If the central widget is not set, return the scene rect instead.
Returns
-------
QtCore.QRectF
"""
if self.__central_widget is None:
return self.scene().itemsBoundingRect().adjusted(*self.__padding)
return self.__central_widget.boundingRect().adjusted(*self.__padding)
def recalculate_and_fit(self):
"""Recalculate the optimal zoom and fits the content into view.
Should be called if the scene contents change, so that the optimal zoom
can be recalculated.
Returns
-------
"""
if self.__central_widget is not None:
self.fitInView(self.central_widget_rect(), Qt.KeepAspectRatio)
else:
self.fitInView(self.scene().sceneRect(), Qt.KeepAspectRatio)
self.__initial_zoom = self.transform().m11()
self.zoom = self.__initial_zoom
def reset_zoom(self):
"""Reset the zoom to the optimal factor."""
self.zoom = self.__initial_zoom
self.__zoomout_limit_reached = False
if self.__needs_to_recalculate_initial:
self.recalculate_and_fit()
else:
self.setTransform(QTransform().scale(self.zoom, self.zoom))
class PannableGraphicsView(QGraphicsView):
"""Pannable graphics view.
Enables panning the graphics view.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setDragMode(QGraphicsView.ScrollHandDrag)
def enterEvent(self, event):
self.viewport().setCursor(Qt.ArrowCursor)
super().enterEvent(event)
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self.viewport().setCursor(Qt.ArrowCursor)
class PreventDefaultWheelEvent(QGraphicsView):
"""Prevent the default wheel event.
The default wheel event pans the view around, if using the
`ZoomableGraphicsView`, this will prevent that behaviour.
"""
def wheelEvent(self, event):
event.accept()
|