Textual Python: запросы DOM, query_one и CSS-селекторы

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

Если вы только осваиваете интерфейсы на Python, рядом стоит держать базовые материалы по UI-подходам. Например, в статье про создание простого GUI приложения с Python и Tkinter хорошо видна разница между классическим оконным интерфейсом и современным TUI-подходом Textual

Метод query_one

Вы встретите метод query_one() во многих документациях Textual и в приложениях на GitHub. Его можно использовать для получения одного виджета, который соответствует CSS-селектору или типу виджета

В метод query_one() можно передать до двух параметров:

  • CSS-селектор
  • Тип виджета
  • Или оба одновременно

Если передаете оба параметра, сначала укажите CSS-селектор, затем тип виджета

Попробуйте это на практике: откройте ваш Python-редактор и создайте файл с названием query_input.py. Введите в него следующий код:

# query_input.py
from textual.app import App, ComposeResult
from textual.widgets import Button, Input
class QueryInput(App):
    def compose(self) -> ComposeResult:
        yield Input()
        yield Button("Обновить ввод")
    def on_button_pressed(self) -> None:
        input_widget = self.query_one(Input)
        new_string = f"Вы ввели: {input_widget.value}"
        input_widget.value = new_string
if __name__ == "__main__":
    app = QueryInput()
    app.run()

Этот код создает виджеты Input и Button. Введите текст в Input и нажмите кнопку. Метод on_button_pressed() вызовется, в нем будет вызов query_one() и передача типа Input. Затем обновится значение этого виджета

Когда использовать query_one, а когда нет

query_one() удобен, когда в интерфейсе должен быть ровно один подходящий виджет. Это хороший выбор для поля поиска, основного заголовка, панели статуса или конкретной кнопки с уникальным id. Такой код легко читать: по вызову сразу видно, что приложение ожидает один объект

Но у этого удобства есть обратная сторона. Если селектор ничего не найдет или найдет несколько подходящих элементов в ситуации, где ожидается один, приложение получит ошибку. Поэтому query_one() лучше использовать там, где структура интерфейса стабильна и вы контролируете compose()

Для динамических списков, таблиц, повторяющихся карточек и наборов кнопок безопаснее использовать query(). Он возвращает коллекцию, с которой можно работать в цикле, фильтровать результат и постепенно обновлять несколько элементов

Практическое правило простое: если виджет уникален по смыслу, используйте query_one(). Если элементов может быть несколько, используйте query()

Запросы Textual

Textual поддерживает несколько способов запроса DOM. Можно использовать метод query(), чтобы найти множество виджетов. Он возвращает объект DOMQuery, который ведет себя как список виджетов

Вот как это работает. Создайте новый Python-файл с названием query_all.py и добавьте код:

# query_all.py
from textual.app import App, ComposeResult
from textual.widgets import Button, Label
class QueryApp(App):
    def compose(self) -> ComposeResult:
        yield Label("Нажмите кнопку", id="label")
        yield Button("Тест", id="button")
    def on_button_pressed(self) -> None:
        widgets = self.query()
        s = ""
        for widget in widgets:
            s += f"{widget}\n"
        label = self.query_one("#label")
        label.update(s)
if __name__ == "__main__":
    app = QueryApp()
    app.run()

Идея в том, чтобы получить все виджеты в приложении и вывести их. Поскольку вывести что-либо на терминал с блокированием stdout нельзя, создается строка с виджетами, разделенными переводами строк, и она обновляет Label

Возможно, вы удивитесь, увидев вывод. Кажется, что в списке должны быть только Label и Button, но здесь есть еще Screen, ToastRack и Tooltip, которые идут с приложением. ToastRack располагает Toast-виджеты — уведомления, а Tooltip показывает подсказки при наведении мышкой

Подробнее об этих виджетах сейчас знать не нужно

Обратите внимание, что все методы query можно использовать как с App, так и с Widget

Вы можете использовать CSS селекторы с query() так же, как и с query_one(). Разница в том, что query() возвращает всегда итерируемый DOMObject

Предположим, вам нужно получить все виджеты Button и пройтись по ним. Создайте python-файл query_button.py с кодом:

