textual-tui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Textual TUI Development

Textual TUI 开发

Build production-quality terminal user interfaces using Textual, a modern Python framework for creating interactive TUI applications.
使用Textual构建生产级终端用户界面,Textual是一款用于创建交互式TUI应用的现代化Python框架。

Quick Start

快速开始

Install Textual:
bash
pip install textual textual-dev
Basic app structure:
python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Button

class MyApp(App):
    """A simple Textual app."""
    
    def compose(self) -> ComposeResult:
        """Create child widgets."""
        yield Header()
        yield Button("Click me!", id="click")
        yield Footer()
    
    def on_button_pressed(self, event: Button.Pressed) -> None:
        """Handle button press."""
        self.exit()

if __name__ == "__main__":
    app = MyApp()
    app.run()
Run with hot reload during development:
bash
textual run --dev your_app.py
Use the Textual console for debugging:
bash
textual console
安装Textual:
bash
pip install textual textual-dev
基础应用结构:
python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Button

class MyApp(App):
    """A simple Textual app."""
    
    def compose(self) -> ComposeResult:
        """Create child widgets."""
        yield Header()
        yield Button("Click me!", id="click")
        yield Footer()
    
    def on_button_pressed(self, event: Button.Pressed) -> None:
        """Handle button press."""
        self.exit()

if __name__ == "__main__":
    app = MyApp()
    app.run()
开发期间使用热重载运行:
bash
textual run --dev your_app.py
使用Textual控制台进行调试:
bash
textual console

Core Architecture

核心架构

App Lifecycle

应用生命周期

  1. Initialization: Create App instance with config
  2. Composition: Build widget tree via
    compose()
    method
  3. Mounting: Widgets mounted to DOM
  4. Running: Event loop processes messages and renders UI
  5. Shutdown: Cleanup and exit
  1. 初始化:创建带配置的App实例
  2. 组件组合:通过
    compose()
    方法构建组件树
  3. 挂载:将组件挂载到DOM
  4. 运行:事件循环处理消息并渲染UI
  5. 关闭:清理资源并退出

Message Passing System

消息传递系统

Textual uses an async message queue for all interactions:
python
from textual.message import Message

class CustomMessage(Message):
    """Custom message with data."""
    def __init__(self, value: int) -> None:
        self.value = value
        super().__init__()

class MyWidget(Widget):
    def on_click(self) -> None:
        # Post message to parent
        self.post_message(CustomMessage(42))

class MyApp(App):
    def on_custom_message(self, message: CustomMessage) -> None:
        # Handle message with naming convention: on_{message_name}
        self.log(f"Received: {message.value}")
Textual使用异步消息队列处理所有交互:
python
from textual.message import Message

class CustomMessage(Message):
    """Custom message with data."""
    def __init__(self, value: int) -> None:
        self.value = value
        super().__init__()

class MyWidget(Widget):
    def on_click(self) -> None:
        # 向父组件发送消息
        self.post_message(CustomMessage(42))

class MyApp(App):
    def on_custom_message(self, message: CustomMessage) -> None:
        # 按照on_{message_name}的命名约定处理消息
        self.log(f"Received: {message.value}")

Reactive Programming

响应式编程

Use reactive attributes for automatic UI updates:
python
from textual.reactive import reactive

class Counter(Widget):
    count = reactive(0)  # Reactive attribute
    
    def watch_count(self, new_value: int) -> None:
        """Called automatically when count changes."""
        self.refresh()
    
    def increment(self) -> None:
        self.count += 1  # Triggers watch_count
使用响应式属性实现UI自动更新:
python
from textual.reactive import reactive

class Counter(Widget):
    count = reactive(0)  # 响应式属性
    
    def watch_count(self, new_value: int) -> None:
        """当count变化时自动调用。"""
        self.refresh()
    
    def increment(self) -> None:
        self.count += 1  # 触发watch_count

Layout System

布局系统

Container Layouts

容器布局

Textual provides flexible layout options:
Vertical Layout (default):
python
def compose(self) -> ComposeResult:
    yield Label("Top")
    yield Label("Bottom")
