Базовый калькулятор графического интерфейса Python с использованием tkinter

Может ли кто-нибудь просмотреть мой код калькулятора с помощью tkinter?

Я пытаюсь использовать два класса: один с функциями калькулятора, а второй – с графическим интерфейсом.

Кажется, он работает нормально (хотя я бы хотел добавить функциональность клавиатуры), поэтому я хотел бы спросить, есть ли что-нибудь, что я мог бы сделать лучше. Код основан на учебнике, который я нашел в Интернете. Алгоритм для evaluate также можно найти в Интернете, но я изменил его для работы с программой.

Моя программа около 400 строк, так что я действительно думаю, что есть способ ее улучшить?

Учебник Вот

Исходный оценочный код Вот

Вот мой код ..

import tkinter as tk  # Import tkinter library
import math  # Import math lib
import re  # Import this thing too


class Calculator:
    """
    A class used to implement the functions of a calculator
    """

    def __init__(self):
        self.answer = tk.StringVar()
        self.equation = tk.StringVar()
        self.expression = ""
        self.paren = False
        self.prev_expression = []
        self.itr = ""

    def set_prev_expr(self):
        """
        Stores all changes to the expression in a list
        """
        self.prev_expression.append(self.expression)

    def get_prev_expr(self):
        try:
            print("Getting last entry")
            self.expression = self.prev_expression.pop()
            self.equation.set(self.expression)
        except IndexError:
            print("No values in undo")
            self.answer.set("Can't undo")

    def clear(self):
        """
        Resets Variables used to defaults
        """
        self.set_prev_expr()
        print("Clearing")
        self.paren = False
        self.expression = ""
        self.answer.set(self.expression)
        self.equation.set(self.expression)
        self.itr = ""
        print("Clearing complete")

    def insert_paren(self):
        """
        Inserts paren into equation
        """
        self.set_prev_expr()
        if not self.paren:
            self.expression += "("
            self.equation.set(self.expression)
            self.paren = True  # Keeps track of paren orientation
            print(self.expression)
        else:
            self.expression += ")"
            self.paren = False
            self.equation.set(self.expression)
            print(self.expression)

    def percent(self):
        """
        divides expression by 100
        """
        self.set_prev_expr()
        self.expression += " / 100"
        self.evaluate(self.expression)

    def square(self):
        self.set_prev_expr()
        if True:  # If the last number is in paren applies to entire paren block
            match = re.findall('[[^]]*]|([^)]*)|"[^"]*"|S+', self.expression)
            print(match)
            try:
                last = float(self.evaluate(match.pop(-1)))
                self.expression = " ".join(match) + " " + str(math.pow(last, 2))  # Always pos, no chance of dash causing error
                print(self.expression)
                self.evaluate(self.expression)
            except:  # Any errors should be picked up by evaluate function so no need to print to screen
                print("Error")
                self.answer.set("Cannot Calculate Ans")

        

    def press(self, num: str):
        self.set_prev_expr()
        if num in ["*", "/", "+", "-"]:  # Adds spaces either side of operators. Special operators are handled separately
            self.expression = str(self.expression) + " " + str(num) + " "
        else:  # Negative is included here
            self.expression = str(self.expression) + str(num)
        self.equation.set(self.expression)
        print(self.expression)

    def square_root(self):
        self.set_prev_expr()
        if True:  # If the last number is in paren applies to entire paren block
            match = re.findall('[[^]]*]|([^)]*)|"[^"]*"|S+', self.expression)
            print(match)
            try:
                last = float(self.evaluate(match.pop(-1)))
                self.expression = " ".join(match) + " " + str(math.sqrt(last))
                print(self.expression)
                self.evaluate(self.expression)
            except ValueError:  # Should be called if try negative num
                print("Error")
                self.answer.set("Imaginary Answer")

    def backspace(self):
        self.set_prev_expr()
        if self.expression[-1] == ")":  # If you delete a paren re-add paren flag
            self.paren = True
        self.expression = self.expression[:-1]
        print(self.expression)
        self.equation.set(self.expression)

    # Function to find weight
    # of operators.
    def _weight(self, op):
        if op == '+' or op == '-':
            return 1
        if op == '*' or op == "https://codereview.stackexchange.com/":
            return 2
        return 0

    # Function to perform arithmetic
    # operations.
    def _arith(self, a, b, op):
        try:
            if op == '+':
                return a + b
            elif op == '-':
                return a - b
            elif op == '*':
                return a * b
            elif op == "https://codereview.stackexchange.com/":
                return a / b
            else:
                return None
        except ZeroDivisionError:
            print("Invalid Operation: Div by Zero")
            self.answer.set("ZeroDivisionError")
            return "ZeroDiv"

    # Function that returns value of
    # expression after evaluation.
    def evaluate(self, tokens: str):
        self.set_prev_expr()
        
        # adds support for negative numbers by adding a valid equation
        token_lst = tokens.split(" ")
        #print(token_lst)
        for index, elem in enumerate(token_lst):
            if "—" in elem:
                token_lst[index] = elem.replace("—", "(0 -") + ")" 
        #print(token_lst)
        tokens = " ".join(token_lst)
        print(tokens)
        
        # stack to store integer values.
        values = []

        # stack to store operators.
        ops = []
        i = 0

        while i < len(tokens):

            # Current token is a whitespace,
            # skip it.
            if tokens[i] == ' ':
                i += 1
                continue

            # Current token is an opening 
            # brace, push it to 'ops'
            elif tokens[i] == '(':
                ops.append(tokens[i])

            # Current token is a number or decimal point, push 
            # it to stack for numbers.
            elif (tokens[i].isdigit()) or (tokens[i] == "."):
                val = ""

                # There may be more than one
                # digits in the number.
                while (i < len(tokens) and
                       (tokens[i].isdigit() or tokens[i] == ".")):
                    val += str(tokens[i])
                    i += 1
                val = float(val)
                values.append(val)

                # right now the i points to 
                # the character next to the digit,
                # since the for loop also increases 
                # the i, we would skip one 
                #  token position; we need to 
                # decrease the value of i by 1 to
                # correct the offset.
                i -= 1

            # Closing brace encountered, 
            # solve entire brace.
            elif tokens[i] == ')':

                while len(ops) != 0 and ops[-1] != '(':
                    try:
                        val2 = values.pop()
                        val1 = values.pop()
                        op = ops.pop()
                    except IndexError:
                        print("Syntax Error")
                        self.answer.set("Syntax Error")
                        self.get_prev_expr()
                        self.get_prev_expr()  # Returns expr to previous state
                        return None

                    values.append(self._arith(val1, val2, op))
                    if values[-1] == "ZeroDiv":
                        return None

                # pop opening brace.
                ops.pop()

            # Current token is an operator.
            else:

                # While top of 'ops' has same or 
                # greater _weight to current 
                # token, which is an operator. 
                # Apply operator on top of 'ops' 
                # to top two elements in values stack.
                while (len(ops) != 0 and
                       self._weight(ops[-1]) >=
                       self._weight(tokens[i])):

                    try:
                        val2 = values.pop()
                        val1 = values.pop()
                        op = ops.pop()
                    except IndexError:
                        print("Syntax Error")
                        self.answer.set("Syntax Error")
                        self.get_prev_expr()  # Returns expr to previous state
                        self.get_prev_expr()
                        return None

                    values.append(self._arith(val1, val2, op))
                    if values[-1] == "ZeroDiv":
                        return None

                # Push current token to 'ops'.
                ops.append(tokens[i])

            i += 1

        # Entire expression has been parsed 
        # at this point, apply remaining ops 
        # to remaining values.
        while len(ops) != 0:

            try:
                val2 = values.pop()
                val1 = values.pop()
                op = ops.pop()
            except IndexError:
                print("Syntax Error")
                self.answer.set("Syntax Error")
                self.get_prev_expr()  # Returns expr to previous state
                self.get_prev_expr()
                return None

            values.append(self._arith(val1, val2, op))
            if values[-1] == "ZeroDiv":
                return None

        # Top of 'values' contains result,
        # return it.
        try:
            if (values[-1] % 1 == 0):  # Checks if the value has decimal
                values[-1] = int(values[-1])
            if (values[-1] >= 9.9e+8) or (values[-1] <= -9.9e+8):
                raise OverflowError
            values[-1] = round(values[-1], 10)  # rounds a decimal number to 10 digits (max on screen is 20)
            self.expression = str(values[-1])
            self.expression = self.expression.replace("-", "—")  # If the answer starts with a dash replace with neg marker
            self.equation.set(self.expression)
            self.answer.set(self.expression)

            return values[-1]
        except SyntaxError:
            print("Syntax Error")
            self.answer.set("Syntax Error")
            return None
        except OverflowError:
            print("Overflow")
            self.answer.set("Overflow")
            self.get_prev_expr() #Returns to previous state (for special funct) deletes extra step in normal ops
            self.get_prev_expr()
            return None


class CalcGui(Calculator):
    
    BOX_HEIGHT = 2
    BOX_WIDTH = 8
    CLEAR_COLOR = "#c2b2b2"
    SPECIAL_BUTTONS_COLOR = "#b1b1b1"
    OPERATOR_COLOR = "dark grey"
    NUM_COLOR = "#cfcaca"
 
    def __init__(self, main_win: object):
        self.main_win = main_win
        Calculator.__init__(self)
        self.create_text_canvas()
        self.create_button_canvas()

    def create_text_canvas(self):
        entry_canv = tk.Canvas(bg="blue")  # Contains the output screens
        entry1 = tk.Entry(entry_canv,
                          text=self.equation,
                          textvariable=self.equation,
                          width=20
                          )
        entry1.pack()

        ans_box = tk.Label(entry_canv,
                           textvariable=self.answer,
                           width=20
                           )
        ans_box.pack()

        entry_canv.pack()
    
    def create_button_canvas(self):
        self.buttons = [  # List of all button info
             #chr.    x  y  color                     command
            ("clear", 0, 0, self.CLEAR_COLOR          , self.clear             ),
            ("↺"   , 1, 0, self.SPECIAL_BUTTONS_COLOR, self.get_prev_expr     ),
            ("x²"   , 2, 0, self.SPECIAL_BUTTONS_COLOR, self.square            ),
            ("√x"   , 3, 0, self.SPECIAL_BUTTONS_COLOR, self.square_root       ),
            ("—"    , 0, 1, self.SPECIAL_BUTTONS_COLOR, lambda: self.press("—")),
            ("()"   , 1, 1, self.SPECIAL_BUTTONS_COLOR, self.insert_paren      ),
            ("%"    , 2, 1, self.SPECIAL_BUTTONS_COLOR, self.percent           ),
            ("÷"    , 3, 1, self.OPERATOR_COLOR       , lambda: self.press("/")),
            ("7"    , 0, 2, self.NUM_COLOR            , lambda: self.press("7")),
            ("8"    , 1, 2, self.NUM_COLOR            , lambda: self.press("8")),
            ("9"    , 2, 2, self.NUM_COLOR            , lambda: self.press("9")),
            ("x"    , 3, 2, self.OPERATOR_COLOR       , lambda: self.press("*")),
            ("4"    , 0, 3, self.NUM_COLOR            , lambda: self.press("4")),
            ("5"    , 1, 3, self.NUM_COLOR            , lambda: self.press("5")),
            ("6"    , 2, 3, self.NUM_COLOR            , lambda: self.press("6")),
            ("-"    , 3, 3, self.OPERATOR_COLOR       , lambda: self.press("-")),
            ("1"    , 0, 4, self.NUM_COLOR            , lambda: self.press("1")),
            ("2"    , 1, 4, self.NUM_COLOR            , lambda: self.press("2")),
            ("3"    , 2, 4, self.NUM_COLOR            , lambda: self.press("3")),
            ("+"    , 3, 4, self.OPERATOR_COLOR       , lambda: self.press("+")),
            ("⌫"   , 0, 5, self.NUM_COLOR            , self.backspace         ),
            ("0"    , 1, 5, self.NUM_COLOR            , lambda: self.press("0")),
            ("."    , 2, 5, self.NUM_COLOR            , lambda: self.press(".")),
            ("="    , 3, 5, "orange"             , lambda: self.evaluate(self.expression)),
            ]

        button_canv = tk.Canvas(bg="red")  # Contains Input buttons
        for (character, x, y, color, command) in self.buttons:
            button = tk.Button(button_canv, text= character, bg= color,  # Unique
                               relief= tk.RAISED, height= self.BOX_HEIGHT, width= self.BOX_WIDTH)  # Defaults
            button.grid(row= y, column= x)
            button.configure(command= command)
        button_canv.pack(padx=5, pady=5)


