Textual 应用焦点事件 AppFocus:原理、监听与焦点恢复机制详解
2026/9/19 23:31:52 网站建设 项目流程

Textual 应用焦点事件 AppFocus:原理、监听与焦点恢复机制详解

【免费下载链接】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 的AppFocus事件展开:它表示"应用本身重新获得了焦点",只在支持 XTerm FocusIn 焦点报告的终端或通过 textual-web 运行时才会被发出。文章结合源码解析该事件的产生链路(终端转义序列解析、Web 驱动)、框架内置的app_focus状态与焦点自动恢复机制,并给出监听、CSS 伪类联动和测试验证的完整实践。

什么是 AppFocus 事件

AppFocus是 Textual 事件体系中的一个应用级事件,定义在 events.py:

class AppFocus(Event, bubble=False): """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. """

它的语义是:应用所在的窗口/标签页重新获得了终端(或浏览器)级别的输入焦点,区别于Focus(某个控件获得焦点)。关键特性如下:

属性取值含义
基类textual.events.Event应用级事件,通常由驱动层直接投递给App
bubbleFalse不冒泡,只发给接收消息的节点(这里是App自身)
verbose未开启默认不进入调试日志
可用前提终端支持FocusIn焦点报告,或经 textual-web 运行普通不发送焦点报告的终端中,该事件永远不会触发

与它配对的是AppBlur——应用失去焦点时发出,对应终端的FocusOut报告。官方 API 文档中两者的"See also"也是互相指向的(见 app_focus.md 与 app_blur.md)。

事件是如何产生的:两条来源链路

终端侧:解析 XTerm FocusIn/FocusOut 转义序列

在终端运行时,AppFocus来自 XTerm 的焦点报告转义序列。xterm 序列解析器 定义了三个特殊序列:

FOCUSIN: Final[str] = "\x1b[I" # ... FOCUSOUT: Final[str] = "\x1b[O" SPECIAL_SEQUENCES = {BRACKETED_PASTE_START, BRACKETED_PASTE_END, FOCUSIN, FOCUSOUT}

