1

I have maybe simple problem with Qt5. I am beginner in Qt5 so it is lack of knowledge. I searched more then 10 answers, code examples and I did not find solution. Problem is with padding added by QGraphicsView (not sure but I set background to red to show it).

I tried many commands to remove padding but without result always red lines/background color is visible. I can not cover full background with content of scene.

Here is image - red is background of `class BoardView(QGraphicsView)' enter image description here

Here is part of code which cause problem (in my opinion) - full code is bellow - you can run it since reference only to Qt5. Requires Python 3.7 or similar.

I have hypothesis that mayb fitInView() adds some margin but I am not sure or I am not clean all margins (lack of knowledge in Qt5) - so maybe I should use other methods to achieve pure board without padding/margin.

class BoardView(QGraphicsView):
    def __init__(self):
        super().__init__()
        logging.debug('size is %s for %s.', self.size(), self.__class__.__name__)

        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setViewportMargins(0, 0, 0, 0)
        self.setContentsMargins(0, 0, 0, 0)
        self.setFrameShape(QFrame.NoFrame)
        # self.setFrameStyle(QFrame.NoFrame)

        # transparent background
        # self.setStyleSheet('QGraphicsView {background: transparent;}')
        self.setStyleSheet('QGraphicsView { background: red; }')
        logging.debug(self.styleSheet())

        scene = BoardScene()
        self.setScene(scene)
        # no frame


        logging.debug('Here is problem with frames!')
        logging.debug('rect is %s for %s.', self.rect(), self.__class__.__name__)
        logging.debug('sceneRect is %s for %s.', self.sceneRect(), self.__class__.__name__)

        logging.debug('frameShape is %s for %s.', self.frameShape(), self.__class__.__name__)
        logging.debug('frameStyle is %s for %s.', self.frameStyle(), self.__class__.__name__)

        logging.debug('Why children rect is too small?')
        logging.debug('How to change children rect?')
        logging.debug('childrenRect is %s for %s.', self.childrenRect(), self.__class__.__name__)
        logging.debug('contentsRect is %s for %s.', self.contentsRect(), self.__class__.__name__)
        logging.debug('frameRect is %s for %s.', self.frameRect(), self.__class__.__name__)

        logging.debug('frameSize is %s for %s.', self.frameSize(), self.__class__.__name__)
        logging.debug('frameWidth is %s for %s.', self.frameWidth(), self.__class__.__name__)
        logging.debug('frameGeometry is %s for %s.', self.frameGeometry(), self.__class__.__name__)
        logging.debug('geometry is %s for %s.', self.geometry(), self.__class__.__name__)

    def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
        super().resizeEvent(event)
        self.fitInView(self.scene().board_container, Qt.KeepAspectRatio)

Fully working code (apart problem with padding)

import logging
import sys
import typing

from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QSize, QPoint, Qt, QRect, QMargins
from PyQt5.QtGui import QFont, QPaintEvent, QPainter, QBrush, QColor, QPen
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QSizePolicy, QVBoxLayout, QHBoxLayout, QGraphicsWidget, \
    QGraphicsScene, QGraphicsView, QGraphicsGridLayout, QStyleOptionGraphicsItem, QGraphicsSceneMouseEvent, QFrame


class Application(QApplication):
    pass


class SquareWidget(QGraphicsWidget):
    def __init__(self, color):
        super().__init__()
        if color:
            self.color = QtCore.Qt.white
        else:
            self.color = QtCore.Qt.black

        # set some size
        self.setMinimumSize(2.0, 2.0)

    def paint(self, painter: QtGui.QPainter, option: QStyleOptionGraphicsItem, widget: typing.Optional[QWidget] = ...) -> None:
        painter.fillRect(option.rect, self.color)


class BoardContainer(QGraphicsWidget):
    def __init__(self):
        super().__init__()
        # require initialization
        self.resize(16.0, 16.0)
        logging.debug('size is %s for %s.', self.size(), self.__class__.__name__)
        logging.debug('geometry is %s for %s.', self.geometry(), self.__class__.__name__)

        grid = QGraphicsGridLayout()
        logging.debug('spacing %s %s', (grid.verticalSpacing(), grid.horizontalSpacing()),
                      self.__class__.__name__)
        grid.setSpacing(0)
        logging.debug('spacing %s %s', (grid.verticalSpacing(), grid.horizontalSpacing()),
                      self.__class__.__name__)
        grid.setContentsMargins(0.0, 0.0, 0.0, 0.0)
        self.setLayout(grid)
        for row in range(8):
            for column in range(8):
                square_widget = SquareWidget((row + column) % 2)
                grid.addItem(square_widget, row, column)
        grid.activate()
        logging.debug('size is %s for %s.', self.size(), self.__class__.__name__)


class BoardScene(QGraphicsScene):
    def __init__(self):
        super().__init__()
        self.board_container = board_container = BoardContainer()
        self.addItem(board_container)


class BoardView(QGraphicsView):
    def __init__(self):
        super().__init__()
        logging.debug('size is %s for %s.', self.size(), self.__class__.__name__)

        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setViewportMargins(0, 0, 0, 0)
        self.setContentsMargins(0, 0, 0, 0)
        self.setFrameShape(QFrame.NoFrame)
        # self.setFrameStyle(QFrame.NoFrame)

        # transparent background
        # self.setStyleSheet('QGraphicsView {background: transparent;}')
        self.setStyleSheet('QGraphicsView { background: red; }')
        logging.debug(self.styleSheet())

        scene = BoardScene()
        self.setScene(scene)
        # no frame


        logging.debug('Here is problem with frames!')
        logging.debug('rect is %s for %s.', self.rect(), self.__class__.__name__)
        logging.debug('sceneRect is %s for %s.', self.sceneRect(), self.__class__.__name__)

        logging.debug('frameShape is %s for %s.', self.frameShape(), self.__class__.__name__)
        logging.debug('frameStyle is %s for %s.', self.frameStyle(), self.__class__.__name__)

        logging.debug('Why children rect is too small?')
        logging.debug('How to change children rect?')
        logging.debug('childrenRect is %s for %s.', self.childrenRect(), self.__class__.__name__)
        logging.debug('contentsRect is %s for %s.', self.contentsRect(), self.__class__.__name__)
        logging.debug('frameRect is %s for %s.', self.frameRect(), self.__class__.__name__)

        logging.debug('frameSize is %s for %s.', self.frameSize(), self.__class__.__name__)
        logging.debug('frameWidth is %s for %s.', self.frameWidth(), self.__class__.__name__)
        logging.debug('frameGeometry is %s for %s.', self.frameGeometry(), self.__class__.__name__)
        logging.debug('geometry is %s for %s.', self.geometry(), self.__class__.__name__)

    def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
        super().resizeEvent(event)
        self.fitInView(self.scene().board_container, Qt.KeepAspectRatio)


class BoardWidget(QWidget):
    def __init__(self):
        super().__init__()
        logging.debug('size is %s for %s.', self.size(), self.__class__.__name__)

        grid = QGridLayout()

        board_view = BoardView()
        grid.addWidget(board_view, 0, 0)

        self.setLayout(grid)


def main():
    # show exceptions
    def excepthook(cls, exception, traceback):
        sys.__excepthook__(cls, exception, traceback)
    sys.excepthook = excepthook

    logging.basicConfig(level=logging.DEBUG)
    app = Application(sys.argv)
    app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)

    default_font = QFont()
    default_font.setPointSize(12)
    app.setFont(default_font)

    board_widget = BoardWidget()
    board_widget.setMinimumSize(640, 640)
    board_widget.show()

    sys.exit(app.exec())


if __name__ == '__main__':
    main()

Chameleon
  • 9,722
  • 16
  • 65
  • 127
  • 1
    I'm not sure I'm following you. What do you want to achieve? As long as you use `KeepAspectRatio` you will probably end with some margin. Do you mean that, in any case, you don't want "that" 1px border? – musicamante Nov 02 '19 at 21:58
  • Yes, I do not want any margin I want achieve pure board. I suspect that Qt5 is adding margin when I am using fitInView. – Chameleon Nov 02 '19 at 22:18
  • Ok, I added a possible solution to your question. Keep in mind that's not perfect for various reasons, and that's mostly due to the way Qt's graphics view framework behaves. – musicamante Nov 02 '19 at 22:47
  • I will study it tomorrow see code of [fitInView()](https://github.com/qt/qtbase/blob/dev/src/widgets/graphicsview/qgraphicsview.cpp) - I put comment to your answer with more details. – Chameleon Nov 02 '19 at 23:02
  • 1
    @Chameleon There is a pyqt reimplementation of `fitInView` [here](https://stackoverflow.com/a/35514531/984421) that gets rid of the annoying margin. – ekhumoro Nov 03 '19 at 13:21

1 Answers1

1

Yes, indeed fitInView adds some "careful" margin to ensure that the whole graphics item is shown; I cannot tell you how that is computed, but I'm assuming it's based on the shape boundingRect, including the (possible) pen of the widget.

As a possible solution, since I'm assuming that the width and height of the board are the same and it will always use the same pen (in this case, a QtCore.Qt.NoPen which usually "translates" the geometries by 0.5 pixel, assuming a cosmetic pen of 1 pixel) and the top-left position of the board will always be (0, 0), I'd suggest the following solution.

It does almost the same as fitInView does (while over-simplifying it a lot), by using the minimum width/height to get the possible scale for the graphics view transformation. It won't be always pixel-perfect, but I think that'd be a good approach for starters.

Keep in mind that you'll (possibly) always end up with at least one pixel error: if you use an integer value for min you'll probably end up with a pixel less than needed (the last row or column will be a pixel shorter), while if you use a 1.0 float value it'll probably be a pixel larger. That depends on how QPainter works and, as long as you don't take that into account in the SquareWidget.paint() method, something will always be off at some point.

    def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
        super().resizeEvent(event)
        minSize = min(event.size().width(), event.size().height()) - 1
        # alternatively use "- 1." to get the full square sizes
        scale = minSize / self.sceneRect().width()
        self.setTransform(QtGui.QTransform().scale(scale, scale))
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Here is magic [fitInView()](https://github.com/qt/qtbase/blob/dev/src/widgets/graphicsview/qgraphicsview.cpp). If you understand c++ like me you can learn it. As you said can be some problems with scaling but it depend on resolution and content. You can also avoid scaling problems with setting size to divisible values. For example I need to round to 8 pixels for board. High resolutions forgives some problems (whatever even Firefox have pixel problems on 4k monitors - it creates line of white pixels at left, bottom and right). – Chameleon Nov 02 '19 at 22:53
  • I have only problem with centerOn() in C++ code but maybe tomorrow I will understand it (today is too late and I feel some cold). – Chameleon Nov 02 '19 at 22:56
  • It is almost O.K.. I think only good way is to reimplement buggy qt5 fitInView to achieve border less view. Normally API should allow to specify margin. – Chameleon Nov 10 '19 at 13:02