news 2026/9/20 3:41:27

Textual 样式指南:用 styles 对象打造精致的终端用户界面

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Textual 样式指南:用 styles 对象打造精致的终端用户界面

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是艳红,#9932CCdark orchid与 CSS 的 hex 写法一致
RGB 十进制rgb后跟三个 0~255 的整数rgb(255,0,0)是艳红,rgb(153,50,204)dark orchid分量取值范围 0~255
HSLhsl后跟 0~360 的角度与两个百分比hsl(0,100%,50%)是艳红,hsl(280,60%,49%)dark orchid依次表示色相、饱和度、明度

backgroundcolor还接受 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_titleborder_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(内容区优先,整体变大)"

[![content-box 模式下 padding 与 border 追加到 Widget 尺寸之外](https://raw.gitcode.com/gh_mirrors/te/textual/raw/1d99508b928a771b51e1a527319c6b87dcff9e05/docs/images/styles/content_box.excalidraw.svg?utm_source=gitcode_repo_files)](https://link.gitcode.com/i/2ac490e399db84a8c6f49b1478c592c1)

=== "border-box(整体尺寸固定,内容区收缩)"

[![border-box 模式下 padding 与 border 从 Widget 尺寸内扣除](https://raw.gitcode.com/gh_mirrors/te/textual/raw/1d99508b928a771b51e1a527319c6b87dcff9e05/docs/images/styles/border_box.excalidraw.svg?utm_source=gitcode_repo_files)](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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 3:39:50

基于Sobel算子的垂直边缘检测系统:原理、实现与工程调参

做图像处理和计算机视觉项目这几年,我发现一个很有意思的现象:很多初学边缘检测的朋友,一上来就对着Canny整条链路猛看,反而忽略了最基础、也最容易被工程化的Sobel算子。实际上在工业视觉、文档扫描、PCB缺陷检测这些场景里&…

作者头像 李华
网站建设 2026/9/20 3:39:03

同版本内链接(自动指向当前浏览的框架)

同版本内链接(自动指向当前浏览的框架) 【免费下载链接】handsontable JavaScript Data Grid / Data Table with a Spreadsheet Look & Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡ 项目地址: https://gitc…

作者头像 李华
网站建设 2026/9/20 3:37:56

GD32H759+RT-Thread环境搭建与点灯实验全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 3:37:29

OpenResearch:构建可复现、可追溯的开放研究工作流

研究做得越久,我越觉得"研究"这件事本身最缺的不是灵感,不是经费,甚至不是时间,而是一套能让人信服的、完整的、可复现的工作方式。这些年我见过太多项目死在"我记得当时好像这样处理过数据",死在…

作者头像 李华
网站建设 2026/9/20 3:37:12

工程级AI编程代理Codex快速入门:CLI与IDE扩展安装配置及避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华