Programmieren mit Python 3 - Tischrechner mit GUI (2)

Tischrechner mit GUI für Expression Parser Klasse

Da wir das Modul mit der grafischen Oberfläche für unser Modul aus dem letzten Kapitel anpassen müssen, habe ich gleich noch zwei Verbesserung vorgenommen:

Das Ergebnis wird jetzt automatisch in die Zwischenablage gestellt. Da dies manche Benutzer sicherlich nicht wollen, ist es mittels einer Checkbox (heißt in Python Checkbutton) abschaltbar.

Python verlangt, dass man einen Dezimalpunkt verwendet (wie im angelsächsischen Raum üblich). Im Deutschen verwendet man allerdings das Dezimalkomma. Daher wird unser neues GUI-Modul es erlauben, dies umzuschalten. Man kann dann den Dezimalpunkt verwenden, muss dann aber bei Funktionen, die mehr als ein Argument erwarten (z. B. atan2(y,x) [Python-Dokumentation - math functions]), das Semikolon statt des Kommas verwenden. Im folgenden Bild sehen Sie, wie unser Rechner am Ende dieses Kapitels aussehen wird:

Bild Tischrechner mit GUI (2) leider nicht vorhanden

Zunächst wieder der komplette Quellcode des Moduls, Erklärungen folgen danach. NEU (Syntax-Hiliting)

##!/usr/bin/python3 # Shebang (if you need it, remove ONLY the first hash (#) # IF YOU HAVE PROBLEMS RUNNING THIS PROGRAM DELETE THIS LINE SO THAT NEXT LINE IS SECOND LINE # -*- encoding: iso-8859-15 -*- """GUI-Calculator with Expression Parser, Copyright 2017, Peter Sulzer, Fuerth Program is released under the GNU General Public License version 1 or above""" import tkinter import tkinter.ttk as ttk # tkinter.ttk may now be abbreviated with ttk (e.g. # myCombo = ttk.Combobox(...) import re # needed for replacing decimal comma with decimal point on user input from exprpars import ExpressionParser as EP # import class ExpressionParser from # our module exprpars as (with name) EP floatPoint=re.compile(r"([0-9]*)(,)([0-9]*)") # pattern when decimal point selected floatComma=re.compile(r"([0-9]*)(\.)([0-9]*)") # pattern when decimal comma selected win=ttk.tkinter.Tk() # Instead of Tk() we now need ttk.tkInter.Tk(), cause # ttk itself (needed for combobox) has no Tk() class win.geometry("620x220") # You know that(!), adjust size of our window win.wm_title("Calculator with Expression Parser") # set title of our program cbV=ttk.tkinter.StringVar() # Create a special "ttk string variable" # Give user a little help after program has started: cbV.set( # You cannot assign to cbV with "=", you must use the set() method " # Insert an expression like sin(pi/6) BEFORE the hash (#), " "hit the <ENTER> key, afterwards fold-out the listbox") cbInList=[] # The "list" (aPython class) for the fold-out list box of our # combobox. At first empty, we will fill it later cbIn=ttk.Combobox(textvariable=cbV,values=cbInList) # Create a Combobox # which displays the value of our cbV variable in its entry field cbIn.pack(fill="x",padx=5,pady=2) # You know that(!), some adjustements cbIn.focus() # Set the focus to our combobox, so that the user must NOT # first click into the combobox to enable the write cursor autoIns=ttk.tkinter.IntVar(value=1) chkAutoIns=ttk.tkinter.Checkbutton(win,text="automatically insert result into clipboard", variable=autoIns) chkAutoIns.pack(anchor="nw",padx=5) useComma=ttk.tkinter.IntVar(value=1) chkUseComma=ttk.tkinter.Checkbutton(win,text="use comma as decimal separator "+ "(decimal point is also allowed) "+ "and semicolon as argument separator ", variable=useComma) chkUseComma.pack(anchor="nw",padx=5) def cbInNewSelection(event): # callback function if user selects an entry from history if useComma.get() == 1: # history stores expressions always with decimal point hlp=cbV.get() hlp=hlp.replace(',',';') # so we must restore the correct argument separator hlp=floatComma.sub(r"\1,\3",hlp) # and convert point to decimal comma cbV.set(hlp) cbIn.bind("<<ComboboxSelected>>",cbInNewSelection) def cbReturn(event): # This method will be called, if the user taps the # <ENTER> key (Carriage Return). We do not need the # event, but it must be specified (required by Tkinter). expr=cbV.get() # First get the input in the entry field of the combobox # if checkbox chkUseComma is checked, we must convert first semicolons # to commas (argument separators) and afterwards decimal commas to points: if useComma.get() == 1: expr=floatPoint.sub(r"\1.\3",expr) # \1=first(...), literal '.' \2=third(...) # NOTE that the string which shall be substituted must follow the # replacement string! expr=expr.replace(';',',') # we must also convert the argument separator cbV.set(expr) if (expr.strip() == ""): # if user entered a blank line cbV.set("") # clear the input line return # and do nothing else # We want to add the user input to the fold-out listbox of the combobox, # so that the user has a "history" of old expressions, which he can # quickly recall by selecting an expression in the listbox: #cbInList.insert(0,expr) # First insert user input to cbInList bound # # to combobox at first position (top) ... cbInList.insert(0,expr) # First insert user input to cbInList bound # to combobox at first position (top) ... # ... and notify the combobox (cbIn) that the bound listbox has changed. # Unfortunately the combobox does not realize this automatically. You # can achieve this with: cbIn["values"] = cbInList # Note the quotation marks around values(!) rslt, err=EP.evaluateExpression(expr) # Call method EvaluateExpression from # class ExpressionParser (imported with Name EP) if (err != ""): cbV.set("Error: {}".format(err)) # The '0' inside {...} is not necessary else: rsltFormatted=EP.formatResult(rslt) # as for the input we must convert decimal comma if chkUseComma is checked: if useComma.get() == 1: rsltFormatted=rsltFormatted.replace(',',';') rsltFormatted=floatComma.sub(r"\1,\3",rsltFormatted) cbV.set(rsltFormatted) # Insert result to clipboard if chkAutoIns checkbox is checked: if autoIns.get() == 1: win.clipboard_clear() win.clipboard_append(cbV.get()) win.update() # now result stays in clipboard after our app is closed cbIn.select_clear() # clear the selection and cbIn.icursor(cbV.get().__len__()) # position cursor after last character # Note: In Python normally the len(string)-function ist used instead of # the ugly __len__() method, but for demonstration we are using the # __len__() method of the python string class (type). See also: # http://stackoverflow.com/questions/237128/is-there-a-reason-python-strings-dont-have-a-string-length-method cbIn.bind('<Return>',cbReturn) # Now we bind the -key to our method above # (only if the combobox has the focus). I.e.: # If the user taps the <ENTER> key, our # function cbReturn will be called win.mainloop() # Let's run our program

Hinweis: Da sich an dem bisherigen existierenden Code relativ wenig geändert hat, werde ich hier nur die Anpassungen erklären, die notwendig sind, damit das Modul mit unserem neuen Parser-Modul funktioniert. Lesen Sie daher gegebenenfalls im Kapitel Tischrechner mit GUI nach. Die neu hinzugekommenen Teile für das Einfügen in die Zwischenablage und der optionalen Verwendung von Dezimalkomma werden natürlich ausführlich erläutert.

Nach den 3 Kommentarzeilen am Anfang kommt zunächst ein Python Dokumentationsstring (siehe vorhergehendes Kapitel). Bei den „import”-Anweisungen wurde das Modul re hinzugefügt, das kennen Sie schon aus dem vorhergehenden Kapitel. Die „import”-Anweisung zur Einbindung unseres Parsers wurde natürlich ebenfalls geändert. Aber auch das wurde schon bei der Kommandozeilenversion am Ende des vorhergehenden Kapitels erklärt.

Als nächstes legen wir zwei reguläre Ausdrücke an (floatPoint und floatComma). Die Muster dafür werden unten erklärt, wenn wir den jeweiligen Ausdruck zum ersten mal verwenden.

Da unser Parser jetzt in einer eigenen Klasse gesichert ist, müssen wir jetzt nicht mehr für alle Variablen die „komischen” Namen mit den Unterstrichen verwenden, können also z. B. win statt _win__ verwenden.

Neu ist der Teil ab der Zeile

autoIns=ttk.tkinter.IntVar(value=1)

Die Klasse ttk.tkinter.IntVar() ist ähnlich wie die Klasse ttk.tkinter.StringVar(), die wir bereits im Kapitel Tischrechner mit GUI kennengelernt haben, allerdings werden in einer IntVar statt Zeichenketten (Strings) Ganzzahlwerte (Integer) gespeichert. Beim Anlegen mit obiger Zeile rufen wir den Konstruktor der Klasse auf und übergeben gleich den Wert 1 an das Attribut value. Diese IntVar benötigen wir für die Checkbox, mit der man einstellen kann, ob das Ergebnis automatisch in die Zwischenablage gestellt werden soll. Wenn in der IntVar der Wert 1 gespeichert ist, ist die Checkbox angehakt (Ergebnis wird in die Zwischenablage gestellt) bei 0 nicht. Wie schon bei StringVar ändert sich der Wert von IntVar automatisch, wenn der Benutzer die Checkbox anklickt. Umgekehrt ändert sich der Status der Checkbox wenn man den Wert der IntVar (autoIns in unserem Programm) ändert.

