跳转至

Textual Python TUI 框架

为什么要学 Textual

Textual 是 Python 中功能最强大的 TUI(终端用户界面)框架,由 Rich 库的作者开发。它使用类似 Web 的 CSS 布局系统和组件化架构来构建终端应用,支持按钮、输入框、表格、树形视图、Markdown 渲染、语法高亮等丰富的组件。对于需要在终端中构建复杂交互界面的 Python 开发者来说,Textual 是无可替代的选择。


核心概念

概念白话解释用途
App应用TUI 应用的根容器
Widget组件UI 的构建块(按钮、输入框等)
Screen屏幕全屏视图(可切换)
CSS样式类似 Web CSS 的样式系统
Binding键绑定快捷键到操作的映射
Message消息组件间的事件通信

安装配置

pip install textual
pip install "textual[dev]"  # 开发工具

快速上手

最简应用

from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static

class MyApp(App):
    CSS = """
    Screen {
        align: center middle;
    }
    #hello {
        width: 40;
        height: 5;
        border: solid green;
        content-align: center middle;
    }
    """

    BINDINGS = [("q", "quit", "退出")]

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("Hello, Textual!", id="hello")
        yield Footer()

if __name__ == "__main__":
    app = MyApp()
    app.run()

使用组件

from textual.app import App, ComposeResult
from textual.widgets import Button, Input, Label, Header, Footer
from textual.containers import Horizontal, Vertical

class TodoApp(App):
    CSS = """
    #input-area { height: 3; }
    #todo-list { height: 1fr; border: solid blue; padding: 1; }
    Button { margin: 0 1; }
    """

    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)
        with Horizontal(id="input-area"):
            yield Input(placeholder="输入待办事项...", id="todo-input")
            yield Button("添加", variant="primary", id="add-btn")
        yield Vertical(id="todo-list")
        yield Footer()

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "add-btn":
            input_widget = self.query_one("#todo-input", Input)
            if input_widget.value:
                self.query_one("#todo-list").mount(
                    Label(f"• {input_widget.value}")
                )
                input_widget.value = ""

    def on_input_submitted(self, event: Input.Submitted) -> None:
        self.on_button_pressed(Button.Pressed(self.query_one("#add-btn")))

if __name__ == "__main__":
    TodoApp().run()

进阶用法

CSS 文件分离

# app.py
class MyApp(App):
    CSS_PATH = "styles.tcss"

# styles.tcss
Screen {
    layout: grid;
    grid-size: 2 3;
    grid-gutter: 1;
}

.panel {
    border: solid $accent;
    padding: 1;
    height: 100%;
}

Button.primary {
    background: $primary;
    color: $text;
}

Button:hover {
    background: $primary-lighten-1;
}

DataTable 数据表格

from textual.widgets import DataTable

class TableApp(App):
    def compose(self) -> ComposeResult:
        yield DataTable()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.add_columns("名称", "语言", "星数")
        table.add_rows([
            ("Textual", "Python", "25k"),
            ("Bubble Tea", "Go", "23k"),
            ("Ratatui", "Rust", "8k"),
        ])
        table.cursor_type = "row"

多屏幕应用

from textual.screen import Screen

class MenuScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Button("开始", id="start")
        yield Button("设置", id="settings")

    def on_button_pressed(self, event: Button.Pressed):
        if event.button.id == "start":
            self.app.push_screen(GameScreen())

class GameScreen(Screen):
    BINDINGS = [("escape", "app.pop_screen", "返回")]

    def compose(self) -> ComposeResult:
        yield Static("游戏画面")

class MyApp(App):
    def on_mount(self):
        self.push_screen(MenuScreen())

异步操作

from textual.app import App
from textual.widgets import Static
import httpx

class AsyncApp(App):
    def compose(self) -> ComposeResult:
        yield Static("加载中...", id="result")

    async def on_mount(self) -> None:
        self.run_worker(self.fetch_data())

    async def fetch_data(self) -> None:
        async with httpx.AsyncClient() as client:
            response = await client.get("https://api.github.com")
            data = response.json()
        self.query_one("#result").update(f"GitHub API: {data.get('current_user_url')}")

开发者工具

# 实时 CSS 调试
textual run --dev app.py

# 控制台(类似浏览器 DevTools)
# 在另一个终端运行
textual console

# 截图
textual run app.py --screenshot output.svg

常见问题

Q: 与 Rich 的关系?

Textual 建立在 Rich 之上。Rich 是输出库(美化打印),Textual 是交互式 TUI 框架。

Q: 支持鼠标吗?

支持。按钮点击、滚动、拖拽等鼠标操作都支持。

Q: 性能如何?

异步架构,可以处理大量数据。DataTable 组件支持虚拟化,百万行也不卡。

Q: 能否发布为独立应用?

可以用 PyInstaller 打包为二进制,或使用 pipx 安装。


参考资源

  • GitHub:https://github.com/Textualize/textual
  • 文档:https://textual.textualize.io/
  • 组件库:https://textual.textualize.io/widget_gallery/
  • CSS 参考:https://textual.textualize.io/css_types/
  • 教程:https://textual.textualize.io/tutorial/