A simple, GUI based calculator in Python using Tkinter that can calculate mathematical expressions involving basic operations such as addition, subtraction, multiplication and division on integers and decimal numbers
This project uses the Tkinter library of Python to implement its graphical user interface. The user interface is composed of a label, and a grid of buttons to represent the numbers and the different mathematical and text operations. The label has been used to minimize inputs from the keyboard thereby reducing the scope of unnecessary error. The buttons only operate on mouse clicks and they have been so labelled as to make the UI as intuitive as possible. The numbered buttons and the buttons labelled with mathematical operators and symbols are built to be used for writing a mathematical expression within the label. The button labelled "CL" will clear the label on click.
The UI as observed immediately after execution of the program
The actual calculation begins when the button labelled "=" is pressed. The algorithm for this calculation can be divided into 4 parts. The first part converts the text present in the label into an infix expression. To achieve this, first, the text obtained from the label is parsed, and any numerical digit or decimal point encountered is added to a string. If an operator(except '-') or a bracket is encountered, the string formed so far is appended to a list that serves as the infix expression, followed by the operator, and the string is made empty. If a '-' is encountered, the algorithm checks its immediately previous element, and if that is an operator or an opening bracket, appends the '-' to the string formed so far. If that is a number or a closed bracket, the string formed so far is appended to the infix list, followed by the '-' operator. Finally, the list representing the infix expression is filtered of any blank strings that may have crept in it during this execution. A snapshot of the code for this part is provided below :
#creating an infix expression from the text for item in e.cget("text"): if(item>='0' and item<='9' or item=='.'): s+=item elif(item=='-'): if(temp in '+-x/('): s+=item else: inf.append(s) s="" inf.append(item) else: inf.append(s) inf.append(item) s="" temp=item if(s!=""): inf.append(s) inf=list(filter(("").__ne__,inf)) e.config(text="")
The second part checks the validity of the infix expression thus formed. It looks for unclosed brackets, brackets opened immediately after entering a number, and brackets closed immediately after being opened. If any of the mentioned discrepancies are found, the label will show the text "Invalid Expression". Else, the list implementing the infix expression will be passed on to the next part.
#checking the infix expression for validity stack= for item in inf: if(item=='('): stack.append(item) elif(item==')'): if(len(stack) and stack[-1]=='('): stack.pop() else: e.config(text="Invalid Expression") return else: continue if(len(stack)): e.config(text="Invalid Expression") return for i in range(1,len(inf)): if(inf[i]=='(' and inf[i-1] not in '+-x/'): e.config(text="Invalid Expression") return if(inf[i]==')' and inf[i-1]=='('): e.config(text="Invalid Expression") return
The next part converts the infix expression(now considered valid) into a postfix expression. The list implementing the infix expression is parsed, and if any number is encountered, it is appended to the list implementing the postfix expression. If an operator or an opening bracket is encountered, it is added to a stack. If the top of the stack contains an operator that is of a lower priority than the operator encountered, it is added to the stack after popping all the elements of the stack and appending them to the postfix list until the stack is empty or the top of the stack contains an operator with higher priority than the encountered operator. If a closing bracket is encountered, all elements of the stack are popped and appended to the list representing the postfix expression until the stack is empty or the top of the stack contains an opening bracket. If the stack still contains some items after parsing the entire infix list, then all the remaining items are popped and appended to the postfix list.
#converting the infix expression to postfix pf= #The postfix expression for item in inf: if(item=='('): stack.append(item) elif(item in '+-x/'): while(len(stack) and prior(stack[-1])>=prior(item)): pf.append(stack.pop()) stack.append(item) elif(item==')'): while(len(stack) and stack[-1]!='('): pf.append(stack.pop()) stack.pop() else: pf.append(item) while(len(stack)): pf.append(stack.pop())
In the final step, the postfix expression formed previously is evaluated. The list implementing the postfix expression is parsed, and if any number is found, it is added to a stack. If an operator is found, then two numbers are successively popped from the stack and are subjected to that operation, and the result of that operation is pushed onto the stack again. This process goes on until the last element of the postfix expression is parsed. If any error occurs during this execution, the label shows the text "Invalid Expression". Else it shows the last remaining element of the stack.
#evaluating the postfix expression try: for item in pf: if(item=='+'): stack.append(float(stack.pop())+float(stack.pop())) elif(item=='-'): s_num=float(stack.pop()) b_num=float(stack.pop()) stack.append(b_num-s_num) elif(item=='x'): stack.append(float(stack.pop())*float(stack.pop())) elif(item=='/'): s_num=float(stack.pop()) b_num=float(stack.pop()) stack.append(b_num/s_num) else: stack.append(item) e.config(text=str(stack.pop())) except: e.config(text="Invalid Expression")
This calculator also has an undo feature that can come in handy in case of mistypes in big expressions. A global variable "history" is maintained, which stores the text entered in the label in the form of a string until a click on the clear button. On clicking the button marked "U", the "undo" function is called which replaces the text in the label with the string stored in history, up to the last element that was entered.
def undo(): global history e.config(text=history[:-1]) history=history[:-1]