def main():
    main_win = tk.Tk()
    main_win.configure(background="light blue")
    main_win.title("Calculator")
    main_win.resizable(False, False)  # Becomes ugly if you resize it
    calculator = CalcGui(main_win)
    main_win.mainloop()


if __name__ == "__main__":
    main()
```

2 ответа
2

Отделение бизнеса от презентации

В основном это хорошо; Calculator эффективно ваш бизнес-уровень. Единственное, что меня немного пугает, это использование tk.StringVar в этом. Один из способов иметь “чистый” Calculator который имеет нулевые требования к tk, должен принимать ссылки на связанные функции на answer.set и equation.set в качестве аргументов вашего конструктора. Класс будет вызывать эти методы, но ему не нужно знать, что они привязаны к tk.

Модульный тест

Calculator хороший кандидат для модульного тестирования; вам, вероятно, даже не нужно ничего высмеивать. Так что попробуйте свои силы в этом.

Нахождение последнего совпадения

Этот:

        match = re.findall('[[^]]*]|([^)]*)|"[^"]*"|S+', self.expression)
        print(match)
        try:
            last = float(self.evaluate(match.pop(-1)))

нужно переосмыслить. Вы определенно не должны звонить findall а затем выбор последнего совпадения – это лишает смысла регулярные выражения. Добавить $ и любые другие промежуточные символы, которые вам нужны, и это будет соответствовать концу строки.

Установить тесты членства

Попробуйте изменить

if num in ["*", "/", "+", "-"]

к

if num in {"*", "/", "+", "-"}

и, возможно, сохранить этот набор как статический класс.

Форматирование

str(self.expression) + " " + str(num) + " "

возможно

f'{self.expression} {num} '

Подсказки по конкретному типу

main_win: object

не помогает. Поместите здесь точку останова и проверьте свой отладчик, чтобы узнать, что такое фактический тип виджета tk.

Изменение размера

main_win.resizable(False, False)  # Becomes ugly if you resize it

Без шуток. Это говорит не о том, что вы должны отключать изменение размера, а о том, что ваш макет выражен неправильно. Это не удивительно, поскольку они жестко запрограммированы:

BOX_HEIGHT = 2
BOX_WIDTH = 8

Я рекомендую вам провести небольшое исследование того, как tk представляет собой макет сетки пропорционального изменения размера.

  • Потратив некоторое время на попытки понять синтаксис re, а затем после сбоя и просмотра обмена стеками, я изменил соответствующий оператор, к которому я привык. for match in re.finditer(r'([^)]*)|S+$', self.expression): # gets last match pass rem_expression = self.expression[:match.span()[0]] + self.expression[match.span()[1]:] match = match.group() Это лучший способ использовать регулярные выражения? Я не мог найти способ, чтобы он попадал только в последний матч, он всегда показывал все вхождения.

    – Ragov


  • Можете ли вы показать пример строки, которую это регулярное выражение пытается проанализировать?

    – Райндериен

  • Что-то вроде этого "2 + (2 * 4) / (3 + 1)'' и он захватил бы "(3 + 1)" или же "2 + 34" и захватит “34”

    – Ragov


  • 1

    Думаю, я понял это после ночного сна. Мне нужен был $ перед оператором OR, потому что тот, который я применил только к последней части выражения …

    – Ragov

В вашем square и square_root функции:

    def square(self):
        self.set_prev_expr()
        if True:  # If the last number is in paren applies to entire paren block
            match = re.findall('[[^]]*]|([^)]*)|"[^"]*"|S+', self.expression)
            print(match)
            try:
                last = float(self.evaluate(match.pop(-1)))
                self.expression = " ".join(match) + " " + str(math.pow(last, 2))  # Always pos, no chance of dash causing error
                print(self.expression)
                self.evaluate(self.expression)
            except:  # Any errors should be picked up by evaluate function so no need to print to screen
                print("Error")
                self.answer.set("Cannot Calculate Ans")

и

    def square_root(self):
        self.set_prev_expr()
        if True:  # If the last number is in paren applies to entire paren block
            match = re.findall('[[^]]*]|([^)]*)|"[^"]*"|S+', self.expression)
            print(match)
            try:
                last = float(self.evaluate(match.pop(-1)))
                self.expression = " ".join(match) + " " + str(math.sqrt(last))
                print(self.expression)
                self.evaluate(self.expression)
            except ValueError:  # Should be called if try negative num
                print("Error")
                self.answer.set("Imaginary Answer")

Видишь ли, if True заявления бессмысленны, потому что True является всегда True, следовательно, блок будет выполнен независимо от значения self.set_prev_expr().

Вместо объединения строки вы можете использовать форматированную строку, которая позволит вам напрямую вставлять нестроковые значения в строку.

Таким образом, они были бы эквивалентом просто

    def square(self):
        self.set_prev_expr()
        match = re.findall('[[^]]*]|([^)]*)|"[^"]*"|S+', self.expression)
        print(match)
        try:
            last = float(self.evaluate(match.pop(-1)))
            self.expression = f'{" ".join(match)} {math.pow(last, 2)}' # Always pos, no chance of dash causing error
            print(self.expression)
            self.evaluate(self.expression)
        except:  # Any errors should be picked up by evaluate function so no need to print to screen
            print("Error")
            self.answer.set("Cannot Calculate Ans")

и

    def square_root(self):
        self.set_prev_expr()
        match = re.findall('[[^]]*]|([^)]*)|"[^"]*"|S+', self.expression)
        print(match)
        try:
            last = float(self.evaluate(match.pop(-1)))
            self.expression = f'{" ".join(match)} {math.sqrt(last)}'
            print(self.expression)
            self.evaluate(self.expression)
        except ValueError:  # Should be called if try negative num
            print("Error")
            self.answer.set("Imaginary Answer")

  • Спасибо, что ответили на этот вопрос. Я постараюсь использовать в своих программах больше отформатированных строк, я, честно говоря, не знал, что их можно использовать таким образом ».

    – Ragov

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *