1. Введение: Почему мы боимся GUI на Python?

Многие из нас, Python-разработчиков, с легкостью пишут сложные бэкенды, автоматизируют рутинные задачи и анализируют гигабайты данных. Наш инструментарий мощен и разнообразен, но когда речь заходит о создании десктопного приложения с графическим интерфейсом, энтузиазм часто угасает. В голове сразу всплывают монструозные фреймворки вроде PyQt или устаревший Tkinter, требующие изучения сложного API, долгой настройки и написания сотен строк кода для простейшего окна.

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

А что, если я скажу, что можно создать современное, красивое и кроссплатформенное приложение, описывая интерфейс так же просто, как вы пишете обычный Python-код?

В этой статье мы познакомимся с Flet — революционным фреймворком, который позволяет делать именно это. Flet дает вам возможность создавать интерактивные GUI на чистом Python, а для отрисовки использует мощный движок Flutter. Результат — быстрые, красивые и нативные приложения для Windows, macOS, Linux и даже для веба, без необходимости писать ни строчки на Dart или JavaScript.

2. Часть I: Установка и "Hello, Flet!"

Главная прелесть Flet — в его простоте и минимальном количестве зависимостей. Для нашего проекта понадобятся всего две библиотеки: сам flet для создания интерфейса и markdown для парсинга разметки, которую будет отображать специальный виджет Flet.

Давайте установим их одной командой в вашем терминале:

pip install flet markdown

Готово! Теперь, чтобы убедиться, что все работает, и познакомиться с базовой структурой Flet-приложения, создадим файл editor.py и напишем в нем классический "Hello, World".

# editor.py
import flet as ft

def main(page: ft.Page):
    # page — это главный "холст" нашего приложения, само окно.
    # Мы можем настраивать его свойства, например, заголовок.
    page.title = "Мое первое Flet-приложение"
    
    # Создаем текстовый элемент (виджет или "контрол" в терминах Flet).
    hello_text = ft.Text(value="Привет, мир Flet!", size=30)
    
    # Добавляем наш элемент на страницу.
    page.add(hello_text)

# Запускаем приложение, указывая нашу главную функцию в качестве цели.
if __name__ == "__main__":
    ft.app(target=main)

Сохраните и запустите этот файл из терминала: python editor.py. Перед вами должно появиться аккуратное окно с нашим приветствием.

Давайте разберем, что здесь произошло:

  1. Мы определили функцию main, которая принимает один обязательный аргумент — page. Объект ft.Page — это, по сути, ваше окно или веб-страница. Он служит контейнером для всех остальных элементов интерфейса.

  2. ft.Text(...) — это виджет (или Control, как их называет Flet). Это строительный блок нашего GUI: текст, кнопка, поле ввода и так далее.

  3. page.add(...) размещает наши виджеты на странице в том порядке, в котором мы их добавляем.

  4. ft.app(target=main) — это команда, которая инициализирует и запускает наше приложение, передавая управление функции main.

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

3. Часть II: Проектирование интерфейса редактора

Теперь, когда мы знаем, как запустить Flet-приложение, давайте набросаем визуальный скелет нашего редактора. Наша цель проста: создать окно, разделенное на две равные вертикальные панели. Слева будет поле для ввода текста, справа — область для предпросмотра.

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

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

# editor.py
import flet as ft

def main(page: ft.Page):
    page.title = "Markdown Редактор"

    # 1. Поле для ввода Markdown текста
    input_field = ft.TextField(
        multiline=True, # Позволяет вводить несколько строк текста
        expand=True,    # Растягивает поле на всю доступную высоту и ширину
        min_lines=40,   # Задает минимальную высоту, чтобы окно не было слишком маленьким
        hint_text="Пишите ваш Markdown здесь..." # Подсказка для пользователя
    )

    # 2. Область для отображения отформатированного результата
    preview_area = ft.Markdown(
        value="Здесь будет ваш **отформатированный** текст.",
        expand=True, # Также растягиваем на все доступное пространство
    )

    # 3. Собираем все в один горизонтальный ряд
    app_layout = ft.Row(
        # `controls` — это список дочерних элементов
        controls=[
            input_field,
            ft.VerticalDivider(), # Аккуратный вертикальный разделитель
            preview_area
        ],
        expand=True, # Растягиваем сам ряд на все окно
    )

    page.add(app_layout)

