1

I am currently having issues understanding the behavior of QGraphicsAnchorLayout within a QGraphicsScene. I create 4 boxes and anchor the corners of each, but no anchors appear to be applied properly, or at least the way I believed they would be.

The yellow box should be on the top left of the QGraphicsScene at all times, even when the GraphicsView is expanded. The blue box is anchored to appear adjacent to the yellow box on the right, with its top the coinciding with the top of the QGraphicsScene/viewport.

The top left corner of the green box is anchored to the bottom right of the blue box and likewise for the red box to the green box. But this is what I am getting:

enter image description here

I expect the yellow box to be at the top of the graphics scene/viewport at all times. And I would like for it always to remain visible even when scrolled right, but I believe that probably would be a separate issue. However, when I expand the window vertically, all the boxes are centered, including the yellow box which I expected to remain at top.

The blue, green and red boxes seem to bear no resemblance to the anchors I applied.

Following is the code I used to generate this. How do these anchors work and what can I do to correct this?

import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
from debug_utils import *
from PyQt5.QtWidgets import QGraphicsAnchorLayout, QGraphicsWidget, QGraphicsLayoutItem

def qp(p):
    return "({}, {})".format(p.x(), p.y())

class box(QtWidgets.QGraphicsWidget):
    pressed = QtCore.pyqtSignal()

    def __init__(self, rect, color, parent=None):
        super(box, self).__init__(parent)
        self.raw_rect = rect
        self.rect = QtCore.QRectF(rect[0], rect[1], rect[2], rect[3])
        self.color = color

    def boundingRect(self):
        pen_adj = 0
        return self.rect.normalized().adjusted(-pen_adj, -pen_adj, pen_adj, pen_adj)

    def paint(self, painter, option, widget):
        r = self.boundingRect()

        brush = QtGui.QBrush()
        brush.setColor(QtGui.QColor(self.color))
        brush.setStyle(Qt.SolidPattern)

        #rect = QtCore.QRect(0, 0, painter.device().width(), painter.device().height())
        painter.fillRect(self.boundingRect(), brush)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.setPen(Qt.darkGray)

        painter.drawRect(self.boundingRect())
        #painter.drawRect(0, 0, max_time*char_spacing, self.bar_height)

    def mousePressEvent(self, ev):
        self.pressed.emit()
        self.update()

    def mouseReleaseEvent(self, ev):
        self.update()

class GraphicsView(QtWidgets.QGraphicsView):
    def __init__(self, parent=None):
        super(GraphicsView, self).__init__(parent)
        scene = QtWidgets.QGraphicsScene(self)
        self.setScene(scene)
        self.numbers = []
        self.setMouseTracking(True)

        l = QGraphicsAnchorLayout()
        l.setSpacing(0)
        w = QGraphicsWidget()
        #painter = QtGui.QPainter(self)
        w.setPos(0, 0)
        w.setLayout(l)
        scene.addItem(w)
        self.main_widget = w
        self.main_layout = l

        self.makeBoxs()

    def makeBoxs(self):
        rect = [0, 0, 600, 250]
        blue_box = box(rect, QtGui.QColor(0, 0, 255, 128))
        green_box = box(rect, QtGui.QColor(0, 255, 0, 128))
        red_box = box([0, 0, 200, 50], QtGui.QColor(255, 0, 0, 128))
        yellow_box_left = box([0, 0, 75, 600], QtGui.QColor(255, 255, 0, 128))
        #self.scene().setSceneRect(blue_box.rect)
        #self.scene().setSceneRect(bar_green.rect)

        # Adding anchors adds the item to the layout which is part of the scene
        self.main_layout.addCornerAnchors(yellow_box_left, Qt.TopLeftCorner, self.main_layout, Qt.TopLeftCorner)
        self.main_layout.addCornerAnchors(blue_box, Qt.TopLeftCorner, yellow_box_left, Qt.TopRightCorner)
        self.main_layout.addCornerAnchors(green_box, Qt.TopLeftCorner, blue_box, Qt.BottomRightCorner)
        self.main_layout.addCornerAnchors(red_box, Qt.TopLeftCorner, green_box, Qt.BottomRightCorner)

        #self.main_layout.addAnchor(bar_green, Qt.AnchorTop, blue_box, Qt.AnchorBottom)
        #self.main_layout.addAnchor(bar_green, Qt.AnchorLeft, blue_box, Qt.AnchorRight)

    def printStatus(self, pos):
        msg = "Viewport Position: " + str(qp(pos))
        v = self.mapToScene(pos)
        v = QtCore.QPoint(v.x(), v.y())
        msg = msg + ",  Mapped to Scene: " + str(qp(v))
        v = self.mapToScene(self.viewport().rect()).boundingRect()
        msg = msg + ",  viewport Mapped to Scene: " + str(qp(v))
        v2 = self.mapToScene(QtCore.QPoint(0, 0))
        msg = msg + ",  (0, 0) to scene: " + qp(v2)
        self.parent().statusBar().showMessage(msg)

    def mouseMoveEvent(self, event):
        pos = event.pos()
        self.printStatus(pos)
        super(GraphicsView, self).mouseMoveEvent(event)

    def resizeEvent(self, event):
        self.printStatus(QtGui.QCursor().pos())
        h = self.mapToScene(self.viewport().rect()).boundingRect().height()
        r = self.sceneRect()
        r.setHeight(h)

        height = self.viewport().height()
        for item in self.items():
            item_height = item.boundingRect().height()

        super(GraphicsView, self).resizeEvent(event)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        gv = GraphicsView()
        self.setCentralWidget(gv)
        self.setGeometry(475, 250, 600, 480)
        scene = self.scene = gv.scene()
        sb = self.statusBar()

def main():
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

EDIT: Adding expected output Based on how the anchors are defined, I expect the output to look something like the following. Since I can't yet actually create what I need, I have created this in PowerPoint. But, of course, in addition to getting this to work, I'm hoping to understand how to use anchors in a more general sense as well.

enter image description here

EDIT 2: Thank you again for updating. It's not quite what I was expecting, and there is a strange artifact when I scroll. Just to clarify,

  1. I expect the yellow widget to be visible at all times, top left of viewport with the highest z-order. I think you provided that.
  2. All the other widgets should scroll, maintaining their relative anchors. I removed the blue box's anchor to the yellow box, but then all boxes align left.
  3. In your current implementation, the non-yellow boxes do not scroll, but when I resize or move the scroll bar right and back left I see this artifact:

enter image description here

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
HonestMath
  • 325
  • 1
  • 2
  • 10

1 Answers1

2

You have 2 errors:

  • Your Box class is poorly built, instead of override boundingRect it only sets the size since the position will be handled by the layout.

  • The position of a layout is always relative to the widget where it is set. And in your case you want the top-left of the main_layout to match the top-left of the viewport so you must modify the top-left of the main_widget to match.

Considering the above, the solution is:

from PyQt5 import QtCore, QtGui, QtWidgets


def qp(p):
    return "({}, {})".format(p.x(), p.y())


class Box(QtWidgets.QGraphicsWidget):
    pressed = QtCore.pyqtSignal()

    def __init__(self, size, color, parent=None):
        super(Box, self).__init__(parent)
        self.setMinimumSize(size)
        self.setMaximumSize(size)
        self.color = color

    def paint(self, painter, option, widget):
        brush = QtGui.QBrush()
        brush.setColor(QtGui.QColor(self.color))
        brush.setStyle(QtCore.Qt.SolidPattern)

        painter.fillRect(self.boundingRect(), brush)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.setPen(QtCore.Qt.darkGray)

        painter.drawRect(self.boundingRect())

    def mousePressEvent(self, event):
        self.pressed.emit()
        super().mousePressEvent(event)


