Textual Focus 事件详解:从 `Focus`/`Blur` 到 `AppFocus`/`DescendantFocus` 的焦点系统全解析
Textual Focus 事件详解从Focus/Blur到AppFocus/DescendantFocus的焦点系统全解析【免费下载链接】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 焦点focus事件体系展开从Focus、Blur两个基础事件入手依次剖析AppFocus/AppBlur应用级焦点、DescendantFocus/DescendantBlur后代焦点冒泡以及Focus.from_app_focus标记的设计意图并结合源码src/textual/events.py、src/textual/widget.py、src/textual/screen.py、src/textual/app.py与测试用例讲清谁在什么时候收到什么焦点事件、事件如何流转、应如何响应。读完你能够正确监听并区分六类焦点事件理解:focusCSS 伪类的底层触发机制并能复现 App 级焦点切换的完整链路。事件一览六类焦点事件在焦点系统中的分工Textual 中与焦点相关的消息定义全部集中在 src/textual/events.py第 799–884 行。焦点事件共六类按作用范围可分为三层部件级Focus/Blur、应用级AppFocus/AppBlur、后代级DescendantFocus/DescendantBlur事件类触发时机是否冒泡是否 Verbose备注Focus部件获得焦点否否携带from_app_focus参数Blur部件失去焦点否否—AppFocus应用重新获得焦点否否需要终端支持FocusIn或经由 textual-webAppBlur应用失去焦点否否需要终端支持FocusOut或经由 textual-webDescendantFocus子部件获得焦点是是携带widget即control字段DescendantBlur子部件失去焦点是是携带widget即control字段对应的事件文档见 docs/events/focus.md、docs/events/blur.md、docs/events/app_focus.md、docs/events/app_blur.md、docs/events/descendant_focus.md、docs/events/descendant_blur.md事件系统总体指南可参考 docs/guide/events.md。冒泡Bubbles与 Verbose 的含义在 Textual 中事件默认会沿着 DOM 树向上冒泡到祖先节点bubbleFalse表示该事件不冒泡只有目标部件自身能收到Verbose 标记只影响日志输出时的详细程度详见 src/textual/message.py 中Message基类的verbose属性。部件级焦点Focus与Blur类签名与from_app_focus参数Focus事件的定义如下src/textual/events.pyclass Focus(Event, bubbleFalse): Sent when a widget is focussed. - [ ] Bubbles - [ ] Verbose Args: from_app_focus: True if this focus event has been sent because the app itself has regained focus (via an AppFocus event). False if the focus came from within the Textual app (e.g. via the user pressing tab or a programmatic setting of the focused widget). def __init__(self, from_app_focus: bool False) - None: self.from_app_focus from_app_focus super().__init__() def __rich_repr__(self) - rich.repr.Result: yield from super().__rich_repr__() yield from_app_focus, self.from_app_focusBlur事件则没有任何字段纯粹表示该部件失去了焦点src/textual/events.pyclass Blur(Event, bubbleFalse): Sent when a widget is blurred (un-focussed). - [ ] Bubbles - [ ] Verbose from_app_focus参数是理解Focus事件的关键它区分了焦点的两种来源——True焦点是因为应用自身重新获得焦点例如用户从其他窗口切回终端应用收到AppFocusTextual 据此恢复此前失去焦点时的部件而产生的False默认值焦点来自应用内部例如用户按下 Tab 键遍历、鼠标点击可聚焦部件或代码中程序化地调用widget.focus()设置焦点。监听方式在 Widget 子类中最直接的方式是重写on_focus/on_blur方法from textual.app import App, ComposeResult from textual.widgets import Input class FocusWatcher(Input): def on_focus(self, event: events.Focus) - None: self.styles.background yellow self.log.info(ffocused, from_app_focus{event.from_app_focus}) def on_blur(self, event: events.Blur) - None: self.styles.background 也可以使用on()装饰器textual.on将处理器绑定到指定部件类型例如在容器中统一监听from textual import on from textual.events import Focus on(Focus) def handle_focus(self, event: Focus) - None: self.log.info(fwidget {event.widget} was focused)注意由于Focus/Blur设置了bubbleFalse它们不会冒泡到祖先部件——祖先如果想感知后代焦点变化需要监听DescendantFocus/DescendantBlur见下文。焦点事件的发送链路从源码看Focus/Blur事件的发送由Screen.set_focus统一负责src/textual/screen.py核心逻辑如下若widget is None对当前已聚焦部件post_message(events.Blur())并将self.focused置空若widget.focusable且与当前聚焦部件不同先对旧部件发送Blur再设置self.focused widget然后向新部件post_message(events.Focus(from_app_focusfrom_app_focus))若scroll_visibleTrue默认通过call_later(scroll_to_center, widget)在刷新后把部件滚动到可见区域最后调用_update_focus_styles刷新:focus相关样式并refresh_bindings更新绑定键位提示。当部件自身收到Focus事件时Textual 内置的Widget._on_focussrc/textual/widget.py会做两件事将self.has_focus置为True并触发self.refresh()重绘同时向父部件发送DescendantFocusdef _on_focus(self, event: events.Focus) - None: self.has_focus True self.refresh() if self.parent is not None: self.parent.post_message(events.DescendantFocus(self))对应的_on_blursrc/textual/widget.py则将has_focus置为False并发送DescendantBlur。程序化设置与解除焦点widget.focus(scroll_visibleTrue)请求将焦点移到此部件src/textual/widget.py。注意它通过self.app.call_later(set_focus, self)异步调度保证在消息循环空闲时才真正执行返回self便于链式调用。widget.blur()解除该部件的焦点焦点会移到焦点链中下一个可用部件src/textual/widget.py。screen.set_focus(widget, scroll_visibleTrue, from_app_focusFalse)底层实现from_app_focus参数在此直接透传给Focus事件。CSS 查询器也提供query(*).focus()与query(*).blur()批量聚焦/失焦方法src/textual/css/query.py。只有focusable为True的部件才能被聚焦set_focus中有显式判断同时 CSS 的:focus伪类样式、[src/textual/widget.py](https://link.gitcode.com/i/d72419d395e58b339a84d89863bdc984)中_has_focus_within等属性共同参与焦点样式的刷新。应用级焦点AppFocus与AppBlur定义与可用前提AppFocus/AppBlur描述的是整个应用窗口获得或失去操作系统焦点的事件src/textual/events.pyclass AppFocus(Event, bubbleFalse): Sent when the app has focus. - [ ] Bubbles - [ ] Verbose Note: Only available when running within a terminal that supports FocusIn, or when running via textual-web. class AppBlur(Event, bubbleFalse): Sent when the app loses focus. - [ ] Bubbles - [ ] Verbose Note: Only available when running within a terminal that supports FocusOut, or when running via textual-web. 可用前提源码 docstring 明确标注仅在支持FocusIn/FocusOut终端事件的终端中运行或通过 textual-web 在浏览器中运行时才会产生这两类事件。XTerm 解析器在收到终端上报的焦点序列时会生成对应事件src/textual/_xterm_parser.pyweb 驱动则在前台/后台切换时发送src/textual/drivers/web_driver.py 与第 243–245 行。App 内的默认行为焦点自动恢复App 级焦点事件不只是通知Textual 用它们实现了应用失焦后恢复焦点的能力src/textual/app.pyasync def _on_app_focus(self, event: events.AppFocus) - None: App has focus. # Required by textual-web to manage focus in a web page. self.app_focus True self.screen.refresh_bindings() async def _on_app_blur(self, event: events.AppBlur) - None: App has lost focus. # Required by textual-web to manage focus in a web page. self.app_focus False self.screen.refresh_bindings()应用内部维护了app_focus这个响应式属性src/textual/app.py并在_watch_app_focus中记录/恢复失焦前的聚焦部件src/textual/app.py收到AppBlur时将self._last_focused_on_app_blur记为当前screen.focused收到AppFocus时若此前记录过聚焦部件且它仍属于当前屏幕则调用screen.set_focus(..., from_app_focusTrue)恢复焦点——这正是Focus.from_app_focusTrue的唯一真实来源。测试用例 tests/test_app_focus_blur.pytest_app_focus_restores_focus验证了该恢复链路tests/input/test_select_on_focus.pytest_focus_from_app_focus_does_not_select则验证了由AppFocus触发的聚焦不应触发选中文本等副作用这一设计约束——这也是应用代码里判断from_app_focus的典型用途区分用户主动交互与应用失而复得。在应用代码中响应from textual.app import App from textual import events class PausableApp(App[None]): def on_app_focus(self, event: events.AppFocus) - None: self.sub_title running (focused) def on_app_blur(self, event: events.AppBlur) - None: self.sub_title paused (blurred)一个更细粒度的做法是结合Focus.from_app_focus若应用恢复焦点后不希望某个敏感部件自动执行聚焦副作用可在其on_focus中检查event.from_app_focus并提前返回。注意AppFocus/AppBlur同样bubbleFalse只在 App 上监听即可。后代焦点冒泡DescendantFocus与DescendantBlur定义与关键字段DescendantFocus/DescendantBlur是事件被发送到父部件时携带子部件引用的版本src/textual/events.py且与前三者不同它们bubbleTrue且verboseTrue会沿 DOM 树一路冒泡到 App。dataclass class DescendantFocus(Event, bubbleTrue, verboseTrue): Sent when a child widget is focussed. - [X] Bubbles - [X] Verbose widget: Widget The widget that was focused. property def control(self) - Widget: The widget that was focused (alias of widget). return self.widget dataclass class DescendantBlur(Event, bubbleTrue, verboseTrue): Sent when a child widget is blurred. - [X] Bubbles - [X] Verbose widget: Widget The widget that was blurred. property def control(self) - Widget: The widget that was blurred (alias of widget). return self.widget两个事件均为dataclass携带widget字段被聚焦/失焦的部件并提供control属性作为其别名——control是 Textual 事件体系的通用命名约定与Click、Key等交互事件的control一致便于统一处理交互来源。事件发送点在Widget._on_focus/_on_blur中src/textual/widget.py即部件聚焦 → 先收到Focus自身→ 父部件收到DescendantFocus可冒泡。典型用法容器统一跟踪焦点由于Focus不冒泡任何希望感知子树焦点变化的容器部件都应监听DescendantFocus/DescendantBlurfrom textual.containers import Vertical from textual import events class FocusTracker(Vertical): Vertical 容器跟踪哪个子部件当前拥有焦点。 def on_descendant_focus(self, event: events.DescendantFocus) - None: self.border_title ffocused: {event.widget.__class__.__name__} def on_descendant_blur(self, event: events.DescendantBlur) - None: self.border_title focused: (none)借助冒泡特性甚至在 App 层也可以统一收集整棵组件树的焦点变化例如实现焦点日志或状态同步因为事件会一路冒泡到根节点。内置组件中也大量使用该机制例如Collapsible、DataTable等组件通过后代焦点事件维护展开/选中状态。焦点样式与:focus的底层联动焦点事件与样式系统紧密联动Screen.set_focus在焦点变更后调用_update_focus_styles(focused, blurred)其实现src/textual/screen.py会对发生变化的部件及祖先存在_has_focus_within的节点整棵子树刷新样式表从而驱动以下 CSS 伪类Screen:focus { /* 屏幕级焦点样式 */ } Button:focus { text-style: bold; background: $accent; } Button:focus-within { /* 焦点在 Button 内部时 */ }Textual 的 CSS 文档docs/guide/CSS.md、docs/styles/index.md中:focus与:focus-within均基于这套事件驱动的样式刷新机制实现部件收到Focus后置has_focusTrue并refresh()随后样式表更新触发重绘快照测试 tests/snapshot_tests/test_snapshots.py 中的test_app_focus_style、test_focus_component_class等用例即为焦点样式的回归验证。完整事件时序一次 Tab 切换焦点发生了什么综合上述源码用户按下 Tab 使焦点从 A 部件切换到 B 部件时事件序列大致如下用户按下 TabApp._on_key→ 按键分发src/textual/_dispatch_key.py找到焦点链中的下一个可聚焦部件 BScreen.set_focus(B, scroll_visibleTrue, from_app_focusFalse)被调用A 收到BlurbubbleFalseWidget._on_blur置has_focusFalse、刷新自身、向父部件发送DescendantBlur(A)可冒泡Screen.focused更新为 BB 收到Focus(from_app_focusFalse)Widget._on_focus置has_focusTrue、刷新自身、向父部件发送DescendantFocus(B)可冒泡_update_focus_styles更新:focus/:focus-within样式并重绘refresh_bindings刷新底部绑定提示。若整个终端窗口失焦再切回则额外发生AppBlur→记录_last_focused_on_app_blur→AppFocus→Screen.set_focus(之前部件, from_app_focusTrue)→ 部件收到Focus(from_app_focusTrue)其from_app_focus属性即为True。实践要点小结自身感知焦点重写on_focus/on_blur或使用on(Focus)/on(Blur)祖先感知子树焦点监听on_descendant_focus/on_descendant_blur通过event.widgetevent.control判断具体部件应用整体失焦/聚焦在 App 上监听on_app_focus/on_app_blur注意可用性取决于终端是否上报FocusIn/FocusOut或是否运行于 textual-web区分焦点来源Focus.from_app_focus为True表示应用失而复得时自动恢复的焦点可用于抑制不希望的聚焦副作用参考 tests/input/test_select_on_focus.py 的设计程序化控制widget.focus()、widget.blur()、screen.set_focus(...)与query().focus()/blur()均可按需触发焦点流转。【免费下载链接】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),仅供参考