if __name__ == "__main__":
    ft.app(target=main)

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

Ключевые моменты, на которые стоит обратить внимание:

  • ft.TextField: Это наш основной инструмент для ввода. Свойство multiline=True превращает его из однострочного поля в полноценную текстовую область.

  • ft.Markdown: Это специальный, очень удобный виджет Flet. Вам не нужно вручную вызывать библиотеку markdown и работать с HTML. Вы просто передаете ему строку с Markdown-разметкой в свойство value, и он сам ее красиво отрисовывает.

  • expand=True: Это одно из самых важных свойств в Flet. Оно говорит виджету: "займи все свободное пространство, которое тебе выделил твой родительский контейнер". Мы применили его к обеим панелям, чтобы они поделили ft.Row поровну, и к самому ft.Row, чтобы он занял все окно page.

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

4. Часть III: Оживляем редактор — логика предпросмотра

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

В Flet, как и в большинстве GUI-фреймворков, интерактивность строится на событиях и обработчиках. Когда пользователь что-то делает (нажимает кнопку, вводит текст), виджет генерирует событие. Наша задача — написать функцию-обработчик, которая будет "слушать" это событие и выполнять нужные действия.

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

Давайте напишем функцию-обработчик и привяжем ее к нашему полю ввода.

Дополним наш код:

# editor.py
import flet as ft

def main(page: ft.Page):
    page.title = "Markdown Редактор"

    # --- 1. Создаем функцию-обработчик ---
    def handle_text_change(e):
        # 'e' — это объект события, который Flet передает в функцию.
        # e.control — это ссылка на виджет, который вызвал событие (наш TextField).
        # e.control.value — это текущий текст в этом виджете.
        
        # Обновляем содержимое правой панели (preview_area)
        preview_area.value = e.control.value
        
        # --- 3. Самый важный шаг: обновляем страницу ---
        page.update()

    input_field = ft.TextField(
        multiline=True,
        expand=True,
        min_lines=40,
        hint_text="Пишите ваш Markdown здесь...",
        # --- 2. Привязываем обработчик к событию on_change ---
        on_change=handle_text_change,
    )

    preview_area = ft.Markdown(
        value="Здесь будет ваш **отформатированный** текст.",
        expand=True,
    )

    app_layout = ft.Row(
        controls=[
            input_field,
            ft.VerticalDivider(),
            preview_area
        ],
        expand=True,
    )

    page.add(app_layout)

if __name__ == "__main__":
    ft.app(target=main)

Давайте разберем, что мы только что сделали:

  1. Создали функцию handle_text_change(e). Это наш обработчик. Flet автоматически передает в него объект события, который мы назвали e. Через e.control.value мы можем получить актуальный текст из поля ввода.

  2. Привязали обработчик к виджету. При создании input_field мы добавили новый параметр: on_change=handle_text_change. Обратите внимание: мы передаем саму функцию, без скобок. Теперь Flet знает, какую функцию нужно вызывать при каждом изменении текста.

  3. Вызвали page.update(). Это, пожалуй, самый важный шаг. После того как мы программно изменили свойство виджета (preview_area.value = ...), Flet не перерисовывает интерфейс автоматически (это сделано для оптимизации). Команда page.update() — это явный приказ: "Эй, Flet, я внес изменения, пожалуйста, обнови окно, чтобы пользователь их увидел!".

Теперь сохраните и снова запустите приложение. Начните вводить в левое поле текст с Markdown-разметкой, например:

# Это главный заголовок

Это обычный параграф с **жирным** и *курсивным* текстом.

- Пункт списка 1
- Пункт списка 2

Вы увидите, как правая панель волшебным образом преображает ваш текст в реальном времени! Наше приложение ожило. Осталось лишь навести немного лоска.

5. Часть IV: Финальные штрихи и полный код

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

Все эти настройки применяются к объекту page в самом начале нашей функции main.

def main(page: ft.Page):
    # --- Настройки окна ---
    page.title = "Flet Markdown Editor"
    page.window_width = 1200
    page.window_height = 800
    page.padding = 10
    
    # Устанавливаем светлую тему по умолчанию
    page.theme_mode = ft.ThemeMode.LIGHT
    
    # ... остальной код ...

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

Теперь, когда все готово, давайте соберем полный, финальный код нашего приложения.

