Place QGraphicsView scrollbars beside rather than within QGraphicsView when there is room

48 Views Asked by At

My Qt application, which I am writing in PyQt6, contains a QGraphicsView. Depending on the size of the application window, the QGraphicsScene underlying this view may be too big to fit in the viewport, so the QGraphicsView automatically adds horizontal and vertical scrollbars as necessary.

I find that when I resize the window to be wide enough that the horizontal scrollbar ought to no longer be necessary, the QGraphicsView doesn't move the vertical scrollbar over into the excess space, and therefore I continue to have a horizontal scrollbar present, which only scrolls by an amount equal to the width of the vertical scrollbar.

When there is sufficient space horizontally, I'd like for the vertical scrollbar of my QGraphicsView to appear to the right of the view, rather than within it, so that a horizontal scrollbar is unnecessary.

(I think this is what QScrollArea.setWidgetResizable(True) is supposed to do, but unfortunately, QGraphicsView subclasses from QAbstractScrollArea rather than QScrollArea so the widgetResizable property is not available.)

How can I make that horizontal scrollbar stop appearing in circumstances where it isn't needed?

MWE:

from PyQt6 import QtCore, QtGui, QtWidgets

class MyBottomWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        scene = QtWidgets.QGraphicsScene()
        rect = QtCore.QRect(0, 0, 700, 700)
        shape = scene.addEllipse(rect)
        shape.setBrush(QtGui.QColor(QtGui.QColorConstants.Blue))
        view = QtWidgets.QGraphicsView(scene)
        view.setSceneRect(rect)
        view.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)

        layout = QtWidgets.QGridLayout(self)
        self.setLayout(layout)
        layout.addWidget(view, 0, 0, QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop)

app = QtWidgets.QApplication([])

window = QtWidgets.QMainWindow()
window.resize(800, 600)
window.setCentralWidget(MyBottomWidget())
window.show()

app.exec()

This horizontal scrollbar would not be necessary… …if this vertical scrollbar was in the ample space to the right of the 700px width of the QGraphicsView image, rather than within it.

1

There are 1 best solutions below

0
On

The solution I came up with was to subclass QGraphicsView and override the sizeHint method to always include the extent of the scrollbar in the width and height.

class MyGraphicsView(QtWidgets.QGraphicsView):
    def __init__(self, scene):
        super().__init__(scene)
        self.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop)
    
    def sizeHint(self):
        w, h, s = self.full_width(), self.full_height(), self.scrollbar_extent()
        return QtCore.QSize(w + s, h + s)
        
    def full_width(self):
        return self.mapFromScene(self.sceneRect()).boundingRect().width()

    def full_height(self):
        return self.mapFromScene(self.sceneRect()).boundingRect().height()
    
    @staticmethod
    def scrollbar_extent():    # Returns 19 on my system. See https://stackoverflow.com/q/16515646/1187304
        extent = QtWidgets.QApplication.instance().style().pixelMetric(QtWidgets.QStyle.PixelMetric.PM_ScrollBarExtent)
        return extent + 3

Using MyGraphicsView instead of QtWidgets.QGraphicsView in my code, I now get the following behavior:

  1. When the window size is sufficiently large, I see a 719px × 719px canvas with my image drawn in the upper-left 700px × 700px.
  2. If the window width and/or height is shrunk such that there is room to display only the first 700px ≤ x < 719px, the excess is not shown but no scrollbar appears.
  3. If the window width or height is shrunk such that there is only room to display less than 700px, a scrollbar appears.
  4. If both the window width and window height are small, such that both scrollbars appear, then each scrollbar has an extra 19px of range to accommodate the other scrollbar.

Not a perfect solution due to point 1 — ideally if there's plenty of space so that scrollbars aren't needed, then the canvas wouldn't have the extra 19px included — but totally satisfactory for my needs. If desired, one could add rectangles in the default background color to the right and bottom edges of the scene to disguise the fact that those extra pixels are included:

        w, h, s = self.full_width(), self.full_height(), self.scrollbar_extent()
        for rect in (QtCore.QRectF(w + 1, 0, s, h + s), QtCore.QRectF(0, h + 1, w + s, s)):
            shape = scene.addRect(rect)
            shape.setBrush(self.palette().window())
            shape.setPen(QtGui.QPen(QtCore.Qt.PenStyle.NoPen))

I've decided against doing this for the time being but it's something one could play with.