Als nächstes legen wir mit

chkAutoIns=ttk.tkinter.Checkbutton(win,text="automatically insert result into clipboard", variable=autoIns) chkAutoIns.pack(anchor="nw",padx=5)

eine Checkbox (ein Objekt der Klasse ttk.tkinter.Checkbutton) an. „win” ist das Fenster in dem die Checkbox angezeigt werden soll, das Attribut text (eine String-Variable) zeigt die Beschreibung für diese Checkbox an. Das Attribut „variable” der Checkbutton-Klasse legt fest, ob die Checkbox angehakt ist oder nicht. An diese muss ein Objekt der Klasse ttk.tkinter.IntVar zugewiesen werden, wir weisen hier unsere oben angelegte Variable autoIns zu. Das folgende „chkAutoIns.pack(...) kennen Sie schon aus unseren vorherigen GUI-Programmen.

Danach legen wir auf ähnliche Weise eine Checkbox an, mit der eingestellt werden kann, ob wir Dezimalpunkt oder Dezimalkomma, falls angehakt, verwenden wollen (wie Sie sehen, können Sie lange Strings mittels des „+”-Operators verketten und so auf mehrere Zeilen verteilen).

Im folgenden benötigen wir eine Callback-Funktion (cbInNewSelection), die aufgerufen wird wenn der Benutzer einen History-Eintrag (aus der ausklappbaren Listbox der Combobox wählt). Dies ist notwendig, weil wir in der History alle Eingaben mit Dezimalpunkt und Komma als Argumenttrenner speichern. Wenn ein Eintrag aus der History gewählt wird müssen wir daher prüfen, ob die Checkbox für Dezimalkomma angehakt ist, und falls ja wandeln wir zunächst alle Kommas in Semikolons um und danach alle Dezimal-Punkte in Kommas um. Letzteres erledigen wir nicht wie beim Argumenttrenner für Funktionsargumente mit der replace() Methode für Strings, sondern mit Hilfe des oben angelegten regulären Ausdrucks „floatComma;” und dessen „sub()” Methode (für substitute, deutsch ersetzen). Das Muster für den regulären Ausdruck floatComma (siehe ziemlich am Anfang des Programmcodes) ist:

r"([0-9]*)(\.)([0-9]*)"

Das r vor dem String-Begrenzer (") bedeutet, dass es ein roher String ist (siehe im Kapitel Ein-/Ausgabe auf Bildschirm). Die 3 Klammerpaare stehen für 3 Gruppen (siehe z. B. hier). Die Gruppen sind fortlaufend nummeriert, beginnend bei 1. Noch nicht benutzt haben wir das spezielle Zeichen „*”, es bedeutet der vorhergehende Teil darf null-, ein- oder mehrmals vorkommen. Das „\.” in der zweiten Gruppe bedeutet, dass dort ein Punkt stehen muss. Der vorhergehende Backslash hebt die Sonderbedeutung des Punkts in einem regulären Ausdruck auf. Mit folgender Anweisung nehmen wir die Ersetzung vor:

hlp=floatComma.sub(r"\1,\3",hlp) # and convert point to decimal comma

Dies bedeutet: Nehme zuerst das gefundene Muster aus Gruppe 1 (\1 steht für Gruppe 1), füge dann ein Komma ein und hänge dann das gefundene Muster aus Gruppe 3 an. Das ganze muss auf den zweiten String-Parameter (hlp) der sub() Methode angewendet werden.

In der Callback-Funktion cbReturn hat sich auch einiges geändert. Nachdem wir mit „expr=cbV.get()” die Benutzereingabe geholt haben, testen wir zunächst mit „if useComma.get() == 1:” ob die Checkbox für Dezimalkomma-Eingabe angehakt ist und falls ja, wandeln wir zuerst alle Dezimalkommas in Punkte um. Dazu verwenden wir den regulären Ausdruck floatPoint und verwenden wieder die sub() Methode. Das funktioniert aber so wie oben die Umwandlung oben von Dezimalpunkt nach Komma. Das sollten Sie daher selbst nachvollziehen können. Danach wandeln wir noch die Semikolons zu Komma. Auch das funktioniert wie oben die Umwandlung von Komma zu Semikolons. Danach hat sich bis zum Aufruf der eigentlichen Berechnungsroutine nichts geändert, diese lautet nun:

rslt, err=EP.evaluateExpression(expr)

Wir rufen die statische Methode „evaluateExpression” aus unserer Klasse „ExpressionParser” (die wir in der import-Anweisung unter dem Namen EP verfügbar gemacht haben) auf. Danach testen wir wie in der ursprünglichen GUI-Version, ob ein Fehler aufgetreten ist, und geben diesen im Eingabefeld unserer Combobox aus. Hier hat sich nichts geändert.

Im „else<”>-Teil (kein Fehler) hat sich aber einiges geändert. Zunächst formatieren wir das Ergebnis (rslt) mit der „formatResult” Methode unserer „ExpressionParser” Klasse. Dann testen wir wieder ob die Checkbox für Dezimalkomma-Eingabe gesetzt ist (das Ergebnis wird immer mit Dezimalpunkt ausgegeben) und wandeln es, bevor wir es im Eingabefeld unserer Combobox ausgeben um (Dezimalkomma und Semikolon als Argument-Trenner). Dies funktioniert aber genauso wie oben in unserer Callback-Funktion „cbInNewSelection”.

Nachdem wir das formatierte (und evtl. umgewandelte) Ergebnis in die Eingabezeile unserer Combobox ausgegeben haben - mit cbV.set(...) - testen wir in der folgenden if-Anweisung, ob das Ergebnis automatisch in die Zwischenablage (Clipboard) gestellt werden soll, damit man es (ohne es erst markieren zu müssen und mit Strg-C kopieren muss) in einem anderen Programm (z. B. einem Textverarbeitungsprogramm) sofort mit Strg-V einfügen kann. Dazu leeren wir zuerst die Zwischenablage mit win.clipboard_clear() (win ist unser Fenster). Dann hängen wir den Inhalt aus dem Eingabefeld unserer Combobox mit win.clipboard_append(...) an die (jetzt geleerte) Zwischenablage an. Das abschließende win.update() sorgt dafür, dass der Inhalt der Zwischenablage auch erhalten bleibt, wenn man das Programm beendet. Leider hat Python/Tkinter hier zur Zeit einen Bug, siehe stackoverflow. Wenn Sie einen Ausdruck berechnen und dann sofort unseren Rechner beenden, steht das Ergebnis nicht in der Zwischenablage. Wenn Sie es aber vor dem Beenden irgendwo einfügen, dann bleibt das Ergebnis in der Zwischenablage.

Danach entfernen wir wie in der ursprünglichen Version eine eventuelle Markierung im Eingabefeld der Combobox und setzen den Cursor (die Schreibmarke) an das Ende, damit der Benutzer sofort mit dem Ergebnis weiter rechnen kann. Zuletzt binden wir die Eingabetaste an unsere obige Callback-Funktion cbReturn und lassen unseren jetzt sehr komfortablen Tischrechner ("Taschenrechner") mit win.mainloop() laufen. Aber das kannten Sie ja schon.

Wie Sie sehen, sind beide Module unseres Programms deutlich umfangreicher geworden. Allerdings ist der Parser jetzt sehr einfach und komfortabel zu benutzen (z. B. müssen wir bis auf wenige Ausnahmen keine Namen mit Unterstrichen verwenden). Siehe hierzu auch das Kommandozeilen Testprogramm aus dem vorhergehenden Kapitel, welches erstaunlich kurz ist. Die Leistungsfähigkeit und Bedienerfreundlichkeit unserer Oberfläche haben deutlich zugenommen. Wie schon bei der ersten grafischen Version empfehle ich Ihnen, den Großteil der Kommentare zu löschen. Wenn Sie sich dann den Code ansehen, wird er Ihnen viel übersichtlicher erscheinen.

Damit schließe ich das Tischrechnerbeispiel erst mal ab. Als Erweiterung können Sie versuchen, ähnlich wie bei meinem Coca, eine virtuelle Tastatur hinzuzufügen, um den Tischrechner direkt über Touchscreen bedienen zu können. Für Literale (Buchstaben und Ziffern) sollte das nicht allzu schwierig sein, verwenden Sie die Button Klasse von Tkinter für die Tasten. Lesen Sie in der offiziellen Python-Dokumentation welche Methoden eine Combobox unterstützt. Bei den Cursor-Tasten (nur Vor und Zurück wird benötigt), der (den) Löschtaste(n) und der Eingabetaste, könnte es eventuell etwas schwierig werden. Wenn ich mal wieder Zeit habe, werde ich das evtl. mal hier ergänzen.

Ansonsten werde ich hier demnächst noch ein abschließendes Kapitel bringen, mit allgemeinen Sachen zu Python, wie diese kleine Einführung benutzt werden kann und Literatur-Hinweisen bzw. Links auf andere Seiten. Schauen Sie also ab und zu mal wieder vorbei.

Last but not least bedanke ich mich dafür, dass Sie diese kleine Einführung gelesen (und hoffentlich verstanden) haben.