textual-event-messages

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Textual Event and Message Handling

Textual 事件与消息处理

Purpose

用途

Implement robust event handling and inter-widget communication in Textual using messages, keyboard bindings, and action dispatch. Messages enable loose coupling between components.
使用消息、键盘绑定和动作分发在Textual中实现可靠的事件处理与组件间通信。消息可实现组件间的松耦合。

Quick Start

快速开始

python
from textual.message import Message
from textual.app import App, ComposeResult
from textual.widgets import Button, Static
from textual import on

class ItemWidget(Static):
    """Widget that emits custom messages."""

    class ItemSelected(Message):
        """Posted when item is selected."""
        def __init__(self, item_id: str) -> None:
            super().__init__()
            self.item_id = item_id

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield ItemWidget()

    @on(ItemWidget.ItemSelected)
    def on_item_selected(self, message: ItemWidget.ItemSelected) -> None:
        """Handle item selection."""
        self.notify(f"Selected: {message.item_id}")
python
from textual.message import Message
from textual.app import App, ComposeResult
from textual.widgets import Button, Static
from textual import on

class ItemWidget(Static):
    """Widget that emits custom messages."""

    class ItemSelected(Message):
        """Posted when item is selected."""
        def __init__(self, item_id: str) -> None:
            super().__init__()
            self.item_id = item_id

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield ItemWidget()

    @on(ItemWidget.ItemSelected)
    def on_item_selected(self, message: ItemWidget.ItemSelected) -> None:
        """Handle item selection."""
        self.notify(f"Selected: {message.item_id}")

Instructions

操作步骤

Step 1: Define Custom Messages

步骤1:定义自定义消息

Create message classes to communicate between widgets:
python
from textual.message import Message
from dataclasses import dataclass
创建消息类以实现组件间通信:
python
from textual.message import Message
from dataclasses import dataclass

Simple message without data

Simple message without data

class DataRefreshed(Message): """Posted when data is refreshed.""" pass
class DataRefreshed(Message): """Posted when data is refreshed.""" pass

Message with data

Message with data

class ItemSelected(Message): """Posted when item is selected."""
def __init__(self, item_id: str, index: int) -> None:
    """Initialize message.

    Args:
        item_id: Selected item ID.
        index: Index in list.
    """
    super().__init__()
    self.item_id = item_id
    self.index = index
class ItemSelected(Message): """Posted when item is selected."""
def __init__(self, item_id: str, index: int) -> None:
    """Initialize message.

    Args:
        item_id: Selected item ID.
        index: Index in list.
    """
    super().__init__()
    self.item_id = item_id
    self.index = index

Message with complex data

Message with complex data

@dataclass(frozen=True) class SearchResult: """Result of search operation.""" query: str items: list[str] total_count: int
class SearchCompleted(Message): """Posted when search completes."""
def __init__(self, result: SearchResult) -> None:
    """Initialize with search result."""
    super().__init__()
    self.result = result

**Message Conventions:**
- Subclass `Message`
- Define immutable data in `__init__`
- Use descriptive PascalCase names
- Document the message purpose in docstring
- Use `frozen=True` for dataclasses to ensure immutability
@dataclass(frozen=True) class SearchResult: """Result of search operation.""" query: str items: list[str] total_count: int
class SearchCompleted(Message): """Posted when search completes."""
def __init__(self, result: SearchResult) -> None:
    """Initialize with search result."""
    super().__init__()
    self.result = result

**消息约定:**
- 继承自`Message`
- 在`__init__`中定义不可变数据
- 使用描述性的大驼峰命名
- 在文档字符串中说明消息用途
- 为数据类使用`frozen=True`以确保不可变性

Step 2: Post and Handle Messages

步骤2:发送与处理消息

Post messages from widgets and handle in parents:
python
from textual import on
from textual.widgets import Static, ListView, ListItem
from textual.app import ComposeResult

