Készítsünk HANGMAN játékot! - 5. befejező rész

 Sokak szerint a Tk felület idejét múlt, vagyis nem "trendi". Halomra dícsérik viszont ugye a gtk és a Qt képességeit. Én ugyan nem egészen értek egyet, mert  az kétségtelen, hogy a Qt egy hihetetlenül gazdag grafikus keretrendszer, a Tk felülettel is kiváló alkalmazásokat alkothatunk, úgy vélem minden eszközt használjunk a megfelelő feladatra.A Tkinter előnye, hogy egyszerű, ezért egyszerűbb dolgokhoz jóval könnyebben tudunk a segítségével guit faragni, mint pl. a Qt-val. Az egyszerűsége viszont rögtön a hátrányára válik, amint egy bonyolult felületet akarunk vele megvalósítani, ráadásul nincs gui szerkesztője mint a gtk-s Glade vagy a Qt-s Qtdesigner.
Láthatóan hangman programunk nem egy fene bonyolult valami, de az érdekesség kedvéért elkészítettem Qt alapokon is. Jelen előnye számunkra mindössze annyi, hogy a Qt "vászon" a Tk megfelelőjével szemben képes a grafikai elemek élsimítására, így sokkal szebben jelennek meg az akasztott alak vonalai.



Első lépésként a jelenleg pythonhoz létező Qt kötések közül kell választanunk: PyQt vagy PySide. A PyQt a Riverbank Ltd. terméke, régóta létezik és megbízható kiforrott rendszer, ám van egy kis licenszelési bibi vele. Mindaddig, amig GPL-es progit alkotunk vele ingyenes, ám ahogy eladható szoftvert szeretnénk vele készíteni, borsos licenszdíjat kell fizetni. Ezzel szemben a PySide LGPL licenszelésű, így kereskedelmi termékekben is könnyedén használható. A PySide a Nokia munkatársainak az alkotása, és bár az 1-es verziószámot már átlépte, könnyedén futhatunk bugba (én is találkoztam buggal). Ennek ellenére én azt javaslom bátran használjátok, mert általános célokra és felületekre tökéletesen és hibamentesen működik, a Model-Nézet implementációknál vannak néhol belső hibácskák, amiket viszont ki lehet kerülni, ha tudja, ismeri már őket az ember.

Én a Hangmanhez a PySide-ot választottam.

A Qt-s Hangman felépítése a Tkintereshez nagyon hasonló, illetve törekedtem is rá, hogy ne legyenek igazán nagy különbségek, csak ott, ahol ez a kétfajta gui megköveteli.
Először elkészítettem a Qtdesignerrel a program ablakát:


(A felületfájlt a zip-ben megtalálod)

A felület lényegi elemei: egy gomb, egy QGraphicsView, négy label. A gomb az indításhoz, újrakezdéshez kell, a QGraphicsView adja "vásznunkat", a négy labelbe pedig irogatjuk a szókitalálás dolgait, és a programüzeneteket.

Mielőtt a részletes forráskódot megvizsgáljuk, szót kell ejtenem a QGraphicsView használatáról. Ez a widget tulajdonképpen csak egy nézet, egy "ablak" egy grafikai "jelenetre", egy QGraphicsScene elemre. A jelenet tartalmazza az elemeket, míg a nézet megmutatja őket. Egy jelenethez több nézet is tartozhat, így ugyanazt a "jelenetet" több szemszögből is megnézhetjük. Ez jól hangzik, de sajnos akkor is így kell eljárnunk, ha csak egy szimpla nézetet szeretnénk pár vonallal.

Miután elmentettem a felületet hangman.ui néven a következő paranccsal python fájlá konvertáltam:


pyside-piuc hangman.ui > ui_hangmanqt.py


Ezután nekiálltam a hangmanqt.py programnak:

#!/usr/bin/python
# -*- coding:utf-8 -*-

#########################
# HANGMAN program
# oktatási célra
# v2.0-
# Qt verzió
#########################

import sys
import os
import os.path
import random
import getopt
from PySide.QtCore import *
from PySide.QtGui import *
from ui_hangmanqt import *

# konfigurációs adatok
VERZIO="v2.0 Qt verzió"
SZOFAJL="hangman_szolista.txt"

def help():
    print _("""HANGMAN program oktatási célra

Használat:
    python hangmanqt.py [paraméterek]

    Paraméterek:
    -h, --help     : ez a leírás
    -v, --version  : a program verziószáma
    -f fajlnév,
    --file fájlnév : a betöltendő szófájl neve
    """)

class Szavak:
    def __init__(self):
        fajl=open(SZOFAJL,'r')
        self.lista=fajl.readlines()
        fajl.close()
        for i in range(len(self.lista)):
            self.lista[i]=self.lista[i][:-1]
    def valaszt(self):
        return self.lista[random.randint(0,len(self.lista)-1)]