Horizontal Layout:
python
class MyApp(App):
    CSS = """
    Screen {
        layout: horizontal;
    }
    """
Grid Layout:
python
class MyApp(App):
    CSS = """
    Screen {
        layout: grid;
        grid-size: 3 2;  /* 3 columns, 2 rows */
    }
    """
Textual提供灵活的布局选项:
垂直布局(默认)
python
def compose(self) -> ComposeResult:
    yield Label("Top")
    yield Label("Bottom")
水平布局
python
class MyApp(App):
    CSS = """
    Screen {
        layout: horizontal;
    }
    """
网格布局
python
class MyApp(App):
    CSS = """
    Screen {
        layout: grid;
        grid-size: 3 2;  /* 3列,2行 */
    }
    """

Sizing and Positioning

尺寸与定位

Control widget dimensions:
python
class MyApp(App):
    CSS = """
    #sidebar {
        width: 30;      /* Fixed width */
        height: 100%;   /* Full height */
    }
    
    #content {
        width: 1fr;     /* Remaining space */
    }
    
    .compact {
        height: auto;   /* Size to content */
    }
    """
控制组件尺寸:
python
class MyApp(App):
    CSS = """
    #sidebar {
        width: 30;      /* 固定宽度 */
        height: 100%;   /* 全屏高度 */
    }
    
    #content {
        width: 1fr;     /* 剩余空间 */
    }
    
    .compact {
        height: auto;   /* 自适应内容尺寸 */
    }
    """

Styling with CSS

使用CSS进行样式设计

Textual uses CSS-like syntax for styling.
Textual使用类CSS语法进行样式设计。

Inline Styles

内联样式

python
class StyledWidget(Widget):
    DEFAULT_CSS = """
    StyledWidget {
        background: $primary;
        color: $text;
        border: solid $accent;
        padding: 1 2;
        margin: 1;
    }
    """
python
class StyledWidget(Widget):
    DEFAULT_CSS = """
    StyledWidget {
        background: $primary;
        color: $text;
        border: solid $accent;
        padding: 1 2;
        margin: 1;
    }
    """

External CSS Files

外部CSS文件

python
class MyApp(App):
    CSS_PATH = "app.tcss"  # Load from file
python
class MyApp(App):
    CSS_PATH = "app.tcss"  # 从文件加载

Color System

颜色系统

Use Textual's semantic colors:
css
.error { background: $error; }
.success { background: $success; }
.warning { background: $warning; }
.primary { background: $primary; }
Or define custom colors:
css
.custom {
    background: #1e3a8a;
    color: rgb(255, 255, 255);
}
使用Textual的语义化颜色:
css
.error { background: $error; }
.success { background: $success; }
.warning { background: $warning; }
.primary { background: $primary; }
或定义自定义颜色:
css
.custom {
    background: #1e3a8a;
    color: rgb(255, 255, 255);
}

Common Widgets

常用组件

Input and Forms

输入与表单

python
from textual.widgets import Input, Button, Select
from textual.containers import Container

def compose(self) -> ComposeResult:
    with Container(id="form"):
        yield Input(placeholder="Enter name", id="name")
        yield Select(options=[("A", 1), ("B", 2)], id="choice")
        yield Button("Submit", variant="primary")

def on_button_pressed(self, event: Button.Pressed) -> None:
    name = self.query_one("#name", Input).value
    choice = self.query_one("#choice", Select).value
python
from textual.widgets import Input, Button, Select
from textual.containers import Container

def compose(self) -> ComposeResult:
    with Container(id="form"):
        yield Input(placeholder="Enter name", id="name")
        yield Select(options=[("A", 1), ("B", 2)], id="choice")
        yield Button("Submit", variant="primary")

def on_button_pressed(self, event: Button.Pressed) -> None:
    name = self.query_one("#name", Input).value
    choice = self.query_one("#choice", Select).value

Data Display

数据展示

python
from textual.widgets import DataTable, Tree, Log
python
from textual.widgets import DataTable, Tree, Log