class ItemWidget(Static):
    """Widget that posts custom messages."""

    class ItemClicked(Message):
        """Posted when item is clicked."""
        def __init__(self, item_id: str) -> None:
            super().__init__()
            self.item_id = item_id

    def __init__(self, item_id: str, label: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._item_id = item_id
        self._label = label

    def render(self) -> str:
        return f"[{self._item_id}] {self._label}"

    def on_click(self) -> None:
        """Post message when clicked."""
        self.post_message(self.ItemClicked(self._item_id))

class ItemListWidget(Static):
    """Parent widget that handles item messages."""

    def compose(self) -> ComposeResult:
        yield ListView(id="item-list")

    async def on_mount(self) -> None:
        """Create items."""
        list_view = self.query_one("#item-list", ListView)
        for i, item_id in enumerate(["a", "b", "c"]):
            await list_view.append(ListItem(
                ItemWidget(item_id, f"Item {item_id}")
            ))

    @on(ItemWidget.ItemClicked)
    async def on_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
        """Handle item click - called automatically."""
        self.notify(f"Clicked: {message.item_id}")
从组件发送消息并在父组件中处理:
python
from textual import on
from textual.widgets import Static, ListView, ListItem
from textual.app import ComposeResult

class ItemWidget(Static):
    """Widget that posts custom messages."""

    class ItemClicked(Message):
        """Posted when item is clicked."""
        def __init__(self, item_id: str) -> None:
            super().__init__()
            self.item_id = item_id

    def __init__(self, item_id: str, label: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._item_id = item_id
        self._label = label

    def render(self) -> str:
        return f"[{self._item_id}] {self._label}"

    def on_click(self) -> None:
        """Post message when clicked."""
        self.post_message(self.ItemClicked(self._item_id))

class ItemListWidget(Static):
    """Parent widget that handles item messages."""

    def compose(self) -> ComposeResult:
        yield ListView(id="item-list")

    async def on_mount(self) -> None:
        """Create items."""
        list_view = self.query_one("#item-list", ListView)
        for i, item_id in enumerate(["a", "b", "c"]):
            await list_view.append(ListItem(
                ItemWidget(item_id, f"Item {item_id}")
            ))

    @on(ItemWidget.ItemClicked)
    async def on_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
        """Handle item click - called automatically."""
        self.notify(f"Clicked: {message.item_id}")

Alternative handler without @on decorator

Alternative handler without @on decorator

class AltListWidget(Static): """Using on_* method naming convention."""
def on_item_widget_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
    """Auto-routed handler.

    Convention: on_{widget_class_snake_case}_{message_class_snake_case}
    """
    self.notify(f"Clicked: {message.item_id}")

**Message Routing:**
1. Widget posts message: `self.post_message(MyMessage())`
2. Message bubbles up to parent widgets
3. Parent handles with `@on(MessageType)` or `on_*` method
4. First handler to handle the message stops propagation (can call `event.stop()`)
class AltListWidget(Static): """Using on_* method naming convention."""
def on_item_widget_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
    """Auto-routed handler.

    Convention: on_{widget_class_snake_case}_{message_class_snake_case}
    """
    self.notify(f"Clicked: {message.item_id}")

**消息路由:**
1. 组件发送消息:`self.post_message(MyMessage())`
2. 消息向上冒泡至父组件
3. 父组件通过`@on(MessageType)`或`on_*`方法处理
4. 第一个处理消息的处理器会停止消息传播(可调用`event.stop()`)

Step 3: Implement Keyboard Event Handlers

步骤3:实现键盘事件处理器

Handle keyboard input directly:
python
from textual.events import Key, Paste
from textual.widgets import Static

class KeyboardWidget(Static):
    """Widget handling keyboard events."""

    def on_key(self, event: Key) -> None:
        """Called when any key is pressed.

        Args:
            event: Key event with key name.
        """
        key_name = event.key
        # Common keys: "up", "down", "left", "right", "enter", "escape"
        # Letters: "a", "b", "ctrl+a", "shift+a"

        if key_name == "enter":
            self.handle_enter()
        elif key_name == "escape":
            self.handle_escape()
        elif key_name == "ctrl+c":
            self.app.exit()

    def handle_enter(self) -> None:
        """Handle Enter key."""
        self.update("Enter pressed")

    def handle_escape(self) -> None:
        """Handle Escape key."""
        self.update("Escape pressed")

    def on_paste(self, event: Paste) -> None:
        """Called when text is pasted.

        Args:
            event: Paste event with text.
        """
        self.update(f"Pasted: {event.text}")
Key Event Handling:
  • on_key()
    called for keyboard events
  • Common keys: arrow keys, enter, escape, tab, delete
  • Modifier keys: "ctrl+key", "shift+key", "alt+key"
  • Special: "home", "end", "page_up", "page_down", "F1"-"F12"
直接处理键盘输入:
python
from textual.events import Key, Paste
from textual.widgets import Static

class KeyboardWidget(Static):
    """Widget handling keyboard events."""

    def on_key(self, event: Key) -> None:
        """Called when any key is pressed.

        Args:
            event: Key event with key name.
        """
        key_name = event.key
        # Common keys: "up", "down", "left", "right", "enter", "escape"
        # Letters: "a", "b", "ctrl+a", "shift+a"

        if key_name == "enter":
            self.handle_enter()
        elif key_name == "escape":
            self.handle_escape()
        elif key_name == "ctrl+c":
            self.app.exit()

    def handle_enter(self) -> None:
        """Handle Enter key."""
        self.update("Enter pressed")

    def handle_escape(self) -> None:
        """Handle Escape key."""
        self.update("Escape pressed")

    def on_paste(self, event: Paste) -> None:
        """Called when text is pasted.

        Args:
            event: Paste event with text.
        """
        self.update(f"Pasted: {event.text}")
键盘事件处理:
  • on_key()
    会响应所有键盘事件
  • 常见按键:方向键、回车键、ESC键、Tab键、删除键
  • 修饰键组合:"ctrl+key"、"shift+key"、"alt+key"
  • 特殊按键:"home"、"end"、"page_up"、"page_down"、"F1"至"F12"

Step 4: Add Keyboard Bindings and Actions

步骤4:添加键盘绑定与动作

Bindings provide discoverable keyboard shortcuts tied to actions:
python
from textual.app import App, ComposeResult
from textual.binding import Binding
from typing import ClassVar

class MyApp(App):
    """App with keyboard bindings."""

    BINDINGS: ClassVar[list[Binding]] = [
        # (key, action, description, show, priority)
        Binding("q", "quit", "Quit", show=True, priority=True),
        Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
        Binding("r", "refresh", "Refresh"),
        Binding("n", "new_item", "New"),
        Binding("?", "show_help", "Help"),
    ]

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("Content")
        yield Footer()  # Shows bindings

    def action_refresh(self) -> None:
        """Action handler for 'refresh' binding."""
        self.notify("Refreshed")

    def action_new_item(self) -> None:
        """Action handler for 'new_item' binding."""
        self.notify("Creating new item")

    async def action_show_help(self) -> None:
        """Action handlers can be async."""
        # Show help dialog
        pass
Binding Conventions:
  • Binding key to action method name
  • Action method:
    action_{action_name}
  • Action methods can be sync or async
  • Bindings shown in Footer if
    show=True
  • priority=True
    for high-priority bindings (quit)
  • Bindings override key event handlers when matched
绑定功能可将可发现的键盘快捷键与动作关联:
python
from textual.app import App, ComposeResult
from textual.binding import Binding
from typing import ClassVar

class MyApp(App):
    """App with keyboard bindings."""

    BINDINGS: ClassVar[list[Binding]] = [
        # (key, action, description, show, priority)
        Binding("q", "quit", "Quit", show=True, priority=True),
        Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
        Binding("r", "refresh", "Refresh"),
        Binding("n", "new_item", "New"),
        Binding("?", "show_help", "Help"),
    ]

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("Content")
        yield Footer()  # Shows bindings

    def action_refresh(self) -> None:
        """Action handler for 'refresh' binding."""
        self.notify("Refreshed")

    def action_new_item(self) -> None:
        """Action handler for 'new_item' binding."""
        self.notify("Creating new item")

    async def action_show_help(self) -> None:
        """Action handlers can be async."""
        # Show help dialog
        pass
绑定约定:
  • 将按键与动作方法名绑定
  • 动作方法命名格式:
    action_{action_name}
  • 动作方法可以是同步或异步的
  • 如果
    show=True
    ,绑定会在Footer中显示
  • 为高优先级绑定(如退出)设置
    priority=True
  • 匹配到绑定时,绑定会覆盖键盘事件处理器

Step 5: Handle Mouse Events

步骤5:处理鼠标事件

React to mouse clicks and movements:
python
from textual.events import MouseDown, MouseUp, MouseMove
from textual.widgets import Static

class MouseWidget(Static):
    """Widget handling mouse events."""

    def on_mouse_down(self, event: MouseDown) -> None:
        """Called when mouse button pressed.

        Args:
            event: MouseDown event with position and button.
        """
        x, y = event.x, event.y
        button = event.button  # 1: left, 2: middle, 3: right

        if button == 1:  # Left click
            self.handle_click(x, y)

    def on_mouse_up(self, event: MouseUp) -> None:
        """Called when mouse button released."""
        pass

    def on_mouse_move(self, event: MouseMove) -> None:
        """Called when mouse moves over widget."""
        x, y = event.x, event.y
        # Update display or state based on position

    def handle_click(self, x: int, y: int) -> None:
        """Handle click at position."""
        self.update(f"Clicked at ({x}, {y})")
Mouse Events:
  • on_mouse_down()
    - Button pressed
  • on_mouse_up()
    - Button released
  • on_mouse_move()
    - Mouse moved
  • on_click()
    - Single click (widget-specific)
  • on_double_click()
    - Double click (widget-specific)
响应鼠标点击与移动:
python
from textual.events import MouseDown, MouseUp, MouseMove
from textual.widgets import Static

class MouseWidget(Static):
    """Widget handling mouse events."""

    def on_mouse_down(self, event: MouseDown) -> None:
        """Called when mouse button pressed.

        Args:
            event: MouseDown event with position and button.
        """
        x, y = event.x, event.y
        button = event.button  # 1: left, 2: middle, 3: right

        if button == 1:  # Left click
            self.handle_click(x, y)

    def on_mouse_up(self, event: MouseUp) -> None:
        """Called when mouse button released."""
        pass

    def on_mouse_move(self, event: MouseMove) -> None:
        """Called when mouse moves over widget."""
        x, y = event.x, event.y
        # Update display or state based on position

    def handle_click(self, x: int, y: int) -> None:
        """Handle click at position."""
        self.update(f"Clicked at ({x}, {y})")
鼠标事件:
  • on_mouse_down()
    - 鼠标按键按下
  • on_mouse_up()
    - 鼠标按键释放
  • on_mouse_move()
    - 鼠标移动
  • on_click()
    - 单击(组件专属)
  • on_double_click()
    - 双击(组件专属)

Step 6: Control Event Flow

步骤6:控制事件流

Stop event propagation and bubble events:
python
from textual.events import Key
from textual.app import ComposeResult
from textual.widgets import Static, Container

class StopPropagationWidget(Static):
    """Widget that stops event propagation."""

    def on_key(self, event: Key) -> None:
        """Handle key and stop propagation."""
        if event.key == "enter":
            self.handle_enter()
            event.stop()  # Stop parent from handling
        # If not handled, event propagates to parent

    def handle_enter(self) -> None:
        """Handle enter key."""
        self.update("Handled locally")

class EventBubblingExample(Container):
    """Demonstrate event bubbling."""

    def compose(self) -> ComposeResult:
        yield StopPropagationWidget(id="inner")

    def on_key(self, event: Key) -> None:
        """Parent key handler."""
        # Called if child doesn't call event.stop()
        pass
Event Control:
  • event.stop()
    - Stop propagation to parent
  • event.prevent_default()
    - Prevent default behavior
  • Messages bubble up automatically
  • Key events can be stopped
停止事件传播与事件冒泡:
python
from textual.events import Key
from textual.app import ComposeResult
from textual.widgets import Static, Container

class StopPropagationWidget(Static):
    """Widget that stops event propagation."""

    def on_key(self, event: Key) -> None:
        """Handle key and stop propagation."""
        if event.key == "enter":
            self.handle_enter()
            event.stop()  # Stop parent from handling
        # If not handled, event propagates to parent

    def handle_enter(self) -> None:
        """Handle enter key."""
        self.update("Handled locally")

class EventBubblingExample(Container):
    """Demonstrate event bubbling."""

    def compose(self) -> ComposeResult:
        yield StopPropagationWidget(id="inner")

    def on_key(self, event: Key) -> None:
        """Parent key handler."""
        # Called if child doesn't call event.stop()
        pass
事件控制:
  • event.stop()
    - 停止向父组件传播事件
  • event.prevent_default()
    - 阻止默认行为
  • 消息会自动向上冒泡
  • 键盘事件可被停止传播

Examples

示例

Example 1: Agent Command Widget with Custom Messages

示例1:带自定义消息的Agent命令组件

python
from textual.message import Message
from textual.widgets import Static, Input, Button
from textual.containers import Container, Horizontal
from textual.app import ComposeResult
from textual import on

class CommandWidget(Container):
    """Widget for entering agent commands."""

    class CommandSubmitted(Message):
        """Posted when command is submitted."""

        def __init__(self, agent_id: str, command: str) -> None:
            super().__init__()
            self.agent_id = agent_id
            self.command = command

    class CommandCancelled(Message):
        """Posted when command is cancelled."""

        def __init__(self, agent_id: str) -> None:
            super().__init__()
            self.agent_id = agent_id

    def __init__(self, agent_id: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._agent_id = agent_id

    def compose(self) -> ComposeResult:
        """Compose command input widget."""
        with Horizontal():
            yield Input(
                placeholder="Enter command...",
                id="command-input",
            )
            yield Button("Send", id="btn-send", variant="primary")
            yield Button("Clear", id="btn-clear")

    async def on_button_pressed(self, event: Button.Pressed) -> None:
        """Handle button press."""
        if event.button.id == "btn-send":
            await self._submit_command()
        elif event.button.id == "btn-clear":
            await self._cancel_command()

    async def _submit_command(self) -> None:
        """Submit command to agent."""
        input_field = self.query_one("#command-input", Input)
        command = input_field.value.strip()

        if command:
            input_field.value = ""
            self.post_message(self.CommandSubmitted(self._agent_id, command))

    async def _cancel_command(self) -> None:
        """Cancel command entry."""
        input_field = self.query_one("#command-input", Input)
        input_field.value = ""
        self.post_message(self.CommandCancelled(self._agent_id))
python
from textual.message import Message
from textual.widgets import Static, Input, Button
from textual.containers import Container, Horizontal
from textual.app import ComposeResult
from textual import on

class CommandWidget(Container):
    """Widget for entering agent commands."""

    class CommandSubmitted(Message):
        """Posted when command is submitted."""

        def __init__(self, agent_id: str, command: str) -> None:
            super().__init__()
            self.agent_id = agent_id
            self.command = command

    class CommandCancelled(Message):
        """Posted when command is cancelled."""

        def __init__(self, agent_id: str) -> None:
            super().__init__()
            self.agent_id = agent_id

    def __init__(self, agent_id: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._agent_id = agent_id

    def compose(self) -> ComposeResult:
        """Compose command input widget."""
        with Horizontal():
            yield Input(
                placeholder="Enter command...",
                id="command-input",
            )
            yield Button("Send", id="btn-send", variant="primary")
            yield Button("Clear", id="btn-clear")

    async def on_button_pressed(self, event: Button.Pressed) -> None:
        """Handle button press."""
        if event.button.id == "btn-send":
            await self._submit_command()
        elif event.button.id == "btn-clear":
            await self._cancel_command()

    async def _submit_command(self) -> None:
        """Submit command to agent."""
        input_field = self.query_one("#command-input", Input)
        command = input_field.value.strip()

        if command:
            input_field.value = ""
            self.post_message(self.CommandSubmitted(self._agent_id, command))

    async def _cancel_command(self) -> None:
        """Cancel command entry."""
        input_field = self.query_one("#command-input", Input)
        input_field.value = ""
        self.post_message(self.CommandCancelled(self._agent_id))

Parent handling messages

Parent handling messages

class AgentConsoleWidget(Static): """Console displaying agent commands and responses."""
def compose(self) -> ComposeResult:
    yield Static("Agent Console", classes="header")
    yield Static("Ready", id="console-output")
    yield CommandWidget("agent-1", id="command-widget")

@on(CommandWidget.CommandSubmitted)
async def on_command_submitted(self, message: CommandWidget.CommandSubmitted) -> None:
    """Handle command submission."""
    output = self.query_one("#console-output", Static)
    output.update(f"Executing: {message.command}\n")
    self.notify(f"Command sent to {message.agent_id}")

@on(CommandWidget.CommandCancelled)
async def on_command_cancelled(self, message: CommandWidget.CommandCancelled) -> None:
    """Handle command cancellation."""
    output = self.query_one("#console-output", Static)
    output.update("Command cancelled\n")
undefined
class AgentConsoleWidget(Static): """Console displaying agent commands and responses."""
def compose(self) -> ComposeResult:
    yield Static("Agent Console", classes="header")
    yield Static("Ready", id="console-output")
    yield CommandWidget("agent-1", id="command-widget")

@on(CommandWidget.CommandSubmitted)
async def on_command_submitted(self, message: CommandWidget.CommandSubmitted) -> None:
    """Handle command submission."""
    output = self.query_one("#console-output", Static)
    output.update(f"Executing: {message.command}\n")
    self.notify(f"Command sent to {message.agent_id}")

@on(CommandWidget.CommandCancelled)
async def on_command_cancelled(self, message: CommandWidget.CommandCancelled) -> None:
    """Handle command cancellation."""
    output = self.query_one("#console-output", Static)
    output.update("Command cancelled\n")
undefined

Example 2: Keyboard Navigation in List

示例2:列表中的键盘导航

python
from textual.widgets import Static, ListView, ListItem
from textual.events import Key
from textual.app import ComposeResult

class NavigableListWidget(Static):
    """List with keyboard navigation."""

    DEFAULT_CSS = """
    NavigableListWidget {
        height: 100%;
    }

    NavigableListWidget ListView {
        height: 1fr;
    }

    NavigableListWidget .highlight {
        background: $boost;
    }
    """

    def compose(self) -> ComposeResult:
        yield ListView(id="item-list")

    async def on_mount(self) -> None:
        """Populate list."""
        list_view = self.query_one("#item-list", ListView)
        for i in range(10):
            await list_view.append(
                ListItem(Static(f"Item {i}"))
            )

    def on_key(self, event: Key) -> None:
        """Handle keyboard navigation."""
        list_view = self.query_one("#item-list", ListView)

        if event.key == "up":
            if list_view.index is not None and list_view.index > 0:
                list_view.index -= 1
                event.stop()

        elif event.key == "down":
            if list_view.index is not None and list_view.index < len(list_view.children) - 1:
                list_view.index += 1
                event.stop()

        elif event.key == "home":
            list_view.index = 0
            event.stop()

        elif event.key == "end":
            list_view.index = len(list_view.children) - 1
            event.stop()
python
from textual.widgets import Static, ListView, ListItem
from textual.events import Key
from textual.app import ComposeResult

class NavigableListWidget(Static):
    """List with keyboard navigation."""

    DEFAULT_CSS = """
    NavigableListWidget {
        height: 100%;
    }

    NavigableListWidget ListView {
        height: 1fr;
    }

    NavigableListWidget .highlight {
        background: $boost;
    }
    """

    def compose(self) -> ComposeResult:
        yield ListView(id="item-list")

    async def on_mount(self) -> None:
        """Populate list."""
        list_view = self.query_one("#item-list", ListView)
        for i in range(10):
            await list_view.append(
                ListItem(Static(f"Item {i}"))
            )

    def on_key(self, event: Key) -> None:
        """Handle keyboard navigation."""
        list_view = self.query_one("#item-list", ListView)

        if event.key == "up":
            if list_view.index is not None and list_view.index > 0:
                list_view.index -= 1
                event.stop()

        elif event.key == "down":
            if list_view.index is not None and list_view.index < len(list_view.children) - 1:
                list_view.index += 1
                event.stop()

        elif event.key == "home":
            list_view.index = 0
            event.stop()

        elif event.key == "end":
            list_view.index = len(list_view.children) - 1
            event.stop()

Requirements

依赖要求

  • Textual >= 0.45.0
  • Python 3.9+
  • Textual >= 0.45.0
  • Python 3.9+

Common Patterns

常见模式

Modal Message Handling

模态消息处理

python
class ConfirmDialog(Screen):
    """Modal dialog that returns a choice."""

    def __init__(self, prompt: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._prompt = prompt

    def action_confirm(self) -> None:
        """Confirm and return True."""
        self.app.pop_screen(result=True)

    def action_cancel(self) -> None:
        """Cancel and return False."""
        self.app.pop_screen(result=False)
python
class ConfirmDialog(Screen):
    """Modal dialog that returns a choice."""

    def __init__(self, prompt: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._prompt = prompt

    def action_confirm(self) -> None:
        """Confirm and return True."""
        self.app.pop_screen(result=True)

    def action_cancel(self) -> None:
        """Cancel and return False."""
        self.app.pop_screen(result=False)

Usage

Usage

async def show_confirm() -> bool: """Show confirmation dialog.""" result = await self.app.push_screen_wait( ConfirmDialog("Are you sure?") ) return result
undefined
async def show_confirm() -> bool: """Show confirmation dialog.""" result = await self.app.push_screen_wait( ConfirmDialog("Are you sure?") ) return result
undefined

Event Debouncing

事件防抖

python
import asyncio
from textual.widgets import Input

class DebouncedInput(Input):
    """Input with debounced on_change events."""

    class ValueChanged(Message):
        def __init__(self, value: str) -> None:
            super().__init__()
            self.value = value

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._debounce_timer: asyncio.Task | None = None

    async def on_input_changed(self, event: Input.Changed) -> None:
        """Handle input change with debounce."""
        # Cancel previous timer
        if self._debounce_timer:
            self._debounce_timer.cancel()

        # Schedule new timer
        self._debounce_timer = asyncio.create_task(self._emit_change(event.value))

    async def _emit_change(self, value: str) -> None:
        """Emit change after delay."""
        await asyncio.sleep(0.5)  # 500ms debounce
        self.post_message(self.ValueChanged(value))
python
import asyncio
from textual.widgets import Input

class DebouncedInput(Input):
    """Input with debounced on_change events."""

    class ValueChanged(Message):
        def __init__(self, value: str) -> None:
            super().__init__()
            self.value = value

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._debounce_timer: asyncio.Task | None = None

    async def on_input_changed(self, event: Input.Changed) -> None:
        """Handle input change with debounce."""
        # Cancel previous timer
        if self._debounce_timer:
            self._debounce_timer.cancel()

        # Schedule new timer
        self._debounce_timer = asyncio.create_task(self._emit_change(event.value))

    async def _emit_change(self, value: str) -> None:
        """Emit change after delay."""
        await asyncio.sleep(0.5)  # 500ms debounce
        self.post_message(self.ValueChanged(value))

See Also

相关链接

  • textual-app-lifecycle.md - Keyboard bindings and actions
  • textual-widget-development.md - Widget lifecycle
  • textual-app-lifecycle.md - 键盘绑定与动作
  • textual-widget-development.md - 组件生命周期