Seite 1 von 2

MineSweeper: wie spreche ich Buttons im Grid an?

Verfasst: Freitag 28. Dezember 2007, 18:33
von Nikolas
Hallo

ich bin gerade dabei, meinen Wortschatz um Python zu werweitern und bin deswegen dabei, mir ein kleines MineSweeper zu bauen.
Jetzt stecke ich gerade bei der GUI. Hierfür habe ich mir eine Klasse geschrieben, die meinem Fenster die passende Anzahl an Buttons hinzufügt. Zur Positionierung benutze ich den grid-Befehl.
Jetzt will ich nachher aber auch alle Button ansprechen können, so dass z.B. alle Buttons anzeigen, ob sie Minen sind, oder nicht.
Bei Delphi z.B. kann ich da einfach einen Namen festlegen und den Button dann über diesen Namen identifizieren.
Nur habe ich keine passende Eigenschaft gefunden. Alternativ war mein Ansatz, eine 2D-Liste an Button anzulegen, wobei ich aber davon ausgehe, dass es da eine schönere Lösung gibt.

Nikolas

Re: MineSweeper: wie spreche ich Buttons im Grid an?

Verfasst: Freitag 28. Dezember 2007, 18:53
von schlangenbeschwörer
Hi!
Nikolas hat geschrieben: Bei Delphi z.B. kann ich da einfach einen Namen festlegen und den Button dann über diesen Namen identifizieren. Nur habe ich keine passende Eigenschaft gefunden.
Und warum glaubst du, das das in Python nicht geht? Wie hast du das denn bis jetzt immer gemacht? Was für eine Eigenschaft suchst du?
Zeig mal deinen Code!
Nikolas hat geschrieben: Alternativ war mein Ansatz, eine 2D-Liste an Button anzulegen, wobei ich aber davon ausgehe, dass es da eine schönere Lösung gibt.
Kommt drauf an, was du meinst und wie der rest deines Programms aussieht.

Prinzipiell solltest du schon in einer schleife Buttons erzeugen und diese nicht an Namen binden sondern in eine Liste packen. Wie du die Buttons ordnest, ob über ne 2d-Liste oder eine eigene erweiterte Buttonklasse oder sonst wie hängt, wie gesagt, vom rest ab.

Verfasst: Freitag 28. Dezember 2007, 19:09
von Nikolas
Ich habe ja nicht gesagt, dass ich glaube dass es nicht geht, ich weiss nur nicht wie :)

Code: Alles auswählen

class addButtons:
    def __init__(self,master,x,y):

        self.newGame = Button(master,text="Neues Spiel")
        self.newGame.pack(side = TOP)

        self.entry_x = Entry(master)
        self.entry_x.pack(side = ???
        
        frame = Frame(master)
        frame.pack() # TODO: vielleicht anpassen

        for j in range(y):
            for i in range(x):
                self.button = Button(frame,text=i) # (1) 
                self.button.grid(row=j, column = i)
              #  self.button.command = printName( self.button["name"] )

root = Tk()
a = addButtons(root,2,5)
root.mainloop()

bei (1) wird der gleiche Name in jedem Durchlauf benutzt, was mir etwas ungesund vorkommt.

Wenn ich die Button dann in eine Liste schreibe, die dann eine Eigenschaft der Klasse ist, kann ich die dann einfach über a.ButtonListe ansprechen?

Verfasst: Freitag 28. Dezember 2007, 19:36
von pyStyler
Hallo,

ich denke, du bist auf dem richtigen weg.

Code: Alles auswählen

def return_XY_button(xb, yb):
    print xb, yb
    
    
class addButtons: 
    def __init__(self, master, x, y): 

        self.newGame = Button(master, text="Neues Spiel") 
        self.newGame.pack(side = TOP) 

        self.entry_x = Entry(master) 
        self.entry_x.pack(fill=X, padx=2, pady=2)
        frame = Frame(master) 
        
        # packen mit ohne side angaben,
        #wird immer auf der obersten ebene des frames gepackt!
        
        frame.pack() # TODO: vielleicht anpassen 

        for j in range(1, y): 
            for i in range(1,x): 
                self.button=Button(frame,
                                    width=2, height=2,
                                    text=i, 
                                    command=lambda x=i, y=j: return_XY_button(y, x) )# (1) 
                            
                self.button.grid(row=j, column = i) 
                
              #  self.button.command = printName( self.button["name"] ) 

root = Tk() 
a = addButtons(root, 10+1, 10+1) 
root.mainloop()
Gruss
pyStyler

Verfasst: Freitag 28. Dezember 2007, 19:39
von schlangenbeschwörer
1) Du solltest die Klasse umbenennen. "addButtons" klingt nach einer Methode, entspricht beiden Fällen aber nicht PEP. Hast du OOP verstanden bzw. weißt du, was du da tust? ZZ macht eine Klasse bei dir nämlich auch keinen Sinn.

2) Was soll das in l. 9 sein? Falls du es nicht weißt, side muss right, left, top oder button sein, das bekommst du sogar gesagt, wenn du da was falsches hinschreibst.

3) Such mal im Forum nach dem Grund, *-Imports zu vermeiden. (Auch wenn er fehlt, nutzt du ihn ja.)

4)hier mal eine minimalistische und daher eher schlechte Möglichkeit:

Code: Alles auswählen

import Tkinter as tk
import random

class BeispielFeld(tk.Frame):
    def __init__(self):
        tk.Frame.__init__(self)
        self.feld = [[random.choice([0,1]) for x in xrange(10)] for y in xrange(10)]
        print self.feld
        for y in xrange(10):
            for x in xrange(10):
                b=tk.Button(text="?", width=4, height=2)
                b["command"] = lambda x=x, y=y, b=b: b.config(text=self.test(x, y))
                b.grid(row=y, column=x)

    def test(self, x, y):
        if self.feld[y][x] == 1:
            return "X"
        else:
            return " "

if __name__ == "__main__":
    BeispielFeld()
    tk.mainloop()
Jetzt bist du hoffentlich in der Lage, es besser zu machen! :wink:

Verfasst: Freitag 28. Dezember 2007, 20:23
von Nikolas
Danke für eure Hinweise.

2) Ich wollte den Button und zwei Entries ganz nach oben legen und habe nach einer Möglichkeit wie z.B. side = (TOP,LEFT) gesucht, um ein Objekt möglichs weit im Nordwesten zu platzieren. (Wobei ich dafür wohl einfach ein Grid nehmen kann...)

3) Werde ich mal machen. Ich habe die grundstruktur (imports und Klassenschreibweise) aus einem Tutorial übernommen, mir kam es auch etwas komisch vor, aber gleich von einer vorgegebenen Methode abzuweichen wollte ich nicht beim ersten Versuch.

4) Mit einem lambda habe ich in Python noch nicht gearbeitet. Entspricht es hier einem lambda-Kalkül wie in Scheme?

Ich wollte Logik und Anzeige möglichst klar trennen, so dass ich z.B. später auch mal eine Canvas zur Anzeige nutzen kann.
Deswegen suche ich nach einer Möglichkeit dieser Beispiel-feld Klasse von aussen einen Befehl zu geben, der dann z.B. alle Felder deaktiviert, weil man gerade das Spiel verloren hat. Und dafür bräuchte ich eben eine Schleife über die angelegten Buttons.
Was ich also versuchen werde, ist eine 2dimensionale Liste an Buttons anzulegen, die dann eine Eigenschaft (oder wie heisst das in Python?) der Klasse ist.
Ist das erfolgversprechend oder soll ich meine Zeit eher nicht damit verschwenden?

Bei deinem Code verstehe ich noch ein paar Sachen nicht: Die Schreibweise vom ersten import ist mir neu, und so ganz verstehe ich die untersten Zeilen auch nicht. Könntest du vielleicht noch mal kurz beschreiben, was das tk genau ist?

