Textual OptionList 组件完全指南:可导航选项列表的构建与交互
【免费下载链接】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
本文面向使用 Python 终端 UI 框架 Textual 的开发者,系统讲解
OptionList组件的完整用法:从最基础的字符串选项列表、带 ID 与禁用状态的Option实例,到以任意 Rich renderable(如表格)作为选项条目的高级用法;并深入解析其响应式属性、事件消息、按键绑定、组件类与常用 API,同时结合仓库源码与测试用例说明底层实现原理。读完本文,你将能独立构建一个可键盘导航、可鼠标点选、可动态增删改的垂直选项列表。
OptionList是 Textual 在0.17.0版本引入的组件,用于展示一个垂直排列的、可导航的选项列表。它属于可聚焦(Focusable)组件(文档标记为[x] Focusable),但不是容器(Container)——它继承自ScrollView(见 源码),专门用于单选场景,例如菜单、命令选择、列表选择等。
与同为列表类组件的ListView相比,OptionList的显著特点是:每个选项的提示内容(prompt)可以是任意 Rich renderable(Rich 渲染对象),因此选项的高度可以任意——这为构建富文本菜单提供了极大的灵活性。
三种构建选项的方式
1. 简单字符串选项
构造OptionList时,最简单的做法是直接传入一串字符串,每个字符串会自动成为一个选项:
from textual.app import App, ComposeResult from textual.widgets import Footer, Header, OptionList class OptionListApp(App[None]): CSS_PATH = "option_list.tcss" def compose(self) -> ComposeResult: yield Header() yield OptionList( "Aerilon", "Aquaria", "Canceron", "Caprica", "Gemenon", "Leonis", "Libran", "Picon", "Sagittaron", "Scorpia", "Tauron", "Virgon", ) yield Footer() if __name__ == "__main__": OptionListApp().run()完整示例见 docs/examples/widgets/option_list_strings.py。对应的样式文件 option_list.tcss 将选项列表居中放置:
Screen { align: center middle; } OptionList { width: 70%; height: 80%; }2. 使用Option实例与分隔线
当需要更精细的控制——例如为选项设置 ID、设置初始禁用状态——应使用Option类。此外,在选项序列中插入None即可在前后选项之间绘制一条分隔线:
from textual.app import App, ComposeResult from textual.widgets import Footer, Header, OptionList from textual.widgets.option_list import Option class OptionListApp(App[None]): CSS_PATH = "option_list.tcss" def compose(self) -> ComposeResult: yield Header() yield OptionList( Option("Aerilon", id="aer"), Option("Aquaria", id="aqu"), None, Option("Canceron", id="can"), Option("Caprica", id="cap", disabled=True), None, Option("Gemenon", id="gem"), None, Option("Leonis", id="leo"), Option("Libran", id="lib"), None, Option("Picon", id="pic"), None, Option("Sagittaron", id="sag"), Option("Scorpia", id="sco"), None, Option("Tauron", id="tau"), None, Option("Virgon", id="vir"), ) yield Footer() if __name__ == "__main__": OptionListApp().run()完整示例见 docs/examples/widgets/option_list_options.py。
Option的构造签名(见 源码)为:
Option(prompt: VisualType, id: str | None = None, disabled: bool = False)| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
prompt | VisualType | 必填 | 选项显示的提示内容(文本或 Rich renderable) |
id | str \| None | None | 选项的 ID,用于之后通过 ID 查询、修改、删除选项 |
disabled | bool | False | 是否禁用该选项。被禁用的选项会以灰色显示,且不可被选中、不可被导航高亮 |
注意:ID 在同一列表中必须唯一。若尝试添加重复 ID 的选项,会抛出DuplicateID异常(见 源码 与测试 tests/option_list/test_option_list_create.py)。ID 在OptionList挂载后即可通过get_option查询(回归测试见test_options_are_available_soon,对应 issue #3903)。
3. 以 Rich renderable 作为选项
由于Option的prompt可以是任意 Rich renderable,选项的高度可以任意。下面的例子用 Rich 的Table作为每个选项的内容,每个选项都是一个独立的表格:
from __future__ import annotations from rich.table import Table from textual.app import App, ComposeResult from textual.widgets import Footer, Header, OptionList COLONIES: tuple[tuple[str, str, str, str], ...] = ( ("Aerilon", "Demeter", "1.2 Billion", "Gaoth"), ("Aquaria", "Hermes", "75,000", "None"), ("Canceron", "Hephaestus", "6.7 Billion", "Hades"), ("Caprica", "Apollo", "4.9 Billion", "Caprica City"), ("Gemenon", "Hera", "2.8 Billion", "Oranu"), ("Leonis", "Artemis", "2.6 Billion", "Luminere"), ("Libran", "Athena", "2.1 Billion", "None"), ("Picon", "Poseidon", "1.4 Billion", "Queenstown"), ("Sagittaron", "Zeus", "1.7 Billion", "Tawa"), ("Scorpia", "Dionysus", "450 Million", "Celeste"), ("Tauron", "Ares", "2.5 Billion", "Hypatia"), ("Virgon", "Hestia", "4.3 Billion", "Boskirk"), ) class OptionListApp(App[None]): CSS_PATH = "option_list.tcss" @staticmethod def colony(name: str, god: str, population: str, capital: str) -> Table: table = Table(title=f"Data for {name}", expand=True) table.add_column("Patron God") table.add_column("Population") table.add_column("Capital City") table.add_row(god, population, capital) return table def compose(self) -> ComposeResult: yield Header() yield OptionList(*[self.colony(*row) for row in COLONIES]) yield Footer() if __name__ == "__main__": OptionListApp().run()完整示例见 docs/examples/widgets/option_list_tables.py。从源码结构看(get_content_height与_update_lines),每个选项的高度由渲染出的视觉内容高度决定,多行选项会被当作多条终端行参与滚动与分页计算,这正是"选项高度任意"的实现基础。
响应式属性(Reactive Attributes)
OptionList对外暴露的核心响应式属性如下(见 源码):
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
highlighted | int \| None | None | 当前高亮选项的索引;None表示没有任何选项被高亮 |
compact | bool | False | 是否启用紧凑显示模式(对应 CSS 类-textual-compact) |
highlighted的值在写入时会经过校验(validate_highlighted):小于 0 会收敛为 0,超过列表末尾会收敛为len(options) - 1。当高亮变化且目标选项未被禁用时,组件会自动滚动到该选项并发布OptionHighlighted消息(watch_highlighted)。
通过highlighted_option属性可以直接获取当前高亮对应的Option对象(源码):
option: Option | None = option_list.highlighted_option消息(Messages)
OptionList会发布两类消息:
OptionList.OptionHighlighted:当某个选项被高亮时发布。OptionList.OptionSelected:当某个选项被选中时发布。
两者都继承自共同的基类OptionList.OptionMessage,因此都具备以下属性(见 源码):
| 属性 | 类型 | 说明 |
|---|---|---|
option_list | OptionList | 发送该消息的 OptionList 实例 |
option | Option | 消息所涉及的选项对象 |
option_id | str \| None | 该选项的 ID(option.id的别名) |
option_index | int | 该选项在列表中的索引 |
control | OptionList | option_list的别名,供on装饰器使用 |
处理方式与 Textual 其他消息一致——在 App 或父级组件中定义on_option_list_option_highlighted/on_option_list_option_selected方法即可。测试 tests/option_list/test_option_messages.py 演示了这两个处理器的签名写法,例如:
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None: self.selected_message = f"Selected {event.option.prompt}"绑定键位(Bindings)
OptionList定义了以下默认按键绑定(见 源码):
| 按键 | 动作 | 说明 |
|---|---|---|
down | cursor_down | 高亮向下移动 |
up | cursor_up | 高亮向上移动 |
home | first | 高亮移动到第一个选项 |
end | last | 高亮移动到最后一个选项 |
pagedown | page_down | 高亮向下翻一页 |
pageup | page_up | 高亮向上翻一页 |
enter | select | 选中当前高亮选项 |
所有绑定在 Footer 中默认隐藏(show=False)。这些动作对应的实现方法(action_cursor_up、action_cursor_down、action_first、action_last、action_page_up、action_page_down、action_select)位于 源码:
- 上下移动通过
_widget_navigation.find_next_enabled实现,会跳过被禁用的选项,因此高亮始终停留在可交互的选项上; - 分页移动通过
_move_page按可视区域高度估算行距,并使用find_next_enabled_no_wrap在目标附近收敛到可用的选项; action_select在存在高亮且高亮选项未禁用时发布OptionSelected消息。
鼠标交互同样受支持:点击未禁用选项会将其高亮并立即选中(_on_click),鼠标悬停会触发option-list--option-hover样式(_on_mouse_move)。
组件类(Component Classes)
OptionList提供了以下组件类,可用于在 CSS 中精细化定制各状态的外观(见 源码):
| 类名 | 说明 |
|---|---|
option-list--option | 默认状态(未禁用、未高亮、鼠标未悬停)的选项 |
option-list--option-disabled | 被禁用的选项 |
option-list--option-highlighted | 被高亮的选项 |
option-list--option-hover | 鼠标悬停的选项 |
option-list--separator | 分隔线 |
其默认 CSS(DEFAULT_CSS)展示了这些类的典型用法与默认外观:
OptionList { height: auto; max-height: 100%; color: $foreground; overflow-x: hidden; border: tall $border-blurred; padding: 0 1; background: $surface; } OptionList:focus { border: tall $border; background-tint: $foreground 5%; }聚焦时高亮选项会采用$block-cursor-*主题色,未聚焦时采用对应的$block-cursor-blurred-*模糊色。你可以通过覆盖这些组件类来定制自己的配色:
OptionList > .option-list--option-highlighted { background: $success; color: $text; text-style: bold; }常用 API 一览
除构造参数(*content选项内容、name、id、classes、disabled、markup、compact)外,OptionList还提供了丰富的增删改查方法,均支持链式调用并返回self:
| 方法 | 说明 |
|---|---|
add_option(option)/add_options(options) | 向列表末尾添加选项;传None表示添加分隔线 |
set_options(options) | 清空现有选项后整体替换(见 tests/option_list/test_option_list_create.py) |
clear_options() | 清空全部选项并重置高亮与滚动位置 |
get_option(option_id) | 按 ID 获取Option,不存在则抛OptionDoesNotExist |
get_option_index(option_id) | 按 ID 获取选项索引 |
get_option_at_index(index) | 按索引获取Option,越界抛OptionDoesNotExist |
enable_option(option_id)/disable_option(option_id) | 按 ID 启用 / 禁用选项(另有_at_index版本) |
remove_option(option_id)/remove_option_at_index(index) | 删除指定选项 |
replace_option_prompt(option_id, prompt)/replace_option_prompt_at_index(index, prompt) | 替换选项的提示内容 |
scroll_to_highlight(top=False) | 滚动到当前高亮选项,top=True时将其置于组件顶部 |
相关异常(见 源码):
OptionListError:选项列表错误的基类;DuplicateID:添加了重复 ID 的选项时抛出;OptionDoesNotExist:按不存在的 ID 或越界索引查询时抛出。
Option同样支持子类化以携带额外数据,测试 tests/option_list/test_option_list_option_subclass.py 展示了自定义OptionWithExtras并添加 100 个实例的用法。
底层实现要点
从源码结构看,OptionList在渲染层面做了如下优化:
- 渲染缓存:使用
LRUCache(容量 2048)按(option, style, padding)缓存已渲染的行(_get_option_render),选项内容变化或组件尺寸变化(_on_resize)时清空缓存; - 行缓存:通过
_LineCache记录"选项索引 → 终端行"的映射,支持任意高度选项的滚动定位(_update_lines); - 分隔线渲染:
None添加的分隔线通过将前一个选项标记_divider = True实现(add_options),渲染时在选项下方追加一条─组成的横线,行高计算与虚拟尺寸计算都会把这条线纳入考虑。
小结
OptionList是 Textual 中构建单选式菜单与选择界面的高效组件:字符串构造开箱即用,Option实例带来 ID 与禁用状态控制,Rich renderable 支持让选项可以承载表格、富文本等任意高度的内容;配合highlighted响应式属性、OptionHighlighted/OptionSelected消息、完整的键盘导航绑定与细粒度的组件类样式,足以覆盖从简单命令菜单到复杂数据浏览面板的各类场景。
【免费下载链接】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),仅供参考