PDA

View Full Version : QGraphicsView viewport and QGraphicsScene/View.SceneRect() are not equal after resize



cr1901
17th January 2016, 06:31
Hello all,

I am implementing a custom slider widget for a client that requires a very large dynamic range. I decided to use QGraphicsView to implement the slider's axis and two cursors because of the built-in mouse handling, and the built-in ability to convert between pixel units (View) and a separate, more appropriate coordinate system (Scene). For reasons described after the code, I have to manually keep a separate mapping of the cursor's position in the Scene to the value sent to the rest of the application, and vice versa. In addition to moving the cursors via mouseEvents, the application can set the values the cursors should take, which may be in between pixel values, so a float-to-float mapping is appropriate; the View can automatically handle by rounding.

My mapping from Scene to "user values" assumes two invariants:

One pixel moved in View coordinates moves one unit in Scene coordinates.
The center of the view is always the origin of the scene, or as close as possible in a viewport with an even number of pixel dimensions.



Unfortunately, I'm am currently unable to satisfy the second condition; during resizeEvents, although I manage to set the sceneRects of both the Scene and View appropriately, and center the View on the origin, the viewport and the Scene and View SceneRects go out of sync.

This test application with two axes centered at the origin demonstrates the behavior. Try resizing the application and seeing how the axis positions move relative to the viewport. If one uncomments the assert on line 62, they will soon get an assertion failure; it is during those resizeEvents that the viewed scene and the scene origin become misaligned. Why does this occur, and is there a method to correct it so that the viewport is always resized such that the scene origin is centered?


from PyQt5 import QtGui, QtCore, QtWidgets

class TestAxis(QtWidgets.QGraphicsWidget):
def __init__(self):
QtWidgets.QGraphicsWidget.__init__(self)
self.boundingRectCopy = QtCore.QRectF()

def boundingRect(self):
# Axis cannot call sceneRect() to get scene size in
# boundingRect() b/c infinite recursion.
return self.boundingRectCopy


class TestAxisX(TestAxis):
def __init__(self):
TestAxis.__init__(self)

def paint(self, painter, op, widget):
sceneRect = self.scene().sceneRect()
dispMin = sceneRect.left()
dispMax = sceneRect.right()
painter.drawLine(dispMin, 0, dispMax, 0)


class TestAxisY(QtWidgets.QGraphicsWidget):
def __init__(self):
TestAxis.__init__(self)

def paint(self, painter, op, widget):
sceneRect = self.scene().sceneRect()
dispMin = sceneRect.top()
dispMax = sceneRect.bottom()
painter.drawLine(0, dispMin, 0,dispMax)


class TestView(QtWidgets.QGraphicsView):
def __init__(self, scene, axisX, axisY):
QtWidgets.QGraphicsView.__init__(self, scene)
self.axisX = axisX
self.axisY = axisY
self.setHorizontalScrollBarPolicy(QtCore.Qt.Scroll BarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBa rAlwaysOff)

def sceneRectFromViewport(self):
viewportOrigin = self.mapToScene(QtCore.QPoint(0,0))
viewportSize = self.mapToScene(QtCore.QPoint(self.viewport().rect ().width(), \
self.viewport().height()))
return QtCore.QRectF(viewportOrigin, viewportSize)

# Force the scene's boundingRect to match the view/ensure axis updated.
def resizeEvent(self, ev):
QtWidgets.QGraphicsView.resizeEvent(self, ev)
self.centerOn(0, 0) # Resizes to make the viewed scene smaller will
# be off-center without this.
sceneRectFromViewport = self.sceneRectFromViewport()

# View and Scene SceneRects are coupled until one or the other is
# manually set. We want them coupled, but without the default rules
# that Scene uses to set its SceneRect size.
self.setSceneRect(sceneRectFromViewport)
self.scene().setSceneRect(sceneRectFromViewport)
# assert self.sceneRectFromViewport() == self.scene().sceneRect(), "viewport {0}, scene {1}".format(self.sceneRectFromViewport(), self.scene().sceneRect())
self.axisX.boundingRectCopy = self.scene().sceneRect()
self.axisY.boundingRectCopy = self.scene().sceneRect()
self.axisX.prepareGeometryChange()
self.axisY.prepareGeometryChange()


if __name__ == "__main__":
app = QtWidgets.QApplication([])
win = QtWidgets.QMainWindow()
axisX = TestAxisX()
axisY = TestAxisY()
scene = QtWidgets.QGraphicsScene()
scene.addItem(axisX)
scene.addItem(axisY)
view = TestView(scene, axisX, axisY)
win.setCentralWidget(view)
win.show()
app.exec()


Some background: Initially, I was using a zoom transformation to get the required dynamic range of the sliders- 1 pixel movement can be an increment from 1e-14 to 1e14). The ItemIgnoresTransformations flag is set on for the cursors, so the X coordinate of the center was the value reported back to the rest of the application. However, QGraphicsView is not able to handle such a large dynamic range, and floating point errors start creeping in. MouseMoveEvents are not honored at all, or the cursors scene positions are updated, but their position within the view is moved only after the mouse has moved a certain number of pixels. So unfortunately, I now have to keep track of this information (scene position to "value sent to the application" mapping) myself using a separate Python class. If there is a better way to accomplish this than QGraphicsView, I will make a separate thread to discuss.

Thanks in advance for any help! The widget is almost complete, and fixing this is hopefully the last bug preventing me from finishing!

cr1901
20th January 2016, 07:30
I'm trying to debug this myself; what is the correct way to get the matrix which transforms the scene to the view (QGraphicsScene doesn't seem to have a matrix member function or property?)? I need to figure out why the view, scene, and viewport do not coincide, and looking at the actual transformation used between the former two may help me in figuring out why the transformation from scene to viewport gets out of sync.