DataTable for tabular data

用于表格数据的DataTable

table = DataTable() table.add_columns("Name", "Age", "City") table.add_row("Alice", 30, "NYC")
table = DataTable() table.add_columns("Name", "Age", "City") table.add_row("Alice", 30, "NYC")

Tree for hierarchical data

用于层级数据的Tree

tree = Tree("Root") tree.root.add("Child 1") tree.root.add("Child 2")
tree = Tree("Root") tree.root.add("Child 1") tree.root.add("Child 2")

Log for streaming output

用于流式输出的Log

log = Log(auto_scroll=True) log.write_line("Log entry")
undefined
log = Log(auto_scroll=True) log.write_line("Log entry")
undefined

Containers and Layout

容器与布局

python
from textual.containers import (
    Container, Horizontal, Vertical,
    Grid, ScrollableContainer
)

def compose(self) -> ComposeResult:
    with Vertical():
        yield Header()
        with Horizontal():
            with Container(id="sidebar"):
                yield Label("Menu")
            with ScrollableContainer(id="content"):
                yield Label("Content...")
        yield Footer()
python
from textual.containers import (
    Container, Horizontal, Vertical,
    Grid, ScrollableContainer
)

def compose(self) -> ComposeResult:
    with Vertical():
        yield Header()
        with Horizontal():
            with Container(id="sidebar"):
                yield Label("Menu")
            with ScrollableContainer(id="content"):
                yield Label("Content...")
        yield Footer()

Event Handling

事件处理

Built-in Events

内置事件

python
from textual.events import Key, Click, Mount

def on_mount(self) -> None:
    """Called when widget is mounted."""
    self.log("Widget mounted!")

def on_key(self, event: Key) -> None:
    """Handle all key presses."""
    if event.key == "q":
        self.app.exit()

def on_click(self, event: Click) -> None:
    """Handle mouse clicks."""
    self.log(f"Clicked at {event.x}, {event.y}")
python
from textual.events import Key, Click, Mount

def on_mount(self) -> None:
    """当组件挂载时调用。"""
    self.log("Widget mounted!")

def on_key(self, event: Key) -> None:
    """处理所有按键操作。"""
    if event.key == "q":
        self.app.exit()

def on_click(self, event: Click) -> None:
    """处理鼠标点击。"""
    self.log(f"Clicked at {event.x}, {event.y}")

Widget-Specific Handlers

组件专属处理器

python
def on_input_submitted(self, event: Input.Submitted) -> None:
    """Handle input submission."""
    self.query_one(Log).write(event.value)

def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
    """Handle table row selection."""
    row_key = event.row_key
python
def on_input_submitted(self, event: Input.Submitted) -> None:
    """处理输入提交。"""
    self.query_one(Log).write(event.value)

def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
    """处理表格行选中事件。"""
    row_key = event.row_key

Keyboard Bindings

键盘绑定

python
class MyApp(App):
    BINDINGS = [
        ("q", "quit", "Quit"),
        ("d", "toggle_dark", "Toggle dark mode"),
        ("ctrl+s", "save", "Save"),
    ]
    
    def action_quit(self) -> None:
        self.exit()
    
    def action_toggle_dark(self) -> None:
        self.dark = not self.dark
python
class MyApp(App):
    BINDINGS = [
        ("q", "quit", "退出"),
        ("d", "toggle_dark", "切换深色模式"),
        ("ctrl+s", "save", "保存"),
    ]
    
    def action_quit(self) -> None:
        self.exit()
    
    def action_toggle_dark(self) -> None:
        self.dark = not self.dark

Advanced Patterns

进阶模式

Custom Widgets

自定义组件

Create reusable components:
python
from textual.widget import Widget
from textual.widgets import Label, Button

class StatusCard(Widget):
    """A card showing status info."""
    
    def __init__(self, title: str, status: str) -> None:
        super().__init__()
        self.title = title
        self.status = status
    
    def compose(self) -> ComposeResult:
        yield Label(self.title, classes="title")
        yield Label(self.status, classes="status")
