Simple GUI calculator in Python # 2

Stosh. In the last article, we designed a calculator. Well, why do we need this bare design without functionality, right?

Importing libraries following the style PEP 8:

import sys

from PySide6.QtWidgets import QApplication, QMainWindow

from design import Ui_MainWindow

Let’s write the default code to run any Qt application with a design file. If you would like to learn more about how each line of code works, I invite you to visit documentation.

class Calculator(QMainWindow):
	def __init__(self):
		super(Calculator, self).__init__()
		self.ui = Ui_MainWindow()
		self.ui.setupUi(self)
        
if __name__ == "__main__":
	app = QApplication(sys.argv)

	window = Calculator()
	window.show()

	sys.exit(app.exec())  

If you do not have a font installed on your system Rubik, then in your application the font will be the default. To solve this problem, you do not need to install the font on the system. We import:

from PySide6.QtGui import QFontDatabase

Now we will use the method of adding the application font to which we will transfer the font file. I did it in the constructor of the class.

QFontDatabase.addApplicationFont("fonts/Rubik-Regular.ttf")

First, let’s create a method for adding a number to an input field.

def add_digit(self):
	btn = self.sender()

Method sender () returns a Qt object that sends signal.

def sender(self): # real signature unknown; restored from __doc__
	""" sender(self) -> PySide6.QtCore.QObject """
	pass

In our case, the signal is a push of a button. Let’s create a tuple with the names of the number buttons.

digit_buttons = ('btn_0', 'btn_1', 'btn_2', 'btn_3', 'btn_4',
                 'btn_5', 'btn_6', 'btn_7', 'btn_8', 'btn_9')

By default, the field is always 0. In this case, if a button with a number is pressed, the field text is replaced with this number. It turns out that when you click on 0 nothing will happen.

if btn.objectName() in digit_buttons:
	if self.ui.le_entry.text() == '0':
		self.ui.le_entry.setText(btn.text())

If the field is not 0, then just add the text of the pressed digit to the field string.

Full code of the digit addition method:

def add_digit(self):
	btn = self.sender()

	digit_buttons = ('btn_0', 'btn_1', 'btn_2', 'btn_3', 'btn_4',
                   'btn_5', 'btn_6', 'btn_7', 'btn_8', 'btn_9')

	if btn.objectName() in digit_buttons:
		if self.ui.le_entry.text() == '0':
			self.ui.le_entry.setText(btn.text())
		else:
			self.ui.le_entry.setText(self.ui.le_entry.text() + btn.text())

Now we need to connect button presses to this method. Let’s write in the constructor of the class.

# digits
self.ui.btn_0.clicked.connect(self.add_digit)
self.ui.btn_1.clicked.connect(self.add_digit)
self.ui.btn_2.clicked.connect(self.add_digit)
self.ui.btn_3.clicked.connect(self.add_digit)
self.ui.btn_4.clicked.connect(self.add_digit)
self.ui.btn_5.clicked.connect(self.add_digit)
self.ui.btn_6.clicked.connect(self.add_digit)
self.ui.btn_7.clicked.connect(self.add_digit)
self.ui.btn_8.clicked.connect(self.add_digit)
self.ui.btn_9.clicked.connect(self.add_digit)
Initially, there was such code, and it seems to me that it worked faster.
def add_digit(self, btn_text: str) -> None:
	if self.ui.le_entry.text() == '0':
		self.ui.le_entry.setText(btn_text)
	else:
		self.ui.le_entry.setText(self.ui.le_entry.text() + btn_text)

Button connections with a method

# digits
self.ui.btn_0.clicked.connect(lambda: self.add_digit('0'))
self.ui.btn_1.clicked.connect(lambda: self.add_digit('1'))
self.ui.btn_2.clicked.connect(lambda: self.add_digit('2'))
self.ui.btn_3.clicked.connect(lambda: self.add_digit('3'))
self.ui.btn_4.clicked.connect(lambda: self.add_digit('4'))
self.ui.btn_5.clicked.connect(lambda: self.add_digit('5'))
self.ui.btn_6.clicked.connect(lambda: self.add_digit('6'))
self.ui.btn_7.clicked.connect(lambda: self.add_digit('7'))
self.ui.btn_8.clicked.connect(lambda: self.add_digit('8'))
self.ui.btn_9.clicked.connect(lambda: self.add_digit('9'))

Let’s see the result.

If it hurts your eyes that the numbers go beyond the boundaries of the field, be patient. We will solve this problem in the next article.

Now let’s write a method to clear the field and label.

def clear_all(self) -> None:
	self.ui.le_entry.setText('0')
	self.ui.lbl_temp.clear()

Let’s do the same method for clearing only the fields.

def clear_entry(self) -> None:
	self.ui.le_entry.setText('0')

We connect.

# actions
self.ui.btn_clear.clicked.connect(self.clear_all)
self.ui.btn_ce.clicked.connect(self.clear_entry)

Let’s write a method for adding a point. Why a dot at all and not a comma? A simple number with a dot can be immediately converted into a real number, and you will have to change the sign with a comma. Yes, I’m too lazy.

The logic is simple. If there is no point in the input field, then we add.

def add_point(self) -> None:
	if '.' not in self.ui.le_entry.text():
		self.ui.le_entry.setText(self.ui.le_entry.text() + '.')

Connect what? That’s right, nay.

self.ui.btn_point.clicked.connect(self.add_point)

Let’s create a method for adding a temporary expression. What is it in general? There are two types of temporary expressions:

1) Number and mathematical sign. Roughly speaking, this is the memory of a calculator.

2) Equality

def add_temp(self) -> None:
	btn = self.sender()

First, we need to make sure there is no text in the label. Then we put in the temporary expression the number from the input field + the text of the button btn.

if not self.ui.lbl_temp.text():
	self.ui.lbl_temp.setText(self.ui.le_entry.text() + f' {btn.text()} ')

You also need to clear the input field. Full method code:

def add_temp(self) -> None:
	btn = self.sender()

	if not self.ui.lbl_temp.text():
		self.ui.lbl_temp.setText(self.ui.le_entry.text() + f' {btn.text()} ')
		self.ui.le_entry.setText('0')

For now, screw on one folding button for the dough.

self.ui.btn_add.clicked.connect(self.add_temp)

The period and trailing zeros are not truncated.

Let’s make a static method to solve this problem. We will pass a string number to the function, get the same thing.

@staticmethod
def remove_trailing_zeros(num: str) -> str:

Let’s introduce the variable n, which casts the argument first to float, then to string.

n = str(float(num))

Casting to float truncates zeros, but not everything. In the end remains .0… We will return a slice of the string without the last two characters if they are equal .0, otherwise we will simply return n.

return n[:-2] if n[-2:] == '.0' else n

Full method code:

@staticmethod
def remove_trailing_zeros(num: str) -> str:
	n = str(float(num))
	return n[:-2] if n[-2:] == '.0' else n

Now let’s add the trimming of insignificant zeros to the method for adding a temporary expression:

def add_temp(self) -> None:
	btn = self.sender()
	entry = self.remove_trailing_zeros(self.ui.le_entry.text())

	if not self.ui.lbl_temp.text():
		self.ui.lbl_temp.setText(entry + f' {btn.text()} ')
		self.ui.le_entry.setText('0')
Old code passing a sign-argument
def add_temp(self, math_sign: str):
	if not self.ui.lbl_temp.text() or self.get_math_sign() == '=':
		self.ui.lbl_temp.setText(
      self.remove_trailing_zeros(self.ui.le_entry.text()) + f' {math_sign} ')
		self.ui.le_entry.setText('0')

Let’s create a method to get a number from an input field. Let’s write the field text to the variable, remove the potential point using strip ().

We return float, if the point is in the variable, otherwise we return int, that is, an integer.

def get_entry_num(self):
	entry = self.ui.le_entry.text().strip('.')
	return float(entry) if '.' in entry else int(entry)

Let’s add type hint to the method. It can only return an integer or real number. To do this, import:

from typing import Union, Optional

We’ll use Optional later.

def get_entry_num(self) -> Union[int, float]:
In Python 3.10, you don’t need to import anything.

You can just write

def get_entry_num(self) -> int | float:

Let’s create a method to get a number from a temporary expression. If there is text in the label, we get it, separate it by spaces and take the first element, that is, the number.

def get_temp_num(self):
	if self.ui.lbl_temp.text():
		temp = self.ui.lbl_temp.text().strip('.').split()[0]
		return float(temp) if '.' in temp else int(temp)

Type hint here – Union[int, float, None]…

def get_temp_num(self) -> Union[int, float, None]:

Let’s create a method to get a sign from a temporary expression. To get the mark, we need to make sure there is text in the label, then get the text out of it, split by spaces and pull out the last element.

def get_math_sign(self):
	if self.ui.lbl_temp.text():
		return self.ui.lbl_temp.text().strip('.').split()[-1]

Type hint here – Optional[str]… This means that the method can return either a string or nothing. How Union[str, None], only more compact and more readable.

def get_math_sign(self) -> Optional[str]:

So, the calculator should count, do I understand correctly? Well then, let’s import addition, subtraction, multiplication and division from the standard library. operator.

from operator import add, sub, mul, truediv

Now let’s create a dictionary with operations. Let us assign its logical function to each sign.

operations = {
    '+': add,
    '−': sub,
    '×': mul,
    '/': truediv
}

Let’s create a calculation method.

def calculate(self):
	entry = self.ui.le_entry.text()
	temp = self.ui.lbl_temp.text()

If there is text in the label, enter the result variable. Cut off trailing zeros, convert to a string. We take the operation from the dictionary by sign, in parentheses we indicate with which numbers to perform the operation. Note that the order in which arguments are passed is important for division and subtraction. First, we pass the number from the temporary expression, and then from the input field.

if temp:
	result = self.remove_trailing_zeros(
		str(operations[self.get_math_sign()](self.get_temp_num(), self.get_entry_num())))

Add a number from the input field and a sign to the label =

self.ui.lbl_temp.setText(temp + self.remove_trailing_zeros(entry) + ' =')

We put the result in the input field and return it.

self.ui.le_entry.setText(result)
return result

Type hint – Optional[str]…

def calculate(self) -> Optional[str]:

Full code of the calculation method:

def calculate(self) -> Optional[str]:
	entry = self.ui.le_entry.text()
	temp = self.ui.lbl_temp.text()

	if temp:
		result = self.remove_trailing_zeros(
			str(operations[self.get_math_sign()](self.get_temp_num(), self.get_entry_num())))
		self.ui.lbl_temp.setText(temp + self.remove_trailing_zeros(entry) + ' =')
		self.ui.le_entry.setText(result)
		return result

We attach.

# math
self.ui.btn_calc.clicked.connect(self.calculate)

Finally, let’s write a math operation function.

def math_operation(self):
	temp = self.ui.lbl_temp.text()
	btn = self.sender()

If there is no expression in the label, we add it, amazing.

if not temp:
	self.add_temp()

If there is an expression, we take the sign. If it is not equal to the sign of the pressed button, then there are two cases. The first is equality. In this case, we just add a temporary expression. Otherwise, we change the sign of the expression to the sign of the pressed button.

else:
	if self.get_math_sign() != btn.text():
		if self.get_math_sign() == '=':
			self.add_temp()
		else:
			self.ui.lbl_temp.setText(temp[:-2] + f'{btn.text()} ')

If the sign is equal to the sign of the pressed button, then we calculate the expression and add this sign to the end of the label.

Full method code:

def math_operation(self) -> None:
	temp = self.ui.lbl_temp.text()
	btn = self.sender()

	if not temp:
		self.add_temp()
	else:
		if self.get_math_sign() != btn.text():
			if self.get_math_sign() == '=':
				self.add_temp()
      else:
        self.ui.lbl_temp.setText(temp[:-2] + f'{btn.text()} ')
    else:
			self.ui.lbl_temp.setText(self.calculate() + f' {btn.text()}')

We connect.

self.ui.btn_add.clicked.connect(self.math_operation)
self.ui.btn_sub.clicked.connect(self.math_operation)
self.ui.btn_mul.clicked.connect(self.math_operation)
self.ui.btn_div.clicked.connect(self.math_operation)
Signed Argument Evaluation Method Old Code
def math_operation(self, math_sign: str):
	temp = self.ui.lbl_temp.text()

	if not temp:
		self.add_temp(math_sign)
	else:
		if self.get_math_sign() != math_sign:
			if self.get_math_sign() == '=':
				self.add_temp(math_sign)
			else:
				self.ui.lbl_temp.setText(temp[:-2] + f'{math_sign} ')
		else:
			self.ui.lbl_temp.setText(self.calculate() + f' {math_sign}')
self.ui.btn_add.clicked.connect(lambda: self.math_operation('+'))
self.ui.btn_sub.clicked.connect(lambda: self.math_operation('−'))
self.ui.btn_mul.clicked.connect(lambda: self.math_operation('×'))
self.ui.btn_div.clicked.connect(lambda: self.math_operation('/'))

Let’s pray for the health of Guido Van Rossum and start the program.

For some reason, he does not want to continue to count with equality. I’ll tell you why. In the method for adding a temporary expression, you need to add an additional condition. As a result, it will turn out “if there is no temporary expression or there is equality.”

def add_temp(self) -> None:
	btn = self.sender()
	entry = self.remove_trailing_zeros(self.ui.le_entry.text())

	if not self.ui.lbl_temp.text() or self.get_math_sign() == '=':
		self.ui.lbl_temp.setText(entry + f' {btn.text()} ')
		self.ui.le_entry.setText('0')

And here’s another show how the sign changes if you constantly miss the button.

Stosh, in the next article we will add a calculator. Let’s do negation, backspace, multiple shortcuts for one button and handle errors. See you.


GitHub repository

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *