Статьи
October 7

Пишем калькулятор на Python с помощью Flet

Введение

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

Чтобы создать привлекательное мобильное приложение, которое будет отлично работать на Android и iOS, обычно требуется значительная доработка существующих инструментов, таких как Kivy или Tkinter. Именно здесь на сцену выходит Flet — фреймворк, который позволяет легко создавать веб-, десктопные и мобильные приложения, используя Flutter, популярный инструмент для создания пользовательских интерфейсов от Google, но на языке Python.

Давайте посмотрим, как создать базовое приложение-калькулятор с помощью Flet, и увидим, насколько простым и эффективным может быть этот фреймворк.

Кто такой этот Flet?

Flet — фреймворк, который позволяет создавать пользовательские интерфейсы непосредственно с помощью инструментария Flutter.

В чём его преимущества?

  • Он сочетает в себе простоту Python и богатые возможности Flutter по созданию интерфейсов, позволяя быстро разрабатывать кроссплатформенные приложения, не требуя большого опыта работы с фронтендом.
  • Для работы с этим инструментом не требуется SDK, а его функционал может быть легко расширен с помощью Flutter SDK.

❕Обратите внимание: для успешного освоения данной темы будет полезно иметь общее представление о некоторых ключевых концепциях фронтенда, таких как блочная модель, макеты Flexbox и Grid, а также о способах позиционирования элементов. Хотя вы можете продолжать работу без глубокого понимания этих тем, всё же настоятельно рекомендуется хотя бы немного с ними ознакомиться.

Давайте наконец создадим наш калькулятор!

Настраиваем окружение

Прежде чем приступить к написанию кода, убедитесь, что на вашей машине установлен Python. Затем выполните следующие шаги, чтобы настроить среду для Flet.

  1. Устанавливаем Flet, например, при помощи pip. Открываем терминал или командную строку и вводим pip install flet
  2. Открываем любимый редактор кода (например, VSCode, Pycharm и т. д.) и создаём новый исполняемый Python-файл.

Давайте сначала проверим, всё ли работает, с помощью самой любимой фразы в сообществе разработчиков «Hello World!».
В нашем Python-файле вводим напишем код:

import flet as ft

def main(page: ft.Page):
    page.add(ft.Text(value="Hello, World!"))

ft.app(target=main)

Запускаем и проверяем, всё ли работает. Если видим на экране надпись «Hello, World!», значит, мы готовы перейти к созданию нашего калькулятора.

Создаём макет

Для начала займёмся структурой калькулятора. Используем виджет для создания колонок, которые будут выступать в роли дисплея и кнопок. Дисплей будет отображать текущий ввод, а кнопки позволят на них нажимать взаимодействовать с пользователем.

Напишем такой код:

from flet import (
    app, Page, Container, Column, Row,
    TextField, colors, border_radius, ElevatedButton, TextAlign, TextStyle
)


def main(page: Page):
    page.title = "Calculator"
    result = TextField(
        hint_text='0', text_size=20,
        color='white', text_align=TextAlign.RIGHT,
        hint_style=TextStyle(
            color=colors.WHITE, size=20
        ),
        read_only=True
    )

    def button_click(e):
        pass

    button_row0 = Row(
        [
            ElevatedButton(text='C',  on_click=button_click),
            ElevatedButton(text='^',  on_click=button_click),
            ElevatedButton(text='%',  on_click=button_click),
            ElevatedButton(text='/',  on_click=button_click),
        ]
    )
    button_row1 = Row(
        [
            ElevatedButton(text='7',  on_click=button_click),
            ElevatedButton(text='8',  on_click=button_click),
            ElevatedButton(text='9',  on_click=button_click),
            ElevatedButton(text='*',  on_click=button_click),
        ]
    )
    button_row2 = Row(
        [
            ElevatedButton(text='4',  on_click=button_click),
            ElevatedButton(text='5',  on_click=button_click),
            ElevatedButton(text='6',  on_click=button_click),
            ElevatedButton(text='-',  on_click=button_click),
        ]
    )
    button_row3 = Row(
        [
            ElevatedButton(text='1',  on_click=button_click),
            ElevatedButton(text='2',  on_click=button_click),
            ElevatedButton(text='3',  on_click=button_click),
            ElevatedButton(text='+',  on_click=button_click),
        ]
    )
    button_row4 = Row(
        [
            ElevatedButton(text='0',  on_click=button_click),
            ElevatedButton(text='.',  on_click=button_click),
            ElevatedButton(text='=',  on_click=button_click),
        ]
    )
    container = Container(
        width=350, padding=20,
        bgcolor=colors. BLACK,
        content=Column(
            [
                result,
                button_row0, button_row1, button_row2,
                button_row3, button_row4
            ]
        )
    )
    page.add(container)