Показать полный код приложения
# editor.py
import flet as ft

def main(page: ft.Page):
    # --- 1. Настройки окна ---
    page.title = "Flet Markdown Editor"
    page.window_width = 1200
    page.window_height = 800
    page.padding = 10
    
    # Устанавливаем светлую тему по умолчанию
    page.theme_mode = ft.ThemeMode.LIGHT

    # --- 2. Логика приложения: обработчик событий ---
    def handle_text_change(e):
        """
        Вызывается при каждом изменении текста в поле ввода.
        Обновляет область предпросмотра.
        """
        preview_area.value = e.control.value
        page.update()

    # --- 3. Виджеты интерфейса ---
    input_field = ft.TextField(
        multiline=True,
        expand=True,
        min_lines=40,
        hint_text="Пишите ваш Markdown здесь...\n\n# Заголовок\n\n* Список\n* Элементов",
        # Убираем стандартную рамку для чистого вида
        border_color="transparent",
        on_change=handle_text_change,
    )

    preview_area = ft.Markdown(
        value="Здесь будет ваш **отформатированный** текст.",
        expand=True,
        # Выбираем тему для подсветки синтаксиса в блоках кода
        code_theme="atom-one-dark",
    )

    # --- 4. Сборка интерфейса ---
    app_layout = ft.Row(
        controls=[
            input_field,
            ft.VerticalDivider(),
            preview_area
        ],
        expand=True,
    )

    # --- 5. Добавляем все на страницу и обновляем ---
    page.add(app_layout)
    page.update()

# --- 6. Запуск приложения ---
if __name__ == "__main__":
    ft.app(target=main)

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

6. Заключение и "Домашнее задание"

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

Но лучший способ закрепить знания — это применить их на практике. Я подготовил три небольших проекта, которые позволят вам расширить функционал нашего редактора и глубже погрузиться в возможности Flet.

Проект 1: Полноценная работа с файлами

Задача: Любому редактору нужны функции открытия и сохранения файлов. Добавьте в приложение верхнюю панель (AppBar) с двумя кнопками-иконками: "Открыть" и "Сохранить".

Что для этого нужно:

  1. ft.AppBar: Создайте верхнюю панель, в которую можно добавлять элементы, например, ft.IconButton.

  2. ft.FilePicker: Это специальный "невидимый" виджет, который нужно добавить в page.overlay. Он умеет вызывать нативный диалог операционной системы для выбора или сохранения файла.

  3. Логика:

    • При нажатии на кнопку "Открыть" вызывайте метод file_picker.pick_files().

    • При нажатии на "Сохранить" — file_picker.save_file().

    • Результат выбора файла (путь) придет в обработчик события on_result у FilePicker. В этом обработчике вам нужно будет либо прочитать содержимое файла и поместить его в input_field, либо записать input_field.value в выбранный файл.

Проект 2: Менеджер тем и настроек

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

Что для этого нужно:

  1. ft.IconButton: Добавьте в AppBar иконку (например, луны или солнца).

  2. Логика:

    • Напишите функцию-обработчик для события on_click этой кнопки.

    • Внутри функции проверяйте текущее значение page.theme_mode.

    • Если тема светлая (ft.ThemeMode.LIGHT), меняйте ее на темную (DARK), и наоборот.

    • Бонусный балл: Сделайте так, чтобы иконка на кнопке тоже менялась в зависимости от выбранной темы (солнце для темной, луна для светлой).

    • Не забудьте в конце обработчика вызвать page.update(), чтобы применить изменения.

Проект 3: Информационная строка состояния (Status Bar)

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

Что для этого нужно:

  1. Изменение компоновки: Вам понадобится обернуть текущий app_layout (наш ft.Row) в ft.Column. Column будет содержать два элемента: app_layout, который растягивается (expand=True), и новый ft.Row внизу, который и будет нашей строкой состояния.

  2. ft.Text: Добавьте в строку состояния один или два текстовых виджета.

  3. Логика:

    • Модифицируйте существующий обработчик handle_text_change.

    • После обновления preview_area, добавьте расчет статистики: len(e.control.value) для символов и len(e.control.value.split()) для слов.

    • Обновите свойство value у текстовых виджетов в строке состояния.

    • Один вызов page.update() в конце функции обновит и предпросмотр, и счетчики.

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

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