Textual 样式指南:用 styles 对象打造精致的终端用户界面
【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual
Textual 为每个 Widget 都内置了一个styles对象,通过为其中的属性赋值,即可实时改变组件的背景、边框、尺寸、边距等视觉表现,终端界面立刻随之刷新。本文以 Textual 官方样式指南为主线,系统讲解styles对象的全部核心能力——从颜色与透明度、尺寸与盒子模型,到边框、内边距与外边距的完整玩法,并辅以仓库源码与可运行示例,帮助你在纯终端环境下也能做出层次分明、观感优雅的应用界面。
一切从 styles 对象开始
Textual 中每个 Widget 类都提供一个styles对象,它包含若干描述"组件应该如何被显示"的属性。任何属性的赋值都会触发界面更新——你不需要手动调用重绘方法,Textual 会自动把新样式渲染到屏幕上。
注意,本文所说的screen(屏幕)指的是终端的内容区域,通常是桌面上的一个窗口。
先看一个最简单的例子,它直接给screen(一个代表屏幕的特殊 Widget)设置样式:
from textual.app import App class ScreenApp(App): def on_mount(self) -> None: self.screen.styles.background = "darkblue" self.screen.styles.border = ("heavy", "white") if __name__ == "__main__": app = ScreenApp() app.run()第一行把 background 设置为"darkblue",整个屏幕背景变为深蓝色;第二行把 border 设置为元组("heavy", "white"),Textual 会用"heavy"(粗线)风格绘制一个白色边框。运行后你会看到一块被粗白边框包围的深蓝色屏幕。
给普通 Widget 应用样式
只给屏幕设置样式远不足以构建真实界面,我们通常还要把样式应用到其他 Widget 上。下面这个例子创建了一个Static组件并为其设置样式:
from textual.app import App, ComposeResult from textual.widgets import Static class WidgetApp(App): def compose(self) -> ComposeResult: self.widget = Static("Textual") yield self.widget def on_mount(self) -> None: self.widget.styles.background = "darkblue" self.widget.styles.border = ("heavy", "white") if __name__ == "__main__": app = WidgetApp() app.run()compose方法在 yield 组件之前先把引用保存到self.widget,随后在on_mount事件处理器里通过该引用设置与屏幕示例相同的样式。
这里有两点值得注意:
- Widget 默认占据其容器的全宽,高度则按内容所需行数自适应。
- 加了边框后,上述 Widget 在终端中实际占 3 行——因为边框会多占 2 行(以及左右各 1 列)。如果去掉设置边框的那一行,Widget 将只占 1 行。
- Widget 默认会自动换行。如果把
"Textual"换成一长段文字,Widget 会向下扩展以容纳全部内容。
颜色:从命名色到 HSL
Textual 有多种接受颜色的样式属性,最常用的是 color(设置 Widget 上文字的默认颜色)与 background(设置文字下方的背景色)。
颜色值可以直接使用预定义的命名色常量,例如"crimson"、"lime"、"palegreen",完整清单见 Color API 中的命名色列表。把屏幕背景设为 lime 只需一行:
self.screen.styles.background = "lime"除命名色外,Textual 还支持以下三种颜色表达方式:
| 格式 | 语法 | 示例 | 说明 |
|---|---|---|---|
| RGB 十六进制 | #后跟红/绿/蓝三对(每对 1~2 位)十六进制数字 | #f00是艳红,#9932CC是dark orchid | 与 CSS 的 hex 写法一致 |
| RGB 十进制 | rgb后跟三个 0~255 的整数 | rgb(255,0,0)是艳红,rgb(153,50,204)是dark orchid | 分量取值范围 0~255 |
| HSL | hsl后跟 0~360 的角度与两个百分比 | hsl(0,100%,50%)是艳红,hsl(280,60%,49%)是dark orchid | 依次表示色相、饱和度、明度 |
background与color还接受 Color 对象(定义于 src/textual/color.py),从而可以在运行时动态构造颜色。看一个综合示例——三个 Widget 分别使用十六进制、HSL 字符串和Color对象三种方式设置背景:
from textual.app import App, ComposeResult from textual.color import Color from textual.widgets import Static class ColorApp(App): def compose(self) -> ComposeResult: self.widget1 = Static("Textual One") yield self.widget1 self.widget2 = Static("Textual Two") yield self.widget2 self.widget3 = Static("Textual Three") yield self.widget3 def on_mount(self) -> None: self.widget1.styles.background = "#9932CC" self.widget2.styles.background = "hsl(150,42.9%,49.4%)" self.widget2.styles.color = "blue" self.widget3.styles.background = Color(191, 78, 96) if __name__ == "__main__": app = ColorApp() app.run()Alpha:颜色的透明度
Textual 内部用三元组(红、绿、蓝)表示颜色,此外还支持第四个通用分量alpha(透明度)。给背景色设置 alpha 时,Textual 会把背景与底层的颜色做混合;给文字颜色设置 alpha 时,则把文字与背景色混合。
设置 alpha 有三种方式:
- 在十六进制颜色末尾追加第 4 对数字:范围 0(完全透明)到 255(完全不透明),0~255 之间的任意值都呈现半透明效果。例如
"#9932CC7f"是约 50% 半透明的 dark orchid。 - 使用
rgba格式:与rgb相同,但多出第 4 个取值在 0~1 之间的值,0 为不可见,1 为不透明。例如"rgba(192,78,96,0.5)"。 - 为
Color对象添加a参数:例如Color(192, 78, 96, a=0.5)创建半透明的暗红。这正是Color类构造器的能力(见 src/textual/color.py)。
下面的例子创建 10 个 Widget,alpha 从 0.1 递增到 1.0,直观展示透明度叠加效果:
from textual.app import App, ComposeResult from textual.color import Color from textual.widgets import Static class ColorApp(App): def compose(self) -> ComposeResult: self.widgets = [Static("") for n in range(10)] yield from self.widgets def on_mount(self) -> None: for index, widget in enumerate(self.widgets, 1): alpha = index * 0.1 widget.update(f"alpha={alpha:.1f}") widget.styles.background = Color(191, 78, 96, a=alpha) if __name__ == "__main__": app = ColorApp() app.run()可以观察到:alpha 为 0.1 时背景几乎与屏幕融为一体,随着 alpha 增大颜色逐渐饱和,到 1.0 时呈现为实心色块。Textual 对颜色字符串的解析、HSL 转换与 alpha 混合逻辑均实现在 src/textual/color.py 中,相关行为有 tests/test_color.py 等测试用例覆盖。
尺寸与盒子模型
Widget 占据屏幕上的一个矩形区域,最小可以只有一个字符,最大可以撑满屏幕——若开启了 scrolling,甚至可以超过屏幕尺寸。
盒子模型(Box Model)
以下样式共同决定 Widget 的尺寸:
- width 与 height:定义 Widget 的大小;
- padding:在内容区周围增加可选空白;
- border:在 padding 与内容区外围绘制可选矩形边框。
此外,margin 在 Widget 边框外侧增加空白——它严格来说不属于 Widget 本身,但提供了 Widget 之间的视觉分隔。这些样式组合起来构成了 Widget 的盒子模型:
宽与高
width限制 Widget 占用的列数,height限制占用的行数。来看同时设置两个维度的例子:
from textual.app import App, ComposeResult from textual.widgets import Static TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.""" class DimensionsApp(App): def compose(self) -> ComposeResult: self.widget = Static(TEXT) yield self.widget def on_mount(self) -> None: self.widget.styles.background = "purple" self.widget.styles.width = 30 self.widget.styles.height = 10 if __name__ == "__main__": app = DimensionsApp() app.run()文本在 30 列内换行,但 10 行高度装不下全部内容,最后一行会被整体裁掉。
自适应尺寸(auto)
实践中我们通常希望组件尺寸随内容变化,把某个维度设为"auto"即可:
from textual.app import App, ComposeResult from textual.widgets import Static TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.""" class DimensionsApp(App): def compose(self) -> ComposeResult: self.widget = Static(TEXT) yield self.widget def on_mount(self) -> None: self.widget.styles.background = "purple" self.widget.styles.width = 30 self.widget.styles.height = "auto" if __name__ == "__main__": app = DimensionsApp() app.run()此时 Widget 高度会自动增长以容纳全部文本。
尺寸单位:百分比、视口与 fr
Textual 提供多种单位,允许尺寸相对于屏幕或容器计算,这样当用户缩放终端窗口时,布局能更好地利用可用空间:
- 百分比(
%):数字后跟%,按父组件尺寸的比例计算。例如宽度设为"50%",Widget 就占父组件宽度的一半; - 视口单位:显式引用某个维度。
vw按终端宽度的百分比计算,vh按终端高度的百分比计算; w单位:按可用宽度的百分比计算(若 Widget 嵌套在另一个组件中,可用宽度可能小于终端尺寸);h单位:按可用高度的百分比计算。
下面例子把宽度设为"50%"、高度设为"80%",缩放终端窗口时 Widget 会始终保持相对比例:
from textual.app import App, ComposeResult from textual.widgets import Static TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.""" class DimensionsApp(App): def compose(self) -> ComposeResult: self.widget = Static(TEXT) yield self.widget def on_mount(self) -> None: self.widget.styles.background = "purple" self.widget.styles.width = "50%" self.widget.styles.height = "80%" if __name__ == "__main__": app = DimensionsApp() app.run()分别在 60×20、80×30、120×40 的终端中运行,可以看到 Widget 始终按比例伸缩——这正是相对单位相比固定像素值的核心优势。
FR 单位:比例分配利器
百分比在某些相对值场景下很别扭:比如想把屏幕三等分,就得写成33.3333333333%。Textual 为此提供了fr单位,更适合此类比例分配需求。
fr的分配规则是:把该维度上的可用空间按所有fr值的总和等分,再按每个 Widget 的fr值分配对应份额。来看一个例子:两个 Widget 高度分别设为"2fr"与"1fr":
from textual.app import App, ComposeResult from textual.widgets import Static TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.""" class DimensionsApp(App): def compose(self) -> ComposeResult: self.widget1 = Static(TEXT) yield self.widget1 self.widget2 = Static(TEXT) yield self.widget2 def on_mount(self) -> None: self.widget1.styles.background = "purple" self.widget2.styles.background = "darkgreen" self.widget1.styles.height = "2fr" self.widget2.styles.height = "1fr" if __name__ == "__main__": app = DimensionsApp() app.run()高度方向上fr总和为 3:第一个 Widget 的2fr占总高度的三分之二,第二个 Widget 的1fr占剩余三分之一。
最小与最大尺寸
同样的单位也可用来给尺寸设置上下限,以下样式接受width/height支持的任何值:
- min-width:最小宽度;
- max-width:最大宽度;
- min-height:最小高度;
- max-height:最大高度。
Padding:内边距
Padding 在内容周围增加空白以提升可读性。设为整数时,会在内容区四周各增加相应数量的行与列。下面的例子把 padding 设为 2:
# 在 on_mount 中设置 self.widget.styles.padding = 2文本四周会多出明显空白。Padding 也可以设为两个整数的元组,分别作用于"上/下"与"左/右"四条边。例如(2, 4)表示上下各加 2 行、左右各加 4 列(完整示例见 padding02.py):
self.widget.styles.padding = (2, 4)Padding 还支持四个值的元组,按上、右、下、左的顺序分别设置每条边的内边距。
Border:边框与标题对齐
border 在 Widget 周围绘制边框,设置方式是赋一个二元组:第一个值是边框类型(字符串),第二个值是边框颜色(接受任何与 color、background 兼容的值)。完整示例见 border01.py:
self.widget.styles.border = ("heavy", "white")边框类型非常丰富。在命令行直接运行以下命令即可预览所有内置边框样式:
textual borders标题与副标题对齐
Widget 还有两个属性border_title与border_subtitle,设置后分别显示在上边框与下边框内。对应的两个对齐样式可取值"left"、"right"、"center":
border-title-align:设置标题(上边框)对齐,默认为"left";border-subtitle-align:设置副标题(下边框)对齐,默认为"right"。
下面的例子同时设置标题与副标题,并把顶部标题改为居中对齐:
from textual.app import App, ComposeResult from textual.widgets import Static TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.""" class BorderTitleApp(App[None]): def compose(self) -> ComposeResult: self.widget = Static(TEXT) yield self.widget def on_mount(self) -> None: self.widget.styles.background = "darkblue" self.widget.styles.width = "50%" self.widget.styles.border = ("heavy", "yellow") self.widget.border_title = "Litany Against Fear" self.widget.border_subtitle = "by Frank Herbert, in “Dune”" self.widget.styles.border_title_align = "center" if __name__ == "__main__": app = BorderTitleApp() app.run()运行后可以看到黄色粗边框内,上方标题居中、下方副标题默认靠右。
Outline:不占空间的描边
Outline 与 border 类似、设置方式相同,区别在于 outline不会改变 Widget 的尺寸,可能会与内容区重叠。完整示例见 outline01.py:
self.widget.styles.outline = ("heavy", "white")可以看到描边压在文字之上。Outline 适合用来强调某个 Widget,但要小心它可能遮挡内容。
Box sizing:两种盒模型算法
设置 padding 或 border 会压缩Widget 的内容区——即设置 padding/border 不会改变 Widget 整体的宽高。这在排布界面时通常是理想行为:加边框或内边距不会破坏已有布局。但偶尔我们希望保持内容区大小不变、让 Widget 整体变大以容纳 padding 与 border,此时需要 box-sizing 样式切换模式:
"border-box"(默认值):padding 与 border 从 Widget 尺寸中扣除,内容区相应缩小;"content-box":padding 与 border 所需空间追加到 Widget 尺寸上,内容区保持不变。
两种模式的盒子模型对比如下:
=== "content-box(内容区优先,整体变大)"
[](https://link.gitcode.com/i/2ac490e399db84a8c6f49b1478c592c1)=== "border-box(整体尺寸固定,内容区收缩)"
[](https://link.gitcode.com/i/2ac490e399db84a8c6f49b1478c592c1)下面例子创建两个 Widget,宽 30、高 6,border 与 padding 均为 1;第一个使用默认"border-box",第二个切换为"content-box":
from textual.app import App, ComposeResult from textual.widgets import Static TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.""" class BoxSizing(App): def compose(self) -> ComposeResult: self.widget1 = Static(TEXT) yield self.widget1 self.widget2 = Static(TEXT) yield self.widget2 def on_mount(self) -> None: self.widget1.styles.background = "purple" self.widget2.styles.background = "darkgreen" self.widget1.styles.width = 30 self.widget2.styles.width = 30 self.widget1.styles.height = 6 self.widget2.styles.height = 6 self.widget1.styles.border = ("heavy", "white") self.widget2.styles.border = ("heavy", "white") self.widget1.styles.padding = 1 self.widget2.styles.padding = 1 self.widget2.styles.box_sizing = "content-box" if __name__ == "__main__": app = BoxSizing() app.run()第一个 Widget 的 padding 与 border 从高度中扣除,内容区只剩 2 行;第二个 Widget 总高同样为 6,但 padding 与 border 额外增加了高度,内容区仍是 6 行。
Margin:组件间距与重叠规则
margin 与 padding 类似都增加空间,但 margin 位于 Widget边框之外,用于拉开 Widget 之间的间距。完整示例见 margin01.py,它为两个 Widget 各设置 margin 为 2:
self.widget1.styles.margin = 2 self.widget2.styles.margin = 2每个 Widget 的边框外会多出两行两列空白。注意 margin 会重叠:上述两个 Widget 的 margin 都是 2,但二者之间只有 2 行间距——相邻 Widget 的 margin 取两者中的较大值,而不是相加。
更多样式与下一步
本文覆盖了 Textual 最基础、最常用的样式能力,但远非全部。Textual 还提供大量其他样式用于定制应用外观的方方面面,完整清单见 Styles 参考,其中每个样式都有独立的参考页(如 align、dock、display、layer 等)。
这些样式的底层实现位于 src/textual/style.py 与 src/textual/css/styles.py 等模块,配以 tests/test_style_parse.py、tests/test_style_properties.py 等测试验证解析与赋值行为。接下来可以继续学习 Textual CSS——一种将样式集中管理的更强大方式,它能让你的代码彻底摆脱零散的样式属性赋值。
【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考