当解析器在读入的字节流中匹配到ESC [ I(终端窗口获得焦点时由终端发出)或ESC [ O(失去焦点)时,会直接构造对应的事件并交给上层(见 解析循环):

if sequence == FOCUSIN: on_token(events.AppFocus()) elif sequence == FOCUSOUT: on_token(events.AppBlur())

需要注意的前提:并不是所有终端都会主动发送焦点报告,事件文档中的 Note 明确写明了这一限制。因此"是否收到AppFocus"依赖于运行环境——这也是为什么框架把它实现为一个"可降级"的事件:收不到就永远不触发,应用逻辑应当把它当作可选信号而非必需信号。

Web 侧:textual-web 驱动直接投递

当应用通过 textual-web 运行在浏览器中时,焦点管理由页面 JavaScript 侧感知。web 驱动 在页面重新聚焦时会向应用投递事件:

self._app.post_message(events.AppFocus())

源码中_on_app_focus处理函数的注释也印证了这一点("Required by textual-web to manage focus in a web page"),见下文。

框架内置行为:app_focus 状态与 CSS 伪类

App内部为每个应用维护了一个响应式(Reactive)状态app_focus,见 app.py:

app_focus = Reactive(True, compute=False)

这个状态有两条用途:

  1. 驱动 CSS:focus/:blur伪类在应用层的解析。app.py 中的伪类映射将"focus"解析为app.app_focus"blur"解析为not app.app_focus。也就是说,当应用失去焦点时,样式中依赖焦点状态的规则会随之切换。
  2. 供 UI 组件判断"应用是否在前台"。例如页脚组件 _footer.py、帮助面板 _help_panel.py、按键面板 _key_panel.py 都会检查screen.app.app_focus,在应用未获焦点时隐藏相关 UI;screen.py 中也有类似判断。

另外还有一条兜底逻辑:如果应用尚未收到任何焦点事件,但用户直接按了键或点击了鼠标,框架会推断应用已重新获得焦点(见 app.py):

if not self.app_focus and isinstance(event, (events.Key, events.MouseDown)): self.app_focus = True

这保证了即使所在终端不支持焦点报告,app_focus状态也能随着用户交互被"救活"。

焦点恢复机制:AppBlur 记录、AppFocus 还原

这是AppFocus在整个框架中最核心的内置消费场景,实现在 App._watch_app_focus 中:

  • 失焦时(app_focus变为False:把当前屏幕的焦点控件记录到self._last_focused_on_app_blur(该字段定义见 app.py,注释即说明它是"上一次AppBlur时持有焦点的控件,用于在AppFocus时恢复正确焦点"),然后调用self.screen.set_focus(None)清空焦点。
  • 重新获焦时(app_focus变为True:如果记录中仍有该控件、它仍在当前屏幕上、且当前没有任何焦点,则通过self.screen.set_focus(..., scroll_visible=False, from_app_focus=True)把焦点还原到原控件,且刻意不滚动(源码注释说明滚动会带来突兀感)。还原完成后清空记录,避免持有已销毁控件的引用。

App对事件本身的直接处理则很轻,见 _on_app_focus:

async 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()

即:置位app_focus状态(触发上面的 watcher 与 CSS 重算),并刷新屏幕绑定显示(页脚按键提示等依赖焦点状态的展示)。_on_app_blur是对称逻辑。

from_app_focus:区分"应用级焦点"与"控件级焦点"

Focus事件携带了一个与AppFocus直接相关的标志from_app_focus(见 events.py):

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).

典型消费方是Input的"获焦即全选"行为(见 _input.py):

if self.select_on_focus and not event.from_app_focus:

即:当焦点是由应用重新获得焦点而"还原"回来时,Input不会触发全选,避免用户切回窗口后正在编辑的文本被整体选中。这是AppFocus与控件级焦点交互的典型联动细节。

在自己的应用中监听 AppFocus

App子类中通过@on装饰器监听即可:

from textual.app import App, ComposeResult from textual.events import AppBlur, AppFocus from textual.widgets import Label, Static class FocusTrackerApp(App): """监听应用级焦点变化的示例。""" def compose(self) -> ComposeResult: yield Label("当前应用状态:待命") yield Static("") @on(AppFocus) def on_app_focus(self) -> None: # 应用重新获得终端/页面焦点 self.query_one("#status", Label).update("当前应用状态:已聚焦") @on(AppBlur) def on_app_blur(self) -> None: # 应用失去焦点(例如用户切换了终端标签页) self.query_one("#status", Label).update("当前应用状态:已失焦")

使用建议:

  • 由于事件"仅在终端支持 FocusIn 或 textual-web 时可用",监听逻辑应当是渐进增强式的:事件不来,应用照常工作;事件来了,做一些暂停计时、暂停动画、保存临时状态之类的善后。
  • AppFocus/AppBlur不冒泡,监听器应放在App(或直接向App投递消息)上,而不是普通控件上。
  • 事件到达时会先经过App内置的_on_app_focus(置位app_focus、刷新绑定),随后@on(AppFocus)注册的用户处理器才被调用,因此你的处理器里读取self.app_focus得到的一定是已更新的值。

如何测试 AppFocus 相关行为

由于真实终端焦点报告无法在 CI 中复现,Textual 的测试方式是在run_test()上下文中直接向应用投递事件(见 test_app_focus_blur.py):

from textual.events import AppBlur, AppFocus async def test_app_focus_restores_focus() -> None: async with FocusBlurApp().run_test() as pilot: assert pilot.app.focused.id == "input-4" # AUTO_FOCUS 初始聚焦 pilot.app.post_message(AppBlur()) await pilot.pause() assert pilot.app.focused is None # 失焦清空焦点 pilot.app.post_message(AppFocus()) await pilot.pause() assert pilot.app.focused.id == "input-4" # 重新获焦后焦点被还原

该测试文件覆盖了焦点恢复机制的全部边界场景,适合作为自己实现参考时对照的用例清单:

  • test_app_blurAppBlur会清空当前焦点;
  • test_app_focus_restores_focusAppFocus将焦点还原到失焦前记录的控件;
  • test_app_focus_restores_none_focus:失焦前若本来就没有焦点,AppFocus不会凭空制造焦点;
  • test_app_focus_handles_missing_widget:失焦期间若原控件已被移除,AppFocus恢复流程安全降级(不报错、不设置焦点);
  • test_app_focus_defers_to_new_focus:失焦期间若已有新控件获得焦点,AppFocus不会覆盖新焦点。

小结

AppFocus(及其配对事件AppBlur)是 Textual 中少数"由运行环境决定是否送达"的事件,其设计要点可以归纳为:

  1. 来源:终端 XTermESC [ I/ESC [ O焦点报告,由 _xterm_parser.py 转译为事件;或 textual-web 驱动 直接投递。
  2. 状态:内置app_focusReactive 驱动 CSS:focus/:blur伪类与页脚等 UI 的显隐,并有按键/点击兜底置位逻辑。
  3. 恢复AppBlur时记录焦点控件,AppFocus时按条件还原(不滚动、不覆盖新焦点、容忍控件已销毁),且通过Focus.from_app_focus标志向Input等控件告知"这次聚焦来自应用级焦点事件",抑制选边副作用(如全选)。
  4. 实践:监听时按"渐进增强"思路编写逻辑,测试时通过pilot.app.post_message(AppFocus())模拟事件即可完整验证。

事件体系的更多背景可参考 事件指南 与 AppBlur 文档。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询