if __name__ == '__main__':
    app(target=main)

После исполнения этого кода мы увидим макет калькулятора, который пока будет выглядеть не очень хорошо, но это не страшно! Мы улучшим его, добавив некоторые интервалы, скругление контейнера и тему, чтобы наш калькулятор выглядел более аккуратным.

Объяснение кода

Вот мы создали макет, но некоторые из вас не поняли, как мы это сделали. Давайте разбираться!

В первой строке нашего кода мы импортируем необходимые элементы управления. Эти элементы, называемые виджетами Flet, играют ключевую роль в создании пользовательского интерфейса нашего приложения. В данном случае мы импортировали такие важные компоненты, как: app, Page, Container, Column, Row, TextField, colors, border_radius, ElevatedButton, TextAlign, TextStyle.

Некоторые из них не являются полноценными виджетами, например, app, colors, border_radius, TextAlign и TextStyle. Это классы и методы, которые добавляют дополнительные функции в наше приложение.

Например, app позволяет нам запускать наше приложение в автономном режиме, ориентируясь на основной экземпляр. colors дает возможность стилизовать наши элементы управления, поддерживающие атрибуты color и bgcolor, без необходимости определять их имена. А border_radius дает возможность закруглять углы наших контейнеров.

В строке 7 мы определяем основной экземпляр нашего приложения как Page. Страница — это контейнер, в котором располагаются элементы управления View. Мы не будем углубляться в детали View, но можно ознакомиться с этим более подробно на официальном сайте.

Теперь мы установим заголовок нашей страницы, используя атрибут page.title. Этот заголовок будет отображаться в строке заголовка нашего приложения.

В строках с 9 по 16 находится блок result. Он обладает различными свойствами, но в этом проекте мы будем использовать лишь некоторые из них. Мы добавили текст «0», установили его размер равным 20, выбрали белый цвет и выровняли по правому краю. Кроме того, мы сделали этот текст недоступным для изменения. Это означает, что пользователи не смогут изменить его, используя клавиатуру.

В строке 18 мы определили обработчик события button_click. В этой функции мы будем применять логику для работы нашего приложения, превращая его в настоящий калькулятор. Но пока там просто стоит pass в качестве заглушки.

В строках с 21 по 59 мы определили наши строки, используя виджет Row. Виджет Row — это элемент управления, который отображает свои дочерние элементы в горизонтальном порядке, слева направо. Подобно линейному макету в разработке Android или линейным элементам в CSS, элемент управления Row работает так же, выстраивая элементы управления по горизонтальной оси.

Затем мы добавили ElevatedButton, который будет представлять кнопки в пользовательском интерфейсе калькулятора. Обратите внимание, что мы задали ему атрибуты text и _onclick. Атрибут text определяет данные, которые будут отображаться на результатах при нажатии, а атрибут onclick вызывает функцию button_click для обработки событий.

Ещё у нас есть контейнер. Он помогает сделать элемент управления красивым. Можно выбрать цвет фона, интервалы, границы и их радиус. Ещё можно расположить элемент управления с помощью padding, margin и выравнивания.

Контейнер следует концепции бокс-модели, подобно той, что используется в CSS, как показано на рисунке ниже:

Элемент управления Column работает как элемент управления Row. Он упорядочивает свои дочерние элементы, как будто выстраивает их в ряд сверху вниз. Так мы можем удобно расположить кнопки в нужном порядке.

После того как мы определили элементы пользовательского интерфейса, нам необходимо отобразить их в нашем приложении и затем вызвать его. Для этого мы используем метод page.add(), который позволяет нам добавлять и логически упорядочивать элементы пользовательского интерфейса.

Далее следует вызов нашего приложения в автономном режиме, что и было реализовано в строках 74-75.

Добавление функциональности

Обновите функцию нажатия на кнопку, чтобы она соответствовала приведенному ниже коду:

def button_click(e):
        if e.control.text == "=":
            try:
                result.value = str(eval(result.value))
            except Exception:
                result.value = "Error"
        elif e.control.text == "C":
            result.value = ""
        # elif e.control.text == "^":
        # logic for powers
        # pass
        else:
            result.value += e.control.text
        result.update()

Объяснение кода

Давайте рассмотрим, что происходит в этом коде. Функция button_click предназначена для обработки различных событий нажатия кнопок в нашем приложении-калькуляторе.

Вот краткое описание того, что в ней происходит:

Получение текста кнопки: Когда пользователь нажимает на кнопку, функция получает текст кнопки (например, '1', '2', '+', '-', 'C', '=') через e.control.text. Это позволяет ей определить, с какой именно кнопкой взаимодействовал пользователь.

Очистка дисплея: При нажатии на кнопку 'C' ввод калькулятора очищается. Результат обнуляется, а в дисплее устанавливается 0. Калькулятор будет готов к новым расчётам.

Расчёт выражений: Если пользователь нажимает кнопку «=», калькулятор должен рассчитать текущее математическое выражение. Для этого мы используем функции str() и eval(), причем первая преобразует результат в строку, а вторая вычисляет его и выводит на дисплей. Если выражение недействительно, будет сгенерировано исключение, и вместо него появится сообщение «Error».

Остальные кнопки: Для остальных кнопок, таких как числа и операторы, функция добавляет текст кнопки на дисплей (который изначально равен «0» или очищается при нажатии на 'C'). Она заменяет «0» на значение кнопки, если это возможно, или добавляет его в конец дисплея.


После обработки нажатия кнопки страница обновляется с помощью метода page.update(), чтобы отобразить новый ввод или результат на дисплее калькулятора. Каждый раз, когда вы нажимаете на кнопку и видите значение на дисплее или результат, именно этот метод page.update() срабатывает.

💀Примечание: Функция eval() может быть опасной, так как она выполняет любой код Python, а он может быть вредоносным. В более надёжном приложении лучше использовать другой, более безопасный способ.

🧠Упражнение: Проверьте свои знания

Попробуйте придумать, как бы вы обработали выражение экспоненты '^' так, чтобы при нажатии кнопки оно возвращало нужный результат. Например, если пользователь введет 2^2, на выходе должно получиться 4, 5^5 — 25, а 3^4 — 81. Вы поняли идею.
Расскажите, как вы решили эту задачу в комментариях 👇

Улучшение пользовательского интерфейса

Пока интерфейс у нас выглядит не так красиво, поэтому давайте его обновим и сделаем кнопки более привлекательными, используя следующий код:

button_row0 = Row(
    [
        ElevatedButton(text='C', expand=1, on_click=button_click,
                       bgcolor=colors.RED_ACCENT, color=colors.WHITE),
        ElevatedButton(text='^', expand=1, on_click=button_click,
                       bgcolor=colors.BLUE_ACCENT_100,
                       color=colors.RED_900
                       ),
        ElevatedButton(text='%', expand=1, on_click=button_click,
                       bgcolor=colors.BLUE_ACCENT_100,
                       color=colors.RED_900
                       ),
        ElevatedButton(text='/', expand=1, on_click=button_click,
                       bgcolor=colors.BLUE_ACCENT_100,
                       color=colors.RED_900
                       ),
    ]
)
button_row1 = Row(
    [
        ElevatedButton(text='7', expand=1, on_click=button_click),
        ElevatedButton(text='8', expand=1, on_click=button_click),
        ElevatedButton(text='9', expand=1, on_click=button_click),
        ElevatedButton(text='*', expand=1, on_click=button_click,
                       bgcolor=colors.BLUE_ACCENT_100,
                       color=colors.RED_900
                       ),
    ]
)
button_row2 = Row(
    [
        ElevatedButton(text='4', expand=1, on_click=button_click),
        ElevatedButton(text='5', expand=1, on_click=button_click),
        ElevatedButton(text='6', expand=1, on_click=button_click),
        ElevatedButton(text='-', expand=1, on_click=button_click, 
                       bgcolor=colors.BLUE_ACCENT_100
                       ),
    ]
)
button_row3 = Row(
    [
        ElevatedButton(text='1', expand=1, on_click=button_click),
        ElevatedButton(text='2', expand=1, on_click=button_click),
        ElevatedButton(text='3', expand=1, on_click=button_click),
        ElevatedButton(text='+', expand=1, on_click=button_click, 
                       bgcolor=colors.BLUE_ACCENT_100,
                       color=colors.RED_900),
    ]
)
button_row4 = Row(
    [
        ElevatedButton(text='0', expand=1, on_click=button_click),
        ElevatedButton(text='.', expand=1, on_click=button_click),
        ElevatedButton(
            text='=', expand=2, on_click=button_click,
            bgcolor=colors.GREEN_ACCENT, color=colors.AMBER
        ),
    ]
)

Что же именно мы изменили?

Для кнопок мы могли бы использовать атрибут width, но это не дало бы желаемого результата и могло бы нарушить пользовательский интерфейс. Вы можете сами убедиться в этом, если попробуете.

Однако у нас есть альтернативный вариант — атрибут expand. Он позволяет задавать значения только двух типов данных: Boolean и int.

