textual-widget-development

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Textual Widget Development

Textual Widget开发

Purpose

开发目标

Build reusable, composable Textual widgets that follow functional principles, proper lifecycle management, and type safety. Widgets are the fundamental building blocks of Textual applications.
开发遵循函数式原则、具备完善生命周期管理与类型安全的可复用、可组合Textual Widget。Widget是Textual应用的基础组成单元。

Quick Start

快速入门

python
from textual.app import ComposeResult
from textual.widgets import Static, Container
from textual.containers import Vertical

class SimpleWidget(Static):
    """A simple reusable widget."""

    DEFAULT_CSS = """
    SimpleWidget {
        height: auto;
        border: solid $primary;
        padding: 1;
    }
    """

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

    def render(self) -> str:
        """Render widget content."""
        return f"Title: {self._title}"
python
from textual.app import ComposeResult
from textual.widgets import Static, Container
from textual.containers import Vertical

class SimpleWidget(Static):
    """A simple reusable widget."""

    DEFAULT_CSS = """
    SimpleWidget {
        height: auto;
        border: solid $primary;
        padding: 1;
    }
    """

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

    def render(self) -> str:
        """Render widget content."""
        return f"Title: {self._title}"

Use in app:

Use in app:

class MyApp(App): def compose(self) -> ComposeResult: yield SimpleWidget("Hello")
undefined
class MyApp(App): def compose(self) -> ComposeResult: yield SimpleWidget("Hello")
undefined

Instructions

操作步骤

Step 1: Choose Widget Base Class

步骤1:选择Widget基类

Select appropriate base class based on widget purpose:
python
from textual.widgets import Static, Container, Input, Button
from textual.containers import Vertical, Horizontal, Container as GenericContainer
根据Widget的用途选择合适的基类:
python
from textual.widgets import Static, Container, Input, Button
from textual.containers import Vertical, Horizontal, Container as GenericContainer

For custom content/display - Simple widgets

For custom content/display - Simple widgets

class StatusWidget(Static): """Displays status information.""" pass
class StatusWidget(Static): """Displays status information.""" pass

For layout/composition - Container widgets

For layout/composition - Container widgets

class DashboardWidget(Container): """Composes multiple child widgets.""" pass
class DashboardWidget(Container): """Composes multiple child widgets.""" pass

Built-in widgets (ready to use)

Built-in widgets (ready to use)

- Static: Display text/rich content

- Static: Display text/rich content

- Input: Text input field

- Input: Text input field

- Button: Clickable button

- Button: Clickable button

- Label: Static label

- Label: Static label

- Select: Dropdown selector

- Select: Dropdown selector

- DataTable: Tabular data

- DataTable: Tabular data

- Tree: Hierarchical data

- Tree: Hierarchical data


**Guidelines:**
- Use `Static` for display-only content
- Use `Container` when you need to compose child widgets
- Use built-in widgets first before creating custom ones
- Create custom widgets only when built-in options don't fit

**指导原则:**
- 使用`Static`实现仅用于内容展示Widget
- 需要组合子Widget时使用`Container`
- 优先使用内置Widget,不满足需求时再自定义
- 仅当内置选项无法满足需求时才创建自定义Widget

Step 2: Define Widget Initialization and Configuration

步骤2:定义Widget初始化与配置

Implement
__init__
with proper type hints and parent class initialization:
python
from typing import ClassVar
from textual.app import ComposeResult
from textual.widgets import Static

class ConfigurableWidget(Static):
    """Widget with configurable parameters."""

    DEFAULT_CSS = """
    ConfigurableWidget {
        height: auto;
        border: solid $primary;
        padding: 1;
    }

    ConfigurableWidget .header {
        background: $boost;
        text-style: bold;
    }
    """

    # Class constants
    BORDER_COLOR: ClassVar[str] = "$primary"

    def __init__(
        self,
        title: str,
        content: str = "",
        *,
        name: str | None = None,
        id: str | None = None,  # noqa: A002
        classes: str | None = None,
        variant: str = "default",
    ) -> None:
        """Initialize widget.

        Args:
            title: Widget title.
            content: Initial content.
            name: Widget name.
            id: Widget ID for querying.
            classes: CSS classes to apply.
            variant: Visual variant (default, compact, etc).

        Always pass **kwargs to parent:
            super().__init__(name=name, id=id, classes=classes)
        """
        super().__init__(name=name, id=id, classes=classes)
        self._title = title
        self._content = content
        self._variant = variant
