Backtaste

Fragen zu Tkinter.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Wie kann man eine Lösch-Taste einbauen,die nur eine Zahl löscht?

Code: Alles auswählen

from tkinter import *
import tkinter as tk
import math


def click(key):
    global memory
    if key == '=':
        
        str1 = "-+0123456789."
        try:
            result = eval(entry.get())
            entry.insert(tk.END, " = " + str(result))
        except:
            entry.insert(tk.END, "Syntax ERROR")
    
    elif key == 'C':
        entry.delete(0, tk.END)

    elif key == '<~':

    elif key == '->M':
        memory = entry.get()



        if '=' in memory:
            ix = memory.find('=')
            memory = memory[ix+2:]

    elif key == 'M->':
        entry.insert(tk.END, memory)

        
   
        if '=' in entry.get():
            entry.delete(0, tk.END)
        try:
            if entry.get()[0] == '-':
                entry.delete(0)
            else:
                entry.insert(0, '-')
        except IndexError:
            pass
    else:
       
        if '=' in entry.get():
            entry.delete(0, tk.END)
        entry.insert(tk.END, key)


            
root = tk.Tk()
root.geometry('300x300')
root.title("Taschenrechner")
button_list = [
    '7','8','9','*','C',
    '4','5','6','/','<~',
    '1','2','3','-','M-',
    '0','.','=','+','%']
r=1
c=0

for i in button_list:
    rel='ridge'
    cmd=lambda x=i: click(x)
    tk.Button(root, text=i, width=5,height=2,font='bold',relief=rel, command=cmd).grid(row=r, column=c)
    c +=1
    if c>4:
        c=0
        r +=1

entry = tk.Entry(root, width=29 ,bg="white",font='bold',bd=10)
entry.grid(row=0, column=0, columnspan=10)
root.mainloop()

BlackJack

@Commander: Was meinst Du mit „nur eine Zahl”? Irgend etwas komplizierteres als Auslesen des Inhalts, von der Zeichenkette alles ausser dem letzten Zeichen per Slice-Syntax nehmen und wieder als Wert vom Eingabefeld setzen?
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Die letzte eingetippte Zahl im Display.
Schorlem
User
Beiträge: 40
Registriert: Dienstag 3. Juni 2014, 16:37

Commander hat geschrieben:Die letzte eingetippte Zahl im Display.
Also... das, was BlackJack grade geschrieben hat?
Diese Nachricht wurde maschinell erstellt und ist daher ohne Unterschrift gültig.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Wenn es das heißt,was er sagte,dann ja.
Schorlem
User
Beiträge: 40
Registriert: Dienstag 3. Juni 2014, 16:37

Am einfachsten wär dann wohl die direkte Umsetzung (einfach in den if-Block rein):

Code: Alles auswählen

entry.delete(tk.END)
Wenn du noch Fehler durch die Leerzeichen und "=" verhindern willst, kannste dir ja mit re oder .split() noch etwas entsprechendes basteln.
@BlackJack Dank .delete() kann man auch einzelne Zeichen so löschen, ohne das über den Umweg mit Auslesen, Löschen und Einfügen machen zu müssen (es sei denn, der OP will was wie oben beschrieben).
Diese Nachricht wurde maschinell erstellt und ist daher ohne Unterschrift gültig.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Passiert leider nichts.
Habe es so:

Code: Alles auswählen

def click(key):
    global memory
    if key == '=':
        
        str1 = "-+0123456789."
        try:
            result = eval(entry.get())
            entry.insert(tk.END, " = " + str(result))
        except:
            entry.insert(tk.END, "Syntax ERROR")

            
    elif key == 'C':
        entry.delete(0,tk.END)

    elif key == '←':
        entry.delete(tk.END)

Wenn ich irgendwas eingebe und '←' drücke passiert nichts.
Schorlem
User
Beiträge: 40
Registriert: Dienstag 3. Juni 2014, 16:37

Oh, Mist.
Hier, eine hässliche Lösung (mMn.):

Code: Alles auswählen

entry.delete(len(entry.get())-1)
Negative Indizes funktionieren irgendwie nicht, sonst wär's das Ganze schon einfacher...
Diese Nachricht wurde maschinell erstellt und ist daher ohne Unterschrift gültig.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Vielen Dank!
BlackJack

@Commander: Noch ein paar Anmerkungen:

Sternchenimporte sollte man vermeiden. Insbesondere wenn der gar nicht benötigt wird, macht es keinen Sinn sich die ca. 190 Namen ins Modul zu kippen. Das importierte `math`-Modul wird ebenfalls nicht benutzt.

Auf Modulebene sollte man nur Konstanten, Funktion, und Klassen definieren. Das Hauptprogramm sollte dort nicht direkt stehen. Denn dann kann man das Modul nicht importieren ohne dass das Hauptprogramm abläuft, und man läuft Gefahr das in Funktionen und Klassen auf Variablen zugegriffen wird die nicht als Argumente übergeben wurden. In dem Zusammenhang: Vergiss ``global``. Das hat in ordentlichen Programmen nichts zu suchen. Wenn man Zustand über Funktionsaufrufe hinweg benötigt, verwendet man objektorientierte Programmierung. Da kommt man bei GUI-Programmierung sowieso nicht sinnvoll drum herum.

In Namen sollten keine konkreten Typen verwendet werden. Wenn man den Typen mal ändert, muss man sonst überall den Namen ändern, oder man hat falsche und damit irreführende Namen im Programm.

Abkürzungen und einbuchstabige Namen sollte man nur verwenden wenn sie allgemein bekannt sind, oder in sehr eng begrenzten Gültigkeitsbereichen wie Lambda-Definitionen, „list comprehensions”, oder Generatorausdrücken. Ansonsten sind ausgeschriebene Namen lesbarer und zwingen den Leser nicht zu rätseln. So etwas wie `i` in einer Schleife für etwas anderes als ganze Zahlen zu verwenden ist ausserdem zusätzlich verwirrend für erfahrene Programmierer.

Namen sollte man auch nicht durchnummerieren. Das ist fast immer ein Zeichen dafür das man keine einzelnen Namen sondern eine Datenstruktur für die Werte verwenden möchte. Meistens eine Liste. Ausser wenn man einfach nur eine 1 an Namen anhängt die für Einzelwerte stehen. Das ist einfach nur sinnfrei. Wobei `str1` in `click()` insgesamt sinnfrei ist, weil der Wert nirgends verwendet wird.

Anstatt Zeilen- und Spaltennummer umständlich zu berechnen, wäre es einfacher die Struktur schon in der Liste mit den Beschriftungen für die Schaltflächen zu kodieren, also eine Liste mit Listen für die Zeilen anzulegen. Dann muss man die entsprechenden Nummern in den Schleifen einfach nur mit `enumerate()` mitzählen lassen.

Mit dem Schlüsselwort ``lambda`` definiert man anonyme Funktionen, also Funktionen die keinen Namen haben. Diese Funktion dann gleich an einen Namen zu binden macht irgendwie keinen Sinn, denn *dafür* ist die ``def``-Anweisung ja schon gedacht.

In `click()` wird viel zu viel gemacht. Da steckt ja die Behandlung von *allen* Tasten drin. Eine Funktion oder Methode sollte aber immer nur eine Aufgabe erledigen.

Man sollte keine ”nackten” ``except``\s verwenden, sondern dort immer angeben welche Ausnahme(n) man erwartet. Insbesondere *jede* Ausnahme einfach durch einen festen Text zu ersetzen ist keine adäquate Ausnahmebehandlung und erschwert die Fehlersuche ungemein. Der Benutzer bekommt hier SYNTAX ERROR angezeigt, auch wenn es gar kein Syntaxfehler war.

Beim Speichern eines Wertes mit '->M' ist das addieren von 2 zum Index an dem das '=' gefunden wurde nicht robust, weil das an der Stelle davon ausgeht das sich nach dem '=' ein unwichtiges Zeichen befindet. Sollte man das Leerzeichen an anderer Stelle im Code mal entfernen, muss man hier daran denken, dass es noch vorhanden ist.

Der Test auf vorhandensein eines '='-Zeichens und *dann* das ermitteln der Position ist auch irgendwo redundant. Man kann auch gleich die Position ermitteln und entsprechend mit der Ausnahme umgehen. Oder man verwendet die `partition()`-Methode und entscheidet nach vorhandensein eines '=' welchen Teil man speichert.

Die Logik vom 'M->'-Zweig erschliesst sich mir nicht wirklich‽ Die Memory-Funktionalität ist auch mit keiner Taste verbunden, dafür gibt es ein 'M-' als Taste für das es keinen Code gibt.

Die feste Vorgabe der Fenstergrösse ist ein Fehler. Bei mir auf dem Laptop werden deshalb nicht alle Tasten angezeigt und nur der Anfang von der Anzeige.

Ich lande dann bei so etwas (ungetestet):

Code: Alles auswählen

try:
    import Tkinter as tk
except ImportError:
    import tkinter as tk
from functools import partial


class Calculator(object):
    def __init__(self, parent):
        self.memory = ''

        button_texts = [
            ['7', '8', '9', '*', 'C'],
            ['4', '5', '6', '/', '<~'],
            # 
            # FIXME: 'M-' is not connected to any action.
            # 
            ['1', '2', '3', '-', 'M-'],
            ['0', '.', '=', '+', '%'],
        ]
        for row_number, text_row in enumerate(button_texts, 1):
            for column_number, text in enumerate(text_row):
                tk.Button(
                    parent,
                    text=text,
                    width=5,
                    height=2,
                    font='bold',
                    relief=tk.RIDGE,
                    command=partial(self.click, text)
                ).grid(row=row_number, column=column_number)

        self.entry = tk.Entry(
            parent, width=29, bg='white', font='bold', bd=10
        )
        self.entry.grid(row=0, column=0, columnspan=10)

    def click(self, key):
        # 
        # TODO: Break this method up into one method per calculator key (type).
        # 
        if key == '=':
            try:
                result = ' = {0}'.format(eval(self.entry.get()))
            except Exception as error:
                result = ' [{0}]'.format(error)
            self.entry.insert(tk.END, result)
       
        elif key == 'C':
            self.entry.delete(0, tk.END)
     
        elif key == '<~':
            pass

        elif key == '->M':
            pre, equal, post = self.entry.get().partition('=')
            self.memory = (post if equal else pre).strip()
     
        elif key == 'M->':
            self.entry.insert(tk.END, self.memory)
            if '=' in self.entry.get():
                self.entry.delete(0, tk.END)
            try:
                if self.entry.get()[0] == '-':
                    self.entry.delete(0)
                else:
                    self.entry.insert(0, '-')
            except IndexError:
                pass

        else:
            if '=' in self.entry.get():
                self.entry.delete(0, tk.END)
            self.entry.insert(tk.END, key)
 
 
def main():
    root = tk.Tk()
    root.title('Taschenrechner')
    _calculator = Calculator(root)
    root.mainloop()


if __name__ == '__main__':
    main()
Wobei es unschön/unsauber ist hier die Programmlogik und die GUI in einer Klasse zu vermischen, insbesondere weil ein Teil der Datenhaltung in GUI-Widgets passiert, wo sie nicht hingehört. Und `eval()` ist eine billige aber unschöne Lösung. Tatsächlich die Programmlogik und den Zustandsautomaten für einen Taschenrechner zu implementieren ist allerdings auch gar nicht so einfach wie man auf den ersten Blick vielleicht denkt. Das zum Beispiel auf einen Rechner zu beschränken der die „Reverse Polish Notation” (RPN) implementiert, wäre deutlich einfacher.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Math-Modul brauche ich noch für Sinus,Kosinus,Tangens.
Ich glaube,ich lasse es lieber weg.

Leider weiß ich nicht,wie ich es sonst ohne Sternchenimport machen soll.

Einige Funktionen habe ich schon längst wieder entfernt(M-,M->),weil ich mit denen nicht fertig war.
Habe mit einer Sache angefangen,bevor ich mit der nächsten fertig war.

Ja,der Rest ist so richtig,wie du beschrieben hast.
Muss mich noch viel mit Python auseinander setzen,aber jetzt muss ich erst mal einen Taschenrechner programmieren wegen einer Hausarbeit.

Habe auch jetzt root.geometry raus genommen.
Sollte jetzt alle Tasten anzeigen und das Fenster stellt selbst die Größe fest.
Keine Ahnung,wieso ich es überhaupt so eingesetzt habe.

str1 ist auch entfernt worden und ein paar angesprochene Punkte auch verbessert worden.

Danke für deine Anmerkungen.

Wie kann ich noch eine Taste für Prozent einfügen?
Prozent ist ja im Prinzip nichts anderes als durch 100 teilen oder mit 1.0 zu multiplizieren(man möchte z.B. 30% von 250 wissen,also 250*0,3)
BlackJack

@Commander: Der Sternchenimport wird doch gar nicht benötigt. Es ohne zu machen bedeutet in Deinem fall einfach die überflüssige ``import``-Zeile zu entfernen.

Beim '%' musst Du halt die bisher eingegebene Formel nehmen und die so erweitern, dass das gewünschte Ergebnis berechnet wird. Die Prozenttaste auf Taschenrechnern ist eigentlich immer nur durch 100 Teilen. Das Multiplizieren mit einer anderen Zahl muss man auch dort in der Regel von Hand machen.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Ah okay,danke.
Damit wäre alles erledigt.
Danke euch Beiden.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Aller letzte Frage:
Versuche gerade einen Button für Wurzel zu machen.
Habe es auch geschafft,aber ich möchte eine Kleinigkeit dazu haben.

Code: Alles auswählen

    elif key == '√':
        a=int(entry.get())**(1/2)
        entry.insert(tk.END," = " + str(round(a,3)))

Wenn das Ergebnis(also a) keine Nachkommastellen hat(z.B. Wurzel aus 100 ist 10,dann soll er nicht 10.0 anzeigen),soll er diese auch nicht zeigen.
Ansonsten immer 3 Stellen nach dem Komma.
Mir fällt kein Befehl dazu ein.


Und wie kann ich sagen,wenn irgendwas im entry ist,dass er dies und jenes machen soll?
Schorlem
User
Beiträge: 40
Registriert: Dienstag 3. Juni 2014, 16:37

1. Das kannst du mit dem Modulo-Operator lösen:

Code: Alles auswählen

if a % 1 == 0:
    round(a, 0) # <- wirkt sich natürlich auf nichts aus
else:
    round(a, 4) # s.o.
2. 'n bisschen unspezifisch, was genau muss denn passieren? Ich verstehe das jetzt so, dass bei jedweder Eingabe im Entry irgendwas ausgeführt wird. Meinst vielleicht doch eher was auf Tastendruck oder so?
Diese Nachricht wurde maschinell erstellt und ist daher ohne Unterschrift gültig.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Ja genau,bei Tastendruck.

Er soll dann eine bestimmte Taste nicht ausführen,wenn nichts im Entry steht.
Schorlem
User
Beiträge: 40
Registriert: Dienstag 3. Juni 2014, 16:37

Code: Alles auswählen

def do_something(text):
    if text == "":
        return
    else:
        pass

