Search code examples
pythonpyqt5qlabelqlayout

Drawing chess pieces on a QLabel type chessboard in a QLayout PyQT 5


For a school project I am programming a chess game. I've made a first GUI with the following code:

class chessUI(QtWidgets.QMainWindow):

    def __init__(self, partie):

        """super().__init__() ===> pour QtWidgets.QWidget"""
        # Initialisation de la fenêtre
        QtWidgets.QMainWindow.__init__(self)
        self.setGeometry(0, 0, 1100, 1100)
        #self.setFixedSize(1100, 1100)
        self.setWindowTitle("IHM")

        """self.layout1 = QtWidgets.QHBoxLayout()
        self.setLayout(self.layout1)
        self.layout1.addStretch(1)

        self.layout2 = QtWidgets.QHBoxLayout()
        self.layout2.addStretch(1)
        bout1 = QtWidgets.QPushButton('Nouvelle partie', self)
        bout2 = QtWidgets.QPushButton('Custom', self)
        self.layout2.addWidget(bout1)
        self.layout2.addWidget(bout2)

        self.layout3 = QtWidgets.QHBoxLayout()
        im = QtGui.QPixmap("echiquier.png")
        label = QtWidgets.QLabel()
        label.setPixmap(im)
        self.layout3.addWidget(label)"""

        """# Définition des différents boutons de l'interface
        bout1 = QtWidgets.QPushButton('Nouvelle partie', self)
        bout2 = QtWidgets.QPushButton('Custom', self)
        bout1.setToolTip('Nouvelle partie')
        bout2.setToolTip('Custom')
        bout1.move(1350, 100)
        bout2.move(1500, 100)
        bout1.clicked.connect(self.nvl_partie)
        bout2.clicked.connect(self.custom)"""

        """self.layout3.addLayout(self.layout3)
        self.layout1.addLayout(self.layout2)"""

        self.coups = QTableWidget(1, 3, self)
        self.coups.setItem(0, 0, QTableWidgetItem(0))
        self.coups.setItem(0, 1, QTableWidgetItem(0))
        self.coups.setItem(0, 2, QTableWidgetItem(0))
        self.coups.setHorizontalHeaderItem(0, QTableWidgetItem(str("Tour")))
        self.coups.setHorizontalHeaderItem(1, QTableWidgetItem(str("Joueur blanc")))
        self.coups.setHorizontalHeaderItem(2, QTableWidgetItem(str("Joueur noir")))
        self.coups.move(1000, 100)
        self.coups.setFixedSize(500, 840)

        # Définition des paramètres pour le jeu
        self.taille = 105                                                   # taille en pixel d'une case de l'échiquier
        self.partie = partie                                                # partie d'échec associée à la fenêtre
        self.show()

    def paintEvent(self, event = None):
        """
        :return: les différentes actions selon les conditions satisfaites
        """
        qp = QtGui.QPainter()
        qp.begin(self)
        if self.partie is not None:                             # Si une partie d'échecs a été associée :
            self.drawRectangles(qp)                             # On dessine l'échiquier
            j_blanc = self.partie.joueur_1                      # On sélectionne les deux joueurs
            j_noir = self.partie.joueur_2
            if self.partie.type is None :                       # Si la partie est un match et qu'on est au début
                if self.partie.debut:                           # si on est au début, on place les pièces de chaque joueur
                                                                # sur l'échiquier
                    self.partie.debut = False
                    self.set_pieces(qp, j_blanc)
                    self.set_pieces(qp, j_noir)
                else:                                           # Sinon, on place juste les pièces et cela fait la mise
                                                                # à jour du plateau durant la partie
                    self.set_pieces(qp, j_blanc)
                    self.set_pieces(qp, j_noir)
            else :                                              # Le joueur veut étudier différentes stratégies selon
                                                                # des positions de pièces précises : on le laisse placer
                                                                # les pièces sur l'échiquier ==> non fini
                if self.partie.debut:
                    self.partie.debut = False
                    self.choix_pieces(qp)
                    #self.placement_pieces()
                else :
                    self.set_pieces(qp, j_blanc)
                    self.set_pieces(qp, j_noir)
        qp.end()

    def drawRectangles(self, qp):
        """
        :param qp: le module QtGui.QPainter()
        :return: dessine dans l'IHM l'image du jeu d'échec
        """
        taille = self.taille
        qp.drawPixmap(100, 100, taille * 8, taille * 8, QtGui.QPixmap("echiquier.png"))

    def set_pieces(self, qp, joueur):
        """
        :param qp: le module QtGui.QPainter()
        :param joueur: le joueur dont on veut placer les pièces
        :return:
        """
        taille = self.taille
        for pi in joueur.pieces_restantes:                      # Pour chaque pièce restante au joueur, on trouve l'image associée
                                                                # et on la place sur l'échiquier
            y, x = pi.coords
            var = pi.car()
            c = joueur.couleur
            qp.drawPixmap(100 + x* taille, (7-y+1) * taille, 0.9*taille,
                                                0.9*taille, QtGui.QPixmap("impieces/" + var + "_" + c + ".png"))

    def disp_cases(self, qp, cases):
        """
        :param cases: la liste des cases accessibles par la pièce
        :return: affiche les cases accessibles par la pièce
        """
        # parcourir les cases dans la liste cases
        # sur chaque case, faire un qp.drawPixmap pour y mettre le CERCLE !!!!! j'insiste sur le CERCLE :)

    def mousePressEvent(self, event):
        """
        :param event: paramètre de type QMouseEvent
        :return: permet de générer les déplacements des pièces
        """
        y, x = event.x(), event.y()      # récupération des coordonnées de la sourie
        if self.partie is not None and self.partie.type is None and 100 <= x <= 1055 and 0 < y < 945:       # si une partie a été associée et que l'on
                                                                                                            # a bien cliqué sur une case de l'échiquier
            if event.button() == QtCore.Qt.LeftButton:
                self.partie.coupencours.append((7-(x//105-1), y//105-1))
                event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, event.pos(), QtCore.Qt.LeftButton,
                                          QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
                QtWidgets.QMainWindow.mousePressEvent(self, event)                                          # appel récursif à la fonction pour avoir la case destination
                                                                                                            # la case départ est stocké dans la liste coupencours
                if len(self.partie.coupencours) == 2:
                    self.bouger_piece(self.partie.coupencours)
                    self.partie.coupencours = []
        """elif self.partie.type == 'Custom' :
            if 1100 <= x <= 1730 and 300 <= y <= 400:
                index = (x-1100)//105
                self.creer_piece()
            elif 1100 <= x <= 1730 and 400 <= y <= 500:
                index = (x - 1100) // 105
                self.creer_piece()"""

    def bouger_piece(self, coup):
        """
        :param coup: tuple regroupant la case de départ et la case d'arrivée
        :return: si le coup est valable, bouge la pièce et met à jour l'IHM
        """
        case_dep, case_arr = coup[0], coup[1]
        self.coups.setItem(self.partie.tour//2, 0, QTableWidgetItem("{}".format(self.partie.tour//2+1)))
        if case_dep != case_arr:
            pi = self.partie.plateau[case_dep[0], case_dep[1]]
            t = self.partie.tour
            if pi != 0:
                if t % 2 == 1 and pi.joueur == self.partie.joueur_2:  # on vérifie que c'est bien à nous de jouer
                    # et que la pièce nous appartient
                    if pi.move(case_arr):
                        row = self.partie.tour // 2
                        s = "{} à {}".format(case_dep, case_arr)
                        self.coups.setItem(row, 2, QTableWidgetItem(s))
                        self.partie.tour += 1
                        self.coups.insertRow(self.coups.rowCount())
                elif t % 2 == 0 and pi.joueur == self.partie.joueur_1:
                    if pi.move(case_arr):
                        s = "{} à {}".format(case_dep, case_arr)
                        row = self.partie.tour // 2
                        self.coups.setItem(row, 1, QTableWidgetItem(s))
                        self.partie.tour += 1
                self.update()                                                                   # on met à jour l'IHM
                

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = chessUI()
    sys.exit(app.exec_())

This renders the following GUI : :
This renders the following GUI :

However, my code presents a main flaw: it isn't resizable. I've looked around in the documentation and I've found you need to use layouts in order to have it resizable. This brought me to try out the following code:

import sys
import partie as pa
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtWidgets import *


class random_ui(QtWidgets.QWidget):

    def __init__(self):
        super().__init__()
        self.partie = pa.partie()
        self.setWindowTitle("QVBoxLayout Example")
        outerlayout = QHBoxLayout()
        leftlabel = QLabel()
        leftlabel.setGeometry(100, 100, 840, 840)
        leftlabel.setPixmap(QtGui.QPixmap("echiquier.png"))
        outerlayout.addWidget(leftlabel)

        self.coups = QTableWidget(1, 3, self)
        self.coups.setItem(0, 0, QTableWidgetItem(0))
        self.coups.setItem(0, 1, QTableWidgetItem(0))
        self.coups.setItem(0, 2, QTableWidgetItem(0))
        self.coups.setHorizontalHeaderItem(0, QTableWidgetItem(str("Tour")))
        self.coups.setHorizontalHeaderItem(1, QTableWidgetItem(str("Joueur blanc")))
        self.coups.setHorizontalHeaderItem(2, QTableWidgetItem(str("Joueur noir")))
        self.coups.move(1000, 100)
        self.coups.setFixedSize(500, 840)
        outerlayout.addWidget(self.coups)
        self.setLayout(outerlayout)
        self.show()

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = random_ui()
    sys.exit(app.exec_())

Which renders the following ::
Which renders the following :

But when I want to add the images of my chesspieces, I cannot find out how to add it on the chessboard. I keep on having them added to the layout, between the chessboard, and the table widget.

Would anyone have some piece of advice?


Solution

  • Dealing with widgets that have a fixed aspect ratio is not an easy task, and some precautions must be taken in order to ensure that having an "incompatible" parent size doesn't prevent proper display.

    In this case, a possible solution is to use a widget for the chessboard that uses a grid layout for all the squares and pieces.
    Note that a QLabel isn't a good choice for the chessboard, as it doesn't allow a size smaller than the QPixmap, so a QWidget should be subclassed instead.

    The trick is to override the resizeEvent(), ignore the base implementation (which by default adapts the geometry of the layout) and manually set the geometry based on the minimum extent between width and height.

    In order to ensure that the layout has proper equal spacings even when a row or column is empty, setRowStretch() and setColumnStretch() must be called for the whole grid size.

    Then, you add the pieces directly to the layout, and whenever you need to move them you can just create a helper function that uses addWidget() with the correct row/column (which will automatically "move" the widget to the new position).

    screenshot of the ui with squared chessboard

    Here is a possible implementation.

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class Pawn(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            self.image = QtGui.QPixmap('whitepawn.png')
            self.setMinimumSize(32, 32)
    
        def paintEvent(self, event):
            qp = QtGui.QPainter(self)
            size = min(self.width(), self.height())
            qp.drawPixmap(0, 0, self.image.scaled(
                size, size, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
    
    
    class Board(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            layout = QtWidgets.QGridLayout(self)
            layout.setSpacing(0)
            layout.setContentsMargins(0, 0, 0, 0)
            self.background = QtGui.QPixmap('chessboard.png')
    
            for i in range(8):
                layout.setRowStretch(i, 1)
                layout.setColumnStretch(i, 1)
    
            for col in range(8):
                layout.addWidget(Pawn(), 1, col)
    
        def minimumSizeHint(self):
            return QtCore.QSize(256, 256)
    
        def sizesHint(self):
            return QtCore.QSize(768, 768)
    
        def resizeEvent(self, event):
            size = min(self.width(), self.height())
            rect = QtCore.QRect(0, 0, size, size)
            rect.moveCenter(self.rect().center())
            self.layout().setGeometry(rect)
    
        def paintEvent(self, event):
            qp = QtGui.QPainter(self)
            rect = self.layout().geometry()
            qp.drawPixmap(rect, self.background.scaled(rect.size(), 
                QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
    
    
    class ChessGame(QtWidgets.QMainWindow):
        def __init__(self):
            super().__init__()
            central = QtWidgets.QWidget()
            self.setCentralWidget(central)
            layout = QtWidgets.QHBoxLayout(central)
            self.board = Board()
            layout.addWidget(self.board)
            self.table = QtWidgets.QTableWidget(1, 3)
            layout.addWidget(self.table)
    
    
    import sys
    app = QtWidgets.QApplication(sys.argv)
    game = ChessGame()
    game.show()
    sys.exit(app.exec_())
    

    Note that you should also consider using the Graphics View Framework, which provides much more control and features that can be very useful for this kind of interfaces. Be aware that it's as powerful as it's hard to get accustomed to, and it takes a long time to understand all of its aspects.