Textual CSS 完全指南:从选择器到嵌套样式的终端界面样式系统
发布时间:2026/9/19 23:58:45 锦皓数字建站

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导读本文以 Textual 官方指南 docs/guide/CSS.md 为骨架系统讲解 Textual 的样式系统从样式表stylesheet与 DOM 的基本概念到类型 / ID / 类名 / 伪类等各类选择器再到组合器、优先级specificity、CSS 变量、initial重置与嵌套 CSS。读完本文你将能写出结构清晰、可复用、可实时热更新的终端界面样式并理解这些规则在 src/textual/css 源码层是如何被解析与应用的。1. 样式表Textual CSS 是什么Textual 使用 CSSCascading Stylesheet层叠样式表为 widget 应用样式。如果你有过 Web 开发经验一定接触过 CSS即便没有本章也会带你从零上手。与浏览器 CSS 不同Textual 的样式表作用对象是终端里的widget组件其语法规则、层叠优先级、继承与覆盖机制则与 Web CSS 一脉相承。一个典型的 Textual CSS 规则集rule set如下Header { dock: top; height: 3; content-align: center middle; background: blue; color: white; }其中第一行Header是选择器selector指明这条规则作用于哪些 widget。上例中它匹配由 Python 类Header定义的 widget。花括号内部是若干条规则rule每条由规则名rule name与规则值rule value组成以冒号分隔、分号结尾。惯例是每条规则占一行但只要以分号分隔也可以多规则写在同一行。以dock: top;为例规则名dock告诉 Textual 将 widget 停靠到屏幕的某条边上规则值top指定停靠在屏幕顶部。dock的其他合法值还有right、bottom、left而对页眉Header来说top最合适。提示Textual CSS 文件通常使用.tcss扩展名以区别于浏览器 CSS.css。2. DOM样式作用的对象结构DOMDocument Object Model文档对象模型是借用自 Web 世界的术语。Textual 并不处理文档但保留了这一说法在 Textual CSS 中DOM 是 widget 的排列方式可以想象成一棵树——某些 widget 包含其他 widget例如列表控件包含列表项、对话框包含按钮这些子 widget 构成了树的枝条。2.1 最小示例先看一个极简应用docs/examples/guide/dom1.pyfrom textual.app import App class ExampleApp(App): pass if __name__ __main__: app ExampleApp() app.run()创建ExampleApp实例时会隐式创建一个Screen对象在 DOM 术语中Screen是ExampleApp的child子节点。此时 DOM 只有两层尚未呈现树的形态。2.2 添加页眉与页脚给应用添加 Header 和 Footerdocs/examples/guide/dom2.pyfrom textual.app import App, ComposeResult from textual.widgets import Header, Footer class ExampleApp(App): def compose(self) - ComposeResult: yield Header() yield Footer() if __name__ __main__: app ExampleApp() app.run()添加后DOM 变成了带枝条的树Header与Footer都是Screen的子节点。注意这里做了简化。Header和Footer内部其实还有各自的子 widget。使用内置 widget 构建应用时除非你要单独修改其内部组件的样式否则通常无需关心它们的内部构造。2.3 构建一个简单对话框为了进一步探索 DOM我们构建一个带问题和两个按钮的对话框docs/examples/guide/dom3.py引入几个内置 widgettextual.containers.Container作为顶层对话框容器textual.containers.Horizontal将子 widget 从左到右水平排列textual.widgets.Static展示简单内容textual.widgets.Button可点击的按钮。from textual.app import App, ComposeResult from textual.containers import Container, Horizontal from textual.widgets import Button, Footer, Header, Static QUESTION Do you want to learn about Textual CSS? class ExampleApp(App): def compose(self) - ComposeResult: yield Header() yield Footer() yield Container( Static(QUESTION, classesquestion), Horizontal( Button(Yes, variantsuccess), Button(No, varianterror), classesbuttons, ), iddialog, ) if __name__ __main__: app ExampleApp() app.run()这里Container以位置参数接收一批子 widget并将它们作为容器的孩子加入 DOM。注意并非所有 widget 都接受子 widget——例如Button就不需要子节点。注意代码中出现了两个新参数iddialog与classesquestion/buttons它们正是 CSS 用来定位 DOM 节点的身份标识下一节的选择器部分会详细讲解。此时由于尚未引入样式表输出只是一堆元素堆叠还不像一个真正的对话框——这正是样式表要解决的问题。3. CSS 文件如何接入样式3.1 CSS_PATH 类变量要加载样式表只需在App子类中把CSS_PATH类变量设置为相对路径docs/examples/guide/dom4.pyfrom textual.app import App, ComposeResult from textual.containers import Container, Horizontal from textual.widgets import Button, Footer, Header, Static QUESTION Do you want to learn about Textual CSS? class ExampleApp(App): CSS_PATH dom4.tcss def compose(self) - ComposeResult: yield Header() yield Footer() yield Container( Static(QUESTION, classesquestion), Horizontal( Button(Yes, variantsuccess), Button(No, varianterror), classesbuttons, ), iddialog, ) if __name__ __main__: app ExampleApp() app.run()对应的样式文件 docs/examples/guide/dom4.tcss/* The top level dialog (a Container) */ #dialog { height: 100%; margin: 4 8; background: $panel; color: $text; border: tall $background; padding: 1 2; } /* The button class */ Button { width: 1fr; } /* Matches the question text */ .question { text-style: bold; height: 100%; content-align: center middle; } /* Matches the button container */ .buttons { width: 100%; height: auto; dock: bottom; }CSS 中可以添加注释将文字放在/*与*/之间Textual 会忽略注释内容。注释既可以用来给自己留提醒也可以临时禁用某些选择器。引入样式表后同一段代码的输出立即呈现出完整的对话框外观。3.2 加载多个 CSS 文件CSS_PATH也可以设置为路径列表Textual 会合并所有路径中的规则。这一点与源码实现一致在 src/textual/app.py 中CSS_PATH被声明为ClassVar[CSSPathType | None]而CSSPathType正是允许字符串、路径对象或二者的列表App.__init__的css_path参数src/textual/app.py也支持传入路径列表并会按顺序加载。另外App.CSS类变量src/textual/app.py可存放内联 CSS它会在CSS_PATH之后加载因此在优先级冲突时内联 CSS 更占优——这对快速脚本非常方便。3.3 为什么用 CSS使用 CSS 而非直接在 Python 里设置样式主要有三大理由外观与逻辑分离CSS 把应用长什么样与怎么工作解耦。在 Python 代码里混写样式容易产生大量面条式代码掩盖应用的核心逻辑。统一定制内置与第三方 widget你可以像定制自己的应用和 widget 一样轻松定制内置及第三方 widget 的外观。实时编辑live edit用下面的命令运行应用时任何对 CSS 文件的修改都会即时反映到终端textual run my_app.py --dev无需重启应用即可迭代设计这让界面美化更快更轻松。4. 选择器精准定位 widget选择器是花括号前的那段文本它告诉 Textual 规则应用于哪些 widget。选择器既可以匹配某一类 widget也可以精确匹配某个特定 widget——例如你可以让所有按钮生效也可以只改某个对话框里的一个按钮。4.1 类型选择器Type selector类型选择器匹配Python类的名字。考虑如下 widget 类from textual.widgets import Static class Alert(Static): pass可以用下面的 CSS 为Alertwidget 加红色边框Alert { border: solid red; }类型选择器还会匹配 widget 的基类因为Alert继承自Static所以Static选择器也会作用于该按钮Static { background: blue; border: round green; }注意这一点与浏览器 CSS 不同——浏览器 CSS 没有类型选择器匹配基类的概念。上例中border规则同时出现在Static与Alert中。此时 Textual 采用最近定义的子类Alert胜过StaticStatic胜过所有 widget 的基类Widget。因此若两条规则同时存在于样式表Alertwidget 将得到 solid red 边框而不是 round green。4.2 ID 选择器每个 widget 可以有唯一一个id属性通过构造参数设置且在其所属容器内应当唯一id在 widget 构造后不可修改。示例yield Button(idnext)以井号#开头的选择器匹配 ID#next { outline: red; }4.3 类名选择器Class-name selector每个 widget 可以带多个 CSS 类名。这里的 class 借自 Web CSS与 Python 的 class 含义不同——可以把 CSS 类理解为一种标签带相同标签的 widget 共享样式。CSS 类通过构造参数的classes设置yield Button(classessuccess)也可以一次设置多个类名以空格分隔yield Button(classeserror disabled)在 CSS 中用点号.前缀匹配类名.success { background: green; color: white; }类名可以加到任意 widget 上因此不同类型的 widget 也可以共享类名。类名选择器还可以链式组合——追加一个点号再接一个类名匹配同时拥有所有这些类名的 widget。例如下面规则为同时带有error和disabled两个类的 widget 设置深红背景.error.disabled { background: darkred; }与id不同widget 的类名在创建之后可以改变。运行时增删 CSS 类是改变界面显示状态的首选方式。Textual 提供了以下类管理方法可参考 src/textual/dom.py 与 src/textual/css/query.py 的实现方法作用add_class()给 widget 添加一个或多个类remove_class()移除 widget 的一个或多个类toggle_class()类名存在则移除不存在则添加has_class()检查 widget 是否设置了某个些类set_class()根据布尔值决定添加或移除类DOMQuery版本可批量作用于所有匹配节点classeswidget 上全部类名的冻结集合frozen set4.4 通用选择器Universal selector通用选择器用星号*表示匹配所有widget* { outline: solid red; }虽然很少需要对所有 widget 设样式但你可以把通用选择器与父级组合从而选中某个父节点的全部子节点。例如让VerticalScroll的所有子节点背景变红VerticalScroll * { background: red; }关于这种组合的更多细节见下文组合器一节。4.5 伪类Pseudo classes伪类用于匹配处于特定状态的 widget由 Textual 自动设置。例如希望鼠标悬停时按钮变绿Button:hover { background: green; }background: green只作用于鼠标光标下的那个 Button光标移开后恢复原背景色。Textual 支持以下伪类:blur匹配未获得输入焦点的 widget。:dark匹配深色主题中的 widgetApp.theme.dark True。:disabled匹配处于禁用状态的 widget。:empty匹配没有显示子节点的 widget。:enabled匹配处于启用状态的 widget。:even匹配在同级兄弟中处于偶数位置的 widget。:first-child匹配同级兄弟中的第一个 widget。:first-of-type匹配同级兄弟中同类型的第一个 widget。:focus-within匹配包含处于焦点子节点的 widget。:focus匹配拥有输入焦点的 widget。:inline匹配应用运行在内联模式时的 widget。:last-child匹配同级兄弟中的最后一个 widget。:last-of-type匹配同级兄弟中同类型的最后一个 widget。:light匹配浅色主题中的 widgetApp.theme.dark False。:odd匹配同级兄弟中处于奇数位置的 widget。5. 组合器把选择器组合起来更复杂的选择器可以由简单选择器组合而成组合的逻辑称为组合器combinator。5.1 后代组合器Descendant combinator用空格分隔两个选择器时匹配祖先匹配第一个选择器、自身匹配第二个选择器的 widget。假设 DOM 中有一个#dialog容器我们希望对话框内的按钮文本加粗但不影响侧边栏里的按钮#dialog Button { text-style: bold; }#dialog Button匹配所有位于 ID 为dialog的 widget 之下的按钮其他按钮不受影响。组合器可以无限叠加例如下面规则匹配位于Horizontal之下、且位于 ID 为dialog的 widget 之下的Button#dialog Horizontal Button { text-style: bold; }5.2 子组合器Child combinator子组合器与后代组合器类似但只匹配直接子节点。用大于号分隔两个选择器两边的空白会被忽略。假如侧边栏里有一个按钮我们希望只给它加下划线#sidebar Button { text-style: underline; }该规则只匹配父节点 ID 为sidebar的按钮若按钮嵌套更深中间还有其他容器则不会被匹配。6. 优先级Specificity规则冲突时谁说了算多个选择器可能同时命中同一个 widget。若同一条样式被多个选择器应用Textual 按如下规则决定胜者ID 数量最多者胜例如#next胜过.button#dialog #next胜过#next。若 ID 数量相同进入下一条规则。类名数量最多者胜例如.button.success胜过.success。计算时伪类与普通类名同等对待因此.button:hover计为2个类名。若类名数量相同进入下一条规则。类型数量最多者胜例如Container Button胜过Button。6.1 重要规则!important优先级通常足以解决样式表冲突但还有最后一种针对单条规则的裁决手段在规则末尾加上!important该规则将无视优先级无条件胜出Button:hover { background: blue !important; }警告请谨慎甚至尽量不用!important。如果到处都是 important就没有什么是重要的了——它会让你未来难以修改 CSS。7. CSS 变量消除重复、统一设计可以在 CSS 中定义变量来减少重复、强化一致性。Textual CSS 变量以$为前缀例如定义一个名为$border的变量$border: wide green;此后书写$border会被替换为wide green#foo { border: $border; }等价于#foo { border: wide green; }变量的价值在于把可复用的样式集中定义在一处将来想调整设计的某一方面只需修改这一个变量。变量只能用于 CSS 声明declaration的值部分不能用在选择器内部。变量还可以引用其他变量——例如先定义$success: lime;再把$border更新为$border: wide $success;最终会解析为wide lime。提示内置 widget 与主题也大量使用$primary、$panel、$text、$background、$success、$error这类设计令牌变量例如上文dom4.tcss中直接使用了$panel、$text、$background。在定义自己的调色板时优先复用这些语义化变量能让界面风格统一且易于换肤。8. initial 值一键回到默认所有 CSS 规则都支持特殊值initial它把某个值重置回默认状态。例如下面规则将按钮背景设为绿色Button { background: green; }如果希望特定按钮或某组按钮使用默认颜色把值设为initial即可。例如对带dialog类的 widget 内部的所有按钮恢复默认背景.dialog Button { background: initial; }注意initial会把值重置为默认 CSSdefault css见 docs/guide/widgets.md中定义的值。如果在默认 CSS 里使用initial该规则会被当作完全未设置样式来处理。9. 嵌套 CSS让样式表结构更清晰提示嵌套 CSS 自 Textual0.47.0版本起支持。CSS 规则集可以嵌套即规则集内部还可以包含其他规则集。内层规则集会继承外层规则集的选择器。9.1 嵌套前后对比先看不嵌套的写法docs/examples/guide/css/nesting01.tcss演示界面是一个容器里有两个分别写着 Yes 与 No 的框完整运行代码见 docs/examples/guide/css/nesting01.py/* Style the container */ #questions { border: heavy $primary; align: center middle; } /* Style all buttons */ #questions .button { width: 1fr; padding: 1 2; margin: 1 2; text-align: center; border: heavy $panel; } /* Style the Yes button */ #questions .button.affirmative { border: heavy $success; } /* Style the No button */ #questions .button.negative { border: heavy $error; }这些规则的共同点是都以#questions开头。当看到选择器存在公共前缀时就是使用嵌套的好时机。下面这段docs/examples/guide/css/nesting02.tcss对应代码 docs/examples/guide/css/nesting02.py与上面的结果完全一致但加入了嵌套/* Style the container */ #questions { border: heavy $primary; align: center middle; /* Style all buttons */ .button { width: 1fr; padding: 1 2; margin: 1 2; text-align: center; border: heavy $panel; /* Style the Yes button */ .affirmative { border: heavy $success; } /* Style the No button */ .negative { border: heavy $error; } } }第一版中#questions .button匹配位于 ID 为questions的容器内、带button类的任意 widget。第二版里按钮规则的选择器只是.button但它位于#questions规则集内部因此继承外层选择器等价于#questions .button。缩进规则集并非强制要求但能让规则集的层级关系一目了然。9.2 嵌套选择器Nesting selector上例中剩余的 Yes / No 两条规则嵌套在按钮规则内部因此它们继承按钮规则集以及外层#questions规则集的选择器。注意.affirmative这个语法与号称为嵌套选择器它告诉 Textual 该选择器应与外层规则集的选择器拼接。因此.affirmative实际等价于#questions .button.affirmative——匹配同时带有button与affirmative两个类的 widget。若不加写成.affirmative则会变成#questions .button .affirmative注意多了空格只能匹配位于带button类的容器内部、带affirmative类的 widget含义完全不同。9.3 为什么使用嵌套嵌套并非强制要求但把相关规则聚集在一起能避免重复嵌套写法里#questions只出现一次而非嵌套写法要写四次让规则更内聚将来添加其他屏幕或 widget 的选择器时相关的规则更容易被找到提高特异性嵌套会使规则更具体——当发现规则误伤了本不打算影响的 widget 时借助嵌套可以提高优先级精确收窄作用范围。10. 小结Textual CSS 是一套面向终端 widget 的层叠样式系统核心知识链为样式表 → DOM → 选择器类型 / ID / 类名 / 伪类 / 通用→ 组合器后代 / 子→ 优先级 → 变量与initial→ 嵌套。实践中建议通过CSS_PATH或css_path参数、CSS内联变量组织样式用textual run my_app.py --dev获得实时热更新优先用类名配合add_class/remove_class/toggle_class管理运行时的显示状态用$变量统一设计令牌必要时用嵌套归组规则并谨慎使用!important借助:hover、:focus、:disabled等伪类响应交互状态实现更具活力的界面。掌握了以上规则你就可以在 docs/examples/guide 的示例基础上为自己的 Textual 应用写出可维护、可复用的终端界面样式。【免费下载链接】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),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。