Textual ListView 指南:用 Python 构建可键盘导航的垂直列表界面
【免费下载链接】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
导读
ListView是 Textual 内置的垂直列表容器组件,用于展示一组ListItem子项,支持鼠标高亮与键盘导航,是构建菜单、设置页、选择器、日志浏览等终端交互界面的基础构件。本文以官方文档 docs/widgets/list_view.md 为骨架,结合 ListView 源码 与 ListView 测试用例,完整讲解其特性、响应式属性、消息、按键绑定、动态增删项 API 与源码级工作原理,读完即可在自己的 Textual 应用中落地使用。
ListView 是什么
ListView是一个可聚焦(Focusable)的容器组件,以垂直方式展示若干个ListItem,用户可以通过鼠标或键盘在其中移动高亮并选中条目。它自 0.6.0 版本起加入 Textual。
从源码可以看出,ListView 继承自VerticalScroll(垂直滚动容器),并以can_focus=True, can_focus_children=False声明:列表本身可聚焦,但其子项(ListItem)不可单独聚焦,高亮操作统一由列表容器接管——这正是"整表键盘导航"体验的来源。
- 可聚焦(Focusable):Tab 键可以将焦点移到列表上,从而启用键盘操作;
- 容器(Container):作为容器组件,它可以承载多个
ListItem子组件。
它的默认 CSS(见 源码 DEFAULT_CSS)定义了完整的视觉状态:
- 列表背景使用
$surface主题色; - 直接子项
ListItem高度自动、宽度占满(width: 1fr)、溢出隐藏; - 鼠标悬停(
.-hovered)与高亮(.-highlight)分别使用主题中的块级光标配色; - 列表聚焦时叠加
background-tint并切换高亮项为"聚焦态"光标配色($block-cursor-*系列)。
这些样式意味着你无需写任何 CSS 就能获得可用的默认外观,同时也可以通过 CSS 覆盖。
快速上手:最小可运行示例
官方文档提供了开箱即用的示例,代码见 docs/examples/widgets/list_view.py,样式表见 docs/examples/widgets/list_view.tcss。
from textual.app import App, ComposeResult from textual.widgets import Footer, Label, ListItem, ListView class ListViewExample(App): CSS_PATH = "list_view.tcss" def compose(self) -> ComposeResult: yield ListView( ListItem(Label("One")), ListItem(Label("Two")), ListItem(Label("Three")), ) yield Footer() if __name__ == "__main__": app = ListViewExample() app.run()配套的样式表list_view.tcss:
Screen { align: center middle; } ListView { width: 30; height: auto; margin: 2 2; } Label { padding: 1 2; }运行后你会看到一个居中的列表,包含 "One / Two / Three" 三个条目,底部是Footer(会动态显示当前可用按键)。用方向键移动高亮,按 Enter 选中,点击条目也可直接选中。
要点拆解:
- 每个
ListItem内部通常包裹一个Label作为文本内容,但ListItem是普通Widget,内部可以是任意内容(图片、其他组件组合均可); ListView作为容器,子项通过compose或后续的append/extend等 API 添加;- 样式表中
height: auto让列表高度随子项数量自然伸缩。
响应式属性:index
ListView只有一个核心响应式(reactive)属性:
| Name | Type | Default | Description |
|---|---|---|---|
index | int | 0 | 当前高亮条目的索引。 |
在源码中它被声明为:
index = reactive[Optional[int]](None, init=False)几点源码级细节值得注意:
- 默认值语义:虽然文档表中标默认
0,但源码实现中index的初始值是None,真正的高亮位置由构造函数参数initial_index(默认0)在挂载(_on_mount)时确定,见 源码 _on_mount。当initial_index越界时会被重置为0;若initial_index指定的项被禁用(disabled),则从该位置起向后循环查找第一个可用项。 - 校验(validate_index):对
index的赋值会经过 validate_index 夹取到合法范围:小于 0 归 0,大于等于子项数量归到最后一个索引,列表为空时置为None。 - 监听(watch_index):watch_index 在索引变化时负责三件事:将新高亮项滚动到可视区域(
scroll_to_widget)、清除旧项的-highlight、为新项设置-highlight并广播Highlighted消息。若新索引无效或指向被禁用项,则广播的item为None。
因此运行时可以这样读取/修改高亮:
# 读取当前高亮索引 current = my_list_view.index # 直接跳到第 3 项(会经过校验与监听,自动触发滚动和消息) my_list_view.index = 2消息:Highlighted 与 Selected
ListView通过两类消息与外部通信,处理方式遵循 Textual 惯例——在父组件或App中定义on_list_view_highlighted/on_list_view_selected方法即可:
ListView.Highlighted
当高亮项发生变化时发出(例如按下上/下键移动光标),见 源码 Highlighted。
- 属性:
list_view(所属列表)、item(新高亮项,可为None); control属性是list_view的别名,供@on装饰器使用;- 额外的消息匹配属性
item已通过ALLOW_SELECTOR_MATCH = {"item"}注册,可以配合@on(ListView.Highlighted, item=...)做精细匹配。
典型用法:
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None: if event.item is not None: label = event.item.query_one(Label) self.status_bar.update(f"当前高亮:{label.renderable}")ListView.Selected
当用户选中条目时发出(例如按下 Enter 或鼠标点击),见 源码 Selected。
- 属性:
list_view、item(被选中的项)、index(选中项的索引); - 同样支持
@on(ListView.Selected)装饰器匹配,item也可作为匹配键。
典型用法:
def on_list_view_selected(self, event: ListView.Selected) -> None: self.log(f"选中了第 {event.index} 项: {event.item}")注意Highlighted在每次高亮移动时都会触发(频率较高),Selected只在确认选中时触发(频率低),两者分工明确,适合分别驱动"预览/详情"与"确认操作"两类 UI 逻辑。
按键绑定:键盘导航
ListView定义了三个按键绑定,见 源码 BINDINGS:
| Key(s) | Description | | :- | :- | | enter | 选中当前条目。 | | up | 上移光标。 | | down | 下移光标。 |
这三个绑定都声明为show=False,因此不会出现在Footer的默认按键提示中,但ListView示例里Footer仍能显示 enter/up/down 相关提示,因为 Footer 默认展示全局绑定;如需让用户看到这些操作,可以自行在应用层添加带描述的绑定。
绑定背后的动作实现(源码 actions):
action_cursor_down:从当前索引向后查找下一个未禁用的项并高亮;若当前无高亮则从第 0 项开始;action_cursor_up:对称地向前查找上一个未禁用项;无高亮时从末尾项开始;action_select_cursor:取出当前高亮项并广播Selected消息,若没有高亮项则直接返回。
这里的关键细节是导航会跳过被禁用(disabled=True)的ListItem,见 tests/listview/test_listview_navigation.py 中的回归测试:在 0、2、3、6、8 被禁用的列表中连续按 5 次 down 再 5 次 up,高亮依次为1 → 4 → 5 → 7 → 5 → 4 → 1,验证了跳过逻辑的确定性。
鼠标交互
虽然键盘是主要输入方式,ListView同样支持完整的鼠标操作。其机制是:ListItem捕获点击后向上抛出内部消息_ChildClicked,ListView通过_on_list_item__child_clicked接收并处理(见 源码):
- 停止消息继续冒泡(
event.stop()); - 将焦点转移到
ListView本身(self.focus()); - 把
index设置为被点击项的索引(触发高亮更新与Highlighted); - 广播
Selected消息。
相应地,ListItem自己维护两个视觉状态(见 ListItem 源码):
highlighted响应式属性:由ListView写入,通过watch_highlighted切换-highlightCSS 类;- 鼠标悬停:通过
@on(events.Enter)/@on(events.Leave)设置-hovered类。
动态增删项:append / extend / insert / pop / clear / remove_items
ListView内置了完整的"运行时增删"API(全部返回可等待对象,配合await或App.run_async使用),这在构建动态数据驱动的列表(如文件列表、任务队列、日志流)时非常关键:
| 方法 | 作用 | 返回类型 |
|---|---|---|
append(item) | 在末尾追加一个ListItem | AwaitMount |
extend(items) | 批量追加多个ListItem | AwaitMount |
insert(index, items) | 在指定索引处插入一个或多个ListItem | AwaitMount |
pop(index=None) | 移除最后一个(或指定索引处的)ListItem | AwaitComplete |
remove_items(indices) | 按索引批量移除多个ListItem | AwaitComplete |
clear() | 清空所有ListItem | AwaitRemove |
实现要点(见 源码 _list_view.py 动态 API 区段):
append/extend/insert都委托给容器的mount机制,返回AwaitMount,调用方可以用await list_view.append(...)等待 DOM 更新完成;clear使用self.query("ListView > ListItem").remove()移除全部直接子项,并把index置为None;pop在列表为空时会抛出IndexError("pop from empty list"),该行为由 tests/listview/test_listview_remove_items.py 中的test_listview_pop_empty_raises_index_error测试锁定;pop与remove_items在删除项后会自动校正高亮索引:被删项位于高亮之前则索引前移,删除的正是高亮项则触发索引重校验并手动调用watch_index确保-highlight与消息状态同步;remove_items支持负索引,归一化后批量删除,并一次性地计算"删除项位于高亮之前"的数量来平移索引(该行为同样有test_listview_remove_items回归测试覆盖)。
动态使用示例:
async def on_button_pressed(self) -> None: # 动态追加 await self.list_view.append(ListItem(Label("新条目"))) # 批量插入到开头 await self.list_view.insert( 0, [ListItem(Label("A")), ListItem(Label("B"))] ) # 删除第 2 项(0 起始) await self.list_view.pop(2)关于 initial_index 的边界行为
构造函数的initial_index参数决定了列表首次挂载时的高亮位置(默认0,传None表示不预高亮任何项)。源码_on_mount的边界处理(src/textual/widgets/_list_view.py#L159-L170):
- 索引越界(
>= 子项数量)时回退为0; - 指向被禁用项时,从该位置向后循环查找第一个可用的未禁用项;
- 因此
initial_index指向禁用项时最终落点可能不是传入值,而是"向后找到的第一个可用项",详见参数化测试 tests/listview/test_listview_initial_index.py:例如 9 个条目中 0、2、3、6、8 禁用,initial_index=2时最终高亮4,initial_index=8时最终高亮1(循环回绕)。
组件类(Component Classes)
ListView没有定义任何组件类,这是官方文档明确的结论。所有视觉定制都通过常规 CSS 选择器完成,例如覆盖默认高亮配色:
ListView > ListItem.-highlight { background: $accent; color: $text; }若需要在自定义场景中调整悬停/聚焦样式,直接对.-hovered、.-highlight以及ListView:focus > ListItem.-highlight写规则即可。
与相近组件的选型对比
如果你正在搭建选择类界面,Textual 还提供若干与ListView定位相近的组件,可参考官方 widgets 文档目录 按需选用:
OptionList:面向"纯文本选项 + 可选元数据"的轻量列表,API 更简单,适合配置菜单、命令列表等无需复杂子内容结构的场景;SelectionList:带复选框的多选列表,适合批量选择;Select:单行下拉选择器,适合空间受限的表单场景;Tree/DirectoryTree:层级树形结构,适合目录浏览等有父子关系的场景;ListView的优势在于:它是通用容器,ListItem内部可以承载任意组件组合(图片、进度条、按钮、嵌套布局等),并自带完整的键盘导航、滚动跟随与动态增删 API,是自由度最高的通用列表方案。
小结
ListView是一个"开箱即用"的垂直列表容器:官方文档(docs/widgets/list_view.md)提供了最小示例与 API 总览,而源码(src/textual/widgets/_list_view.py)与测试(tests/listview/)则进一步揭示了索引校验、禁用项跳过、滚动跟随、消息广播与动态增删的完整实现。掌握以下要点即可在生产代码中熟练使用:
- 用
ListItem(Label(...))组合条目,交给ListView统一管理焦点与高亮; - 通过
index响应式属性读写高亮,通过Highlighted/Selected消息响应交互; - 上下方向键导航会自动跳过
disabled条目,Enter 或点击触发选中; - 用
append/extend/insert/pop/remove_items/clear动态维护列表内容,注意它们返回可等待对象; - 视觉定制通过 CSS 对
.-highlight、.-hovered与ListView:focus规则完成,无需组件类。
现在就可以参照 docs/examples/widgets/list_view.py 在终端里跑起你的第一个可键盘导航的 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),仅供参考