Important Rules:
  • Always call
    super().__init__()
    with name, id, classes
  • Store configuration in instance variables (prefix with
    _
    )
  • Use type hints for all parameters
  • Document all parameters with docstrings
  • Use keyword-only arguments (after
    *
    ) for optional parameters
实现带有正确类型提示和父类初始化逻辑的
__init__
方法:
python
from typing import ClassVar
from textual.app import ComposeResult
from textual.widgets import Static

class ConfigurableWidget(Static):
    """Widget with configurable parameters."""

    DEFAULT_CSS = """
    ConfigurableWidget {
        height: auto;
        border: solid $primary;
        padding: 1;
    }

    ConfigurableWidget .header {
        background: $boost;
        text-style: bold;
    }
    """

    # Class constants
    BORDER_COLOR: ClassVar[str] = "$primary"

    def __init__(
        self,
        title: str,
        content: str = "",
        *,
        name: str | None = None,
        id: str | None = None,  # noqa: A002
        classes: str | None = None,
        variant: str = "default",
    ) -> None:
        """Initialize widget.

        Args:
            title: Widget title.
            content: Initial content.
            name: Widget name.
            id: Widget ID for querying.
            classes: CSS classes to apply.
            variant: Visual variant (default, compact, etc).

        Always pass **kwargs to parent:
            super().__init__(name=name, id=id, classes=classes)
        """
        super().__init__(name=name, id=id, classes=classes)
        self._title = title
        self._content = content
        self._variant = variant
重要规则:
  • 务必调用
    super().__init__()
    并传入name、id、classes参数
  • 配置信息存储在以下划线
    _
    开头的实例变量中
  • 为所有参数添加类型提示
  • 使用文档字符串记录所有参数
  • 可选参数使用仅关键字参数(放在
    *
    之后)

Step 3: Implement Widget Composition

步骤3:实现Widget组件组合

For complex widgets that contain child widgets:
python
from textual.app import ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets import Static, Button, Label