class GraphicsView(QtWidgets.QGraphicsView):
    messageChanged = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super(GraphicsView, self).__init__(parent)
        self.setMouseTracking(True)
        self.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
        scene = QtWidgets.QGraphicsScene(self)
        self.setScene(scene)

        l = QtWidgets.QGraphicsAnchorLayout()
        l.setSpacing(0)
        w = QtWidgets.QGraphicsWidget()
        self.scene().sceneRectChanged.connect(self.update_widget)
        self.horizontalScrollBar().valueChanged.connect(self.update_widget)
        self.verticalScrollBar().valueChanged.connect(self.update_widget)
        w.setLayout(l)
        scene.addItem(w)
        self.main_widget = w
        self.main_layout = l

        self.makeBoxs()

    def makeBoxs(self):
        blue_box = Box(QtCore.QSizeF(300, 125), QtGui.QColor(0, 0, 255, 128))
        green_box = Box(QtCore.QSizeF(300, 125), QtGui.QColor(0, 255, 0, 128))
        red_box = Box(QtCore.QSizeF(100, 25), QtGui.QColor(255, 0, 0, 128))
        yellow_box = Box(QtCore.QSizeF(37.5, 300), QtGui.QColor(255, 255, 0, 128))

        # yellow_box_left top-left
        self.main_layout.addCornerAnchors(
            yellow_box,
            QtCore.Qt.TopLeftCorner,
            self.main_layout,
            QtCore.Qt.TopLeftCorner,
        )
        self.main_layout.addCornerAnchors(
            blue_box, QtCore.Qt.TopLeftCorner, yellow_box, QtCore.Qt.TopRightCorner
        )
        self.main_layout.addCornerAnchors(
            green_box, QtCore.Qt.TopLeftCorner, blue_box, QtCore.Qt.BottomRightCorner
        )
        self.main_layout.addCornerAnchors(
            red_box, QtCore.Qt.TopLeftCorner, green_box, QtCore.Qt.BottomRightCorner
        )

        # self.setSceneRect(self.scene().itemsBoundingRect())

    def update_widget(self):
        vp = self.viewport().mapFromParent(QtCore.QPoint())
        tl = self.mapToScene(vp)
        geo = self.main_widget.geometry()
        geo.setTopLeft(tl)
        self.main_widget.setGeometry(geo)

    def resizeEvent(self, event):
        self.update_widget()
        super().resizeEvent(event)

    def mouseMoveEvent(self, event):
        pos = event.pos()
        self.printStatus(pos)
        super(GraphicsView, self).mouseMoveEvent(event)

    def printStatus(self, pos):
        msg = "Viewport Position: " + str(qp(pos))
        v = self.mapToScene(pos)
        v = QtCore.QPoint(v.x(), v.y())
        msg = msg + ",  Mapped to Scene: " + str(qp(v))
        v = self.mapToScene(self.viewport().rect()).boundingRect()
        msg = msg + ",  viewport Mapped to Scene: " + str(qp(v))
        v2 = self.mapToScene(QtCore.QPoint(0, 0))
        msg = msg + ",  (0, 0) to scene: " + qp(v2)
        self.messageChanged.emit(msg)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        gv = GraphicsView()
        self.setCentralWidget(gv)

        self.setGeometry(475, 250, 600, 480)

        gv.messageChanged.connect(self.statusBar().showMessage)


def main():
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

enter image description here

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Brilliant, many thanks! Two questions arise: 1. In your update_widget, if you are setting the top left of the yellow box to the top left of the viewport, why does the yellow box disappear when you scroll right? 2. When I scroll right until the scrollbar disappears, why is there so much additional whitespace? Shouldn't the GraphicsScene cover just the bounding box of the colored boxes? Eventually my goal is to fix the yellow box in the viewport and let the other boxes scroll underneath to the left, but for now I need to understand why setting the top left only sets the top and not the left. – HonestMath Oct 04 '19 at 21:41
  • @HonestMath 1) I didn't realize that mistake. 2) The GraphicsScene is like the world and the QGraphicsView is the camera so the world not only covers the items, it can cover more. Do you just want me to cover the items? If so, that contradicts your previous requirement. – eyllanesc Oct 04 '19 at 21:50
  • I apologize if what I said was confusing. I anchored the blue box to the yellow box as a test because I could not get it to work. I actually wish for the yellow box to remain at the top left of the viewport regardless of the horizontal scroll. All the other boxes should behave normally in the sense that they will scroll left and right with fixed positions in the GraphicsScene. I expect the scrollbar to disappear as soon as I enlarge the scene enough to contain all the boxes. Does this clarify? – HonestMath Oct 04 '19 at 22:11
  • Thank you, I tried your updated solution but I see some strange behavior. I updates the question with some more comments. – HonestMath Oct 04 '19 at 23:21
  • @HonestMath What you show I do not get, Maybe it is a bug of your version of PyQt5, I use the latest version: PyQt5 5.13.1 on Linux What version do you use? What is your OS? – eyllanesc Oct 04 '19 at 23:27
  • I use Anaconda Python 3.7 on Windows. It shows PyQt version 5.9.2, which is the latest I get from conda. – HonestMath Oct 05 '19 at 00:41
  • 1
    @HonestMath conda, ...., mmm, The latest versions of conda have several bugs with PyQt5, in recent weeks questions have been published that works with PyQt5 of pypi but not with the one that provides conda, and the solution was to use the pypi version , so I recommend you not to use the PyQt5 provided by conda. – eyllanesc Oct 05 '19 at 00:45
  • Well, I've updated to pypi PyQt5 and tried multiple things, but I still get strange behaviors. When the window is shrunk, the position of the yellow bar stays in the viewport, but becomes highly negative, enlarging the scroll area beyond the widgets' bounding box. I also see paint artifacts, and scrolling the scrollbar actually increases the scroll area too (because the yellow bar is re positioned). As SO is complaining about extended comment discussions, shall I pose a new question? – HonestMath Oct 05 '19 at 16:10