Перейти к содержанию

Flet: основные элементы#

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

Вы можете построить пользовательский интерфейс (UI) для вашей программы с помощью элементов управления Flet, которые основаны на Flutter от Google.

Flet — это не просто обёртка над виджетами Flutter. Он добавляет свой собственный подход, объединяя мелкие виджеты, упрощая сложные моменты, реализуя лучшие практики UI и применяя разумные настройки по умолчанию.

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

Пример приложения Flet#

import flet as ft

def main(page: ft.Page):
    page.title = "Flet counter example"
    page.vertical_alignment = ft.MainAxisAlignment.CENTER

    txt_number = ft.TextField(value="0", text_align=ft.TextAlign.RIGHT, width=100)

    def minus_click(e):
        txt_number.value = str(int(txt_number.value) - 1)
        page.update()

    def plus_click(e):
        txt_number.value = str(int(txt_number.value) + 1)
        page.update()

    page.add(
        ft.Row(
            [
                ft.IconButton(ft.Icons.REMOVE, on_click=minus_click),
                txt_number,
                ft.IconButton(ft.Icons.ADD, on_click=plus_click),
            ],
            alignment=ft.MainAxisAlignment.CENTER,
        )
    )

ft.app(main)

Tip

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

flet run --web counter.py

Подготовка дирректории#

Создайте новый каталог (или каталог с уже существующим файлом pyproject.toml, если он был инициализирован с помощью poetry или uv) и переключитесь на него. Чтобы создать новое «минимальное» приложение Flet, выполните следующую команду:

flet create

Команда создаст следующую структуру каталогов:

├── README.md
├── pyproject.toml
├── src
│   ├── assets
│   │   └── icon.png
│   └── main.py
└── storage
    ├── data
    └── temp

Оригинальный файл pyproject.toml, созданный с помощью uv init или poetry init, будет заменён на файл из шаблона приложения Flet.

Если вы столкнётесь с ошибкой
Error creating the project from a template: 'git' is not installed.,
это означает, что у вас не установлен Git.

Пожалуйста, посетите сайт git-scm.com/downloads и установите последнюю версию Git.
Чтобы проверить установку, введите git в терминале.

Обратите внимание, что Git — это не то же самое, что GitHub CLI, который не является альтернативой для работы с Flet.

Файл src/main.py содержит программу на Flet. В нём есть функция main(), в которой вы добавляете элементы интерфейса (элементы управления) на страницу или окно.

Приложение завершается вызовом блокирующей функции ft.app(), которая инициализирует приложение Flet и запускает функцию main().

Элементы управления Flet#

Пользовательский интерфейс состоит из элементов управления (также называемых виджетами). Чтобы элементы управления были видны пользователю, их нужно добавить на страницу (Page) или внутрь других элементов управления.

Page — это самый верхний элемент управления. Вложенность элементов управления друг в друга можно представить в виде дерева, где Page является корнем.

Элементы управления — это обычные классы Python. Создавайте их экземпляры через конструкторы с параметрами, соответствующими их свойствам, например:

t = ft.Text(value="Hello, world!", color="green")

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

import flet as ft

def main(page: ft.Page):
    t = ft.Text(value="Hello, world!", color="green")
    page.controls.append(t)
    page.update()

ft.app(main)

Мониторинг порта

Note

В следующих примерах мы будем показывать только содержимое функции main.

Вы можете изменить свойства элемента управления, и пользовательский интерфейс будет обновлен на следующей page.update():

t = ft.Text()
page.add(t) # Это сокращение для page.controls.append(t), а затем page.update()

for i in range(10):
    t.value = f"Step {i}"
    page.update()
    time.sleep(1)

Некоторые элементы управления являются «контейнерными» (например, Page), то есть могут содержать другие элементы управления. Например, элемент управления Row позволяет размещать другие элементы управления в ряд один за другим.

page.add(
    ft.Row(controls=[
        ft.Text("A"),
        ft.Text("B"),
        ft.Text("C")
    ])
)

или TextField и ElevatedButton рядом с ним:

page.add(
    ft.Row(controls=[
        ft.TextField(label="Your name"),
        ft.ElevatedButton(text="Say my name!")
    ])
)

Метод page.update() достаточно умен, чтобы отправлять только те изменения, которые произошли с момента последнего вызова. Таким образом, вы можете добавить на страницу несколько новых элементов управления, удалить некоторые из них, изменить свойства других элементов управления, а затем вызвать page.update(), чтобы выполнить пакетное обновление, например:

for i in range(10):
    page.controls.append(ft.Text(f"Line {i}"))
    if i > 4:
        page.controls.pop(0)
    page.update()
    time.sleep(0.3)

Некоторые элементы управления, например кнопки, могут иметь обработчики событий, реагирующие на действия пользователя, например ElevatedButton.on_click:

def button_clicked(e):
    page.add(ft.Text("Clicked!"))

page.add(ft.ElevatedButton(text="Click me", on_click=button_clicked))

и более сложный пример простого списка дел:

import flet as ft

def main(page):
    def add_clicked(e):
        page.add(ft.Checkbox(label=new_task.value))
        new_task.value = ""
        new_task.focus()
        new_task.update()

    new_task = ft.TextField(hint_text="What's needs to be done?", width=300)
    page.add(ft.Row([new_task, ft.ElevatedButton("Add", on_click=add_clicked)]))

ft.app(main)

Мониторинг порта

Note

Flet реализует **императивную модель UI**, где вы «вручную» создаёте пользовательский интерфейс приложения с использованием состояний элементов управления, а затем изменяете его, обновляя свойства этих элементов.

Flutter, напротив, использует **декларативную модель**, при которой UI автоматически перестраивается при изменении данных приложения.

Управление состоянием приложения в современных фронтенд-приложениях — задача по своей природе сложная, и поэтому «старомодный» подход Flet может быть более привлекательным для разработчиков без опыта во фронтенде.

Свойство visible#

