MineSweeper: wie spreche ich Buttons im Grid an?

Fragen zu Tkinter.
Antworten
Nikolas
User
Beiträge: 102
Registriert: Dienstag 25. Dezember 2007, 22:53
Wohnort: Freiburg im Breisgau

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
Erwarte das Beste und sei auf das Schlimmste vorbereitet.
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

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.
Nikolas
User
Beiträge: 102
Registriert: Dienstag 25. Dezember 2007, 22:53
Wohnort: Freiburg im Breisgau

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?
Erwarte das Beste und sei auf das Schlimmste vorbereitet.
pyStyler
User
Beiträge: 311
Registriert: Montag 12. Juni 2006, 14:24

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
Zuletzt geändert von pyStyler am Freitag 28. Dezember 2007, 19:40, insgesamt 1-mal geändert.
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

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:
Nikolas
User
Beiträge: 102
Registriert: Dienstag 25. Dezember 2007, 22:53
Wohnort: Freiburg im Breisgau

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?
Erwarte das Beste und sei auf das Schlimmste vorbereitet.
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

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:
Nikolas
User
Beiträge: 102
Registriert: Dienstag 25. Dezember 2007, 22:53
Wohnort: Freiburg im Breisgau

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?
Erwarte das Beste und sei auf das Schlimmste vorbereitet.
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.
Nikolas
User
Beiträge: 102
Registriert: Dienstag 25. Dezember 2007, 22:53
Wohnort: Freiburg im Breisgau

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:
Erwarte das Beste und sei auf das Schlimmste vorbereitet.
Benutzeravatar
HWK
User
Beiträge: 1295
Registriert: Mittwoch 7. Juni 2006, 20:44

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
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

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
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.
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

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.
Nikolas
User
Beiträge: 102
Registriert: Dienstag 25. Dezember 2007, 22:53
Wohnort: Freiburg im Breisgau

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?
Erwarte das Beste und sei auf das Schlimmste vorbereitet.
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

Das mit dem "master" hast du jetzt etwsa durcheinander gebracht. Das bezog sich auf die Variante, in der du von tk.Frame geerbt hast. Da musst/solltest du der "tk.Frame.__init__" "master" als 2. Argument übergeben, wobei "master" dann eine tk.Frame oder tk.Tk Instanz sein muss (oder ein anderes geeignetes tk-Widget, wzB tk.Canvas). Da du jetzt aber eine eigene Klasse hast, die selbst ein tk.Tk-Fenster erzeugt, brauchst du das nicht. Ansonsten ist das aber eine Möglichkeit.
Nikolas
User
Beiträge: 102
Registriert: Dienstag 25. Dezember 2007, 22:53
Wohnort: Freiburg im Breisgau

Den "master"-Eintrag in der GUI Klasse nutze ich dazu, eine Schnittstelle zwischen Darstellung und Logik zu haben. Jetzt kann ich recht einfach den Buttons sagen, dass sie eine Funktion der Klasse oben drüber aufrufen sollen. Oder gibt es da einen einfacheren Weg, also hat eine Klasse immer schon die Information, von wem sie erschaffen wurde?
Erwarte das Beste und sei auf das Schlimmste vorbereitet.
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

Achso, das ist extra so. Na dann ist's ok, ich dachte nur, weil's vorher auch um master und so ging.
Im Grunde ist es ok was du machst, meistens ist es aber einfacher, die GUI-Klasse oben zu haben, denn dort hast du Events, die du verarbeitest ect. Die Datengeschichten kannst du meist in Methoden packen, die du von der Graphik aufrufst. Und wenns nicht ganz so einfach ist, kannst du deine GUI von der Data-Class erben lassen. Dann hast du alles in einem und doch getrennt.
Nikolas
User
Beiträge: 102
Registriert: Dienstag 25. Dezember 2007, 22:53
Wohnort: Freiburg im Breisgau

Das mit der Vererbung von Logik zu Darstellung ist eine nette Idee, aber noch mal stelle ich jetzt nicht mehr um :)

Danke für deine Hilfe.
Erwarte das Beste und sei auf das Schlimmste vorbereitet.
Antworten