# query_buttons.py
from textual.app import App, ComposeResult
from textual.widgets import Button, Label
class QueryApp(App):
    def compose(self) -> ComposeResult:
        yield Label("Нажмите кнопку", id="label")
        yield Button("Один", id="one")
        yield Button("Два", id="two")
        yield Button("Три")
    def on_button_pressed(self) -> None:
        s = ""
        for widget in self.query("Button"):
            s += f"{widget}\n"
        label = self.query_one("#label")
        label.update(s)
if __name__ == "__main__":
    app = QueryApp()
    app.run()

Вы передаете строку «Button» в query(). В query_one передавали тип Button. При запуске и нажатии вы увидите вывод:

Все работает хорошо! Вы запросили DOM и вернули все Button-виджеты

CSS-селекторы в Textual

Запросы Textual похожи на CSS-селекторы в веб-разработке. Это удобно, потому что один и тот же принцип можно использовать и для стилизации, и для поиска элементов в дереве интерфейса

Самые частые варианты:

  • #label — найти виджет по id
  • .disabled — найти виджеты с CSS-классом
  • Button — найти все кнопки
  • Button.disabled — найти кнопки с конкретным классом

На практике лучше давать важным виджетам понятные id. Например, id="status", id="search" или id="result_list" читаются лучше, чем поиск по общей структуре дерева. Это особенно важно, когда приложение растет и один экран начинает содержать много похожих виджетов

Если вы строите собственные компоненты, посмотрите также материал Textual: создание пользовательского чекбокса в Python. Он дополняет эту тему: сначала вы находите виджеты через DOM-запросы, затем начинаете проектировать свои элементы интерфейса

Если нужно найти все отключенные кнопки, можно использовать стиль disabled или CSS-атрибут. Обновите запрос так:

widgets = self.query("Button.disabled")

Объекты query имеют метод results(), который можно использовать вместо обхода в цикле. Например:

widgets = self.query(".disabled").results(Button)
s = ""
for widget in widgets:
    s += f"{widget}\n"

Этот код сочетает последний запрос с примером. Хотя он длиннее, такой код легче читать

Еще одно преимущество results() в том, что тайпчекеры как Mypy могут определить тип виджета в цикле. Без results() они видят объект Widget, а не Button

Почему results() помогает с типизацией

В небольшом примере разница кажется несущественной: можно пройтись по результатам query() и вручную работать с каждым объектом. Но в реальном приложении это быстро становится неудобным. Редактор хуже подсказывает методы, типы становятся менее очевидными, а ошибки всплывают уже во время запуска

Метод results(Button) явно говорит коду и разработчику: дальше мы работаем именно с кнопками. Это помогает Mypy, Pyright и другим инструментам статической проверки. Если внутри цикла вы случайно обратитесь к методу, которого нет у Button, подсветка ошибки появится раньше

Для командной разработки это особенно полезно. Один разработчик может изменить структуру экрана, другой — обработчик событий. Чем точнее описан тип результата, тем меньше шанс, что изменение интерфейса незаметно сломает логику

Типичные ошибки при DOM-запросах

Первая ошибка — искать виджет слишком общим селектором. Например, self.query_one(Button) работает, пока кнопка одна. Как только на экране появится вторая кнопка, логика станет неоднозначной. Лучше заранее использовать id или класс

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

Третья ошибка — забывать, что DOM Textual включает не только ваши виджеты. В выдаче query() могут быть системные элементы вроде Screen, ToastRack или Tooltip. Это нормально, но поэтому для рабочих сценариев лучше уточнять селектор, а не полагаться на полный список всех объектов

Четвертая ошибка — обновлять текстовое поле без учета исходного значения. В примере выше строка перезаписывается результатом Вы ввели: .... Для учебного кода это нормально, но в приложении стоит отдельно хранить исходные данные, чтобы повторное нажатие кнопки не добавляло префикс поверх уже измененного текста

Вы узнали основы работы с методами запросов DOM в Textual. Вы можете получить доступ к одному или нескольким виджетам интерфейса

В частности, в статье рассмотрены:

  • Метод query_one
  • Запросы Textual
  • CSS-селекторы для DOM
  • Разница между query(), query_one() и results()

Textual — отличный способ создания интерфейсов на Python. Рекомендуется ознакомиться!

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x