У каждого элемента управления есть свойство visible, которое по умолчанию равно true — элемент отображается на странице.
Если установить visible в false, элемент (а также все его дочерние элементы, если они есть) полностью перестанет отображаться на странице.
Скрытые элементы нельзя выделить или сфокусировать с помощью клавиатуры или мыши, и они не генерируют никакие события.


Свойство disabled#

У каждого элемента управления есть свойство disabled, которое по умолчанию равно false — элемент и все его дочерние элементы активны.
Свойство disabled чаще всего используется для элементов ввода данных, таких как TextField, Dropdown, Checkbox, кнопки и другие.
При установке disabled в true у родительского элемента это значение рекурсивно применяется ко всем его дочерним элементам.

Например, если у вас есть форма с несколькими полями для ввода, вы можете установить свойство disabled для каждого поля отдельно:

    first_name = ft.TextField()
    last_name = ft.TextField()
    first_name.disabled = True
    last_name.disabled = True
    page.add(first_name, last_name)

или вы можете поместить элементы управления формой в контейнер, например в Column, а затем отключить этот столбец:

first_name = ft.TextField()
last_name = ft.TextField()
c = ft.Column(controls=[
    first_name,
    last_name
])
c.disabled = True
page.add(c)

Куда же без кнопок?#

Кнопка(Button) — это самый важный элемент управления, который при нажатии генерирует событие клика.

btn = ft.ElevatedButton("Click me!")
page.add(btn)

Все события, генерируемые элементами управления на веб-странице, постоянно передаются обратно в ваш скрипт. Как же реагировать на нажатие кнопки?

Обработчики событий#

Кнопки с событиями в приложении «Счётчик»:

import flet as ft

def main(page: ft.Page):
    page.title = "Flet counter example"
    page.vertical_alignment = ft.MainAxisAlignment.CENTER

    txt_number = ft.TextField(value="0", text_align="right", width=100)

    def minus_click(e):
        txt_number.value = str(int(txt_number.value) - 1)
        page.update()

    def plus_click(e):
        txt_number.value = str(int(txt_number.value) + 1)
        page.update()

    page.add(
        ft.Row(
            [
                ft.IconButton(ft.Icons.REMOVE, on_click=minus_click),
                txt_number,
                ft.IconButton(ft.Icons.ADD, on_click=plus_click),
            ],
            alignment=ft.MainAxisAlignment.CENTER,
        )
    )

ft.app(main)

Мониторинг порта

Textbox#

Flet предоставляет несколько элементов управления для создания форм: TextField, Checkbox, Dropdown, ElevatedButton.

Например, спросим у пользователя имя:

import flet as ft

def main(page):
    def btn_click(e):
        if not txt_name.value:
            txt_name.error_text = "Please enter your name"
            page.update()
        else:
            name = txt_name.value
            page.clean()
            page.add(ft.Text(f"Hello, {name}!"))

    txt_name = ft.TextField(label="Your name")

    page.add(txt_name, ft.ElevatedButton("Say hello!", on_click=btn_click))

ft.app(main)

Мониторинг порта

Checkbox#

Элемент управления Checkbox предоставляет разные свойства и обработчики событий для удобства использования.

Создадим простой чекбокс ToDo:

import flet as ft

def main(page):
    def checkbox_changed(e):
        output_text.value = (
            f"Вы отметили задачу 'Научиться кататься на лыжах': {todo_check.value}."
        )
        page.update()

    output_text = ft.Text()
    todo_check = ft.Checkbox(
        label="ToDo: Научиться кататься на лыжах", 
        value=False, 
        on_change=checkbox_changed
    )
    page.add(todo_check, output_text)

ft.app(main)

Мониторинг порта

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

import flet as ft


def main(page: ft.Page):
    def button_clicked(e):
        output_text.value = f"Dropdown value is:  {color_dropdown.value}"
        page.update()

    output_text = ft.Text()
    submit_btn = ft.ElevatedButton(text="Submit", on_click=button_clicked)
    color_dropdown = ft.Dropdown(
        width=100,
        options=[
            ft.dropdown.Option("Red"),
            ft.dropdown.Option("Green"),
            ft.dropdown.Option("Blue"),
        ],
    )
    page.add(color_dropdown, submit_btn, output_text)

ft.app(main)

Мониторинг порта

Пользовательские элементы управления#

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

Вы можете создавать кастомные элементы управления в Python, стилизуя и/или комбинируя существующие элементы Flet.

Стилизованные элементы управления#

Самый простой кастомный элемент управления — это стилизованный элемент, например, кнопка с определённым цветом и поведением, которую вы будете использовать многократно в приложении.

Чтобы создать такой стилизованный элемент, нужно определить новый класс в Python, который наследуется от элемента Flet, который вы хотите кастомизировать. В данном случае — от ElevatedButton:

class MyButton(ft.ElevatedButton):
    def __init__(self, text):
        super().__init__()
        self.bgcolor = ft.Colors.ORANGE_300
        self.color = ft.Colors.GREEN_800
        self.text = text     

В вашем элементе управления есть конструктор, который позволяет настраивать свойства, события и передавать пользовательские данные. Обратите внимание, что в своём конструкторе обязательно нужно вызвать super().init(), чтобы получить доступ к свойствам и методам родительского элемента Flet, от которого вы наследуетесь.

Теперь вы можете использовать свой новый элемент управления в приложении:

import flet as ft

def main(page: ft.Page):

    page.add(MyButton(text="OK"), MyButton(text="Cancel"))

ft.app(main)

Обработка событий#

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

import flet as ft

class MyButton(ft.ElevatedButton):
    def __init__(self, text, on_click):
        super().__init__()
        self.bgcolor = ft.Colors.ORANGE_300
        self.color = ft.Colors.GREEN_800
        self.text = text
        self.on_click = on_click

def main(page: ft.Page):

    def ok_clicked(e):
        print("OK clicked")

    def cancel_clicked(e):
        print("Cancel clicked")

    page.add(
        MyButton(text="OK", on_click=ok_clicked),
        MyButton(text="Cancel", on_click=cancel_clicked),
    )

ft.app(main)

Составные элементы управления#

Составные пользовательские элементы управления наследуются от контейнерных элементов управления, таких как Column, Row, Stack или даже View, и позволяют объединять несколько элементов управления Flet. Ниже приведен пример элемента управления Task, который можно использовать в приложении "Список дел":

import flet as ft
class Task(ft.Row):
    def __init__(self, text):
        super().__init__()
        self.text_view = ft.Text(text)
        self.text_edit = ft.TextField(text, visible=False)
        self.edit_button = ft.IconButton(icon=ft.Icons.EDIT, on_click=self.edit)
        self.save_button = ft.IconButton(
            visible=False, icon=ft.Icons.SAVE, on_click=self.save
        )
        self.controls = [
            ft.Checkbox(),
            self.text_view,
            self.text_edit,
            self.edit_button,
            self.save_button,
        ]

    def edit(self, e):
        self.edit_button.visible = False
        self.save_button.visible = True
        self.text_view.visible = False
        self.text_edit.visible = True
        self.update()

    def save(self, e):
        self.edit_button.visible = True
        self.save_button.visible = False
        self.text_view.visible = True
        self.text_edit.visible = False
        self.text_view.value = self.text_edit.value
        self.update()

def main(page: ft.Page):

    page.add(
        Task(text="Do laundry"),
        Task(text="Cook dinner"),
    )


ft.app(main)

Мониторинг порта

Самостоятельная работа "Приложение Список дел на Python с помощью Flet"#

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

Это однострочный консольный скрипт всего из 172 строк аккуратно отформатированного Python-кода, который при этом является кроссплатформенным приложением с богатым и отзывчивым интерфейсом:

Начало работы с Flet#

Чтобы создать кроссплатформенное приложение на Python с помощью Flet, вам не нужно знать HTML, CSS или JavaScript, но потребуется базовое знание Python и объектно-ориентированного программирования.

Прежде чем создавать первое приложение на Flet, нужно настроить среду разработки — для этого требуется Python версии 3.9 и выше, а также пакет flet.

После установки Flet создадим простое приложение "Hello, world".

Создайте файл hello.py со следующим содержимым:

import flet as ft

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

ft.app(main)

Запустите это приложение, и вы увидите новое окно с приветствием:

Мониторинг порта

Добавление элементов управления на страницу и обработка событий#

Теперь мы готовы создать много пользовательское приложение список дел.

Для начала нам понадобится TextField для ввода названия задачи и плавающая кнопка "+" (FloatingActionButton) с обработчиком событий, который будет добавлять новую задачу в виде Checkbox.

Создайте файл todo.py со следующим содержимым:

import flet as ft

def main(page: ft.Page):
    def add_clicked(e):
        page.add(ft.Checkbox(label=new_task.value))
        new_task.value = ""
        page.update()

    new_task = ft.TextField(hint_text="Что нужно сделать?")

    page.add(new_task, ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked))

ft.app(main)

Запустите приложение, и вы увидите примерно такую страницу:

Мониторинг порта

Макет страницы#

Теперь давайте сделаем так, чтобы приложение выглядело красиво! Мы хотим, чтобы всё приложение располагалось в верхней центральной части страницы и занимало ширину в 600 пикселей. Текстовое поле и кнопка «+» должны быть выровнены по горизонтали и занимать всю ширину приложения:

Мониторинг порта

Row — элемент управления, который располагает дочерние элементы по горизонтали на странице.
Column — элемент управления, который располагает дочерние элементы по вертикали на странице.

Замените содержимое файла todo.py следующим кодом:

import flet as ft

def main(page: ft.Page):
    def add_clicked(e):
        tasks_view.controls.append(ft.Checkbox(label=new_task.value))
        new_task.value = ""
        view.update()

    new_task = ft.TextField(hint_text="Что нужно сделать?", expand=True)
    tasks_view = ft.Column()
    view = ft.Column(
        width=600,
        controls=[
            ft.Row(
                controls=[
                    new_task,
                    ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
                ],
            ),
            tasks_view,
        ],
    )

    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(view)

ft.app(main)

Мониторинг порта

Повторно используемые компоненты пользовательского интерфейса#

Хотя можно продолжать писать приложение прямо в функции main, лучшей практикой является создание повторно используемых компонентов интерфейса. Представьте, что вы работаете над заголовком приложения, боковым меню или любым UI, который будет частью большого проекта. Даже если сейчас таких задач нет, мы рекомендуем создавать все приложения на Flet с учётом композируемости и повторного использования.

Чтобы сделать повторно используемый компонент для приложения список дел, мы инкапсулируем состояние и логику отображения в отдельный класс:

import flet as ft

class TodoApp(ft.Column):
    # корневой элемент приложения — Column, содержащий все остальные элементы
    def __init__(self):
        super().__init__()
        self.new_task = ft.TextField(hint_text="Что нужно сделать?", expand=True)
        self.tasks_view = ft.Column()
        self.width = 600
        self.controls = [
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ],
            ),
            self.tasks_view,
        ]

    def add_clicked(self, e):
        self.tasks_view.controls.append(ft.Checkbox(label=self.new_task.value))
        self.new_task.value = ""
        self.update()


def main(page: ft.Page):
    page.title = "Приложение список дел"
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.update()

    # создаём экземпляр приложения
    todo = TodoApp()

    # добавляем корневой элемент приложения на страницу
    page.add(todo)

ft.app(main)

Question

Попробуйте добавить на страницу два компонента в ваше приложение:

app1 = TodoApp()
app2 = TodoApp()

page.add(app1, app2)

Просмотр, редактирование и удаление элементов списка#

На предыдущем шаге мы создали базовое приложение список дел, где задачи отображаются в виде чекбоксов. Давайте улучшить приложение, добавив кнопки «Редактировать» и «Удалить» рядом с названием задачи. Кнопка «Редактировать» будет переключать задачу в режим редактирования.

Мониторинг порта

Каждый элемент задачи теперь представлен двумя строками:
- display_view — ряд с Checkbox и кнопками «Редактировать» и «Удалить»
- edit_view — ряд с TextField и кнопкой «Сохранить»

Колонка view служит контейнером для этих двух рядов.

Ранее код был достаточно коротким, чтобы полностью помещаться в уроке. В дальнейшем мы будем выделять только изменения, внесённые на каждом шаге.

Для инкапсуляции представления и действий над задачей мы ввели новый класс Task:

class Task(ft.Column):
    def __init__(self, task_name, task_delete):
        super().__init__()
        self.task_name = task_name
        self.task_delete = task_delete
        self.display_task = ft.Checkbox(value=False, label=self.task_name)
        self.edit_name = ft.TextField(expand=1)

        self.display_view = ft.Row(
            alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
            vertical_alignment=ft.CrossAxisAlignment.CENTER,
            controls=[
                self.display_task,
                ft.Row(
                    spacing=0,
                    controls=[
                        ft.IconButton(
                            icon=ft.Icons.CREATE_OUTLINED,
                            tooltip="Редактировать задачу",
                            on_click=self.edit_clicked,
                        ),
                        ft.IconButton(
                            ft.Icons.DELETE_OUTLINE,
                            tooltip="Удалить задачу",
                            on_click=self.delete_clicked,
                        ),
                    ],
                ),
            ],
        )

        self.edit_view = ft.Row(
            visible=False,
            alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
            vertical_alignment=ft.CrossAxisAlignment.CENTER,
            controls=[
                self.edit_name,
                ft.IconButton(
                    icon=ft.Icons.DONE_OUTLINE_OUTLINED,
                    icon_color=ft.Colors.GREEN,
                    tooltip="Сохранить задачу",
                    on_click=self.save_clicked,
                ),
            ],
        )
        self.controls = [self.display_view, self.edit_view]

    def edit_clicked(self, e):
        self.edit_name.value = self.display_task.label
        self.display_view.visible = False
        self.edit_view.visible = True
        self.update()

    def save_clicked(self, e):
        self.display_task.label = self.edit_name.value
        self.display_view.visible = True
        self.edit_view.visible = False
        self.update()

    def delete_clicked(self, e):
        self.task_delete(self)

Также мы изменили класс TodoApp, чтобы создавать и хранить экземпляры Task при нажатии кнопки «Добавить»:

class TodoApp(ft.Column):
    # Корневой контрол приложения — колонка, содержащая все остальные контролы
    def __init__(self):
        super().__init__()
        self.new_task = ft.TextField(hint_text="Что нужно сделать?", expand=True)
        self.tasks = ft.Column()
        self.width = 600
        self.controls = [
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ],
            ),
            self.tasks,
        ]

    def add_clicked(self, e):
        task = Task(self.new_task.value, self.task_delete)
        self.tasks.controls.append(task)
        self.new_task.value = ""
        self.update()

    def task_delete(self, task):
        self.tasks.controls.remove(task)
        self.update()

Для операции удаления задачи мы реализовали метод task_delete() в классе TodoApp, который принимает экземпляр задачи (контрол) в качестве параметра.

Далее мы передали ссылку на этот метод в конструктор класса Task и вызываем его при обработке события нажатия кнопки «Удалить».

Запустите приложение и попробуйте редактировать и удалять задачи:

Мониторинг порта

Фильтрация элементов списка#

У нас уже есть рабочее приложение "Список дел", где можно создавать, редактировать и удалять задачи. Чтобы повысить продуктивность, добавим возможность фильтрации задач по их статусу.

Для этого мы используем элемент управления Tabs, который отображает фильтры:

# ...

class TodoApp(ft.Column):
    # корневой контрол приложения — Column, содержащий все остальные контролы
    def __init__(self):
        super().__init__()
        self.new_task = ft.TextField(hint_text="Что нужно сделать?", expand=True)
        self.tasks = ft.Column()

        self.filter = ft.Tabs(
            selected_index=0,
            on_change=self.tabs_changed,
            tabs=[
                ft.Tab(text="all"),
                ft.Tab(text="active"),
                ft.Tab(text="completed")
            ],
        )
    # ....

Чтобы показывать разные списки задач в зависимости от их статуса, можно было бы вести три отдельных списка: "Все", "Активные" и "Выполненные". Однако мы выбрали более простой подход — хранить все задачи в одном списке и менять только их видимость через свойство visible в зависимости от статуса.

Для этого в классе TodoApp мы переопределили метод before_update(), который вызывается каждый раз перед обновлением контрола. В этом методе мы перебираем все задачи и обновляем свойство visible в зависимости от текущего выбранного фильтра:

class TodoApp(ft.Column):

    # ...

    def before_update(self):
        status = self.filter.tabs[self.filter.selected_index].text
        for task in self.tasks.controls:
            task.visible = (
                status == "all"
                or (status == "active" and task.completed == False)
                or (status == "completed" and task.completed)
            )

Фильтрация должна происходить, когда мы нажимаем на вкладку или меняем статус задачи. Метод TodoApp.before_update() вызывается при изменении выбранного значения на вкладках или при установке флажка для элемента задачи:

class TodoApp(ft.Column):

    # ...

    def tabs_changed(self, e):
        self.update()

    def task_status_change(self, e):
        self.update()

    def add_clicked(self, e):
        task = Task(self.new_task.value, self.task_status_change, self.task_delete)
    # ...

class Task(ft.Column):
    def __init__(self, task_name, task_status_change, task_delete):
        super().__init__()
        self.completed = False
        self.task_name = task_name
        self.task_status_change = task_status_change
        self.task_delete = task_delete
        self.display_task = ft.Checkbox(
            value=False, label=self.task_name, on_change=self.status_changed
        )
        # ...

    def status_changed(self, e):
        self.completed = self.display_task.value
        self.task_status_change()

Запустите приложение и попробуйте отфильтровать задачи, нажав на вкладки:

Мониторинг порта

Последние штрихи#

