title: QGraphicsItem line width / transformation changes from PyQt4 to PyQt5
body: <p>I have some small utility GUIs that I have made with PyQt, which use a QGraphicsScene with some items in it, and a view that response to user clicks (for making boxes, selecting points, etc).</p>
  
  <p>On the (offline) machine I use, the software was just upgraded from Anaconda 2.5 to Anaconda 4.3, including the switch from PyQt4 to PyQt5.  Everything still works, except somehow the transformations for my various QGraphicsItem objects are messed up if the scene rect is defined in anything but pixel coordinates.</p>
  
  <p><strong>Upfront question: What changed as far as item transformations from PyQt4 to PyQt5?</strong></p>
  
  <p>Here is an example of what I'm talking about: The top row is a box selector containing a dummy grayscale in a scene with a bounding rect of <code>(0, 0, 2pi, 4pi)</code>.  The green box is a QGraphicsRectItem drawn by the user from which I get the LL and UR points (in scene coordinates) after "Done" is clicked. The bottom row is a point layout with a user-clicked ellipse, on a small dummy image that has been zoomed in by 20.</p>
  
  <p>The left and right were made with identical code. The version on the left is the result using PyQt4 under Anaconda 2.5 Python 3.5, whereas the result on the right is using PyQt5 under Anaconda 4.3 Python 3.6.</p>
  
  <p><a href="https://i.stack.imgur.com/ESm7G.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/ESm7G.png" alt="PyQt4 version of box"></a> <a href="https://i.stack.imgur.com/iQ7nS.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/iQ7nS.png" alt="PyQt5 version of box"></a></p>
  
  <p><a href="https://i.stack.imgur.com/2EDIx.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/2EDIx.png" alt="PyQt4 version of ellipse"></a> <a href="https://i.stack.imgur.com/YJnUn.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/YJnUn.png" alt="PyQt5 version of ellipse"></a></p>
  
  <p>Clearly there is some sort of item transformation that is handled differently but I haven't been able to find it in any of the PyQt4->PyQt5 documentation (it's all about the API changes).</p>
  
  <p><strong>How do I go about making the line width of the <code>QGraphicsItem</code>s be one in device coordinates while still maintaining the correct positions in scene coordinates?  More generally, how do I scale a general QGraphicsItem so that it doesn't blow up or get fat based on scene size?</strong></p>
  
  <p>The code is below. <code>SimpleDialog</code> is the primary base class I use for various picker utilities, and it includes <code>MouseView</code> and <code>ImageScene</code> which automatically build in a vertical flip and a background image. The two utilities I used here are <code>BoxSelector</code> and <code>PointLayout</code>.</p>
  
  
  
  <pre class="lang-python prettyprint-override"><code># Imports
  import numpy as np
  try:
      from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, QPointF
      from PyQt5.QtGui import QImage, QPixmap, QFont, QBrush, QPen, QTransform
      from PyQt5.QtWidgets import (QGraphicsView, QGraphicsScene, QDialog, QSizePolicy,
                                   QVBoxLayout, QPushButton, QMainWindow, QApplication)
  except ImportError:
      from PyQt4.QtCore import Qt, pyqtSignal, pyqtSlot, QPointF
      from PyQt4.QtGui import (QImage, QPixmap, QFont, QBrush, QPen, QTransform,
                               QGraphicsView, QGraphicsScene, QDialog, QSizePolicy,
                               QVBoxLayout, QPushButton, QMainWindow, QApplication)
  
  class MouseView(QGraphicsView):
      """A subclass of QGraphicsView that returns mouse click events."""
  
      mousedown = pyqtSignal(QPointF)
      mouseup = pyqtSignal(QPointF)
      mousemove = pyqtSignal(QPointF)
  
      def __init__(self, scene, parent=None):
          super(MouseView, self).__init__(scene, parent=parent)
  
          self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
          self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
          self.setSizePolicy(QSizePolicy.Fixed,
                             QSizePolicy.Fixed)
          self.scale(1, -1)
  
          self.moving = False
  
      def mousePressEvent(self, event):
          """Emit a mouse click signal."""
          self.mousedown.emit(self.mapToScene(event.pos()))
  
      def mouseReleaseEvent(self, event):
          """Emit a mouse release signal."""
          self.mouseup.emit(self.mapToScene(event.pos()))
  
      def mouseMoveEvent(self, event):
          """Emit a mouse move signal."""
          if self.moving:
              self.mousemove.emit(self.mapToScene(event.pos()))
  
  
  class ImageScene(QGraphicsScene):
      """A subclass of QGraphicsScene that includes a background pixmap."""
  
      def __init__(self, data, scene_rect, parent=None):
          super(ImageScene, self).__init__(parent=parent)
  
          bdata = ((data - np.min(data)) / (np.max(data) - np.min(data)) * 255).astype(np.uint8)
          wid, hgt = data.shape
  
          img = QImage(bdata.T.copy(), wid, hgt, wid, QImage.Format_Indexed8)
  
          self.setSceneRect(*scene_rect)
  
          px = QPixmap.fromImage(img)
  
          self.px = self.addPixmap(px)
  
          px_trans = QTransform.fromTranslate(scene_rect[0], scene_rect[1])
          px_trans = px_trans.scale(scene_rect[2]/wid, scene_rect[3]/hgt)
          self.px.setTransform(px_trans)
  
  class SimpleDialog(QDialog):
      """A base class for utility dialogs using a background image in scene."""
  
      def __init__(self, data, bounds=None, grow=[1.0, 1.0], wsize=None, parent=None):
          super(SimpleDialog, self).__init__(parent=parent)
  
          self.grow = grow
  
          wid, hgt = data.shape
          if bounds is None:
              bounds = [0, 0, wid, hgt]
  
          if wsize is None:
              wsize = [wid, hgt]
  
          vscale = [grow[0]*wsize[0]/bounds[2], grow[1]*wsize[1]/bounds[3]]
  
          self.scene = ImageScene(data, bounds, parent=self)
  
          self.view = MouseView(self.scene, parent=self)
          self.view.scale(vscale[0], vscale[1])
  
          quitb = QPushButton("Done")
          quitb.clicked.connect(self.close)
  
          lay = QVBoxLayout()
          lay.addWidget(self.view)
          lay.addWidget(quitb)
  
          self.setLayout(lay)
  
      def close(self):
          self.accept()
  
  class BoxSelector(SimpleDialog):
      """Simple box selector."""
  
      def __init__(self, *args, **kwargs):
          super(BoxSelector, self).__init__(*args, **kwargs)
  
          self.rpen = QPen(Qt.green)
          self.rect = self.scene.addRect(0, 0, 0, 0, pen=self.rpen)
  
          self.view.mousedown.connect(self.start_box)
          self.view.mouseup.connect(self.end_box)
          self.view.mousemove.connect(self.draw_box)
  
          self.start_point = []
          self.points = []
  
          self.setWindowTitle('Box Selector')
  
      @pyqtSlot(QPointF)
      def start_box(self, xy):
          self.start_point = [xy.x(), xy.y()]
          self.view.moving = True
  
      @pyqtSlot(QPointF)
      def end_box(self, xy):
          lx = np.minimum(xy.x(), self.start_point[0])
          ly = np.minimum(xy.y(), self.start_point[1])
          rx = np.maximum(xy.x(), self.start_point[0])
          ry = np.maximum(xy.y(), self.start_point[1])
          self.points = [[lx, ly], [rx, ry]]
          self.view.moving = False
  
      @pyqtSlot(QPointF)
      def draw_box(self, xy):
          newpoint = [xy.x(), xy.y()]
          minx = np.minimum(self.start_point[0], newpoint[0])
          miny = np.minimum(self.start_point[1], newpoint[1])
          size = [np.abs(i - j) for i, j in zip(self.start_point, newpoint)]
          self.rect.setRect(minx, miny, size[0], size[1])
  
  class PointLayout(SimpleDialog):
      """Simple point display."""
  
      def __init__(self, *args, **kwargs):
          super(PointLayout, self).__init__(*args, **kwargs)
  
          self.pen = QPen(Qt.green)
  
          self.view.mousedown.connect(self.mouse_click)
  
          self.circles = []
          self.points = []
  
          self.setWindowTitle('Point Layout')
  
      @pyqtSlot(QPointF)
      def mouse_click(self, xy):
          self.points.append((xy.x(), xy.y()))
          pt = self.scene.addEllipse(xy.x()-0.5, xy.y()-0.5, 1, 1, pen=self.pen)
          self.circles.append(pt)
  </code></pre>
  
  <p>And here is the code I used to do the tests:</p>
  
  <pre class="lang-python prettyprint-override"><code>def test_box():
  
      x, y = np.mgrid[0:175, 0:100]
      img = x * y
  
      app = QApplication.instance()
      if app is None:
          app = QApplication(['python'])
  
      picker = BoxSelector(img, bounds=[0, 0, 2*np.pi, 4*np.pi])
      picker.show()
      app.exec_()
  
      return picker
  
  def test_point():
  
      np.random.seed(159753)
      img = np.random.randn(10, 5)
  
      app = QApplication.instance()
      if app is None:
          app = QApplication(['python'])
  
      pointer = PointLayout(img, bounds=[0, 0, 10, 5], grow=[20, 20])
      pointer.show()
  
      app.exec_()
  
      return pointer
  
  if __name__ == "__main__":
      pick = test_box()
      point = test_point()
  </code></pre>