textual-widget-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTextual 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")
undefinedclass MyApp(App):
def compose(self) -> ComposeResult:
yield SimpleWidget("Hello")
undefinedInstructions
操作步骤
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 GenericContainerFor 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,不满足需求时再自定义
- 仅当内置选项无法满足需求时才创建自定义WidgetStep 2: Define Widget Initialization and Configuration
步骤2:定义Widget初始化与配置
Implement with proper type hints and parent class initialization:
__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 = variantImportant Rules:
- Always call with name, id, classes
super().__init__() - 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重要规则:
- 务必调用并传入name、id、classes参数
super().__init__() - 配置信息存储在以下划线开头的实例变量中
_ - 为所有参数添加类型提示
- 使用文档字符串记录所有参数
- 可选参数使用仅关键字参数(放在之后)
*
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 to yield child widgets
compose() - Use containers (Vertical, Horizontal) for layout
- Use after children are mounted
on_mount() - 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))组件组合模式:
- 重写方法以返回子Widget
compose() - 使用容器组件(Vertical、Horizontal)实现布局
- 在子Widget挂载完成后使用方法
on_mount() - 使用通过ID查询子Widget
self.query_one()
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 tableRendering Methods:
- - Return displayable content
render() - - Update rendered content
update(content) - - Force re-render
refresh()
对于使用方法的展示型Widget:
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渲染方法:
- - 返回可展示的内容
render() - - 更新已渲染的内容
update(content) - - 强制重新渲染
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 = FalseLifecycle Events:
- - After widget mounted and can query children
on_mount() - - After widget removed, before destruction
on_unmount() - - Widget gained focus
on_focus() - - Widget lost focus
on_blur()
实现用于初始化与清理的生命周期钩子:
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生命周期事件:
- - Widget挂载到DOM后调用,可用于查询子Widget
on_mount() - - Widget从DOM移除后调用,在销毁前执行
on_unmount() - - Widget获得焦点时调用
on_focus() - - Widget失去焦点时调用
on_blur()
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 treeclass 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:
- Custom Status Display Widget (rendering with Rich)
- 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示例:
- 自定义状态展示Widget(使用Rich渲染)
- 可复用数据表格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 looppython
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 loopSee 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样式与布局