Search code examples
pythonpyqt5qpolygon

Texturing a QPolygon element


I'm currently working on a hex-based 2D board game using Python. I'm not using PyGame, I'm doing this the harder way in order to practise. The graphics are currently created using PyQt5. The hexagon tiles are created using QPolygonF and QGraphicsPolygonItem, and images from image files are added using QPixmap and QGraphicsPixmapItem. "Moving the camera" is simply done by adjusting the origin point from which all the placement calculations stem from.

The thing is, I would like to have some texture to the tiles from ready image files. This way, the board can look prettier than just solid colors. I could draw ready hexagon image files and place them, but that looks good only if the image is already a hexagon, and the image is carefully scaled and placed. If I later change my mind, for example I want to change from hexagons to squares or perspectived hexagons or other shapes, all that work has gone to waste. For this reason, setting a texture to the shape would be better - it would be more generic.

I discovered that one could set texture to the QBrush with setTexture(). At first this sounded like the perfect solution, except that no, it's not. The texture is, lacking a better term, global for some reason.

Also, the texture doesn't "stick" to the tiles, as "camera" (actually tiles) are moved

I first tried to fix this by testing what the scroll() for the pixmap does. I have no idea what in the nine circles of hell is going on in here. Also, it was clearly heavy for the computer, the frame rate of the program tanked heavily.

I need advice, for I don't know what to do and professor Google wasn't of much help.

The absolute best solution would be one where the addition of a couple of lines of code makes the image cropped into the area described by the QPolygon. I can also work with a global texture, as long as I can move it alongside the tiles, to "stick" the texture. Since I'm fairly early into the project, if changing libraries or something else like that is deemed necessary, it too is still within a reasonable workload.

Here's the relevant code:

import sys
from PyQt5.QtWidgets import QApplication
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.Qt import QPixmap
from PyQt5.QtGui import QBrush
import math

def main():
    global app
    app = QApplication(sys.argv)
    
    window = Window()
    
    sys.exit(app.exec_())


class Window(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setCentralWidget(QtWidgets.QWidget())
        self.horizontal = QtWidgets.QHBoxLayout()
        self.centralWidget().setLayout(self.horizontal)
        
        self.x0 = 30
        self.y0 = 30
        self.scale = 1.0
        self.board = Board(8,8)
    
        self.scene = QtWidgets.QGraphicsScene()
        self.initWindow()
        self.update()
        
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.update)
        self.timer.start(int(1000 / 60)) # Milliseconds
    
    
    "Initialize the window"
    def initWindow(self):
        self.setGeometry(300,200,800,800)
        self.show()

        self.scene.setSceneRect(0, 0, 700, 700)

        self.view = QtWidgets.QGraphicsView(self.scene, self)
        self.view.adjustSize()
        self.view.show()
        self.horizontal.addWidget(self.view)
    
    
    def update(self):
        self.scene.clear() # The window is wiped clean before adding updated graphics
    
        r = self.scale * 50
        h = r * math.sin(math.pi/3)
        midpointX = 0
        midpointY = 0
    
        for x in range(self.board.width):
            for y in range(self.board.height):                
                qt_polygon = QtGui.QPolygonF()
                
                if y % 2 == 0:
                    midpointX = ( self.x0 + h*2*x )
                else:
                    midpointX = ( self.x0 + h + h*2*x )
                midpointY = ( self.y0 + 1.5*r*y )
                
                qt_polygon.append(QtCore.QPointF( midpointX, midpointY - r ))
                qt_polygon.append(QtCore.QPointF( midpointX + h, midpointY - 0.5*r ))
                qt_polygon.append(QtCore.QPointF( midpointX + h, midpointY + 0.5*r ))
                qt_polygon.append(QtCore.QPointF( midpointX, midpointY + r ))
                qt_polygon.append(QtCore.QPointF( midpointX - h, midpointY + 0.5*r ))
                qt_polygon.append(QtCore.QPointF( midpointX - h, midpointY - 0.5*r ))
                
                polygon = QtWidgets.QGraphicsPolygonItem(qt_polygon)
                
                path = <insert path>
                pixmap = QPixmap(path)
                
                brush = QBrush()
                brush.setTexture(pixmap)
                polygon.setBrush(brush)
                
                self.scene.addItem(polygon)
    
    "Keyboard commands"
    def keyPressEvent(self, event):
        if event.key() == QtCore.Qt.Key_A:
            self.x0 -= 5
        if event.key() == QtCore.Qt.Key_D:
            self.x0 += 5
        if event.key() == QtCore.Qt.Key_W:
            self.y0 -= 5
        if event.key() == QtCore.Qt.Key_S:
            self.y0 += 5
    
class Tile():
    def __init__(self):
        ""

class Board():
    def __init__(self, width, height):
        self.height = height
        self.width = width
        
        self.tiles = [None] * self.width
        for x in range(self.width):
            self.tiles[x] = [None] * self.height
            for y in range(self.height):
                self.tiles[x][y] = Tile()


main()

Solution

  • The brush is always relative to the graphics item position.

    Since a new graphics item is always positioned at (0, 0) coordinates, you will always get the same brush, because the points of the polygons you are creating are always relative to 0.

    What you should do, instead, is to always create the same polygon, and then move it at the appropriate coordinates.

        def update(self):
            self.scene.clear()
        
            r = self.scale * 50
            h = r * math.sin(math.pi/3)
        
            hexagon = QtGui.QPolygonF([
                QtCore.QPointF(h, 0), 
                QtCore.QPointF(h * 2, 25), 
                QtCore.QPointF(h * 2, 75), 
                QtCore.QPointF(h, 100), 
                QtCore.QPointF(0, 75), 
                QtCore.QPointF(0, 25), 
            ])
            for x in range(self.board.width):
                for y in range(self.board.height):                
                    midpointX = 2 * h * x
                    if y & 1:
                        midpointX += h
                    midpointY = 1.5 * r * y
    
                    polygon = QtWidgets.QGraphicsPolygonItem(hexagon)
                    polygon.setPos(midpointX, midpointY)
    
                    path = '/tmp/spacebg.jpg'
                    pixmap = QPixmap(path)
    
                    brush = QBrush()
                    brush.setTexture(pixmap)
                    polygon.setBrush(brush)
                    
                    self.scene.addItem(polygon)
    

    An extremely important note: while the graphics view framework is pretty fast, it's not usually very effective to continuously clear the scene and recreate it again; instead, find ways to reuse existing items and eventually update them.

    If you just want to move items when a key is pressed, constantly rebuilding the whole scene is pointless and completely unnecessary, other than a total waste of resources. Instead, create a function that actually moves those items, then call it only when the proper key have pressed. Also note that if you actually just want to scroll the view of the scene, you should use setSceneRect() of the view, or eventually use the scroll bars (supposing that the scene is big enough).

    Finally, be aware that update() is an existing function of any QWidget, which is not only quite important, but also a slot, so you should not overwrite it unless you really know what you're doing it.