Для обычных кнопок, таких как операторы, числа и кнопка очистки ввода, мы увеличили значение expand на 1, а для кнопки равно — на 2.

Теперь о том, что делает атрибут expand. Этот атрибут позволяет элементу управления заполнять свободное пространство в заданном контейнере. Таким образом, кнопки с expand 1 будут иметь одинаковую ширину, а кнопка "равно" расширится на 2, что означает, что ее размер будет равен ширине двух кнопок.

Обратите внимание, что мы добавили цвета и фоновые цвета к некоторым из наших кнопок, чтобы они выделялись на фоне кнопок с цифрами.

И ещё давайте добавим закругление в контейнер сразу поле атрибута padding так:border_radius=border_radius.all(20),

Теперь у вас есть полнофункциональный калькулятор, созданный с помощью Flet! Попробуйте донастроить его по своему вкусу или добавить дополнительные функции. Также можно упаковать его как отдельный APK, AAB для загрузки в Google Play Store или Apple App Store.

Вот полный код:

from flet import (
    app, Page, Container, Column, Row,
    TextField, colors, border_radius, ElevatedButton, TextAlign, TextStyle
)
from flet_core import ThemeMode


def main(page: Page):
    page.title = "Calculator"
    page.theme_mode = ThemeMode.DARK
    page.horizontal_alignment = page.vertical_alignment = 'center'
    result = TextField(
        hint_text='0', text_size=20,
        color='white', text_align=TextAlign.RIGHT,
        hint_style=TextStyle(
            color=colors.WHITE, size=20
        ),
        read_only=True
    )

    def button_click(e):
        if e.control.text == "=":
            try:
                result.value = str(eval(result.value))
            except Exception:
                result.value = "Error"
        elif e.control.text == "C":
            result.value = ""
        # elif e.control.text == "^":
        # logic for powers
        # pass
        else:
            result.value += e.control.text
        result.update()

    button_row0 = Row(
        [
            ElevatedButton(text='C', expand=1, on_click=button_click,
                           bgcolor=colors.RED_ACCENT, color=colors.WHITE),
            ElevatedButton(text='^', expand=1, on_click=button_click,
                           bgcolor=colors.BLUE_ACCENT_100,
                           color=colors.RED_900
                           ),
            ElevatedButton(text='%', expand=1, on_click=button_click,
                           bgcolor=colors.BLUE_ACCENT_100,
                           color=colors.RED_900
                           ),
            ElevatedButton(text='/', expand=1, on_click=button_click,
                           bgcolor=colors.BLUE_ACCENT_100,
                           color=colors.RED_900
                           ),
        ]
    )
    button_row1 = Row(
        [
            ElevatedButton(text='7', expand=1, on_click=button_click),
            ElevatedButton(text='8', expand=1, on_click=button_click),
            ElevatedButton(text='9', expand=1, on_click=button_click),
            ElevatedButton(text='*', expand=1, on_click=button_click,
                           bgcolor=colors.BLUE_ACCENT_100,
                           color=colors.RED_900
                           ),
        ]
    )
    button_row2 = Row(
        [
            ElevatedButton(text='4', expand=1, on_click=button_click),
            ElevatedButton(text='5', expand=1, on_click=button_click),
            ElevatedButton(text='6', expand=1, on_click=button_click),
            ElevatedButton(text='-', expand=1, on_click=button_click, 
                           bgcolor=colors.BLUE_ACCENT_100
                           ),
        ]
    )
    button_row3 = Row(
        [
            ElevatedButton(text='1', expand=1, on_click=button_click),
            ElevatedButton(text='2', expand=1, on_click=button_click),
            ElevatedButton(text='3', expand=1, on_click=button_click),
            ElevatedButton(text='+', expand=1, on_click=button_click, 
                           bgcolor=colors.BLUE_ACCENT_100,
                           color=colors.RED_900),
        ]
    )
    button_row4 = Row(
        [
            ElevatedButton(text='0', expand=1, on_click=button_click),
            ElevatedButton(text='.', expand=1, on_click=button_click),
            ElevatedButton(
                text='=', expand=2, on_click=button_click,
                bgcolor=colors.GREEN_ACCENT, color=colors.AMBER
            ),
        ]
    )
    container = Container(
        width=350, padding=20,
        bgcolor=colors.BLACK, border_radius=border_radius.all(20),
        content=Column(
            [
                result,
                button_row0, button_row1, button_row2,
                button_row3, button_row4
            ]
        )
    )
    page.add(container)


if __name__ == '__main__':
    app(target=main)

👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶

Источник: dev.to