Az importok közé bekerültek a PySide importjai, valamint az előzőekben konvertált felületfájlunk python változata. A help függvényünk és Szavak osztályunk megegyezik az előző verziókban használttal. Az Akasztofa osztályunkat és a Jatek osztály Qt-sítani kell. Erre hamarosan rátérünk, de előbb nézzük meg hogy néz ki a programtörzsünk (ez ugye az osztálydefiníciók után, a program végén helyezkedik el):

if __name__=="__main__":
    try:
        optlist, args = getopt.getopt(sys.argv[1:], "hvf:", ["help", "version", "file="])
    except getopt.GetoptError:
        help()
        sys.exit(2)
    for opt,param in optlist:
        if opt in ('-h','--help'):
            help()
            sys.exit(0)
        elif opt in ('-v', '--version'):
            print _("Hangman game %s") % VERZIO
            sys.exit(0)
        elif opt in ('-f','--file'):
            SZOFAJL=param

    app=QApplication(sys.argv)
    j=Jatek()
    j.show()
    app.exec_()

Ez is az előző verziókhoz hasonló, az eltérés Qt specifikus. Minden Qt programban kell készítenünk egy QApplication objektumot, ami a Qt könyvtárral fogja tartani a kapcsolatot. Ezután példányosítjuk a Jatek osztályt, megkérjük hogy jelenítse meg magát a show() metódussal, majd indítjuk a Qt feldolgozót az app._exec() paranccsal.

Az Akasztofa osztályunk a QGraphicsScene osztályon alapul, vagyis egy jelenet, aminek lehetnek elemei:

class Akasztofa(QGraphicsScene):
    def __init__(self,parent):
        super(Akasztofa,self).__init__()
        self.elemek = [
                        ['line',150,25,150,50,'#000000'],
                        ['circle',125,50,50,50,'#ff0000'],
                        ['line',150,100,150,160,'#613E20'],
                        ['line',150,115,110,140,'#003700'],
                        ['line',150,115,190,140,'#003700'],
                        ['line',150,160,125,220,'#0000ff'],
                        ['line',150,160,175,220,'#0000ff'],
                        ]
        self.alap = [
                        ['line',150,25,275,25,"#4E1B01"],
                        ['line',275,25,275,350,"#4E1B01"],
                        ['line',285,350,50,350,"#4E1B01"],
                        ['line',225,27,273,75,"#4E1B01"],
                    ]
        self.level=0
        self.alaprajz()

    def alaprajz(self):
        for vonal in self.alap:
            pen=QPen(QColor(vonal[5]),8)
            self.addLine(vonal[1],vonal[2],vonal[3],vonal[4],pen)

    def rajzol(self,level):
        pen=QPen(QColor(self.elemek[level][5]),4,Qt.SolidLine,Qt.RoundCap)
        e=self.elemek[level]
        if e[0]=='line':
            self.addLine(e[1],e[2],e[3],e[4],pen)
        elif e[0]=='circle':
            self.addEllipse(e[1],e[2],e[3],e[4],pen)

Felépítése hasonlít a Tkinteres változathoz. A jelenetet használat előtt inicializálnunk kell, ezért a konstruktorból meg kell hívnunk a szülő konstruktorát is. A self.elemek lista az egyes hibaszintekhez tartozó, kirajzolandó elemek adatait tartalmazza, a self.alap lista pedig az akasztófa vázát alkotó vonalak adatait. Az alaprajz metódusunk mindössze az akasztófaváz elemeit rajzolja ki. A jelenetre rajzolandó elemek kirajzolásához definiálnunk kell egy rajzoló "tollat", ehhez a QPen osztályt kell használnunk. A definiált toll segítségével vonalakat adunk a jelenethez. Hasonlóképpen működik a rajzol() metódusunk is, csak ebbe beleépítettük a kör és vonal rajzolás lehetőségét is.

Ezek után már csak a Jatek osztályunk maradt hátra:

