0

I write some code for visualization of board and I have problem with initialization. I want generate view filled with board from the start to end (is it is shown on 3rd image). I try use many Qt5 methods but without results (I am beginner in Qt5). View looks perfect after first resize.

I have no idea what I am doing wrong with this initialization.

Just after .show():

enter image description here

After focus lost (I switch to write this question):

enter image description here

After resize it become as should look like/what I want to achieve: enter image description here

How to fix this code to make it working form initialization - I am level 1 in Qt5 (beginner) and level 7 in programming. Maybe it need very simple change.

Here is working code Python 3.8/Qt5:

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


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

    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__()
        logging.debug('size is %s for %s.', self.size(), self.__class__.__name__)

        grid = QGraphicsGridLayout()
        grid.setSpacing(0)
        grid.setContentsMargins(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)


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__)

        scene = BoardScene()
        self.setScene(scene)
        # no frame
        self.setFrameShape(0)
        # transparent background
        # self.setStyleSheet('QGraphicsView {background: transparent;}')
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    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

2 Answers2

2

As the grid has no clues on the BoardContainer size, it can not layout items correctly on 0 size. You could explicitly set the size of the BoardContainer to whatever size you want. Here is the fixed part of the code :

class BoardContainer(QGraphicsWidget):
    def __init__(self):
        super().__init__()
        self.resize(100,100)
        logging.debug('size is %s for %s.', self.size(), self.__class__.__name__)
        grid = QGraphicsGridLayout()
        grid.setSpacing(0)
        grid.setContentsMargins(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()  

after all, calling grid.activate() forces to layout based on the size.

To encourage you on using QML, here is a minimal QML example of the same application with a fancy animation to demonstrate how it's easy to use animations on QML. If you remove the added animation from this implementation to become just like the c++ version of your code, it's only 30 lines of code which is fantastic IMHO.

import QtQml 2.12
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12

ApplicationWindow {

    id:mywin
    visible: true
    width: 640
    height: 640
    minimumWidth: 640
    minimumHeight: 480
    title: qsTr("Fancy Board")

    Item {
        anchors.centerIn: parent
        width: Math.min(parent.width,parent.height);
        height: width

        GridLayout{
            id : grid
            anchors.fill: parent
            rows: 8
            columns: 8
            rowSpacing: 0
            columnSpacing: 0
            Repeater{
                model: 64
                Rectangle{
                    Layout.fillWidth: true
                    Layout.fillHeight: true
                    color: ((index%8) - (index/8 | 0)) %2 === 0 ? 'black' : 'white'

                    Rectangle{
                        anchors.fill: parent
                        opacity: mouseArea.containsMouse ? 0.5 : 0
                        scale: mouseArea.containsMouse? 1 : 0
                        color: 'red'
                        Behavior on opacity{
                            NumberAnimation{ duration: 300 }
                        }
                        Behavior on scale {
                            NumberAnimation{ duration: 300; easing.type: Easing.OutCubic}
                        }
                    }
                    MouseArea{
                        id: mouseArea
                        anchors.fill: parent
                        hoverEnabled: true
                    }
                }
            }
        }
    }
}

You can also copy and paste this code into a main.qml file, zip it, and upload it to https://qt-webassembly.io/designviewer/ to run it on your browser to see that it can run under your browser as well.

Soheil Armin
  • 2,725
  • 1
  • 6
  • 18
  • I tried to add setSceneRect to code for scene or view and nothing changed after set - are you sure that it will work. Can you add this line of code and test it? – Chameleon Nov 02 '19 at 13:18
  • @Chameleon, I'm just trying to test it. I'm installing Python 3.8 to keep things the same as your env. – Soheil Armin Nov 02 '19 at 13:22
  • @Chameleon, I think that the call to fitInView is not appropriate. let me try another approach and I will update you with that. – Soheil Armin Nov 02 '19 at 13:30
  • I was tested it on Python 3.7 (misspell in text but it is no matter I think if 3.8). fitInView maybe wrong but works (there is some artifact in bottom 1 pixel size line). – Chameleon Nov 02 '19 at 13:48
  • @Chameleon, fitInView works. The problem is that the BoardContainer has no size as you just using it as a container. Actually I'm always using the same approach. After the first paint, the actual size of the widget is calculated based on the layout. just after the resize event of the QGraphicsView is triggered for the very first time. – Soheil Armin Nov 02 '19 at 14:04
  • @Chameleon, the answer is updated with the working code. I could not produce the focus bug you mentioned in your question. QML is much easier to deal with. I have more than 12 years of experience in Qt. I was resisting to use QML for about 3 years. But now I'm in love with that ;) – Soheil Armin Nov 02 '19 at 14:15
  • Looks that `resize()` solves problem without `activate()` but I need to study it. I have beginner experience in Qt5 (level 1 - few weeks). I like Qt5 too but my knowledge is weak in this domain. I do much more GUI application in HTML/JS (10+ years). The but is maybe because dual screen with different DPI ratios. – Chameleon Nov 02 '19 at 15:08
  • @Chameleon, so when you are a js expert, then you will love QML. you can even use popular js libraries like momentjs or lodash with QML. – Soheil Armin Nov 02 '19 at 15:10
  • I will try QML. I found that only `setMinimumSize` in container widget solves problem also. Now when it is done I do not understand why I have white frame around (one pixel) - I do `self.setFrameShape(0)` on `BoardView` and `setSpacing` on `QGraphicsWidget`. Thanks for suggestions without it I will not do more progress. – Chameleon Nov 02 '19 at 15:21
  • 1
    You need to use setSceneRect from x=0 and y=0, then your widget won't be centered anymore. – Soheil Armin Nov 02 '19 at 16:01
  • I tried with setSceneRect but without success I do some logging. Maybe I will create new question since it is different problem. Maybe problem is here `DEBUG:root:childrenRect is PyQt5.QtCore.QRect(1, 1, 638, 478) for BoardView. DEBUG:root:contentsRect is PyQt5.QtCore.QRect(0, 0, 640, 480) for BoardView.`. I was made background red on BoardView and it is visible so and children rect is too small so something is here I think. – Chameleon Nov 02 '19 at 19:53
  • I found some interesting idea that both showEvent and resizeEvent should be handled some explanation is here -> https://stackoverflow.com/questions/17028680/qt5-c-qgraphicsview-images-dont-fit-view-frame. It is not solution of problem but shows that maybe showEvent is improtant. I also study C++ code and I found that fitInView contains strange code which can explain 2px margins but I am still not sure. `// Find the ideal x / y scaling ratio to fit \a rect in the view. int margin = 2; QRectF viewRect = viewport()->rect().adjusted(margin, margin, -margin, -margin);` – Chameleon Nov 02 '19 at 21:47
2

Try it:

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


class Application(QApplication):
    pass


class SquareWidget(QGraphicsWidget):
    def __init__(self, color):
        super().__init__()

        self.resize(640, 640)                                    # +++        

        if color:
            self.color = QtCore.Qt.white
        else:
            self.color = QtCore.Qt.black

    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__()
        logging.debug('size is %s for %s.', self.size(), self.__class__.__name__)

        self.resize(640, 640)                                     # +++
        self.setMinimumSize(80, 80)                               # +++       


        grid = QGraphicsGridLayout()
        grid.setSpacing(0)
        grid.setContentsMargins(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)


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__)

        scene = BoardScene()
        self.setScene(scene)
        # no frame
        self.setFrameShape(0)
        # transparent background
        # self.setStyleSheet('QGraphicsView {background: transparent;}')
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    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()

enter image description here

S. Nick
  • 12,879
  • 8
  • 25
  • 33