Source code for axopy.gui.canvas

"""2D canvas style graphics functionality backed by Qt's QGraphicsView."""

from PyQt5 import QtCore, QtGui, QtWidgets


[docs]class Canvas(QtWidgets.QGraphicsView): """A 2D canvas interface implemented using a QGraphicsView. This view essentially just holds a QGraphicsScene that grows to fit the size of the view, keeping the aspect ratio square. The scene is displayed with a gray (by default) border. See Qt's documentation for more information about working with QGraphicsView (https://doc.qt.io/Qt-5/qgraphicsview.html). """ scaler = 1 border_width = 0.01 default_border_color = '#444444' default_bg_color = '#dddddd' def __init__(self, draw_border=True, bg_color=None, border_color=None, parent=None, invert_x=False, invert_y=False): super(Canvas, self).__init__(parent=parent) if bg_color is None: bg_color = self.default_bg_color self.bg_color = bg_color if border_color is None: border_color = self.default_border_color self.border_color = border_color self.invert_x = invert_x self.invert_y = invert_y self._init_scene() if draw_border: self._init_border() def _init_scene(self): scene = QtWidgets.QGraphicsScene() # x, y, width, height scene.setSceneRect(-self.scaler, -self.scaler, self.scaler*2, self.scaler*2) self.setScene(scene) if self.invert_x: self.setTransform(QtGui.QTransform.fromScale(-1, 1), combine=True) # Qt is positive downward, so invert logic for y inversion if not self.invert_y: self.setTransform(QtGui.QTransform.fromScale(1, -1), combine=True) self.setRenderHint(QtGui.QPainter.Antialiasing) self.setBackgroundBrush(QtGui.QColor(self.bg_color)) def _init_border(self): rect = self.scene().sceneRect() pen = QtGui.QPen(QtGui.QColor(self.border_color), self.border_width) lines = [ QtCore.QLineF(rect.topLeft(), rect.topRight()), QtCore.QLineF(rect.topLeft(), rect.bottomLeft()), QtCore.QLineF(rect.topRight(), rect.bottomRight()), QtCore.QLineF(rect.bottomLeft(), rect.bottomRight()) ] for line in lines: self.scene().addLine(line, pen)
[docs] def add_item(self, item): """Add an item to the canvas. Parameters ---------- item : Item or QGraphicsItem The item to add to the canvas. This can be either one of AxoPy's built-in items (:class:`Circle`, :class:`Text`, etc.) or any QGraphicsItem. """ if isinstance(item, Item): self.scene().addItem(item.qitem) else: self.scene().addItem(item)
[docs] def resizeEvent(self, event): # override resize event to keep the scene rect intact (everything # scales with the window changing size, aspect ratio is preserved) super().resizeEvent(event) self.fitInView(self.sceneRect(), QtCore.Qt.KeepAspectRatio)
[docs]class Item(object): """Canvas item base class. This is simply a wrapper around any kind of ``QGraphicsItem``, adding the ability to set some properties of the underlying item with a more Pythonic API. You can always access the ``QGraphicsItem`` with the ``qitem`` attribute. Once you know what kind of ``QGraphicsItem`` is being wrapped, you can use the corresponding Qt documentation to make use of more complete functionality. Attributes ---------- qitem : QGraphicsItem The QGraphicsItem being wrapped. You can use this attribute to access methods and properties of the item not exposed by the wrapper class. If you find yourself routinely using a method of the QGraphicsItem, consider recommending it for addition to AxoPy. """ def __init__(self, qitem): self.qitem = qitem @property def x(self): """X coordinate of the item in the canvas.""" return self.qitem.x() @x.setter def x(self, x): self.qitem.setX(x) @property def y(self): """Y coordinate of the item in the canvas.""" return self.qitem.y() @y.setter def y(self, y): self.qitem.setY(y) @property def pos(self): """Both X and Y coordinates of the item in the canvas.""" return self.x, self.y @pos.setter def pos(self, pos): self.qitem.setPos(*pos) @property def visible(self): """Visibility of the item.""" return self.qitem.isVisible() @visible.setter def visible(self, visible): self.qitem.setVisible(visible) @property def opacity(self): """Opacity of the item (between 0 and 1).""" self.qitem.opacity() @opacity.setter def opacity(self, opacity): self.qitem.setOpacity(opacity) @property def color(self): """Color of the item.""" return self.qitem.brush().color().getRgb() @color.setter def color(self, color): self.qitem.setBrush(QtGui.QColor(color))
[docs] def show(self): """Set the item to visible.""" self.qitem.show()
[docs] def hide(self): """Set the item to invisible.""" self.qitem.hide()
[docs] def set(self, **kwargs): """Set any properties of the underlying QGraphicsItem.""" for prop, val in kwargs.items(): self._qmeth(prop)(val)
[docs] def get(self, prop, *args, **kwargs): """Get any property of the underlying QGraphicsItem.""" self._qmeth(prop)(*args, **kwargs)
[docs] def collides_with(self, item): """Determine if the item intersects with another item.""" return self.qitem.collidesWithItem(item.qitem)
def _qmeth(self, prop): return getattr(self.qitem, _to_camel_case(prop))
def _to_camel_case(snake_str): components = snake_str.split('_') return components[0] + ''.join(x.title() for x in components[1:])
[docs]class Circle(Item): """Circular item. The coordinates of this item correspond to the center of the circle. Parameters ---------- dia : float Diameter of the circle with respect to the scene coordinate system. color : str Hex string to set the color of the circle. You can use the underlying ``qitem`` attribute to get the underlying QGraphicsEllipseItem to set stroke color vs. fill color, etc. if needed. """ def __init__(self, diameter, color='#333333'): qitem = QtWidgets.QGraphicsEllipseItem(-diameter/2, -diameter/2, diameter, diameter) qitem.setPen(QtGui.QPen(QtGui.QBrush(), 0)) super(Circle, self).__init__(qitem) self.color = color
[docs]class Cross(Item): """Collection of two lines oriented as a "plus sign". The coordinates of this item correspond to the center of the cross. This item's ``qitem`` attribute is a ``QGraphicsItemGroup`` (a group of two lines). Parameters ---------- size : float The size is the length of each line making up the cross. linewidth : float Thickness of each line making up the cross. color : str Color of the lines making up the cross. """ def __init__(self, size=0.05, linewidth=0.01, color='#333333'): qitem = QtWidgets.QGraphicsItemGroup() self._lh = Line(-size/2, 0, size/2, 0, width=linewidth, color=color) self._lv = Line(0, -size/2, 0, size/2, width=linewidth, color=color) qitem.addToGroup(self._lh.qitem) qitem.addToGroup(self._lv.qitem) super(Cross, self).__init__(qitem) @property def color(self): """Color of the lines in the cross.""" return self._lv.color @color.setter def color(self, color): self._lh.color = color self._lv.color = color
[docs]class Line(Item): """Line item.""" def __init__(self, x1, y1, x2, y2, width=0.01, color='#333333'): self.width = width qitem = QtWidgets.QGraphicsLineItem(x1, y1, x2, y2) super(Line, self).__init__(qitem) self.color = color @property def color(self): return self.qitem.pen().color().getRgb() @color.setter def color(self, color): pen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(color)), self.width) pen.setCapStyle(QtCore.Qt.FlatCap) self.qitem.setPen(pen)
[docs]class Text(Item): """Text item.""" def __init__(self, text, color='#333333'): qitem = QtWidgets.QGraphicsSimpleTextItem(text) super(Text, self).__init__(qitem) self.color = color # invert because Canvas is inverted self.qitem.setTransform(QtGui.QTransform.fromScale(0.01, -0.01)) self._center() def _center(self): scene_bounds = self.qitem.sceneBoundingRect() self.pos = -scene_bounds.width() / 2, scene_bounds.height() / 2
[docs]class Rectangle(Item): """Rectangular item. This is a filled retangle that allows you to set the size, color, position, etc. By default, the item's position is its *center*. """ def __init__(self, width, height, x=0, y=0, color='#333333', penwidth=0.01): self.penwidth = penwidth qitem = QtWidgets.QGraphicsRectItem(x, y, width, height) qitem.setTransformOriginPoint(width/2, height/2) qitem.setTransform(QtGui.QTransform().translate(-width/2, -height/2)) super(Rectangle, self).__init__(qitem) self.pos = x, y self.color = color @property def color(self): """Color of the rectangle.""" return self.qitem.pen().color().getRgb() @color.setter def color(self, color): """Color of the rectangle.""" br = QtGui.QBrush(QtGui.QColor(color)) pen = QtGui.QPen(br, self.penwidth) pen.setCapStyle(QtCore.Qt.FlatCap) self.qitem.setBrush(br) self.qitem.setPen(pen) @property def width(self): return self.qitem.rect().width() @width.setter def width(self, width): p = self.pos rect = self.qitem.rect() rect.setWidth(width) self.qitem.setRect(rect) self.pos = p