创建可复用组件:
python
from textual.widget import Widget
from textual.widgets import Label, Button

class StatusCard(Widget):
    """展示状态信息的卡片组件。"""
    
    def __init__(self, title: str, status: str) -> None:
        super().__init__()
        self.title = title
        self.status = status
    
    def compose(self) -> ComposeResult:
        yield Label(self.title, classes="title")
        yield Label(self.status, classes="status")

Workers and Background Tasks

Worker与后台任务

CRITICAL: Use workers for any long-running operations to prevent blocking the UI. The event loop must remain responsive.
重要提示:任何长时间运行的操作都必须使用Worker,以避免阻塞UI。事件循环必须保持响应性。

Basic Worker Usage

基础Worker使用

Run tasks in background threads:
python
from textual.worker import Worker, WorkerState

class MyApp(App):
    def on_button_pressed(self, event: Button.Pressed) -> None:
        # Start background task
        self.run_worker(self.process_data(), exclusive=True)
    
    async def process_data(self) -> str:
        """Long-running task."""
        # Simulate work
        await asyncio.sleep(5)
        return "Processing complete"
在后台线程中运行任务:
python
from textual.worker import Worker, WorkerState

class MyApp(App):
    def on_button_pressed(self, event: Button.Pressed) -> None:
        # 启动后台任务
        self.run_worker(self.process_data(), exclusive=True)
    
    async def process_data(self) -> str:
        """长时间运行的任务。"""
        # 模拟任务执行
        await asyncio.sleep(5)
        return "处理完成"

Worker with Progress Updates

带进度更新的Worker

Update UI during processing:
python
from textual.widgets import ProgressBar

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield ProgressBar(total=100, id="progress")
    
    def on_mount(self) -> None:
        self.run_worker(self.long_task())
    
    async def long_task(self) -> None:
        """Task with progress updates."""
        progress = self.query_one(ProgressBar)
        
        for i in range(100):
            await asyncio.sleep(0.1)
            progress.update(progress=i + 1)
            # Use call_from_thread for thread safety
            self.call_from_thread(progress.update, progress=i + 1)
在处理过程中更新UI:
python
from textual.widgets import ProgressBar

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield ProgressBar(total=100, id="progress")
    
    def on_mount(self) -> None:
        self.run_worker(self.long_task())
    
    async def long_task(self) -> None:
        """带进度更新的任务。"""
        progress = self.query_one(ProgressBar)
        
        for i in range(100):
            await asyncio.sleep(0.1)
            progress.update(progress=i + 1)
            # 使用call_from_thread保证线程安全
            self.call_from_thread(progress.update, progress=i + 1)

Worker Communication Patterns

Worker通信模式

Use
call_from_thread
for thread-safe UI updates:
python
import time
from threading import Thread

class MyApp(App):
    def on_mount(self) -> None:
        self.run_worker(self.fetch_data(), thread=True)
    
    def fetch_data(self) -> None:
        """CPU-bound task in thread."""
        # Blocking operation
        result = expensive_computation()
        
        # Update UI safely from thread
        self.call_from_thread(self.display_result, result)
    
    def display_result(self, result: str) -> None:
        """Called on main thread."""
        self.query_one("#output").update(result)
使用
call_from_thread
实现线程安全的UI更新:
python
import time
from threading import Thread

class MyApp(App):
    def on_mount(self) -> None:
        self.run_worker(self.fetch_data(), thread=True)
    
    def fetch_data(self) -> None:
        """在线程中执行CPU密集型任务。"""
        # 阻塞操作
        result = expensive_computation()
        
        # 从线程安全更新UI
        self.call_from_thread(self.display_result, result)
    
    def display_result(self, result: str) -> None:
        """在主线程中调用。"""
        self.query_one("#output").update(result)

Worker Cancellation

Worker取消

