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
соответственно.
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 *
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
)