Наше приложение Todo почти готово. В качестве последнего штриха мы добавим нижний колонтитул (элемент управления Column), в котором будет отображаться количество незавершенных задач (элемент управления Text), а также кнопка «Очистить выполненные».

import flet as ft


class Task(ft.Column):
    def __init__(self, task_name, task_status_change, task_delete):
        super().__init__()
        self.completed = False
        self.task_name = task_name
        self.task_status_change = task_status_change
        self.task_delete = task_delete
        self.display_task = ft.Checkbox(
            value=False, label=self.task_name, on_change=self.status_changed
        )
        self.edit_name = ft.TextField(expand=1)

        self.display_view = ft.Row(
            alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
            vertical_alignment=ft.CrossAxisAlignment.CENTER,
            controls=[
                self.display_task,
                ft.Row(
                    spacing=0,
                    controls=[
                        ft.IconButton(
                            icon=ft.Icons.CREATE_OUTLINED,
                            tooltip="Редактировать задачу",
                            on_click=self.edit_clicked,
                        ),
                        ft.IconButton(
                            ft.Icons.DELETE_OUTLINE,
                            tooltip="Удалить задачу",
                            on_click=self.delete_clicked,
                        ),
                    ],
                ),
            ],
        )

        self.edit_view = ft.Row(
            visible=False,
            alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
            vertical_alignment=ft.CrossAxisAlignment.CENTER,
            controls=[
                self.edit_name,
                ft.IconButton(
                    icon=ft.Icons.DONE_OUTLINE_OUTLINED,
                    icon_color=ft.Colors.GREEN,
                    tooltip="Сохранить задачу",
                    on_click=self.save_clicked,
                ),
            ],
        )
        self.controls = [self.display_view, self.edit_view]

    def edit_clicked(self, e):
        self.edit_name.value = self.display_task.label
        self.display_view.visible = False
        self.edit_view.visible = True
        self.update()

    def save_clicked(self, e):
        self.display_task.label = self.edit_name.value
        self.display_view.visible = True
        self.edit_view.visible = False
        self.update()

    def status_changed(self, e):
        self.completed = self.display_task.value
        self.task_status_change(self)

    def delete_clicked(self, e):
        self.task_delete(self)


class TodoApp(ft.Column):
    # Корневой элемент приложения — Column, содержащий все остальные элементы
    def __init__(self):
        super().__init__()
        self.new_task = ft.TextField(
            hint_text="Что нужно сделать?", on_submit=self.add_clicked, expand=True
        )
        self.tasks = ft.Column()

        self.filter = ft.Tabs(
            scrollable=False,
            selected_index=0,
            on_change=self.tabs_changed,
            tabs=[ft.Tab(text="все"), ft.Tab(text="активные"), ft.Tab(text="выполненные")],
        )

        self.items_left = ft.Text("0 задач осталось")

        self.width = 600
        self.controls = [
            ft.Row(
                [ft.Text(value="Список задач", theme_style=ft.TextThemeStyle.HEADLINE_MEDIUM)],
                alignment=ft.MainAxisAlignment.CENTER,
            ),
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ],
            ),
            ft.Column(
                spacing=25,
                controls=[
                    self.filter,
                    self.tasks,
                    ft.Row(
                        alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
                        vertical_alignment=ft.CrossAxisAlignment.CENTER,
                        controls=[
                            self.items_left,
                            ft.OutlinedButton(
                                text="Очистить выполненные", on_click=self.clear_clicked
                            ),
                        ],
                    ),
                ],
            ),
        ]

    def add_clicked(self, e):
        if self.new_task.value:
            task = Task(self.new_task.value, self.task_status_change, self.task_delete)
            self.tasks.controls.append(task)
            self.new_task.value = ""
            self.new_task.focus()
            self.update()

    def task_status_change(self, task):
        self.update()

    def task_delete(self, task):
        self.tasks.controls.remove(task)
        self.update()

    def tabs_changed(self, e):
        self.update()

    def clear_clicked(self, e):
        for task in self.tasks.controls[:]:
            if task.completed:
                self.task_delete(task)

    def before_update(self):
        status = self.filter.tabs[self.filter.selected_index].text
        count = 0
        for task in self.tasks.controls:
            task.visible = (
                status == "все"
                or (status == "активные" and task.completed == False)
                or (status == "выполненные" and task.completed)
            )
            if not task.completed:
                count += 1
        self.items_left.value = f"{count} активная задача(и) осталось"


def main(page: ft.Page):
    page.title = "Приложение Список Задач"
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.scroll = ft.ScrollMode.ADAPTIVE

    # Создаем корневой элемент приложения и добавляем его на страницу
    page.add(TodoApp())


ft.app(main)

Ниже мы выделили изменения, которые внесли для реализации нижнего колонтитула:

class TodoApp():
    def __init__(self):
        # ...

        self.items_left = ft.Text("0 задач осталось")

        self.width = 600
        self.controls = [
            ft.Row(
                [ft.Text(value="Список задач", theme_style=ft.TextThemeStyle.HEADLINE_MEDIUM)],
                alignment=ft.MainAxisAlignment.CENTER,
            ),
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ],
            ),
            ft.Column(
                spacing=25,
                controls=[
                    self.filter,
                    self.tasks,
                    ft.Row(
                        alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
                        vertical_alignment=ft.CrossAxisAlignment.CENTER,
                        controls=[
                            self.items_left,
                            ft.OutlinedButton(
                                text="Очистить выполненные", on_click=self.clear_clicked
                            ),
                        ],
                    ),
                ],
            ),
        ]

    # ...

    def clear_clicked(self, e):
        for task in self.tasks.controls[:]:
            if task.completed:
                self.task_delete(task)

    def before_update(self):
        status = self.filter.tabs[self.filter.selected_index].text
        count = 0
        for task in self.tasks.controls:
            task.visible = (
                status == "все"
                or (status == "активные" and task.completed == False)
                or (status == "выполненные" and task.completed)
            )
            if not task.completed:
                count += 1
        self.items_left.value = f"{count} активная задача(и) осталось"

Запустите приложение:

Мониторинг порта

Доп.пример Калькулятор#

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

  • настольное приложение,
  • мобильное приложение,
  • или веб-приложение.

Приложение представляет собой простую программу, но при этом это мультиплатформенное приложение с интерфейсом, похожим на калькулятор iPhone.

Мониторинг порта

Начало работы с Flet#

Чтобы создать мультиплатформенное приложение на Python с использованием Flet,вам не нужно знать HTML, CSS или JavaScript, но необходимо базовое знание Python и объектно-ориентированного программирования.

Прежде чем вы сможете создать своё первое приложение на Flet, необходимо настроить среду разработки, что требует Python 3.9 или выше и установку пакета flet.

После установки Flet, давайте создадим простое приложение Hello, world.

Создайте файл hello.py со следующим содержимым:

import flet as ft

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

ft.app(main)

Добавление элементов управления страницей#

Теперь вы готовы создать приложение-калькулятор.

Для начала вам понадобится элемент Text для отображения результата вычислений
и несколько кнопок ElevatedButton — для цифр и операций.

Создайте файл calc.py со следующим содержимым:

import flet as ft


def main(page: ft.Page):
    page.title = "Калькулятор"
    result = ft.Text(value="0")

    page.add(
        result,
        ft.ElevatedButton(text="AC"),
        ft.ElevatedButton(text="+/-"),
        ft.ElevatedButton(text="%"),
        ft.ElevatedButton(text="/"),
        ft.ElevatedButton(text="7"),
        ft.ElevatedButton(text="8"),
        ft.ElevatedButton(text="9"),
        ft.ElevatedButton(text="*"),
        ft.ElevatedButton(text="4"),
        ft.ElevatedButton(text="5"),
        ft.ElevatedButton(text="6"),
        ft.ElevatedButton(text="-"),
        ft.ElevatedButton(text="1"),
        ft.ElevatedButton(text="2"),
        ft.ElevatedButton(text="3"),
        ft.ElevatedButton(text="+"),
        ft.ElevatedButton(text="0"),
        ft.ElevatedButton(text="."),
        ft.ElevatedButton(text="="),
    )


ft.app(main)

Запустите приложение, и вы увидите примерно такую страницу:

Мониторинг порта

Question

Что же пошло не так?

Создание макета страницы#

Теперь давайте расположим текст и кнопки в 6 горизонтальных рядов. Замените содержимое calc.py следующим кодом:

import flet as ft


def main(page: ft.Page):
    page.title = "Калькулятор"
    result = ft.Text(value="0")

    page.add(
        ft.Row(controls=[result]),
        ft.Row(
            controls=[
                ft.ElevatedButton(text="AC"),
                ft.ElevatedButton(text="+/-"),
                ft.ElevatedButton(text="%"),
                ft.ElevatedButton(text="/"),
            ]
        ),
        ft.Row(
            controls=[
                ft.ElevatedButton(text="7"),
                ft.ElevatedButton(text="8"),
                ft.ElevatedButton(text="9"),
                ft.ElevatedButton(text="*"),
            ]
        ),
        ft.Row(
            controls=[
                ft.ElevatedButton(text="4"),
                ft.ElevatedButton(text="5"),
                ft.ElevatedButton(text="6"),
                ft.ElevatedButton(text="-"),
            ]
        ),
        ft.Row(
            controls=[
                ft.ElevatedButton(text="1"),
                ft.ElevatedButton(text="2"),
                ft.ElevatedButton(text="3"),
                ft.ElevatedButton(text="+"),
            ]
        ),
        ft.Row(
             controls=[
                ft.ElevatedButton(text="0"),
                ft.ElevatedButton(text="."),
                ft.ElevatedButton(text="="),
            ]
        ),
    )


ft.app(main)

Запустите приложение, и вы увидите примерно такую страницу:

Мониторинг порта

Использование контейнера для дизайнерской мысли#

Чтобы добавить чёрный фон с закруглённой рамкой вокруг калькулятора, мы будем использовать элемент управления Container.

Container может оформлять только один элемент, поэтому все 6 строк кнопок нужно обернуть в вертикальный Column, который и станет содержимым контейнера:

Мониторинг порта

Вот код для добавления контейнера на страницу:

    page.add(
        ft.Container(
            width=350,
            bgcolor=ft.Colors.BLACK,
            border_radius=ft.border_radius.all(20),
            padding=20,
            content=ft.Column(
                controls=
                    [] # Элементы управления для шести строк с текстом
                       # и кнопки калькулятора..
            )
        )
    )

Стилизованные элементы управления#

Чтобы завершить оформление интерфейса, необходимо обновить стили для текста с результатом и кнопок, чтобы они выглядели как в калькуляторе на iPhone.

Для текста с результатом укажем его цвет и размер:

result = ft.Text(value="0", color=ft.Colors.WHITE, size=20)

Для кнопок, если снова взглянуть на интерфейс, к которому мы стремимся, можно выделить 3 типа кнопок:

  • Цифровые кнопки — тёмно-серый фон, белый текст, одинаковый размер.
  • Кнопки действий — оранжевый фон, белый текст, одинаковый размер (за исключением кнопки 0, которая в два раза шире).
  • Дополнительные кнопки — светло-серый фон, тёмный текст, одинаковый размер.

Поскольку кнопки будут использоваться многократно, мы создадим настраиваемые компоненты (Styled Controls) для повторного использования кода.

Так как все эти типы должны наследоваться от класса ElevatedButton и иметь общие свойства text и expand, создадим родительский класс CalcButton:

    class CalcButton(ft.ElevatedButton):
        def __init__(self, text, expand=1):
            super().__init__()
            self.text = text
            self.expand = expand

Теперь давайте создадим дочерние классы для всех трёх типов кнопок:

    class DigitButton(CalcButton):
        def __init__(self, text, expand=1):
            CalcButton.__init__(self, text, expand)
            self.bgcolor = ft.Colors.WHITE24
            self.color = ft.Colors.WHITE

    class ActionButton(CalcButton):
        def __init__(self, text):
            CalcButton.__init__(self, text)
            self.bgcolor = ft.Colors.ORANGE
            self.color = ft.Colors.WHITE

    class ExtraActionButton(CalcButton):
        def __init__(self, text):
            CalcButton.__init__(self, text)
            self.bgcolor = ft.Colors.BLUE_GREY_100
            self.color = ft.Colors.BLACK

Сейчас мы будем использовать эти новые классы для создания рядов кнопок в контейнере:

import flet as ft


def main(page: ft.Page):
    page.title = "Калькулятор"
    result = ft.Text(value="0", color=ft.colors.WHITE, size=20)

    class CalcButton(ft.ElevatedButton):
        def __init__(self, text, expand=1):
            super().__init__()
            self.text = text
            self.expand = expand

    class DigitButton(CalcButton):
        def __init__(self, text, expand=1):
            CalcButton.__init__(self, text, expand)
            self.bgcolor = ft.colors.WHITE24
            self.color = ft.colors.WHITE

    class ActionButton(CalcButton):
        def __init__(self, text):
            CalcButton.__init__(self, text)
            self.bgcolor = ft.colors.ORANGE
            self.color = ft.colors.WHITE

    class ExtraActionButton(CalcButton):
        def __init__(self, text):
            CalcButton.__init__(self, text)
            self.bgcolor = ft.colors.BLUE_GREY_100
            self.color = ft.colors.BLACK

    page.add(
        ft.Container(
            width=350,
            bgcolor=ft.colors.BLACK,
            border_radius=ft.border_radius.all(20),
            padding=20,
            content=ft.Column(
                controls=[
                    ft.Row(controls=[result], alignment="end"),
                    ft.Row(
                        controls=[
                            ExtraActionButton(text="AC"),
                            ExtraActionButton(text="+/-"),
                            ExtraActionButton(text="%"),
                            ActionButton(text="/"),
                        ]
                    ),
                    ft.Row(
                        controls=[
                            DigitButton(text="7"),
                            DigitButton(text="8"),
                            DigitButton(text="9"),
                            ActionButton(text="*"),
                        ]
                    ),
                    ft.Row(
                        controls=[
                            DigitButton(text="4"),
                            DigitButton(text="5"),
                            DigitButton(text="6"),
                            ActionButton(text="-"),
                        ]
                    ),
                    ft.Row(
                        controls=[
                            DigitButton(text="1"),
                            DigitButton(text="2"),
                            DigitButton(text="3"),
                            ActionButton(text="+"),
                        ]
                    ),
                    ft.Row(
                        controls=[
                            DigitButton(text="0", expand=2),
                            DigitButton(text="."),
                            ActionButton(text="="),
                        ]
                    ),
                ]
            ),
        )
    )


ft.app(target=main)

Мониторинг порта

Повторно используемые компоненты пользовательского интерфейса#

Хотя вы можете продолжить писать приложение прямо в функции main, наилучшей практикой считается создание переиспользуемого UI-компонента.

Представьте, что вы работаете над заголовком приложения, боковым меню или интерфейсом, который будет частью более крупного проекта (например, в Flet мы будем использовать это приложение-калькулятор в составе большого приложения "Gallery", где показываются все примеры для фреймворка Flet).

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

Чтобы сделать переиспользуемый компонент калькулятора, мы инкапсулируем его состояние и логику отображения в отдельный класс CalculatorApp.

Скопируйте весь код для этого шага отсюда.

import flet as ft


class CalcButton(ft.ElevatedButton):
    def __init__(self, text, expand=1):
        super().__init__()
        self.text = text
        self.expand = expand


class DigitButton(CalcButton):
    def __init__(self, text, expand=1):
        CalcButton.__init__(self, text, expand)
        self.bgcolor = ft.colors.WHITE24
        self.color = ft.colors.WHITE


class ActionButton(CalcButton):
    def __init__(self, text):
        CalcButton.__init__(self, text)
        self.bgcolor = ft.colors.ORANGE
        self.color = ft.colors.WHITE


class ExtraActionButton(CalcButton):
    def __init__(self, text):
        CalcButton.__init__(self, text)
        self.bgcolor = ft.colors.BLUE_GREY_100
        self.color = ft.colors.BLACK


class CalculatorApp(ft.Container):
    def __init__(self):
        super().__init__()

        self.result = ft.Text(value="0", color=ft.colors.WHITE, size=20)
        self.width = 350
        self.bgcolor = ft.colors.BLACK
        self.border_radius = ft.border_radius.all(20)
        self.padding = 20
        self.content = ft.Column(
            controls=[
                ft.Row(controls=[self.result], alignment="end"),
                ft.Row(
                    controls=[
                        ExtraActionButton(text="AC"),
                        ExtraActionButton(text="+/-"),
                        ExtraActionButton(text="%"),
                        ActionButton(text="/"),
                    ]
                ),
                ft.Row(
                    controls=[
                        DigitButton(text="7"),
                        DigitButton(text="8"),
                        DigitButton(text="9"),
                        ActionButton(text="*"),
                    ]
                ),
                ft.Row(
                    controls=[
                        DigitButton(text="4"),
                        DigitButton(text="5"),
                        DigitButton(text="6"),
                        ActionButton(text="-"),
                    ]
                ),
                ft.Row(
                    controls=[
                        DigitButton(text="1"),
                        DigitButton(text="2"),
                        DigitButton(text="3"),
                        ActionButton(text="+"),
                    ]
                ),
                ft.Row(
                    controls=[
                        DigitButton(text="0", expand=2),
                        DigitButton(text="."),
                        ActionButton(text="="),
                    ]
                ),
            ]
        )


def main(page: ft.Page):
    page.title = "Калькулятор"
    calc = CalculatorApp()


    page.add(calc)


ft.app(target=main)

Обработка событий#

Теперь давайте заставим калькулятор выполнять свои функции. Мы будем использовать один обработчик событий для всех кнопок и применять свойство data, чтобы различать действия в зависимости от нажатой кнопки.

Для класса CalcButton зададим обработчик события on_click=button_clicked и установим свойство data, равное тексту кнопки.

class CalcButton(ft.ElevatedButton):
    def __init__(self, text, button_clicked, expand=1):
        super().__init__()
        self.text = text
        self.expand = expand
        self.on_click = button_clicked
        self.data = text

Определим метод button_click в классе CalculatorApp и передадим его каждой кнопке. Ниже представлен обработчик события on_click, который будет сбрасывать значение текста при нажатии кнопки "AC":

def button_clicked(self, e):
    if e.control.data == "AC":
        self.result.value = "0"

Аналогичным образом метод button_click будет обрабатывать разные действия калькулятора в зависимости от свойства data каждой кнопки. Скопируйте весь код этого шага отсюда:

import flet as ft


class CalcButton(ft.ElevatedButton):
    def __init__(self, text, button_clicked, expand=1):
        super().__init__()
        self.text = text
        self.expand = expand
        self.on_click = button_clicked
        self.data = text


class DigitButton(CalcButton):
    def __init__(self, text, button_clicked, expand=1):
        CalcButton.__init__(self, text, button_clicked, expand)
        self.bgcolor = ft.colors.WHITE24
        self.color = ft.colors.WHITE


class ActionButton(CalcButton):
    def __init__(self, text, button_clicked):
        CalcButton.__init__(self, text, button_clicked)
        self.bgcolor = ft.colors.ORANGE
        self.color = ft.colors.WHITE


class ExtraActionButton(CalcButton):
    def __init__(self, text, button_clicked):
        CalcButton.__init__(self, text, button_clicked)
        self.bgcolor = ft.colors.BLUE_GREY_100
        self.color = ft.colors.BLACK


class CalculatorApp(ft.Container):
    def __init__(self):
        super().__init__()
        self.reset()

        self.result = ft.Text(value="0", color=ft.colors.WHITE, size=20)
        self.width = 350
        self.bgcolor = ft.colors.BLACK
        self.border_radius = ft.border_radius.all(20)
        self.padding = 20
        self.content = ft.Column(
            controls=[
                ft.Row(controls=[self.result], alignment="end"),
                ft.Row(
                    controls=[
                        ExtraActionButton(
                            text="AC", button_clicked=self.button_clicked
                        ),
                        ExtraActionButton(
                            text="+/-", button_clicked=self.button_clicked
                        ),
                        ExtraActionButton(text="%", button_clicked=self.button_clicked),
                        ActionButton(text="/", button_clicked=self.button_clicked),
                    ]
                ),
                ft.Row(
                    controls=[
                        DigitButton(text="7", button_clicked=self.button_clicked),
                        DigitButton(text="8", button_clicked=self.button_clicked),
                        DigitButton(text="9", button_clicked=self.button_clicked),
                        ActionButton(text="*", button_clicked=self.button_clicked),
                    ]
                ),
                ft.Row(
                    controls=[
                        DigitButton(text="4", button_clicked=self.button_clicked),
                        DigitButton(text="5", button_clicked=self.button_clicked),
                        DigitButton(text="6", button_clicked=self.button_clicked),
                        ActionButton(text="-", button_clicked=self.button_clicked),
                    ]
                ),
                ft.Row(
                    controls=[
                        DigitButton(text="1", button_clicked=self.button_clicked),
                        DigitButton(text="2", button_clicked=self.button_clicked),
                        DigitButton(text="3", button_clicked=self.button_clicked),
                        ActionButton(text="+", button_clicked=self.button_clicked),
                    ]
                ),
                ft.Row(
                    controls=[
                        DigitButton(
                            text="0", expand=2, button_clicked=self.button_clicked
                        ),
                        DigitButton(text=".", button_clicked=self.button_clicked),
                        ActionButton(text="=", button_clicked=self.button_clicked),
                    ]
                ),
            ]
        )

    def button_clicked(self, e):
        data = e.control.data
        print(f"Кнопка нажата со следующими параметрами = {data}")
        if self.result.value == "Error" or data == "AC":
            self.result.value = "0"
            self.reset()

        elif data in ("1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "."):
            if self.result.value == "0" or self.new_operand == True:
                self.result.value = data
                self.new_operand = False
            else:
                self.result.value = self.result.value + data

        elif data in ("+", "-", "*", "/"):
            self.result.value = self.calculate(
                self.operand1, float(self.result.value), self.operator
            )
            self.operator = data
            if self.result.value == "Error":
                self.operand1 = "0"
            else:
                self.operand1 = float(self.result.value)
            self.new_operand = True

        elif data in ("="):
            self.result.value = self.calculate(
                self.operand1, float(self.result.value), self.operator
            )
            self.reset()

        elif data in ("%"):
            self.result.value = float(self.result.value) / 100
            self.reset()

        elif data in ("+/-"):
            if float(self.result.value) > 0:
                self.result.value = "-" + str(self.result.value)

            elif float(self.result.value) < 0:
                self.result.value = str(
                    self.format_number(abs(float(self.result.value)))
                )

        self.update()

    def format_number(self, num):
        if num % 1 == 0:
            return int(num)
        else:
            return num

    def calculate(self, operand1, operand2, operator):

        if operator == "+":
            return self.format_number(operand1 + operand2)

        elif operator == "-":
            return self.format_number(operand1 - operand2)

        elif operator == "*":
            return self.format_number(operand1 * operand2)

        elif operator == "/":
            if operand2 == 0:
                return "Error"
            else:
                return self.format_number(operand1 / operand2)

    def reset(self):
        self.operator = "+"
        self.operand1 = 0
        self.new_operand = True


def main(page: ft.Page):
    page.title = "Калькулятор"
    calc = CalculatorApp()

    page.add(calc)


ft.app(target=main)

Запустите приложение и посмотрите, как оно работает.