Введение: От скрипта к полноценному приложению

Каждому Python-разработчику знакома ситуация: готов полезный скрипт, но он заперт в консоли. Как поделиться им с коллегой, далеким от терминала? Раньше это означало погружаться в дебри Tkinter, изучать монструозный PyQt или тащить тяжеловесный Electron ради пары кнопок.

К счастью, эти времена прошли. Знакомьтесь, Flet — фреймворк, который позволяет создавать современные кросс-платформенные приложения, используя только Python. В его основе лежит Flutter, но вам не придется писать ни строчки на Dart. Вы просто описываете интерфейс Python-объектами, а Flet берет на себя всю магию по его отрисовке.

В этой статье мы не будем разбирать теорию. Мы с нуля напишем полезную утилиту — мини-редактор изображений, который умеет открывать файлы, применять базовые фильтры (Ч/Б, размытие, поворот) и сохранять результат.

Вот что у нас получится в итоге:

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

Часть 1: Подготовка и создание каркаса приложения

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

Шаг 0: Виртуальное окружение — цифровая гигиена

Хороший тон в Python-разработке — начинать любой проект с создания виртуального окружения. Это изолированное пространство, которое позволяет устанавливать зависимости для конкретного проекта, не засоряя глобальную установку Python и избегая конфликтов версий библиотек.

Откройте терминал в папке вашего проекта и выполните следующие команды:

  1. Создаем окружение (назовем его venv):

    python -m venv venv
    
  2. Активируем его:

    • Windows (Command Prompt / PowerShell):

      venv\Scripts\activate
      
    • macOS / Linux:

      source venv/bin/activate
      

После активации вы увидите (venv) в начале строки вашего терминала. Это значит, что мы готовы к работе.

Шаг 1: Установка зависимостей

Нашему приложению понадобятся всего две библиотеки: flet для создания интерфейса и pillow для всех манипуляций с изображениями. Установим их одной командой:

pip install flet pillow

Вот и вся подготовка. Просто, не так ли?

Шаг 2: Базовая структура Flet-приложения

Теперь создадим наш главный Python-файл, например, main.py. Любое Flet-приложение имеет простую и понятную структуру:

  1. Импортируем библиотеку, обычно как ft.

  2. Создаем функцию main, которая принимает один аргумент — page. Этот объект page является нашим главным окном или холстом, куда мы будем добавлять все элементы.

  3. Запускаем приложение с помощью ft.app().

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

# main.py
import flet as ft

def main(page: ft.Page):
    # Настраиваем окно приложения
    page.title = "Flet Image Editor"
    page.window_width = 600
    page.window_height = 800
    page.vertical_alignment = ft.MainAxisAlignment.CENTER
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER

    # Добавляем на страницу простой текст
    page.add(
        ft.Text("Каркас нашего приложения готов!", size=20)
    )

# Запускаем приложение, указывая нашу main-функцию как цель
ft.app(target=main)

Запустите этот файл (python main.py), и вы должны увидеть окно с заголовком и приветственным текстом. Отлично, мы на верном пути!

Шаг 3: Проектирование и создание каркаса интерфейса

Теперь самое интересное — спроектируем наш интерфейс. Мы хотим разместить элементы вертикально, один под другим. Для этого идеально подходит виджет ft.Column. Внутри него мы расположим горизонтальные ряды (ft.Row) с кнопками и основной виджет для отображения картинки (ft.Image).

Давайте заменим наш приветственный текст на реальные, но пока нефункциональные элементы управления.

# main.py
import flet as ft

def main(page: ft.Page):
    page.title = "Flet Image Editor"
    page.window_width = 600
    page.window_height = 800
    # Выравниваем все по центру
    page.vertical_alignment = ft.MainAxisAlignment.CENTER
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER

    # --- Виджеты нашего приложения ---

    # Кнопки для работы с файлами
    open_button = ft.ElevatedButton("Открыть")
    save_button = ft.ElevatedButton("Сохранить", disabled=True) # Кнопка неактивна, пока нет изображения

    # Изображение, которое мы будем редактировать
    # Изначально оно невидимо
    image_view = ft.Image(
        visible=False,
        width=500,
        height=500,
        fit=ft.ImageFit.CONTAIN,
    )

    # Кнопки фильтров
    filters_row = ft.Row(
        controls=[
            ft.ElevatedButton("Ч/Б"),
            ft.ElevatedButton("Размытие"),
            ft.ElevatedButton("Поворот"),
        ],
        visible=False, # Панель фильтров тоже скрыта
        alignment=ft.MainAxisAlignment.CENTER,
    )

    # --- Собираем интерфейс ---

    # Добавляем все виджеты на страницу в одну колонку
    page.add(
        ft.Row([open_button, save_button], alignment=ft.MainAxisAlignment.CENTER),
        image_view,
        filters_row
    )

ft.app(target=main)

Запустите код еще раз. Теперь у вас есть окно с кнопками "Открыть" и неактивной "Сохранить". Изображение и фильтры пока скрыты — мы покажем их, как только пользователь выберет файл.

Наш каркас готов! Мы создали структуру приложения и разместили все необходимые элементы.

Часть 2: Открываем и отображаем изображение

Чтобы приложение могло "общаться" с файловой системой компьютера — открывать стандартные диалоговые окна "Выбор файла" или "Сохранить как" — Flet предоставляет специальный виджет-помощник: ft.FilePicker.

Шаг 1: Добавляем FilePicker на страницу

В отличие от кнопок и полей, FilePicker — это невидимый компонент. Он живет в "слое оверлеев" (page.overlay) и ждет, пока его вызовут.

Давайте создадим экземпляр FilePicker и добавим его на нашу страницу. Также нам нужно сразу "подписать" его на событие on_result — это событие сработает, когда пользователь выберет файл и закроет диалоговое окно.

Добавьте этот код в функцию main, перед тем как мы собираем интерфейс.

# ... внутри def main(page: ft.Page):

# --- Диалоговое окно для выбора файла ---
def on_file_picker_result(e: ft.FilePickerResultEvent):
    # Эта функция будет вызвана, когда пользователь выберет файл
    # Пока оставим её пустой
    print("Выбран файл:", e.files)

file_picker = ft.FilePicker(on_result=on_file_picker_result)

# Добавляем FilePicker в оверлей страницы, чтобы он был доступен
page.overlay.append(file_picker)

# --- Виджеты нашего приложения ---
# ... (остальной код виджетов)

Мы создали сам FilePicker и функцию on_file_picker_result, которая будет обрабатывать его результат.

Шаг 2: Оживляем кнопку "Открыть"

Теперь нам нужно связать нашу кнопку "Открыть" с FilePicker. Делается это очень просто: по нажатию на кнопку мы должны вызывать метод pick_files() у нашего file_picker. Этот метод и откроет системное диалоговое окно.

Мы можем добавить обработчик on_click прямо при создании кнопки.

# Заменим создание кнопки open_button
open_button = ft.ElevatedButton(
    "Открыть",
    on_click=lambda _: file_picker.pick_files(
        allow_multiple=False, # Запрещаем выбор нескольких файлов
        allowed_extensions=["jpg", "jpeg", "png", "bmp"], # Фильтруем типы файлов
        dialog_title="Выберите изображение"
    )
)

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

Ша-г 3: Обрабатываем выбор пользователя и отображаем картинку

Самая важная часть. Когда пользователь выбе��ет файл, сработает событие on_result, и Flet вызовет нашу функцию on_file_picker_result. Внутри этой функции нам нужно:

  1. Проверить, что пользователь действительно выбрал файл, а не закрыл окно.

  2. Получить путь к выбранному файлу.

  3. Установить этот путь как источник (src) для нашего виджета ft.Image.

  4. Сделать видимыми само изображение и панель с фильтрами.

  5. Активировать кнопку "Сохранить".

  6. Вызвать page.update(), чтобы все эти изменения отобразились на экране.

Давайте наполним нашу функцию on_file_picker_result логикой:

# Заполняем нашу функцию-обработчик
def on_file_picker_result(e: ft.FilePickerResultEvent):
    # Если пользователь отменил выбор, e.files будет None
    if e.files:
        # Получаем путь к первому выбранному файлу
        file_path = e.files[0].path
        
        # Обновляем источник изображения
        image_view.src = file_path
        image_view.visible = True
        
        # Делаем видимой панель фильтров и активной кнопку "Сохранить"
        filters_row.visible = True
        save_button.disabled = False
        
        # Обязательно обновляем страницу, чтобы применить изменения
        page.update()

Вот и все! Давайте посмотрим на полный код main.py после всех изменений.

# main.py
import flet as ft