Verfasst: Freitag 28. Dezember 2007, 21:23
von schlangenbeschwörer
Also wenn du "from Tkinter import *" schreibst, holst du dir alle Sachen des Tkintermoduls in den eigenen Namensraum. Dabei kannst du Sachen überschreiben, es gibt aber noch andere Gründe. Besser ist, das Modul mit "import Tkinter" zu importieren. Dann musst du aber jedesmal ein "Tkinter." vor alle Sachen schreiben wzB "Tkinter.Button(...)". Das ist recht umständlich, daher steht in Tutorial oft der *-Import. Durch das "... as tk" kannst du das Modul nun tk nennen, sodass du nurnoch "tk.Button" schreiben musst. Du kannst stattdessen auch "import Tkinter as meinGanz_Personal_nameDenSonstKeiner__BenutzT__HAHA" schreiben, doch das würde den Sinn verfehlen. :D
Die untersten Zeilen sind dazu da um sicherzustellen, das der Code im if-Block nur ausgeführt wird, wenn das modul nicht importiert wurde. So kannst du es so nutzen und zum testen ausführen oder importieren, ohne dass dein Test ausgeführt wid.
Mit "lambda" kannst du annonyme Funktionen schreiben. Du musst dem Button eine Funktion/Methode übergeben, die beim Klick ausgeführt wird. Wenn du aber der Funktion noch Argumente mitgeben willst, musst du das in ein Lambdakonstrukt packen.
Daten und Darstellung zu Trennen ist eine sehr gute Überlegung und sollte normal auch gemacht werden. Bei meinem Beispiel wollte ich einfach eine Minimallösung zeigen. Da noch zu trennen wäre dann doch zu viel gewesen.
Deine sonstigen Überlegungen sind auch gut. Mache am Besten eine Data-Klasse mit einer 2d-liste (in etwa so wie mein BeispielFeld.feld) und einige nützliche Methoden wie befüllen, erneuern, Stelle x/y prüfen, ...
Dann Schreibst du die Darstellung in eine weitere Klasse die entweder von der ersten erbt oder eine Instanz dieser erzeugt. Da dann eine ähnliche 2d-liste, nur mit Buttons und der Rest ist dann auch schnell getippt. :wink:

Verfasst: Freitag 28. Dezember 2007, 21:42
von Nikolas
Danke. Das mit dem Import wird schon deutlicher :)
nur das

Code: Alles auswählen

if __name__ == "__main__":
    BeispielFeld()
    tk.mainloop()
verschließt sich mir noch. Was ist dieses __name__? Und dann das tk.mainloop(). Das tk steht doch jetzt für das gesamte Tkinter-Modul, ist also eigentlich nur ein Bezeichner. Warum hat das Teil dann eine mainloop? Diese Loop ist doch so etwas wie ein Dämon, der auf Eingaben vom Benutzer reagiert, oder?

Ich habe mir jetzt eine ähnliche Klasse geschrieben:

Code: Alles auswählen

import Tkinter as tk

class MineApp(tk.Frame):

    def __init__(self):
        self.x = -1
        self.y = -1
        tk.Frame.__init__(self)
        self.grid()
        

    def setDimensions(self,x,y):
        self.x = x
        self.y = y
        self.createWidgets()

    def returnID(self,x,y):
        print [x,y]
        
    def createWidgets(self):
        # print self.x
        # self.B = tk.Button(self)#command = lambda x=x, y=y : self.returnID(x,x))

        if self.y< 1 or self.x<1:
            print "createWidgets: x oder y kleiner 1!"
            return 0
    
        L = []
        for i in range( self.y):
                b = [] 
                for j in range( self.x ):
                    but = tk.Button(self,
                                    command = lambda x=j,y=i :
                                    self.returnID(x,y))
                    but.grid(row=i, column = j)
                    b.append( but ) 
                    L.append(b)    

    
app = MineApp()
app.setDimensions(3,5)

app.mainloop()
Dieses Modul werde ich dann in mein eigentliches Programm einbinden und über die app.setDimensions(10,20) dann steuern.

Ist das jetzt halbwegs sauber?

Verfasst: Freitag 28. Dezember 2007, 23:29
von BlackJack
`__name__` ist an in jedem Modul an den Namen des Moduls gebunden, ausser wenn man das Modul nicht ``import``\iert sondern als Programm ausführt, dann ist der Name an die Zeichenkette '__main__' gebunden.

Richtig, `tk` ist an das `Tkinter`-Modul gebunden. Und das enthält eine Funktion mit dem Namen `mainloop`. Das ist *die* Hauptschleife. Die Funktion ist auch über jedes Widget über den gleichen Namen erreichbar.

Das Programm hat kein Hauptfenster. Es muss ein Fenster geben, dass von `Tkinter.Tk()` erzeugt wird.

Üblicherweise sollte die `__init__()`-Methode ein Objekt in einen benutzbaren Zustand versetzen. Wenn man dort doch einmal Attribute mit "Nichts" vorbelegen möchte, sollte man nicht unbedingt Objekte vom Typ benutzen, der später verwendet wird, sondern `None`. Sonst kann es bei Zahlen zum Beispiel passieren, dass man auf einem nicht vollständig "gebrauchsfertigen" Objekt Methoden aufruft, die dann mit "Sonderwerten" wie 0 oder -1 rechnen, ohne dass man den Fehler sofort bemerkt.

Wenn Du die Dimensionen überprüfst, solltest Du das am besten in der Methode machen, in der sie auch übergeben werden. Und dort dann eine Ausnahme und kein ``return`` mit einem Fehlerwert benutzen.

Nach Abarbeitung von `createWidgets()` geht die Liste mit den Referenzen auf die erzeugten `Button`\s verloren. Die sollte man vielleicht unter einem aussagekräftigerem Namen an das Objekt binden. Du willst da ja später noch drauf zugreifen.

Verfasst: Freitag 28. Dezember 2007, 23:35
von Nikolas
Danke für die Hilfe. :D
So langsam bin ich aber zu müde, um mich da noch reinzudenken. Ich les es mir morgen durch und melde mich dann wieder :lol:

Verfasst: Freitag 28. Dezember 2007, 23:53
von HWK
In Zeile 37 dürfte ein Einrückungsfehler vorliegen: L.append soll ja wahrscheinlich erst nach Abschluss der inneren for-Schleife ausgeführt werden.
MfG
HWK

Verfasst: Samstag 29. Dezember 2007, 11:14
von schlangenbeschwörer
BlackJack hat geschrieben:Das Programm hat kein Hauptfenster. Es muss ein Fenster geben, dass von `Tkinter.Tk()` erzeugt wird.
Das ist nicht ganz richtig. Es sollte ein Hauptfenster haben, es geht aber auch ohne, bzw. es wird sonst automatisch eins erzeugt.

Code: Alles auswählen

import sys, Tkinter as tk
tk.Button(text="test", command=lambda:sys.stdout.write("test")).pack()
tk.mainloop() # in der Konsole gehts sogar ohne den mainloop

Verfasst: Samstag 29. Dezember 2007, 11:37
von BlackJack
Wenn Du möchtest, dass das Programm überall läuft, musst Du ein `Tk`-Fenster erzeugen. Das ist ähnlich wie mit unterschiedlichen Threads auf die GUI zugreifen, das kann gut gehen, muss es aber nicht immer.

Ohne `mainloop()` geht's soweit ich weiss auch nur unter Linux/Unix.

Verfasst: Samstag 29. Dezember 2007, 12:18
von schlangenbeschwörer
Also bei mir gehts auch unter Windows, allerdings nur in der "Eingabeaufforderung". Mit fehlenden Tk-Fenstern hatte ich noch keine Probleme, aber das mach ich auch nur zum Testen. Bei richtigen Sachen hab ich schon ein Tk-Fenster.

Verfasst: Samstag 29. Dezember 2007, 12:35
von Nikolas
Das mit der falschen Einschiebung ist nur beim kopieren passiert, im Code ists richtig, danke für den Hinweis. Das mit dem Einrücken ist wirklich etwas merkwürdig, mit einem schönen begin...end oder wenigstens {...}
könnten solche Fehler nicht passieren...

Die Buttons sind jetzt auch etwas haltbarer.

die self.x ist jetzt mit None initialisiert.

Ich habe jetzt für die gesamte GUI so umgestellt: (mit Hauptfenster)

Code: Alles auswählen

import Tkinter as tk
import tkFont

class MineWindowWithButtons():

    def __init__(self,master):
        self.master = master
        self.root = tk.Tk()

        self.x = None
        self.y = None
        
        self.root.newGameButton = tk.Button(text="Neues Spiel")
        self.root.newGameButton.grid(row=0,columnspan=2)

    # reicht die übergebenen Element als Liste weiter
    def returnID(self,x,y):
        print  [x,y]
        self.master.foo(x,y)
Und als Logik dann

Code: Alles auswählen

import Tkinter as tk
import MineGUI as GUI

class App():

    def foo(self,x,y):
        print "Hallo"
        print x
        
    def __init__(self):
        a = GUI.MineWindowWithButtons(self) # (1)
        a.setDimensions(3,3)
        a.root.mainloop() # (3)

App()
Damit habe ich jetzt die Darstellung komplett von der Logik getrennt und muss nur (1) und (3) anpassen, wenn ich eine andere Darstellung nutzen will.
Was haltet ihr jetzt von dieser Version?