Cancel workers when no longer needed:
python
class MyApp(App):
    worker: Worker | None = None
    
    def start_task(self) -> None:
        # Store worker reference
        self.worker = self.run_worker(self.long_task())
    
    def cancel_task(self) -> None:
        # Cancel running worker
        if self.worker and not self.worker.is_finished:
            self.worker.cancel()
            self.notify("Task cancelled")
    
    async def long_task(self) -> None:
        for i in range(1000):
            await asyncio.sleep(0.1)
            # Check if cancelled
            if self.worker.is_cancelled:
                return
在不需要时取消Worker:
python
class MyApp(App):
    worker: Worker | None = None
    
    def start_task(self) -> None:
        # 保存Worker引用
        self.worker = self.run_worker(self.long_task())
    
    def cancel_task(self) -> None:
        # 取消运行中的Worker
        if self.worker and not self.worker.is_finished:
            self.worker.cancel()
            self.notify("任务已取消")
    
    async def long_task(self) -> None:
        for i in range(1000):
            await asyncio.sleep(0.1)
            # 检查是否已取消
            if self.worker.is_cancelled:
                return

Worker Error Handling

Worker错误处理

Handle worker failures gracefully:
python
class MyApp(App):
    def on_mount(self) -> None:
        worker = self.run_worker(self.risky_task())
        worker.name = "data_processor"  # Name for debugging
    
    async def risky_task(self) -> str:
        """Task that might fail."""
        try:
            result = await fetch_from_api()
            return result
        except Exception as e:
            self.notify(f"Error: {e}", severity="error")
            raise
    
    def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
        """Handle worker state changes."""
        if event.state == WorkerState.ERROR:
            self.log.error(f"Worker failed: {event.worker.name}")
        elif event.state == WorkerState.SUCCESS:
            self.log.info(f"Worker completed: {event.worker.name}")
优雅处理Worker失败:
python
class MyApp(App):
    def on_mount(self) -> None:
        worker = self.run_worker(self.risky_task())
        worker.name = "data_processor"  # 命名用于调试
    
    async def risky_task(self) -> str:
        """可能失败的任务。"""
        try:
            result = await fetch_from_api()
            return result
        except Exception as e:
            self.notify(f"错误: {e}", severity="error")
            raise
    
    def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
        """处理Worker状态变化。"""
        if event.state == WorkerState.ERROR:
            self.log.error(f"Worker失败: {event.worker.name}")
        elif event.state == WorkerState.SUCCESS:
            self.log.info(f"Worker完成: {event.worker.name}")

Multiple Workers

多Worker管理

Manage concurrent workers:
python
class MyApp(App):
    def on_mount(self) -> None:
        # Run multiple workers concurrently
        self.run_worker(self.task_one(), name="task1", group="processing")
        self.run_worker(self.task_two(), name="task2", group="processing")
        self.run_worker(self.task_three(), name="task3", group="processing")
    
    async def task_one(self) -> None:
        await asyncio.sleep(2)
        self.notify("Task 1 complete")
    
    async def task_two(self) -> None:
        await asyncio.sleep(3)
        self.notify("Task 2 complete")
    
    async def task_three(self) -> None:
        await asyncio.sleep(1)
        self.notify("Task 3 complete")
    
    def cancel_all_tasks(self) -> None:
        """Cancel all workers in a group."""
        for worker in self.workers:
            if worker.group == "processing":
                worker.cancel()
管理并发Worker:
python
class MyApp(App):
    def on_mount(self) -> None:
        # 并发运行多个Worker
        self.run_worker(self.task_one(), name="task1", group="processing")
        self.run_worker(self.task_two(), name="task2", group="processing")
        self.run_worker(self.task_three(), name="task3", group="processing")
    
    async def task_one(self) -> None:
        await asyncio.sleep(2)
        self.notify("任务1完成")
    
    async def task_two(self) -> None:
        await asyncio.sleep(3)
        self.notify("任务2完成")
    
    async def task_three(self) -> None:
        await asyncio.sleep(1)
        self.notify("任务3完成")
    
    def cancel_all_tasks(self) -> None:
        """取消同一组内的所有Worker。"""
        for worker in self.workers:
            if worker.group == "processing":
                worker.cancel()

Thread vs Process Workers

线程与进程Worker选择

Choose the right worker type:
python
class MyApp(App):
    def on_mount(self) -> None:
        # Async task (default) - for I/O bound operations
        self.run_worker(self.fetch_data())
        
        # Thread worker - for CPU-bound tasks
        self.run_worker(self.process_data(), thread=True)
    
    async def fetch_data(self) -> str:
        """I/O bound: use async."""
        async with httpx.AsyncClient() as client:
            response = await client.get("https://api.example.com")
            return response.text
    
    def process_data(self) -> str:
        """CPU bound: use thread."""
        # Heavy computation
        result = [i**2 for i in range(1000000)]
        return str(sum(result))
选择合适的Worker类型:
python
class MyApp(App):
    def on_mount(self) -> None:
        # 异步任务(默认)- 适用于I/O密集型操作
        self.run_worker(self.fetch_data())
        
        # 线程Worker - 适用于CPU密集型任务
        self.run_worker(self.process_data(), thread=True)
    
    async def fetch_data(self) -> str:
        """I/O密集型:使用异步。"""
        async with httpx.AsyncClient() as client:
            response = await client.get("https://api.example.com")
            return response.text
    
    def process_data(self) -> str:
        """CPU密集型:使用线程。"""
        # 大量计算
        result = [i**2 for i in range(1000000)]
        return str(sum(result))

Worker Best Practices

Worker最佳实践

  1. Always use workers for:
    • Network requests
    • File I/O
    • Database queries
    • CPU-intensive computations
    • Anything taking > 100ms
  2. Worker patterns:
    • Use
      exclusive=True
      to prevent duplicate workers
    • Name workers for easier debugging
    • Group related workers for batch cancellation
    • Always handle worker errors
  3. Thread safety:
    • Use
      call_from_thread()
      for UI updates from threads
    • Never modify widgets directly from threads
    • Use locks for shared mutable state
  4. Cancellation:
    • Store worker references if you need to cancel
    • Check
      worker.is_cancelled
      in long loops
    • Clean up resources in finally blocks
  1. 以下场景必须使用Worker
    • 网络请求
    • 文件I/O
    • 数据库查询
    • CPU密集型计算
    • 任何耗时超过100ms的操作
  2. Worker模式
    • 使用
      exclusive=True
      避免重复Worker
    • 为Worker命名以便调试
    • 将相关Worker分组以便批量取消
    • 始终处理Worker错误
  3. 线程安全
    • 从线程更新UI时使用
      call_from_thread()
    • 绝不要从线程直接修改组件
    • 对共享可变状态使用锁
  4. 取消操作
    • 如果需要取消,保存Worker引用
    • 在长循环中检查
      worker.is_cancelled
    • 在finally块中清理资源

Modal Dialogs

模态对话框

python
from textual.screen import ModalScreen

class ConfirmDialog(ModalScreen[bool]):
    """Modal confirmation dialog."""
    
    def compose(self) -> ComposeResult:
        with Container(id="dialog"):
            yield Label("Are you sure?")
            with Horizontal():
                yield Button("Yes", variant="primary", id="yes")
                yield Button("No", variant="error", id="no")
    
    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.dismiss(event.button.id == "yes")
python
from textual.screen import ModalScreen

class ConfirmDialog(ModalScreen[bool]):
    """模态确认对话框。"""
    
    def compose(self) -> ComposeResult:
        with Container(id="dialog"):
            yield Label("确定要执行此操作吗?")
            with Horizontal():
                yield Button("是", variant="primary", id="yes")
                yield Button("否", variant="error", id="no")
    
    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.dismiss(event.button.id == "yes")

Use in app

在应用中使用

async def confirm_action(self) -> None: result = await self.push_screen_wait(ConfirmDialog()) if result: self.log("Confirmed!")
undefined
async def confirm_action(self) -> None: result = await self.push_screen_wait(ConfirmDialog()) if result: self.log("已确认!")
undefined

Screens and Navigation

屏幕与导航

python
from textual.screen import Screen

class MainScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Header()
        yield Button("Go to Settings")
        yield Footer()
    
    def on_button_pressed(self) -> None:
        self.app.push_screen("settings")

class SettingsScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Label("Settings")
        yield Button("Back")
    
    def on_button_pressed(self) -> None:
        self.app.pop_screen()

class MyApp(App):
    SCREENS = {
        "main": MainScreen(),
        "settings": SettingsScreen(),
    }
python
from textual.screen import Screen

class MainScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Header()
        yield Button("进入设置")
        yield Footer()
    
    def on_button_pressed(self) -> None:
        self.app.push_screen("settings")

class SettingsScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Label("设置")
        yield Button("返回")
    
    def on_button_pressed(self) -> None:
        self.app.pop_screen()

class MyApp(App):
    SCREENS = {
        "main": MainScreen(),
        "settings": SettingsScreen(),
    }

Testing

测试

Test Textual apps with pytest and the Pilot API:
python
import pytest
from textual.pilot import Pilot
from my_app import MyApp

@pytest.mark.asyncio
async def test_app_starts():
    app = MyApp()
    async with app.run_test() as pilot:
        assert app.screen is not None

@pytest.mark.asyncio
async def test_button_click():
    app = MyApp()
    async with app.run_test() as pilot:
        await pilot.click("#my-button")
        # Assert expected state changes
        
@pytest.mark.asyncio
async def test_keyboard_input():
    app = MyApp()
    async with app.run_test() as pilot:
        await pilot.press("q")
        # Verify app exited or state changed
使用pytest和Pilot API测试Textual应用:
python
import pytest
from textual.pilot import Pilot
from my_app import MyApp

@pytest.mark.asyncio
async def test_app_starts():
    app = MyApp()
    async with app.run_test() as pilot:
        assert app.screen is not None

@pytest.mark.asyncio
async def test_button_click():
    app = MyApp()
    async with app.run_test() as pilot:
        await pilot.click("#my-button")
        # 断言预期状态变化
        
@pytest.mark.asyncio
async def test_keyboard_input():
    app = MyApp()
    async with app.run_test() as pilot:
        await pilot.press("q")
        # 验证应用是否退出或状态变化

Best Practices

最佳实践

Performance

性能

  • Use
    Lazy
    for expensive widgets loaded on demand
  • Implement efficient
    render()
    methods, avoid unnecessary work
  • Use reactive attributes sparingly for truly dynamic values
  • Batch UI updates when processing multiple changes
  • 对按需加载的重型组件使用
    Lazy
  • 实现高效的
    render()
    方法,避免不必要的操作
  • 仅对真正动态的值使用响应式属性
  • 处理多个变更时批量更新UI

State Management

状态管理

  • Keep app state in the App instance for global access
  • Use reactive attributes for UI-bound state
  • Store complex state in dedicated data models
  • Avoid deeply nested widget communication
  • 将应用状态保存在App实例中以便全局访问
  • 对UI绑定的状态使用响应式属性
  • 将复杂状态存储在专用数据模型中
  • 避免深度嵌套的组件通信

Error Handling

错误处理

python
from textual.widgets import RichLog

def compose(self) -> ComposeResult:
    yield RichLog(id="log")

async def action_risky_operation(self) -> None:
    try:
        result = await some_async_operation()
        self.notify("Success!", severity="information")
    except Exception as e:
        self.notify(f"Error: {e}", severity="error")
        self.query_one(RichLog).write(f"[red]Error:[/] {e}")
python
from textual.widgets import RichLog

def compose(self) -> ComposeResult:
    yield RichLog(id="log")

async def action_risky_operation(self) -> None:
    try:
        result = await some_async_operation()
        self.notify("操作成功!", severity="information")
    except Exception as e:
        self.notify(f"错误: {e}", severity="error")
        self.query_one(RichLog).write(f"[red]错误:[/] {e}")

Accessibility

可访问性

  • Always provide keyboard navigation
  • Use semantic widget names and IDs
  • Include ARIA-like descriptions where appropriate
  • Test with screen reader compatibility in mind
  • 始终提供键盘导航
  • 使用语义化的组件名称和ID
  • 适当添加类ARIA描述
  • 考虑屏幕阅读器兼容性测试

Development Tools

开发工具

Textual Console

Textual控制台

Debug running apps:
bash
undefined
调试运行中的应用:
bash
undefined

Terminal 1: Run console

终端1: 启动控制台

textual console
textual console

Terminal 2: Run app with console enabled

终端2: 启用控制台运行应用

textual run --dev app.py

App code to enable console:
```python
self.log("Debug message")  # Appears in console
self.log.info("Info level")
self.log.error("Error level")
textual run --dev app.py

在应用代码中启用控制台:
```python
self.log("调试消息")  # 显示在控制台中
self.log.info("信息级别")
self.log.error("错误级别")

Textual Devtools

Textual开发工具

Use the devtools for live inspection:
bash
pip install textual-dev
textual run --dev app.py  # Enables hot reload
使用开发工具进行实时检查:
bash
pip install textual-dev
textual run --dev app.py  # 启用热重载

References

参考资料

  • Widget Gallery: See references/widgets.md for comprehensive widget examples
  • Layout Patterns: See references/layouts.md for common layout recipes
  • Styling Guide: See references/styling.md for CSS patterns and themes
  • Official Guides Index: See references/official-guides-index.md for URLs to all official Textual documentation guides (use web_fetch for detailed information on-demand)
  • Example Apps: See assets/ for complete example applications
  • 组件库:查看references/widgets.md获取全面的组件示例
  • 布局模式:查看references/layouts.md获取常见布局方案
  • 样式指南:查看references/styling.md获取CSS模式和主题
  • 官方指南索引:查看references/official-guides-index.md获取所有官方Textual文档指南的URL(可使用web_fetch按需获取详细信息)
  • 示例应用:查看assets/获取完整的示例应用

Common Pitfalls

常见陷阱

  1. Forgetting async/await: Many Textual methods are async, always await them
  2. Blocking the event loop: CRITICAL - Use
    run_worker()
    for long-running tasks (network, I/O, heavy computation). Never use
    time.sleep()
    or blocking operations in the main thread
  3. Incorrect message handling: Method names must match
    on_{message_name}
    pattern
  4. CSS specificity issues: Use IDs and classes appropriately for targeted styling
  5. Not using query methods: Use
    query_one()
    and
    query()
    instead of manual traversal
  6. Thread safety violations: Never modify widgets directly from worker threads - use
    call_from_thread()
  7. Not cancelling workers: Workers continue running even when screens close - always cancel or store references
  8. Using time.sleep in async: Use
    await asyncio.sleep()
    instead of
    time.sleep()
    in async functions
  9. Not handling worker errors: Workers can fail silently - always implement error handling
  10. Wrong worker type: Use async workers for I/O, thread workers for CPU-bound tasks
  1. 忘记async/await:许多Textual方法是异步的,必须始终使用await
  2. 阻塞事件循环:重要提示 - 对长时间运行的任务(网络、I/O、重型计算)使用
    run_worker()
    。绝不要在主线程中使用
    time.sleep()
    或阻塞操作
  3. 消息处理错误:方法名称必须匹配
    on_{message_name}
    的命名模式
  4. CSS优先级问题:正确使用ID和类进行目标样式设计
  5. 未使用查询方法:使用
    query_one()
    query()
    代替手动遍历
  6. 线程安全违规:绝不要从Worker线程直接修改组件 - 使用
    call_from_thread()
  7. 未取消Worker:即使屏幕关闭,Worker仍会继续运行 - 始终取消或保存引用
  8. 在异步中使用time.sleep:在异步函数中使用
    await asyncio.sleep()
    代替
    time.sleep()
  9. 未处理Worker错误:Worker可能会静默失败 - 始终实现错误处理
  10. 错误的Worker类型:对I/O密集型任务使用异步Worker,对CPU密集型任务使用线程Worker