DevGang
Авторизоваться

TaiPy — некоторые причуды с аккуратной библиотекой графического интерфейса Python

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

TaiPy — это очень новая библиотека, поэтому в интернете почти НИЧЕГО о ней нет, но это отличный инструмент. Вот почему я решил написать об этом.

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

Ваш код всегда незащищенный, если вы ничего с ним не делаете.

TaiPy по умолчанию предоставляет каждый файл вашей реализации на веб-сервере. Единственный способ обойти это — создать файл .taipyignore, который скроет их. Но имейте в виду, что если вам нужно открыть файлы, например изображения, это также отфильтрует их. Файл имеет тот же синтаксис, что и файл .gitignore.

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

*
!/gui/images/*

Подробнее здесь

Элементы живого обновления

Я обнаружил, что изменение, внесенное в переменную, не отражается, если вы не обновите ее вручную. Кажется, это относится к таблицам, даже если для параметра rebuild установлено значение True. Я предполагаю, что это может быть ошибка TaiPy 2.4.0, но не уверен.

Вот пример:

def on_click_add_button(state):
    HOME_PAGE_TABLE.add_item(INPUT)
    state.refresh("HOME_PAGE_TABLE")

Если state.refresh не вызывается, изменения в таблице отражаются только при полном обновлении страницы или если пользователь каким-либо образом принудительно обновляет таблицу (например, путем изменения порядка элементов).

Поток выполнения

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

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

from taipy.gui import State
# STATS_TABLE is a Python object that TaiPy can interpret as a Table
# STATS_TABLE.tick() will update the values in the variable
def _timer_loop(state:State):
    STATS_TABLE.reset()
    while TIMER.is_running:
        STATS_TABLE.tick()
        state.refresh(STATS_TABLE)

state.refresh необходим, чтобы обновление отражалось в графическом интерфейсе. Согласно документации, в этом нет необходимости, если у вас установлен параметр rebuild для таблиц, но это не очень надежно работает с TaiPy v2.4.0.

Итак, если вы впервые подходите к этому, вы можете подумать о том, чтобы сделать что-то вроде

import threading

def timer_start(state:State):
    """THIS WILL NOT WORK"""
    thr = threading.Thread(target=_timer_loop, args=[state])
    thr.start()

Однако это приведет к исключению RuntimeError: Working outside of application context. Кажется, это происходит со всем, что пытается запустить поток. Способ сделать это можно найти здесь, который использует функцию invoke_long_callback, ЕСЛИ вам не нужна переменная состояния

Случай 1: мне не нужна переменная state

Если вам это не нужно, просто сделайте

invoke_long_callback(state, _timer_loop, user_function_args=[])

Но имейте в виду, что переменная state по-прежнему должна быть предоставлена ​​​​функции invoke_long_callback. Однако, если вам нужно передать state вашей функции, добавление его в параметр user_function_args не работает!

Случай 2: мне нужна переменная состояния

Как упоминалось в случае 1, state не может быть передано в user_function_args. Итак, как это сделать?

Нам понадобится функция get_state_id из taipy.gui. При этом мы можем косвенно получить переменную state и передать ее функции invoke_callback, но есть один недостаток, о котором я расскажу позже.

import threading
from taipy.gui import get_state_id, State, Gui

# Later in the code, pages can be added to the Gui with
# GUI_OBJ.add_page
# Then at the end we can do
# GUI_OBJ.run
GUI_OBJ = Gui()

def timer_start(state:State):
    thr = threading.Thread(target=_start_timer_thread, args=[get_state_id(state)])
    thr.start()

# You can probably merge these two functions into one but I haven't tested it ;) Might do in the near future
def _start_timer_thread(state_id: str):
    TIMER.start_timer()
    invoke_callback(GUI_OBJ, state_id, _timer_loop, args=[])

def _timer_loop(state:State):
    STATS_TABLE.reset()
    while TIMER.is_running:
        STATS_TABLE.tick()
        state.refresh(STATS_TABLE)

Недостатком этого является то, что нам нужен объект Gui, что вынуждает нас иметь более сложную структуру, потому что обычно GUI_OBJ объявляется после объявления всего остального из-за того, как работает область переменных TaiPy.

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

Область видимости переменных — неполное руководство

Когда дело доходит до этого, я до сих пор не уверен на 100%, как работает TaiPy. Надеюсь, в будущем я смогу отредактировать этот пост и разместить более полную информацию. Но вот мой опыт:

Обычно ваши программы TaiPy будут написаны примерно так (ОЧЕНЬ упрощенная версия):

import threading
from taipy.gui import Gui, get_state_id, invoke_callback, State
from components import STATS_TABLE, TIMER

root_page="""
<|{STATS_TABLE.table}|table|on_edit=on_edit_func|>
<|button|label=Start Timer|on_action=timer_start
"""

def on_edit_func(state):
     print("The table was edited!")

def timer_start(state:State):
    thr = threading.Thread(target=_start_timer_thread, args=[get_state_id(state)])
    thr.start()

def _start_timer_thread(state_id: str):
    TIMER.start_timer()
    invoke_callback(GUI_OBJ, state_id, _timer_loop, args=[])

def _timer_loop(state:State):
    STATS_TABLE.reset()
    while TIMER.is_running:
        STATS_TABLE.tick()
        state.refresh(STATS_TABLE)

GUI_OBJ = Gui(page=root_page)

GUI_OBJ.run(
        host="127.0.0.1",
        port=1234,
        dark_mode=False,
        debug=True,
        use_reloader=True,
)

Заметно, что он уже изрядно загружается всего одной кнопкой и таблицей, поэтому нам нужно разделить этот файл. Но как?

Обычно прямой способ сделать это, может быть, сделать что-то вроде этого?

""" This won't work! """
from taipy.gui import Gui

from pages import root_page
from callbacks import on_edit_func, timer_start
from components import STATS_TABLE, TIMER

GUI_OBJ = Gui(page=root_page)

GUI_OBJ.run(
        host="127.0.0.1",
        port=1234,
        dark_mode=False,
        debug=True,
        use_reloader=True
)

Может быть, не лучшее решение, но должно работать, нет? Не совсем, потому что, если вы помните предыдущий раздел, для работы timer_start требуется GUI_OBJ!

Если бы не это, этот код, скорее всего, работал бы.

Теперь дело в том, что TaiPy НУЖНО, чтобы все, что будет использовать графический интерфейс, находилось в той же области, что и при первом объявлении объекта графического интерфейса. Это также означает функцию timer_start, для которой нужен сам графический интерфейс.

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

  • Файл со ВСЕМИ необходимыми импортами и инициализацией Gui все в одном месте.
  • Затем назначение страниц графическому интерфейсу и запуск производится в другом файле.

Так, например, файлы gui_functional.py и main.py соответственно.

gui_functional.py
from taipy.gui import Gui

GUI_OBJ = Gui()

from components import STATS_TABLE, TIMER
from callbacks import on_edit_func, timer_start
from pages import *
main.py
from gui_functional import GUI_OBJ
from callbacks import root_page

GUI_OBJ.add_page("home", page=root_page)

GUI_OBJ.run(
        host="127.0.0.1",
        port=1234,
        dark_mode=False,
        debug=True,
        use_reloader=True
)

Источник:

#Python
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу