Python Textual库的基础使用

Gr3yPh4ntom 发布于 2026-02-20 171 次阅读


心血来潮想在终端里打音游,就上网搜搜cli有没有图形渲染引擎一类的玩意,然后找到了textual。的确有过往命令行里加入图形界面组件的设想,但没想到已经有现成品了,还是得来学习学习(ʃƪ^3^)

一、起步

使用前先安装textual库:

pip install textual

来编写第一个程序:

from textual.app import App

#导入用到的组件:静态文字、标题栏、页脚栏
from textual.widgets import Static,Header,Footer

class TestApp(App):
    #绑定按键
    BINDINGS = [
        ("q", "quit", "Quit"),
    ]
    #简单的样式表
    CSS = """
    Static{
        content-align: center middle;
        height: 100%;
    }
    """

    def compose(self):
        #加入组件
        yield Static("Hello, World!")
        yield Header()
        yield Footer()

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

二、组件

和qt与WinForm类似,都有组件这玩意,使用前需要预先从textual.widgets导入:

1.基础展示类

yield Header()                      # 顶部状态栏
yield Footer()                      # 底部快捷键栏
yield Label("我是只读文本标签")       # 简单文本
yield Static("我是基础容器/方块")     # 最通用的方块
yield Sparkline([1, 3, 2, 5, 4])    # 迷你趋势图

2.交互输入类

yield Button("点击我", variant="success") # 按钮 (success/error/warning)
yield Input(placeholder="请输入内容...")  # 文本输入框
yield Checkbox("记得勾选我")              # 复选框
yield Switch()                           # 开关
yield RadioSet("选项A", "选项B")          # 单选框组
yield Select([("显示", "值")], prompt="请选择") # 下拉选择器

3.数据与列表类

yield ListView(ListItem(Static("列表项"))) # 列表视图
yield DirectoryTree("./")                 # 文件目录树
yield DataTable()                         # 数据库样式的表格
yield OptionList("项目1", "项目2")         # 纯文本选项列表

4.反馈与进度类

yield ProgressBar(total=100, show_eta=True) # 进度条
yield Log()                                 # 实时日志输出框
yield RichLog()                             # 支持带颜色格式的日志
yield LoadingIndicator()                    # 正在加载的转圈动画

5.一堆容器

#须从textual.containers导入
with Vertical():           # 垂直堆叠
    ...
with Horizontal():         # 水平并排
    ...
with Grid():               # 网格布局
    ...
with ScrollableContainer():# 带滚动条的盒子
    ...
with TabbedContent():      # 选项卡切换
    with TabPane("标签1"):
        yield Static("内容1")
    with TabPane("标签2"):
        yield Static("内容2")

三、样式

自己家的TCSS,和qt样式表有着异曲同工之妙(果然css还是好用

特性CSSTextual TCSS
选择器选择器 { 属性: 值; }一模一样
层叠性#id, .class, tag一模一样
伪类:hover, :active, :focus:hover, :focus
布局极强(flex和grid)也很强(也是flex和grid)

1.盒子模型

width: 100%;             /* 宽度:支持 1fr, 50%, 40(字符), auto */
height: 1fr;              /* 高度:1fr 代表占用剩余所有空间 */
min-width: 20;            /* 最小宽度:防止容器被挤没 */
max-height: 10;           /* 最大高度 */

margin: 1 2;              /* 外边距:1(上下) 2(左右) 的字符空隙 */
padding: 1;               /* 内边距:内容离边框的距离 */

border: solid $accent;    /* 边框:样式(solid/double/heavy/round) 颜色 */
border-title-align: center; /* 边框标题对齐:配合组件的 border_title 属性 */

2.布局

layout: vertical;         /* 布局模式:内容垂直堆叠(类似标准div) */
layout: horizontal;       /* 布局模式:内容水平排队 */

align: center middle;     /* 子组件对齐:让里面的小方块在自己中心 */
content-align: left top;  /* 内容对齐:让文字在自己内部的对齐位置 */

dock: left;               /* 悬浮停靠:直接锁在左侧(固定侧边栏) */
layer: top;               /* 层级:类似 z-index,决定重叠时的上下顺序 */

3.视觉风格

background: $surface;     /* 背景色:推荐用变量,也支持 #hex 或 rgb() */
color: white;             /* 文字颜色 */
opacity: 80%;             /* 透明度:0% 到 100% */

text-style: bold italic;  /* 文字样式:加粗、斜体、下划线(underline) */
text-align: center;       /* 文字水平对齐 */
text-opacity: 50%;        /* 仅文字透明:背景保持原色 */

4.动效

display: none;            /* 是否渲染:完全消失且不占位 */
visibility: hidden;       /* 是否可见:消失但位置还留着 */

offset-x: 2;              /* 水平位移:相对原位置向右挪2格 */
offset-y: -1;             /* 垂直位移:相对原位置向上挪1格 */

scrollbar-gutter: stable; /* 滚动条占位:防止滚动条出现时画面左右晃动 */

四、事件

1.经典on_语法

直接在程序类下面新建 on_组件_事件 函数就可以创建事件:

from textual.widgets import Button, Input

class MyView(Static):
    def compose(self):
        yield Input(placeholder="输入你的名字", id="user_name")
        yield Button("打招呼", id="greet_btn")

    # 当 ID 为 greet_btn 的按钮被按下时
    def on_button_pressed(self, event: Button.Pressed) -> None:
        name = self.query_one("#user_name").value
        self.notify(f"你好,{name}!") # 弹出一个通知

注意这个事件没有绑定按键的ID,这就是textual事件系统独特的地方:消息冒泡。

实例中on_button_pressed函数实际是一个捕获器,当button被点击时会发出Button.Pressed消息,被捕获器捕获后执行。

因此我们需要使用event.button.id进一步确认:

def on_button_pressed(self, event: Button.Pressed) -> None:
    if event.button.id == "ok":
        self.notify("你点了确定")
    elif event.button.id == "cancel":
        self.notify("你点了取消")

2.监听器装饰器@on

@on比较的精简直观清晰,而且它也可以直接绑定id:

from textual import on

# 监听任何按钮的点击
@on(Button.Pressed)
def handle_all_buttons(self):
    self.log("某个按钮被按下了")

# 只监听特定 ID 的按钮
@on(Button.Pressed, "#start-btn")
def start_game(self):
    self.start_logic()

3.操作其他组件:query_one

很类似dom操作了,可以根据选择器查找一个或一组组件:

self.query_one("#id")   #找一个组件
self.query(".my-class") #找一组组件

操作举例:

def on_switch_changed(self, event: Switch.Changed) -> None:
    # 找到页面上的 Label 并修改它
    label = self.query_one("#status-label")
    if event.value: # 开关的状态
        label.update("服务已开启")
        label.styles.color = "green"
    else:
        label.update("服务已关闭")
        label.styles.color = "red"

4.全局快捷键:BINDINGS结合action_

简述:BINDINGS中指定的快捷键可以直接对应action_xxx函数:

class MusicApp(App):
    BINDINGS = [
        ("space", "play_pause", "播放/暂停"),
        ("up", "volume_up", "音量+"),
    ]

    def action_play_pause(self) -> None:
        # 当按下空格键时执行
        self.is_playing = not self.is_playing
        self.notify("状态已切换")

    def action_volume_up(self) -> None:
        self.volume += 10

5.定时器

on_mount在页面启动时执行(结束前就是on_unmount),set_interval用于定时

def on_mount(self):
    # 每 0.1 秒执行一次 self.tick
    self.set_interval(0.1, self.tick)

def tick(self):
    # 更新进度条
    progress = self.query_one(ProgressBar)
    progress.advance(1)

五、切换页面

可以定义多个screen类(就像不同网页文件),然后用push_screen把新页面盖在旧页面上(着实新奇):

from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Button, Label

# --- 定义第二个页面(游戏页) ---
class GameScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Label("这里是游戏界面![正在播放音乐...]")
        yield Button("返回主菜单", id="back")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.app.pop_screen()  # 把当前页面“拿走”,露出下面的页面

# --- 主程序 ---
class MyApp(App):
    def compose(self) -> ComposeResult:
        yield Label("这是主菜单")
        yield Button("开始游戏", id="start")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "start":
            self.push_screen(GameScreen()) # 推入新页面

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

六、更多操作

1.响应式设计

from textual.reactive import reactive

class MyWidget(Static):
    count = reactive(0)  # 只要 count 变了,watch_count 就会触发

    def watch_count(self, new_value: int):
        self.update(f"当前计数: {new_value}")

2.工作线程

#不就是防止卡前台嘛。。
self.run_worker(你的函数)

3.自定义组件

​当官方提供的 Button、Input 不够用时,你可以通过继承 Static 并重写 render() 方法,用 Rich 库的语法自己画 UI(比如画个字符画、圆角矩形等)。

这话不是我说的哈,我还得琢磨琢磨呢。。

七、结尾

所以基础的也就这些东西了是吧,搞WinForm这么久是时候来点新东西了!