class CompositeWidget(Vertical):
    """Widget that composes multiple child widgets."""

    DEFAULT_CSS = """
    CompositeWidget {
        height: auto;
        border: solid $primary;
    }

    CompositeWidget .header {
        height: 3;
        background: $boost;
        text-style: bold;
    }

    CompositeWidget .content {
        height: 1fr;
        overflow: auto;
    }

    CompositeWidget .footer {
        height: auto;
        border-top: solid $primary;
    }
    """

    def __init__(self, title: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._title = title
        self._items: list[str] = []

    def compose(self) -> ComposeResult:
        """Compose child widgets.

        Yields:
            Child widgets in order they should appear.
        """
        # Header
        yield Static(f"Title: {self._title}", classes="header")

        # Content area with items
        yield Vertical(
            Static(
                "Content area" if not self._items else "\n".join(self._items),
                id="content-area",
            ),
            classes="content",
        )

        # Footer with buttons
        yield Horizontal(
            Button("Add", id="btn-add", variant="primary"),
            Button("Remove", id="btn-remove"),
            classes="footer",
        )

    async def on_mount(self) -> None:
        """Initialize after composition."""
        # Can now query child widgets
        content = self.query_one("#content-area", Static)
        content.update("Initialized")

    async def add_item(self, item: str) -> None:
        """Add item to widget."""
        self._items.append(item)
        content = self.query_one("#content-area", Static)
        content.update("\n".join(self._items))
Composition Pattern:
  • Override
    compose()
    to yield child widgets
  • Use containers (Vertical, Horizontal) for layout
  • Use
    on_mount()
    after children are mounted
  • Query children by ID using
    self.query_one()
对于包含子Widget的复杂Widget:
python
from textual.app import ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets import Static, Button, Label

class CompositeWidget(Vertical):
    """Widget that composes multiple child widgets."""

    DEFAULT_CSS = """
    CompositeWidget {
        height: auto;
        border: solid $primary;
    }

    CompositeWidget .header {
        height: 3;
        background: $boost;
        text-style: bold;
    }

    CompositeWidget .content {
        height: 1fr;
        overflow: auto;
    }

    CompositeWidget .footer {
        height: auto;
        border-top: solid $primary;
    }
    """

    def __init__(self, title: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._title = title
        self._items: list[str] = []

    def compose(self) -> ComposeResult:
        """Compose child widgets.

        Yields:
            Child widgets in order they should appear.
        """
        # Header
        yield Static(f"Title: {self._title}", classes="header")

        # Content area with items
        yield Vertical(
            Static(
                "Content area" if not self._items else "\n".join(self._items),
                id="content-area",
            ),
            classes="content",
        )

        # Footer with buttons
        yield Horizontal(
            Button("Add", id="btn-add", variant="primary"),
            Button("Remove", id="btn-remove"),
            classes="footer",
        )

    async def on_mount(self) -> None:
        """Initialize after composition."""
        # Can now query child widgets
        content = self.query_one("#content-area", Static)
        content.update("Initialized")

    async def add_item(self, item: str) -> None:
        """Add item to widget."""
        self._items.append(item)
        content = self.query_one("#content-area", Static)
        content.update("\n".join(self._items))
组件组合模式:
  • 重写
    compose()
    方法以返回子Widget
  • 使用容器组件(Vertical、Horizontal)实现布局
  • 在子Widget挂载完成后使用
    on_mount()
    方法
  • 使用
    self.query_one()
    通过ID查询子Widget

Step 4: Implement Widget Rendering

步骤4:实现Widget渲染逻辑

For display widgets using
render()
:
python
from rich.console import Console
from rich.table import Table
from rich.text import Text
from textual.widgets import Static

class RichWidget(Static):
    """Widget that renders Rich objects."""

    def __init__(self, data: dict, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._data = data

    def render(self) -> str | Text | Table:
        """Render widget content as Rich object.

        Returns:
            str, Text, or Rich-renderable object.
            Textual converts to displayable content.
        """
        # Simple text
        return f"Data: {self._data}"

        # Rich Text with styling
        text = Text()
        text.append("Status: ", style="bold")
        text.append(self._data.get("status", "unknown"), style="green")
        return text

        # Rich Table
        table = Table(title="Data")
        table.add_column("Key", style="cyan")
        table.add_column("Value", style="magenta")
        for key, value in self._data.items():
            table.add_row(key, str(value))
        return table
Rendering Methods:
  1. render()
    - Return displayable content
  2. update(content)
    - Update rendered content
  3. refresh()
    - Force re-render
对于使用
render()
方法的展示型Widget:
python
from rich.console import Console
from rich.table import Table
from rich.text import Text
from textual.widgets import Static

class RichWidget(Static):
    """Widget that renders Rich objects."""

    def __init__(self, data: dict, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._data = data

    def render(self) -> str | Text | Table:
        """Render widget content as Rich object.

        Returns:
            str, Text, or Rich-renderable object.
            Textual converts to displayable content.
        """
        # Simple text
        return f"Data: {self._data}"

        # Rich Text with styling
        text = Text()
        text.append("Status: ", style="bold")
        text.append(self._data.get("status", "unknown"), style="green")
        return text

        # Rich Table
        table = Table(title="Data")
        table.add_column("Key", style="cyan")
        table.add_column("Value", style="magenta")
        for key, value in self._data.items():
            table.add_row(key, str(value))
        return table
渲染方法:
  1. render()
    - 返回可展示的内容
  2. update(content)
    - 更新已渲染的内容
  3. refresh()
    - 强制重新渲染

Step 5: Add Widget Lifecycle Methods

步骤5:添加Widget生命周期方法

Implement lifecycle hooks for initialization and cleanup:
python
class LifecycleWidget(Static):
    """Widget with full lifecycle implementation."""

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._initialized = False

    async def on_mount(self) -> None:
        """Called when widget is mounted to DOM.

        Use for:
        - Initializing state
        - Starting background tasks
        - Loading data
        - Querying sibling widgets
        """
        self._initialized = True
        self.update("Widget mounted and ready")

        # Start background task
        self.app.run_worker(self._background_work())

    async def _background_work(self) -> None:
        """Background async work."""
        import asyncio
        while self._initialized:
            await asyncio.sleep(1)
            self.refresh()

    def on_unmount(self) -> None:
        """Called when widget is removed from DOM.

        Use for:
        - Cleanup
        - Stopping background tasks
        - Closing connections
        """
        self._initialized = False
Lifecycle Events:
  • on_mount()
    - After widget mounted and can query children
  • on_unmount()
    - After widget removed, before destruction
  • on_focus()
    - Widget gained focus
  • on_blur()
    - Widget lost focus
实现用于初始化与清理的生命周期钩子:
python
class LifecycleWidget(Static):
    """Widget with full lifecycle implementation."""

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._initialized = False

    async def on_mount(self) -> None:
        """Called when widget is mounted to DOM.

        Use for:
        - Initializing state
        - Starting background tasks
        - Loading data
        - Querying sibling widgets
        """
        self._initialized = True
        self.update("Widget mounted and ready")

        # Start background task
        self.app.run_worker(self._background_work())

    async def _background_work(self) -> None:
        """Background async work."""
        import asyncio
        while self._initialized:
            await asyncio.sleep(1)
            self.refresh()

    def on_unmount(self) -> None:
        """Called when widget is removed from DOM.

        Use for:
        - Cleanup
        - Stopping background tasks
        - Closing connections
        """
        self._initialized = False
生命周期事件:
  • on_mount()
    - Widget挂载到DOM后调用,可用于查询子Widget
  • on_unmount()
    - Widget从DOM移除后调用,在销毁前执行
  • on_focus()
    - Widget获得焦点时调用
  • on_blur()
    - Widget失去焦点时调用

Step 6: Implement Widget Actions and Messages

步骤6:实现Widget交互与消息机制

Add interactivity through actions and custom messages:
python
from textual.message import Message
from textual import on
from textual.widgets import Button

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:
        """Handle click event."""
        # Post message that parent can handle
        self.post_message(self.ItemClicked(self._item_id))
通过动作与自定义消息添加交互性:
python
from textual.message import Message
from textual import on
from textual.widgets import Button

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:
        """Handle click event."""
        # Post message that parent can handle
        self.post_message(self.ItemClicked(self._item_id))

Parent widget handling custom messages

Parent widget handling custom messages

class ItemList(Vertical): """Widget that handles ItemWidget messages."""
@on(ItemWidget.ItemClicked)
async def on_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
    """Handle item click message."""
    print(f"Item clicked: {message.item_id}")
    self.notify(f"Selected: {message.item_id}")

**Message Pattern:**
1. Define custom Message subclass
2. Post message with `self.post_message()`
3. Parent handles with `@on(MessageType)` decorator
4. Messages bubble up the widget tree
class ItemList(Vertical): """Widget that handles ItemWidget messages."""
@on(ItemWidget.ItemClicked)
async def on_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
    """Handle item click message."""
    print(f"Item clicked: {message.item_id}")
    self.notify(f"Selected: {message.item_id}")

**消息机制模式:**
1. 定义自定义Message子类
2. 使用`self.post_message()`发送消息
3. 父Widget使用`@on(MessageType)`装饰器处理消息
4. 消息会沿Widget树向上冒泡

Step 7: Add CSS Styling

步骤7:添加CSS样式

Define DEFAULT_CSS for widget styling:
python
class StyledWidget(Static):
    """Widget with comprehensive CSS styling."""

    DEFAULT_CSS = """
    StyledWidget {
        width: 100%;
        height: auto;
        border: solid $primary;
        padding: 1 2;
        background: $surface;
    }

    StyledWidget .header {
        width: 100%;
        height: 3;
        background: $boost;
        text-style: bold;
        content-align: center middle;
        color: $text;
    }

    StyledWidget .content {
        width: 1fr;
        height: 1fr;
        padding: 1;
        overflow: auto;
    }

    StyledWidget .status-active {
        color: $success;
        text-style: bold;
    }

    StyledWidget .status-inactive {
        color: $error;
        text-style: dim;
    }

    StyledWidget:focus {
        border: double $primary;
    }

    StyledWidget:disabled {
        opacity: 0.5;
    }
    """

    def render(self) -> str:
        return "Styled Widget"
CSS Best Practices:
  • Use CSS variables ($primary, $success, etc.) for consistency
  • Keep DEFAULT_CSS in the widget file
  • Use classes for variants
  • Use pseudo-classes (:focus, :hover, :disabled)
  • Use width/height in fr (fraction) or auto
为Widget定义DEFAULT_CSS实现样式定制:
python
class StyledWidget(Static):
    """Widget with comprehensive CSS styling."""

    DEFAULT_CSS = """
    StyledWidget {
        width: 100%;
        height: auto;
        border: solid $primary;
        padding: 1 2;
        background: $surface;
    }

    StyledWidget .header {
        width: 100%;
        height: 3;
        background: $boost;
        text-style: bold;
        content-align: center middle;
        color: $text;
    }

    StyledWidget .content {
        width: 1fr;
        height: 1fr;
        padding: 1;
        overflow: auto;
    }

    StyledWidget .status-active {
        color: $success;
        text-style: bold;
    }

    StyledWidget .status-inactive {
        color: $error;
        text-style: dim;
    }

    StyledWidget:focus {
        border: double $primary;
    }

    StyledWidget:disabled {
        opacity: 0.5;
    }
    """

    def render(self) -> str:
        return "Styled Widget"
CSS最佳实践:
  • 使用CSS变量($primary、$success等)保证样式一致性
  • 将DEFAULT_CSS与Widget代码放在同一文件中
  • 使用类实现不同样式变体
  • 使用伪类(:focus、:hover、:disabled)实现交互样式
  • 使用fr(比例单位)或auto设置宽高

Examples

示例

Basic Widget Examples

基础Widget示例

See above instructions for two fundamental widget examples:
  1. Custom Status Display Widget (rendering with Rich)
  2. Reusable Data Table Widget (composition pattern)
For advanced widget patterns, see references/advanced-patterns.md:
  • Complex container widgets with multiple child types
  • Dynamic widget mounting/unmounting
  • Lazy loading patterns
  • Advanced reactive patterns with watchers
  • Custom message bubbling
  • Performance optimization (virtual scrolling, debouncing)
  • Comprehensive testing patterns
请参考上述步骤中的两个基础Widget示例:
  1. 自定义状态展示Widget(使用Rich渲染)
  2. 可复用数据表格Widget(组件组合模式)
高级Widget模式请查看references/advanced-patterns.md
  • 包含多种子Widget类型的复杂容器Widget
  • 动态挂载/卸载Widget
  • 懒加载模式
  • 结合监听器的高级响应式模式
  • 自定义消息冒泡
  • 性能优化(虚拟滚动、防抖)
  • �全面的测试模式

Requirements

环境要求

  • Textual >= 0.45.0
  • Python 3.9+ for type hints
  • Rich (installed with Textual for rendering)
  • Textual >= 0.45.0
  • Python 3.9+(支持类型提示)
  • Rich(随Textual安装,用于渲染)

Common Patterns

常见模式

Creating Reusable Containers

创建可复用容器

python
def create_section(title: str, content: Static) -> Container:
    """Factory function for creating sections."""
    return Container(
        Static(title, classes="section-header"),
        content,
        classes="section",
    )
python
def create_section(title: str, content: Static) -> Container:
    """Factory function for creating sections."""
    return Container(
        Static(title, classes="section-header"),
        content,
        classes="section",
    )

Lazy Widget Mounting

�懒加载Widget挂载

python
async def lazy_mount_children(self) -> None:
    """Mount child widgets gradually."""
    for i, item in enumerate(self._items):
        await container.mount(self._create_item_widget(item))
        if i % 10 == 0:
            await asyncio.sleep(0)  # Yield to event loop
python
async def lazy_mount_children(self) -> None:
    """Mount child widgets gradually."""
    for i, item in enumerate(self._items):
        await container.mount(self._create_item_widget(item))
        if i % 10 == 0:
            await asyncio.sleep(0)  # Yield to event loop

See Also

�相关文档

  • textual-app-lifecycle.md - App initialization and lifecycle
  • textual-event-messages.md - Event and message handling
  • textual-layout-styling.md - CSS styling and layout
  • textual-app-lifecycle.md - 应用初始化与生命周期
  • textual-event-messages.md - 事件与消息处理
  • textual-layout-styling.md - CSS样式与布局