class Jatek(QMainWindow,Ui_MainWindow):
    def __init__(self, parent=None):
        super(Jatek, self).__init__(parent)
        self.setupUi(self)
        self.szavak=Szavak()
        self.akasztofa=Akasztofa(self.horizontalLayout)
        self.vaszon.setScene(self.akasztofa)
        self.vaszon.setRenderHint(QPainter.Antialiasing)
        self.StartButton.clicked.connect(self.start)
        self.vege=True

    def start(self):
        self.segedszoveg.setText(u"Gondoltam egy szóra, próbáld kitalálni!")
        self.StartButton.setText(u"Új játék")
        self.StartButton.clicked.connect(self.ujrakezdes)
        self.vege=False
        self.nyert=False
        self.szo=unicode(self.szavak.valaszt(),'utf-8')
        self.eredmeny=[0]*len(self.szo)
        self.hiba=0
        self.maxhiba=7
        self.tippek_=[]
        self.szokiir()
        self.tippkiir()

    def ujrakezdes(self):
        self.segedszoveg.setText(u"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">p, li { white-space: pre-wrap; }</style></head><body style=\" font-family:\'Droid Sans\'; font-size:10pt; font-weight:400; font-style:normal;\"><p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">A kezdéshez kattints a</p><p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-weight:600;\">"Start"</span></p><p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">gombra.</p></body></html>")
        self.StartButton.setText(u"Start")
        self.tippek.setText("")
        self.nyertszoveg.setText("")
        self.szokiiro.setText("")
        self.StartButton.clicked.connect(self.start)
        for elem in self.akasztofa.items():
            self.akasztofa.removeItem(elem)
        self.akasztofa.alaprajz()

    def szokiir(self):
        szo=" "
        for c in range(len(self.szo)):
            if self.eredmeny[c]==0:
                szo+="_ "
            else:
                szo+=self.szo[c]+" "
        self.szokiiro.setText(u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"><html><head><meta name="qrichtext" content="1" /><style type="text/css">p, li { white-space: pre-wrap; }</style></head><body><p style="font-size:14pt;color:#009800;">' + szo + u'</p></body></html>')

    def tippkiir(self):
        self.tippek.setText("Eddigi tippjeid: %s" % ", ".join(self.tippek_))

    def keyPressEvent(self,event):
        if self.vege==True:
            return
        self.beker(event)

    def beker(self,event):
        tipp=event.text() 
        if tipp in self.szo:
            for c in range(len(self.szo)):
                if tipp==self.szo[c]:
                    self.eredmeny[c]=1
            if self.eredmeny.count(1)==len(self.szo):
                self.vege=self.nyert=True
        else:
            self.hiba+=1
            self.akasztofa.rajzol(self.hiba-1)
            if self.hiba>=self.maxhiba:
                self.vege=True
        self.tippek_.append(tipp)
        self.szokiir()
        self.tippkiir()
        if self.vege:
            if self.nyert:
                self.nyertszoveg.setText(u"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\"><html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">p, li { white-space: pre-wrap; }</style></head><body style=\" font-family:'Droid Sans'; font-size:10pt; font-weight:400; font-style:normal;\"><p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-size:14pt; color:#f26f10;\">Gratulálok! Kitaláltad!</span></p></body></html>")
            else:
                self.nyertszoveg.setText(u"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\"><html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">p, li { white-space: pre-wrap; }</style></head><body style=\" font-family:'Droid Sans'; font-size:10pt; font-weight:400; font-style:normal;\"><p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-size:14pt; color:#f26f10;\">Vesztettél!<br>Érezd magad felakasztva :D<br>A megfejtés: "+self.szo+"</span></p></body></html>")

A Jatek osztályunk a Qt-s QMainWindow és a felületfájlból (ui_hangmanqt.py) származó Ui_MainWindow osztályokon alapul. A konstruktorban (__init__) nem feledkezünk el meghívni a szülő konstruktorát a super függvény segítségével, inicializáljuk a felületünket -> self.setupUi(self), létrehozzuk a Szavak és Akasztofa osztályok egy-egy példányát objektumváltozóként, és csatoljuk az akasztófánkat (ami egy QGraphicsScene!) a vászonhoz (ami egy QGraphicsView!). Csatoljuk a Start gomb kattintás szignáljához a self.start() metódust, és a self.vege változóban jelezzük, hogy nincs folyamatban játék.
Induláskor a start metódusunk lecseréli a gombot "Újrakezdés"-re, választ egy kitalálandó szót, és beállítja a játékváltozókat. A játék működése szempontjából a leglényegesebb metódus a keyPressEvent, ami egy felüldefiniált metódusa a QMainWindow osztálynak. Minden gombnyomás esetén ez a metódus meghívásra kerül. Megnézi, hogy van-e folyamatban játék, és ha igen, meghívja a beker() metódust. Ha nincs folyamatban játék, egyszerűen visszatér.
A beker(), szokiir() és tippkiir() metódusok működése gyakorlatilag megegyezik a Tkinteres megfelelőjükkel, ezért nagyon nem taglalom őket, de azt vegyük észre, hogy a Qt a címkékben is RichText képességekkel rendelkezik, így azok html-szerűen formázhatók.

A teljes program letölthető egy zipben innen.

Várom a hozzászólásokat! ;)

Nincsenek megjegyzések:

Megjegyzés küldése