entry.bind("<Return>", command=lambda: do_something(entry.get())
Ich schätze mal, dass die Enter-Taste hier angebracht ist. Eine Liste der Tastennamen findest du hier.
Diese Nachricht wurde maschinell erstellt und ist daher ohne Unterschrift gültig.
BlackJack

@Commander: Ich würde an Deiner Stelle ja nicht anfangen unterschiedliche ”Arten” von Knöpfen zu erstellen. Deine Wurzelfunktionalität funktioniert ja nur wenn vorher eine Zahl eingegeben wurde. Wenn der Benutzer vorher aber eine Rechnung eingibt und davon dann die Wurzel haben möchte geht das nicht. Das wäre aus Benutzersicht ein sehr komischer Rechner mit sehr willkürlich wirkenden Einschränkungen.

Besser wäre es bei '=' zum Berechnen zu bleiben, und bei allen anderen Operationen nur die Formel zu erweitern. Also dass wenn als Formel schon dort steht '42+23' und dann der Benutzer die Wurzeltaste drückt, die Formel entsprechend zu 'sqrt(42+23)' erweitert wird. Die Funktionen würde ich dann bei `eval()` als Argument angeben. Damit hast Du unter Kontrolle welche Funktionen verfügbar sind, und unter welchem Namen jeweils. Ausserdem würde ich das `Entry` gegen direkte Benutzereingaben sperren. Sonst kann dort der Benutzer wegen dem `eval()` nahezu beliebigen Python-Code ausführen.

Desweiteren würde ich wenn ein '=' im Display steht, also ein Ergebnis angezeigt wird, bei allen Rechenaktionen den Teil vor dem '=' entfernen und mit dem Ergebnis als neuem Wert weiter machen. Dann kann sich der Benutzer mit '=' Zwischenergebnisse anzeigen lassen und danach gleich damit weiter rechnen.

Und wie schon gesagt, sollte man die Programmlogik von der GUI trennen, denn eigentlich möchte man für diese Sachen automatisierte Tests schreiben, die alles was ein Benutzer mit dem Rechner so anstellen können muss, durchgehen. Denn man macht sich bei solchen Sachen gerne mit neuer Funktionalität bereits vorhandene kaputt ohne das gleich zu merken. Mit automatischen, wiederholbaren Tests fällt so etwas sofort auf.
Commander
User
Beiträge: 15
Registriert: Dienstag 20. Mai 2014, 16:05

Das mit der Wurzel ändere ich,danke.
Aber ich kriege den Fehler,dass sqrt nicht definiert ist.
Ich dachte,Python wüsste es.
Math Modul habe ich auch importiert

Was kann ich statt eval() benutzen?

Habe es jetzt auch mit dem = geändert,vielen Dank.


Ich weiß leider nicht,wie ich beides trennen soll.
Anders habe ich es leider nicht im Internet gefunden.

Ahja ich meinte die Tasten auf dem Taschenrechner,sorry.
Wenn man % drückt,wird /100 eingefügt und wenn nichts steht und man aus versehen % drückt,muss man die Löschtaste drücken und den Weg kann man sich ersparen,indem einfach nichts passiert.
Hat ja keinen Nachteil,wenn man % erst nach einer Eingabe einer anderen Zahl eingeben kann.

Edit:Das mit Wurzel habe ich gelöst.
BlackJack

@Commander: Du hast das `math`-Modul importiert aber nicht die `sqrt()`-Funktion daraus. Die könntest Du so ja auch in dem Modul im Quelltext selber nur über das `math`-Modul erreichen und nicht direkt. Letztendlich wäre es aber sowieso besser alle Funktionen die beim `eval()` verwendet werden dürfen sollen in ein Wörterbuch zu stecken und als zweites Argument zu übergeben. Und dort auch das Schlüssel/Wert-Paar ('__builtins__', None) einzutragen, damit auch aus den eingebauten Funktionen nicht einfach so alle verwendet werden können.

*Du* kannst anstelle von `eval()` bei dem bisherigen Wissensstand wohl nichts anderes benutzen. Ausser den Taschenrechner deutlich zu vereinfachen, und zum Beispiel einen RPN-Rechner zu implementieren.

Was heisst Du hast es im Internet nicht anders gefunden? Programmieren heisst nicht im Internet Beispiele zu suchen und die dann zu einem neuen Quelltext zusammen zu setzen. Und das trennen ist im Grunde einfach: Programmiere zuerst nur die Taschenrechnerlogik, ohne GUI, also ohne Ein- und Ausgabe, nur mit Funktionen beziehungweise Klassen und Methoden, und wenn das funktioniert, schreibt man dazu die GUI-Klasse(n).

Musst Du denn '%' überhaupt implementieren? Ich denke das ist komplizierter als es aussieht. Du gehst hier anscheinend davon aus das da nur eine Zahl im Display steht. Da kann ja aber jede beliebige Formel stehen die man mit den Knöpfen so eingeben kann, und worauf sich das % dann bezieht ist vielleicht nicht so eindeutig.
Antworten