"""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