Search code examples
pythonpyqt5drawingqpainter

using QPainter to draw a hangman


I am using python and am new to PyQt (and a bit new to python as well). I want to create a basic hangman game, and I have all of the controls figured out except I don't know how to draw the head, body, etc when a wrong letter is pressed. I tried using QPainter somehow but it doesn't work and upon research, some people say I have to use QPix map which I haven't heard of before so I am a bit confused. Here is the main bit of my code (I skipped me configuring the buttons for letters):

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QInputDialog, QWidget
from PyQt5.QtGui import QPainter, QBrush, QPen
from PyQt5.QtCore import Qt
import re

allowed = re.compile('[a-zA-Z ]')


class Ui_MainWindow(object):
    answer = [] #list of correct charcters they have inputted
    required = '' #string containing the solution
    indicies = {} #indicies mapping the index of each correct character in solution to the actual character
    wrong_count = 0

    def setupUi(self, MainWindow):
        ### sets up all the buttons for letters of the alphabet, lines, etc. I have this figured out.
        self.newgame = QtWidgets.QPushButton(self.centralwidget)
        self.newgame.setGeometry(QtCore.QRect(800, 290, 231, 51))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.newgame.setFont(font)
        self.newgame.setObjectName("newgame")
        self.newgame.setText('New game')
        self.newgame.clicked.connect(self.restart)
        self.newgame.hide()

        self.start = QtWidgets.QPushButton(self.centralwidget)
        self.start.setGeometry(QtCore.QRect(480, 380, 231, 51))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.start.setFont(font)
        self.start.setObjectName("start")
        self.start.setText("Begin")
        self.start.clicked.connect(self.get_name)

        self.answerbox = QtWidgets.QLabel(self.centralwidget)
        self.answerbox.setGeometry(QtCore.QRect(340, 480, 601, 91))
        font = QtGui.QFont()
        font.setPointSize(28)
        self.answerbox.setFont(font)
        self.answerbox.setText("")
        self.answerbox.setObjectName("answerbox")

        self.win = QtWidgets.QLabel(self.centralwidget)
        self.win.setGeometry(QtCore.QRect(340, 400, 601, 91))
        font = QtGui.QFont()
        font.setPointSize(28)
        self.win.setFont(font)
        self.win.setText("")
        self.win.setObjectName("win")

        self.wrongtext = QtWidgets.QLabel(self.centralwidget)
        self.wrongtext.setGeometry(QtCore.QRect(340, 500, 601, 91))
        font = QtGui.QFont()
        font.setPointSize(28)
        self.wrongtext.setFont(font)
        self.wrongtext.setText("Sorry, please type a valid string, without any special characters")
        self.wrongtext.adjustSize()
        self.wrongtext.setObjectName("wrongtext")
        self.wrongtext.hide()

        self.lost = QtWidgets.QLabel(self.centralwidget)
        self.lost.setGeometry(QtCore.QRect(340, 500, 601, 91))
        font = QtGui.QFont()
        font.setPointSize(28)
        self.lost.setFont(font)
        self.lost.setText("Sorry, you lost")
        self.lost.adjustSize()
        self.lost.setObjectName("lost")
        self.lost.hide()

    def get_name(self):
        text = App().text ##prompts user input for word
        if text == '':
            return
        elif allowed.match(text):
            Ui_MainWindow.required = text
            self.answerbox.setText('_ '*len(text))
            self.wrongtext.hide()
            self.check_space(text)
            self.answerbox.adjustSize()
            self.start.hide()
        else:
            self.wrongtext.show()


    def check_space(self, text):
        list1 = list(text)
        for ch in list1:
            if ch == ' ':
                self.revealer(ch)
                Ui_MainWindow.answer.append(ch)

    def clicked(self, ans): ### connected to letter buttons
        if ans.lower() in Ui_MainWindow.required:
            for _ in range(Ui_MainWindow.required.count(ans.lower())):
                Ui_MainWindow.answer.append(ans.lower())
            self.revealer(ans.lower())
            self.checker()
        else:
            Ui_MainWindow.wrong_count += 1
            self.paintEvent()

    def paintEvent(self): ##### this is what doesn't work but I want it to be something like this
        if Ui_MainWindow.wrong_count == 1: ### face
            painter = QPainter(self)
            painter.setPen(QPen(Qt.black, 10, Qt.SolidLine))
            painter.drawEllipse(250, 320, 20, 20)
            return
        if Ui_MainWindow.wrong_count == 2: ### body
            painter = QPainter(self)
            painter.begin(self)
            painter.setRenderHint(QPainter.Antialiasing)
            painter.setPen(QtCore.Qt.black)
            painter.setBrush(QtCore.Qt.black)
            painter.drawLine(250, 340, 250, 360)
            return
        if Ui_MainWindow.wrong_count == 3: ###legs
            painter = QPainter(self)
            painter.begin(self)
            painter.setRenderHint(QPainter.Antialiasing)
            painter.setPen(QtCore.Qt.black)
            painter.setBrush(QtCore.Qt.black)
            painter.drawLine(250, 360, 260, 370)
            return
        if Ui_MainWindow.wrong_count == 4:
            painter = QPainter(self)
            painter.begin(self)
            painter.setRenderHint(QPainter.Antialiasing)
            painter.setPen(QtCore.Qt.black)
            painter.setBrush(QtCore.Qt.black)
            painter.drawLine(250, 360, 240, 370)
            return
        if Ui_MainWindow.wrong_count == 5: ####arms
            painter = QPainter(self)
            painter.begin(self)
            painter.setRenderHint(QPainter.Antialiasing)
            painter.setPen(QtCore.Qt.black)
            painter.setBrush(QtCore.Qt.black)
            painter.drawLine(250, 350, 260, 340)
            return
        if Ui_MainWindow.wrong_count == 6:
            painter = QPainter(self)
            painter.begin(self)
            painter.setRenderHint(QPainter.Antialiasing)
            painter.setPen(QtCore.Qt.black)
            painter.setBrush(QtCore.Qt.black)
            painter.drawLine(250, 350, 240, 340)
            self.lost.show()
            self.newgame.show()


    def revealer(self, ans):
        index = Ui_MainWindow.required.index(ans) #find the index of a character from answer list in required list
        Ui_MainWindow.indicies[index*2] = ans #assigns index to the character, to be used for displayed
        try:
            while True: ##checks if clicked character appears multiple times in the answer string and appends all of them
                index = Ui_MainWindow.required.index(ans, index+1)
                Ui_MainWindow.indicies[index*2] = ans
        except:
            pass
        displayed = '_ '*len(Ui_MainWindow.required)
        temp_list = list(displayed)
        for index, ch in Ui_MainWindow.indicies.items():
            temp_list[index] = ch
        str2 = ''
        displayed = str2.join(temp_list)
        self.answerbox.setText(displayed)

    def checker(self):
        if sorted(Ui_MainWindow.answer) == sorted(list(Ui_MainWindow.required)):
            self.win.setText("You won!")
            self.newgame.show()
    def restart(self):
        Ui_MainWindow.answer = []
        Ui_MainWindow.required = ''
        Ui_MainWindow.indicies = {}
        Ui_MainWindow.wrong_count = 1
        self.win.setText("")
        self.answerbox.setText("")
        self.newgame.hide()
        self.lost.hide()
        self.start.show()

class App(QWidget):

    def __init__(self):
        super().__init__()
        self.title = 'PyQt5 input dialogs - pythonspot.com'
        self.initUI()

    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(380, 280, 640, 480)
        self.get_name()

    def get_name(self):
        text, ok = QInputDialog.getText(self, 'Text Input Dialog', 'Enter your word:')
        self.text = text
        if text and ok:
            return str(text)

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

Sorry it's quite long. Basically when I run it everything works the way I want it, except if I press the wrong letter, nothing happens. It doesn't throw an error or anything. I tried using just QPainter separately to see how it works, and I made it draw a circle, line, etc, but not one by one the way I want. Thanks for reading and I'd appreciate any answers. Sorry if my layout for my question was bad, I only started using this website yesterday, so am not familiar with how things work just yet.


Solution

  • paintEvent should be part of your QMainWindow instance, not of the object you use for setting up the ui. In fact, most of the methods in Ui_MainWindow really belong to your MainWindow instance, so probably the easiest way to fix this is to just make Ui_MainWindow a subclass of QMainWindow, i.e.

    class Ui_MainWindow(QtWidgets.QMainWindow):
        # You could consider making these instance variables instead of class variables
        answer = [] #list of correct charcters they have inputted
        required = '' #string containing the solution
        indicies = {} #indicies mapping the index of each correct character in solution to the actual character
        wrong_count = 0
    
        def __init__(self):
            super().__init__()
            self.centralwidget = QWidget(self)
            self.setCentralWidget(self.centralwidget)
    
            self.setupUi(self)
    
        def paintEvent(self, event):    # <-- paintEvent receives the paint event as an input parameter
            ....
    
        # all other methods as before
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication([])
        MainWindow = Ui_MainWindow()
        MainWindow.show()
        MainWindow.resize(800,600)
        app.exec()
    

    This will at least call the paintEvent, but there are still a number of issues with the implementation of paintEvent. For one, painEvent is called automatically every time the window needs redrawing. You shouldn't try to call this function yourself. If you think your widget needs redrawing you could use update instead.

    Also, paintEvent is meant just for painting a widget and nothing else. This means that everything not-paint related like showing or hiding widgets should be moved elsewhere. In your case, the lines

    self.lost.show()
    self.newgame.show()
    

    should be removed from paintEvent and moved to for example clicked instead, e.g.

    def clicked(self, ans): ### connected to letter buttons
        if ans.lower() in Ui_MainWindow.required:
            for _ in range(Ui_MainWindow.required.count(ans.lower())):
                Ui_MainWindow.answer.append(ans.lower())
            self.revealer(ans.lower())
            self.checker()
        else:
            Ui_MainWindow.wrong_count += 1
            if self.wrong_count >= 6:
                self.lost.show()
                self.newgame.show()
    

    Finally, since you only check for equality of wrong_count in paintEvent, at most one limb will be drawn at any time. This can be fixed by checking if wrong_count is larger than a certain value instead of equal to, e.g.

        def paintEvent(self, event): 
            painter = QPainter(self)
            painter.begin(self)
            if Ui_MainWindow.wrong_count >= 1: ### face
                painter.setPen(QPen(Qt.black, 10, Qt.SolidLine))
                painter.drawEllipse(250, 320, 20, 20)
            painter.setRenderHint(QPainter.Antialiasing)
            painter.setPen(QtCore.Qt.black)
            painter.setBrush(QtCore.Qt.black)
            if Ui_MainWindow.wrong_count >= 2: ### body
                painter.drawLine(250, 340, 250, 360)
            if Ui_MainWindow.wrong_count >= 3: ###legs
                painter.drawLine(250, 360, 260, 370)
            if Ui_MainWindow.wrong_count >= 4:
                painter.drawLine(250, 360, 240, 370)
            if Ui_MainWindow.wrong_count >= 5: ####arms
                painter.drawLine(250, 350, 260, 340)
            if Ui_MainWindow.wrong_count >= 6:
                painter.drawLine(250, 350, 240, 340)
            painter.end()