def main(page: ft.Page):
    page.title = "Flet Image Editor"
    page.window_width = 600
    page.window_height = 800
    page.vertical_alignment = ft.MainAxisAlignment.CENTER
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER

    def on_file_picker_result(e: ft.FilePickerResultEvent):
        if e.files:
            file_path = e.files[0].path
            image_view.src = file_path
            image_view.visible = True
            filters_row.visible = True
            save_button.disabled = False
            page.update()

    file_picker = ft.FilePicker(on_result=on_file_picker_result)
    page.overlay.append(file_picker)

    open_button = ft.ElevatedButton(
        "Открыть",
        on_click=lambda _: file_picker.pick_files(
            allow_multiple=False,
            allowed_extensions=["jpg", "jpeg", "png", "bmp"],
            dialog_title="Выберите изображение"
        )
    )
    save_button = ft.ElevatedButton("Сохранить", disabled=True)

    image_view = ft.Image(
        visible=False,
        width=500,
        height=500,
        fit=ft.ImageFit.CONTAIN,
    )

    filters_row = ft.Row(
        controls=[
            ft.ElevatedButton("Ч/Б"),
            ft.ElevatedButton("Размытие"),
            ft.ElevatedButton("Поворот"),
        ],
        visible=False,
        alignment=ft.MainAxisAlignment.CENTER,
    )

    page.add(
        ft.Row([open_button, save_button], alignment=ft.MainAxisAlignment.CENTER),
        image_view,
        filters_row
    )

ft.app(target=main)

Запустите скрипт. Теперь кнопка "Открыть" активна. Нажмите на нее, выберите любое изображение на вашем компьютере, и оно тут же появится в окне приложения вместе с панелью фильтров.

Наше приложение стало интерактивным!

Часть 3: "Швейцарский нож" в действии — применяем фильтры

Сейчас наши кнопки "Ч/Б", "Размытие" и "Поворот" — просто элементы интерфейса. Наша задача — связать их с реальными операциями по обработке изображений. Flet будет отвечать за нажатия кнопок, а всю тяжелую работу по пиксельным манипуляциям возьмет на себя Pillow.

Шаг 1: Состояние приложения — где хранить изображение?

Когда пользователь применяет фильтр, мы не можем просто изменить исходный файл на диске. Нам нужно работать с копией изображения в памяти. Давайте заведем переменную, в которой будем хранить текущее состояние изображения в виде объекта Pillow.

Добавьте в начало функции main импорты из Pillow и io (он понадобится нам для работы с данными в памяти), а также создайте переменную-хранилище current_image.

# main.py
import flet as ft
from PIL import Image, ImageFilter # Импортируем Image и ImageFilter
import io                         # Импортируем io
import base64                     # Импортируем base64

def main(page: ft.Page):
    # ... настройки страницы ...

    # Переменная для хранения текущего изображения в формате Pillow
    current_image: Image = None
    
    # ... остальной код ...

Теперь, когда пользователь выбирает файл, нам нужно не только показать его, но и загрузить в нашу переменную current_image. Обновим функцию on_file_picker_result:

# Обновляем on_file_picker_result
def on_file_picker_result(e: ft.FilePickerResultEvent):
    nonlocal current_image # Указываем, что будем изменять внешнюю переменную
    if e.files:
        file_path = e.files[0].path
        
        # Загружаем изображение в Pillow и сохраняем его
        current_image = Image.open(file_path)
        
        image_view.src = file_path
        image_view.visible = True
        filters_row.visible = True
        save_button.disabled = False
        page.update()

Шаг 2: Ключевая магия — как показать измененное изображение

Вот тут и кроется главный технический момент. Когда мы применим фильтр, у нас будет измененный объект Image от Pillow в памяти. Но как его показать в виджете ft.Image? Ведь его свойство src ожидает путь к файлу.

Решение: свойство src_base64. Оно позволяет загрузить изображение напрямую из строки, закодированной в формате Base64. Наш план:

  1. Взять измененный объект Pillow Image.

  2. "Сохранить" его не на диск, а в специальный буфер в оперативной памяти (io.BytesIO).

  3. Получить из буфера байты изображения.

  4. Закодировать эти байты в строку Base64.

  5. Передать эту строку в image_view.src_base64.

Давайте напишем для этого небольшую вспомогательную функцию, чтобы не дублировать код.

# Добавим эту функцию внутрь main
def update_image_view(img_obj: Image):
    """Конвертирует объект Pillow в base64 и обновляет виджет."""
    buffer = io.BytesIO()
    img_obj.save(buffer, format="PNG") # Сохраняем в буфер в формате PNG
    base64_img = base64.b64encode(buffer.getvalue()).decode("utf-8")
    
    image_view.src_base64 = base64_img
    page.update()

Шаг 3: Реализуем фильтры

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

Начнем с черно-белого фильтра:

# Функция для Ч/Б фильтра
def apply_bw_filter(e):
    nonlocal current_image
    if current_image:
        # Применяем фильтр
        bw_image = current_image.convert("L")
        # Обновляем текущее изображение
        current_image = bw_image
        # Обновляем виджет на странице
        update_image_view(current_image)

Размытие (Blur):

# Функция для размытия
def apply_blur_filter(e):
    nonlocal current_image
    if current_image:
        blurred_image = current_image.filter(ImageFilter.BLUR)
        current_image = blurred_image
        update_image_view(current_image)

Поворот:

# Функция для поворота
def apply_rotate_filter(e):
    nonlocal current_image
    if current_image:
        # Поворачиваем на 90 градусов против часовой стрелки
        rotated_image = current_image.rotate(90, expand=True)
        current_image = rotated_image
        update_image_view(current_image)

Осталось только привязать эти функции к on_click наших кнопок. Найдите место, где создается filters_row, и обновите его:

# Обновляем filters_row
filters_row = ft.Row(
    controls=[
        ft.ElevatedButton("Ч/Б", on_click=apply_bw_filter),
        ft.ElevatedButton("Размытие", on_click=apply_blur_filter),
        ft.ElevatedButton("Поворот", on_click=apply_rotate_filter),
    ],
    visible=False,
    alignment=ft.MainAxisAlignment.CENTER,
)

Готово! Запустите приложение, откройте изображение и попробуйте нажать на кнопки фильтров. Вы увидите, как картинка мгновенно меняется. Причем фильтры применяются последовательно: вы можете сделать изображение черно-белым, а затем повернуть его.

Часть 4: Сохранение результата и обратная связь

Сейчас кнопка "Сохранить" активна, но безжизненна. Мы это исправим. Вся логика у нас уже есть: измененное изображение хранится в переменной current_image. Нам лишь нужно спросить у пользователя, куда его сохранить.

Шаг 1: Настраиваем сохранение файла

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

  1. Создадим новый FilePicker и его функцию-обработчик on_save_file_result.

  2. Добавим его также в page.overlay.

  3. Привяжем его вызов к кнопке "Сохранить".

# Добавляем этот код внутрь main, рядом с первым пикером

# --- Диалоговое окно для СОХРАНЕНИЯ файла ---
def on_save_file_result(e: ft.FilePickerResultEvent):
    # Логика сохранения будет здесь
    pass

save_file_picker = ft.FilePicker(on_result=on_save_file_result)
page.overlay.append(save_file_picker)

# ...

# Обновляем создание кнопки "Сохранить"
save_button = ft.ElevatedButton(
    "Сохранить",
    disabled=True,
    on_click=lambda _: save_file_picker.save_file(
        dialog_title="Сохранить как...",
        file_name="edited_image.png", # Имя файла по умолчанию
        allowed_extensions=["png", "jpg", "bmp"]
    )
)

Шаг 2: Реализуем логику сохранения и добавляем обратную связь

Теперь наполним нашу новую функцию on_save_file_result. Она должна:

  1. Проверить, что пользователь указал путь для сохранения.

  2. Вызвать метод save() у нашего объекта current_image из Pillow.

  3. Показать пользователю уведомление, что все прошло успешно.

Для уведомлений в Flet есть замечательный виджет — ft.SnackBar. Он появляется внизу экрана и через несколько секунд исчезает.

Давайте допишем финальный код.

# Добавляем создание SnackBar в начало функции main
def main(page: ft.Page):
    # ...
    # Создаем SnackBar для уведомлений
    page.snack_bar = ft.SnackBar(content=ft.Text("Сообщение!"))
    # ...

# Теперь заполняем функцию on_save_file_result
def on_save_file_result(e: ft.FilePickerResultEvent):
    # Если пользователь выбрал путь для сохранения
    if e.path:
        try:
            current_image.save(e.path)
            # Показываем уведомление об успехе
            page.snack_bar.content = ft.Text(f"Изображение успешно сохранено в {e.path}")
            page.snack_bar.open = True
            page.update()
        except Exception as ex:
            # Показываем уведомление об ошибке
            page.snack_bar.content = ft.Text(f"Ошибка при сохранении: {ex}")
            page.snack_bar.open = True
            page.update()

Мы обернули сохранение в try...except, чтобы поймать возможные ошибки (например, нет прав на запись) и вежливо сообщить о них пользователю.

Поздравляю! Наше приложение полностью функционально. Мы прошли весь путь от пустого файла до полноценного десктопного редактора изображений. Мы научились работать с файловой системой, обрабатывать события, интегрировать мощные библиотеки и предоставлять пользователю качественную обратную связь.

▶️ Полный код main.py
import flet as ft
from PIL import Image, ImageFilter
import io
import base64

def main(page: ft.Page):
    # --- НАСТРОЙКИ ОКНА ---
    page.title = "Flet Image Editor"
    page.window_width = 600
    page.window_height = 800
    page.vertical_alignment = ft.MainAxisAlignment.CENTER
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.snack_bar = ft.SnackBar(content=ft.Text("Сообщение!"))

    # --- ПЕРЕМЕННЫЕ СОСТОЯНИЯ ---
    current_image: Image = None

    # --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
    def update_image_view(img_obj: Image):
        """Конвертирует объект Pillow в base64 и обновляет виджет."""
        buffer = io.BytesIO()
        img_obj.save(buffer, format="PNG")
        base64_img = base64.b64encode(buffer.getvalue()).decode("utf-8")
        image_view.src_base64 = base64_img
        page.update()

    # --- ОБРАБОТЧИКИ СОБЫТИЙ ---
    def on_file_picker_result(e: ft.FilePickerResultEvent):
        nonlocal current_image
        if e.files:
            file_path = e.files[0].path
            current_image = Image.open(file_path)
            image_view.src = file_path
            image_view.visible = True
            filters_row.visible = True
            save_button.disabled = False
            page.update()

    def on_save_file_result(e: ft.FilePickerResultEvent):
        if e.path and current_image:
            try:
                current_image.save(e.path)
                page.snack_bar.content = ft.Text(f"Изображение успешно сохранено в {e.path}")
                page.snack_bar.open = True
                page.update()
            except Exception as ex:
                page.snack_bar.content = ft.Text(f"Ошибка при сохранении: {ex}")
                page.snack_bar.open = True
                page.update()

    def apply_bw_filter(e):
        nonlocal current_image
        if current_image:
            current_image = current_image.convert("L")
            update_image_view(current_image)

    def apply_blur_filter(e):
        nonlocal current_image
        if current_image:
            current_image = current_image.filter(ImageFilter.BLUR)
            update_image_view(current_image)

    def apply_rotate_filter(e):
        nonlocal current_image
        if current_image:
            current_image = current_image.rotate(90, expand=True)
            update_image_view(current_image)

    # --- FILE PICKERS ---
    open_file_picker = ft.FilePicker(on_result=on_file_picker_result)
    save_file_picker = ft.FilePicker(on_result=on_save_file_result)
    page.overlay.extend([open_file_picker, save_file_picker])

    # --- ВИДЖЕТЫ ИНТЕРФЕЙСА ---
    open_button = ft.ElevatedButton(
        "Открыть",
        on_click=lambda _: open_file_picker.pick_files(
            allow_multiple=False,
            allowed_extensions=["jpg", "jpeg", "png", "bmp"],
            dialog_title="Выберите изображение"
        )
    )
    save_button = ft.ElevatedButton(
        "Сохранить",
        disabled=True,
        on_click=lambda _: save_file_picker.save_file(
            dialog_title="Сохранить как...",
            file_name="edited_image.png",
            allowed_extensions=["png", "jpg", "bmp"]
        )
    )

    image_view = ft.Image(
        visible=False,
        width=500,
        height=500,
        fit=ft.ImageFit.CONTAIN,
    )

    filters_row = ft.Row(
        controls=[
            ft.ElevatedButton("Ч/Б", on_click=apply_bw_filter),
            ft.ElevatedButton("Размытие", on_click=apply_blur_filter),
            ft.ElevatedButton("Поворот", on_click=apply_rotate_filter),
        ],
        visible=False,
        alignment=ft.MainAxisAlignment.CENTER,
    )

    # --- СБОРКА ИНТЕРФЕЙСА ---
    page.add(
        ft.Row([open_button, save_button], alignment=ft.MainAxisAlignment.CENTER),
        image_view,
        filters_row
    )

# --- ЗАПУСК ПРИЛОЖЕНИЯ ---
ft.app(target=main)

Домашнее задание: Что можно улучшить?

Поздравляю, у вас есть рабочее приложение! Но, как и любой хороший проект, его всегда можно сделать еще лучше. Эти задания помогут вам глубже понять Flet и добавить в редактор профессиональные функции. Раскройте каждое задание, чтобы увидеть условие.

1. Регулировка силы эффекта с помощью слайдера

Задача: Сейчас фильтр "Размытие" применяетcя с фиксированной силой. Замените его на ft.Slider, который позволит пользователю плавно регулировать степень размытия от 0 до 10.

Подсказки:

  • Добавьте виджет ft.Slider в filters_row. Установите ему минимальное (min=0) и максимальное (max=10) значения.

  • Вместо события on_click у кнопки используйте событие on_change у слайдера. Ваша функция-обработчик будет получать значение слайдера.

  • В Pillow для регулируемого размытия используйте не ImageFilter.BLUR, а ImageFilter.GaussianBlur(radius=value), где value — это значение, пришедшее от слайдера.

  • Не забудьте, что применять фильтр нужно к исходному изображению, а не к уже размытому, иначе эффект будет накапливаться с каждым движением слайдера.

2. Кнопка "Сброс" для отмены всех изменений

Задача: Пользователь может применить несколько фильтров подряд, но у него нет способа вернуться к оригиналу, кроме как открыть файл заново. Добавьте кнопку "Сброс", которая отменяет все примененные эффекты.

Подсказки:

  • Вам понадобится еще одна переменная состояния, помимо current_image. Назовите ее, например, original_image.

  • В функции on_file_picker_result (при открытии файла) сохраняйте объект Pillow и в original_image, и в current_image.

  • Функция-обработчик для кнопки "Сброс" должна будет просто скопировать объект из original_image в current_image и обновить image_view.

3. Добавление новых фильтров

Задача: Расширьте функциональность редактора, добавив как минимум два новых фильтра из библиотеки Pillow.

Подсказки:

  • Добавьте новые виджеты ft.ElevatedButton в filters_row.

  • Напишите для них новые функции-обработчики по аналогии с существующими.

  • Изучите документа��ию ImageFilter в Pillow. Попробуйте реализовать, например, ImageFilter.CONTOUR (выделение контуров) или ImageFilter.SHARPEN (увеличение резкости). Они применяются так же просто, как и размытие.

4. Отображение информации об изображении

Задача: Сделайте приложение более информативным, добавив под изображением текстовую строку, которая показывает его размеры и формат. Например: "Размер: 1920x1080 | Формат: PNG".

Подсказки:

  • Добавьте виджет ft.Text в основной Column вашего приложения, под image_view. Изначально он может быть пустым или скрытым.

  • В функции on_file_picker_result, после загрузки изображения в current_image, вы можете получить его свойства: current_image.size (это кортеж, например (1920, 1080)) и current_image.format (это строка, например "PNG").

  • Сформируйте нужную строку и установите ее как значение (value) для вашего ft.Text виджета, после чего обновите страницу.

Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.

Уверен, у вас все получится. Вперед, к практике!

Комментарии (3)


  1. Octagon77
    14.11.2025 11:27

    Это всё замечательно, но если в статье упомянут Flutter, то нужно как-то это показать. Рисовать то на экране может каждый, а вот в документации по Flet написано

    # Signing Android bundle
    
    TBD
    
    [tool.flet.android.signing]
    # store and key passwords can be passed with `--android-signing-key-store-password`
    # and `--android-signing-key-password` options or
    # FLET_ANDROID_SIGNING_KEY_STORE_PASSWORD
    # and FLET_ANDROID_SIGNING_KEY_PASSWORD environment variables.
    key_store = "path/to/store.jks" # --android-signing-key-store
    key_alias = "upload"
    
    # Splash screen

    Иными словами, APK замучаешься получать, Flet сложным не заморачивается - TBD и отвали.

    А если Автор, задним числом, попытается сказать, что статью писал не об этом, то тогда, в статье не об этом,

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

    просто и тупо неверно - есть масса приемлимых альтернатив начиная с PyGame.


  1. danial72
    14.11.2025 11:27

    Зачем если все это на flutter пишется раза в 2 проще и быстрее ?


  1. kAIST
    14.11.2025 11:27

    Изображение перегонять в base64, это огромный костыль. Если ui фреймворк не умеет по другому, то надо думать о его смене.