系統(tǒng)全解析)
Textual Focus 事件詳解從Focus/Blur到AppFocus/DescendantFocus的焦點(diǎn)系統(tǒng)全解析【免費(fèi)下載鏈接】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.項(xiàng)目地址: https://gitcode.com/gh_mirrors/te/textual本文圍繞 Textual 焦點(diǎn)focus事件體系展開從Focus、Blur兩個(gè)基礎(chǔ)事件入手依次剖析AppFocus/AppBlur應(yīng)用級(jí)焦點(diǎn)、DescendantFocus/DescendantBlur后代焦點(diǎn)冒泡以及Focus.from_app_focus標(biāo)記的設(shè)計(jì)意圖并結(jié)合源碼src/textual/events.py、src/textual/widget.py、src/textual/screen.py、src/textual/app.py與測(cè)試用例講清誰在什么時(shí)候收到什么焦點(diǎn)事件、事件如何流轉(zhuǎn)、應(yīng)如何響應(yīng)。讀完你能夠正確監(jiān)聽并區(qū)分六類焦點(diǎn)事件理解:focusCSS 偽類的底層觸發(fā)機(jī)制并能復(fù)現(xiàn) App 級(jí)焦點(diǎn)切換的完整鏈路。事件一覽六類焦點(diǎn)事件在焦點(diǎn)系統(tǒng)中的分工Textual 中與焦點(diǎn)相關(guān)的消息定義全部集中在 src/textual/events.py第 799–884 行。焦點(diǎn)事件共六類按作用范圍可分為三層部件級(jí)Focus/Blur、應(yīng)用級(jí)AppFocus/AppBlur、后代級(jí)DescendantFocus/DescendantBlur事件類觸發(fā)時(shí)機(jī)是否冒泡是否 Verbose備注Focus部件獲得焦點(diǎn)否否攜帶from_app_focus參數(shù)Blur部件失去焦點(diǎn)否否—AppFocus應(yīng)用重新獲得焦點(diǎn)否否需要終端支持FocusIn或經(jīng)由 textual-webAppBlur應(yīng)用失去焦點(diǎn)否否需要終端支持FocusOut或經(jīng)由 textual-webDescendantFocus子部件獲得焦點(diǎn)是是攜帶widget即control字段DescendantBlur子部件失去焦點(diǎn)是是攜帶widget即control字段對(duì)應(yīng)的事件文檔見 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事件系統(tǒng)總體指南可參考 docs/guide/events.md。冒泡Bubbles與 Verbose 的含義在 Textual 中事件默認(rèn)會(huì)沿著 DOM 樹向上冒泡到祖先節(jié)點(diǎn)bubbleFalse表示該事件不冒泡只有目標(biāo)部件自身能收到Verbose 標(biāo)記只影響日志輸出時(shí)的詳細(xì)程度詳見 src/textual/message.py 中Message基類的verbose屬性。部件級(jí)焦點(diǎn)Focus與Blur類簽名與from_app_focus參數(shù)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事件則沒有任何字段純粹表示該部件失去了焦點(diǎn)src/textual/events.pyclass Blur(Event, bubbleFalse): Sent when a widget is blurred (un-focussed). - [ ] Bubbles - [ ] Verbose from_app_focus參數(shù)是理解Focus事件的關(guān)鍵它區(qū)分了焦點(diǎn)的兩種來源——True焦點(diǎn)是因?yàn)閼?yīng)用自身重新獲得焦點(diǎn)例如用戶從其他窗口切回終端應(yīng)用收到AppFocusTextual 據(jù)此恢復(fù)此前失去焦點(diǎn)時(shí)的部件而產(chǎn)生的False默認(rèn)值焦點(diǎn)來自應(yīng)用內(nèi)部例如用戶按下 Tab 鍵遍歷、鼠標(biāo)點(diǎn)擊可聚焦部件或代碼中程序化地調(diào)用widget.focus()設(shè)置焦點(diǎn)。監(jiān)聽方式在 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將處理器綁定到指定部件類型例如在容器中統(tǒng)一監(jiān)聽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設(shè)置了bubbleFalse它們不會(huì)冒泡到祖先部件——祖先如果想感知后代焦點(diǎn)變化需要監(jiān)聽DescendantFocus/DescendantBlur見下文。焦點(diǎn)事件的發(fā)送鏈路從源碼看Focus/Blur事件的發(fā)送由Screen.set_focus統(tǒng)一負(fù)責(zé)src/textual/screen.py核心邏輯如下若widget is None對(duì)當(dāng)前已聚焦部件post_message(events.Blur())并將self.focused置空若widget.focusable且與當(dāng)前聚焦部件不同先對(duì)舊部件發(fā)送Blur再設(shè)置self.focused widget然后向新部件post_message(events.Focus(from_app_focusfrom_app_focus))若scroll_visibleTrue默認(rèn)通過call_later(scroll_to_center, widget)在刷新后把部件滾動(dòng)到可見區(qū)域最后調(diào)用_update_focus_styles刷新:focus相關(guān)樣式并refresh_bindings更新綁定鍵位提示。當(dāng)部件自身收到Focus事件時(shí)Textual 內(nèi)置的Widget._on_focussrc/textual/widget.py會(huì)做兩件事將self.has_focus置為True并觸發(fā)self.refresh()重繪同時(shí)向父部件發(fā)送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))對(duì)應(yīng)的_on_blursrc/textual/widget.py則將has_focus置為False并發(fā)送DescendantBlur。程序化設(shè)置與解除焦點(diǎn)widget.focus(scroll_visibleTrue)請(qǐng)求將焦點(diǎn)移到此部件src/textual/widget.py。注意它通過self.app.call_later(set_focus, self)異步調(diào)度保證在消息循環(huán)空閑時(shí)才真正執(zhí)行返回self便于鏈?zhǔn)秸{(diào)用。widget.blur()解除該部件的焦點(diǎn)焦點(diǎn)會(huì)移到焦點(diǎn)鏈中下一個(gè)可用部件src/textual/widget.py。screen.set_focus(widget, scroll_visibleTrue, from_app_focusFalse)底層實(shí)現(xiàn)from_app_focus參數(shù)在此直接透傳給Focus事件。CSS 查詢器也提供query(*).focus()與query(*).blur()批量聚焦/失焦方法src/textual/css/query.py。只有focusable為True的部件才能被聚焦set_focus中有顯式判斷同時(shí) CSS 的:focus偽類樣式、[src/textual/widget.py](https://link.gitcode.com/i/d72419d395e58b339a84d89863bdc984)中_has_focus_within等屬性共同參與焦點(diǎn)樣式的刷新。應(yīng)用級(jí)焦點(diǎn)AppFocus與AppBlur定義與可用前提AppFocus/AppBlur描述的是整個(gè)應(yīng)用窗口獲得或失去操作系統(tǒng)焦點(diǎn)的事件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 明確標(biāo)注僅在支持FocusIn/FocusOut終端事件的終端中運(yùn)行或通過 textual-web 在瀏覽器中運(yùn)行時(shí)才會(huì)產(chǎn)生這兩類事件。XTerm 解析器在收到終端上報(bào)的焦點(diǎn)序列時(shí)會(huì)生成對(duì)應(yīng)事件src/textual/_xterm_parser.pyweb 驅(qū)動(dòng)則在前臺(tái)/后臺(tái)切換時(shí)發(fā)送src/textual/drivers/web_driver.py 與第 243–245 行。App 內(nèi)的默認(rèn)行為焦點(diǎn)自動(dòng)恢復(fù)App 級(jí)焦點(diǎn)事件不只是通知Textual 用它們實(shí)現(xiàn)了應(yīng)用失焦后恢復(fù)焦點(diǎn)的能力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()應(yīng)用內(nèi)部維護(hù)了app_focus這個(gè)響應(yīng)式屬性src/textual/app.py并在_watch_app_focus中記錄/恢復(fù)失焦前的聚焦部件src/textual/app.py收到AppBlur時(shí)將self._last_focused_on_app_blur記為當(dāng)前screen.focused收到AppFocus時(shí)若此前記錄過聚焦部件且它仍屬于當(dāng)前屏幕則調(diào)用screen.set_focus(..., from_app_focusTrue)恢復(fù)焦點(diǎn)——這正是Focus.from_app_focusTrue的唯一真實(shí)來源。測(cè)試用例 tests/test_app_focus_blur.pytest_app_focus_restores_focus驗(yàn)證了該恢復(fù)鏈路tests/input/test_select_on_focus.pytest_focus_from_app_focus_does_not_select則驗(yàn)證了由AppFocus觸發(fā)的聚焦不應(yīng)觸發(fā)選中文本等副作用這一設(shè)計(jì)約束——這也是應(yīng)用代碼里判斷from_app_focus的典型用途區(qū)分用戶主動(dòng)交互與應(yīng)用失而復(fù)得。在應(yīng)用代碼中響應(yīng)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)一個(gè)更細(xì)粒度的做法是結(jié)合Focus.from_app_focus若應(yīng)用恢復(fù)焦點(diǎn)后不希望某個(gè)敏感部件自動(dòng)執(zhí)行聚焦副作用可在其on_focus中檢查event.from_app_focus并提前返回。注意AppFocus/AppBlur同樣bubbleFalse只在 App 上監(jiān)聽即可。后代焦點(diǎn)冒泡DescendantFocus與DescendantBlur定義與關(guān)鍵字段DescendantFocus/DescendantBlur是事件被發(fā)送到父部件時(shí)攜帶子部件引用的版本src/textual/events.py且與前三者不同它們bubbleTrue且verboseTrue會(huì)沿 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兩個(gè)事件均為dataclass攜帶widget字段被聚焦/失焦的部件并提供control屬性作為其別名——control是 Textual 事件體系的通用命名約定與Click、Key等交互事件的control一致便于統(tǒng)一處理交互來源。事件發(fā)送點(diǎn)在Widget._on_focus/_on_blur中src/textual/widget.py即部件聚焦 → 先收到Focus自身→ 父部件收到DescendantFocus可冒泡。典型用法容器統(tǒng)一跟蹤焦點(diǎn)由于Focus不冒泡任何希望感知子樹焦點(diǎn)變化的容器部件都應(yīng)監(jiān)聽DescendantFocus/DescendantBlurfrom textual.containers import Vertical from textual import events class FocusTracker(Vertical): Vertical 容器跟蹤哪個(gè)子部件當(dāng)前擁有焦點(diǎn)。 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 層也可以統(tǒng)一收集整棵組件樹的焦點(diǎn)變化例如實(shí)現(xiàn)焦點(diǎn)日志或狀態(tài)同步因?yàn)槭录?huì)一路冒泡到根節(jié)點(diǎn)。內(nèi)置組件中也大量使用該機(jī)制例如Collapsible、DataTable等組件通過后代焦點(diǎn)事件維護(hù)展開/選中狀態(tài)。焦點(diǎn)樣式與:focus的底層聯(lián)動(dòng)焦點(diǎn)事件與樣式系統(tǒng)緊密聯(lián)動(dòng)Screen.set_focus在焦點(diǎn)變更后調(diào)用_update_focus_styles(focused, blurred)其實(shí)現(xiàn)src/textual/screen.py會(huì)對(duì)發(fā)生變化的部件及祖先存在_has_focus_within的節(jié)點(diǎn)整棵子樹刷新樣式表從而驅(qū)動(dòng)以下 CSS 偽類Screen:focus { /* 屏幕級(jí)焦點(diǎn)樣式 */ } Button:focus { text-style: bold; background: $accent; } Button:focus-within { /* 焦點(diǎn)在 Button 內(nèi)部時(shí) */ }Textual 的 CSS 文檔docs/guide/CSS.md、docs/styles/index.md中:focus與:focus-within均基于這套事件驅(qū)動(dòng)的樣式刷新機(jī)制實(shí)現(xiàn)部件收到Focus后置has_focusTrue并refresh()隨后樣式表更新觸發(fā)重繪快照測(cè)試 tests/snapshot_tests/test_snapshots.py 中的test_app_focus_style、test_focus_component_class等用例即為焦點(diǎn)樣式的回歸驗(yàn)證。完整事件時(shí)序一次 Tab 切換焦點(diǎn)發(fā)生了什么綜合上述源碼用戶按下 Tab 使焦點(diǎn)從 A 部件切換到 B 部件時(shí)事件序列大致如下用戶按下 TabApp._on_key→ 按鍵分發(fā)src/textual/_dispatch_key.py找到焦點(diǎn)鏈中的下一個(gè)可聚焦部件 BScreen.set_focus(B, scroll_visibleTrue, from_app_focusFalse)被調(diào)用A 收到BlurbubbleFalseWidget._on_blur置has_focusFalse、刷新自身、向父部件發(fā)送DescendantBlur(A)可冒泡Screen.focused更新為 BB 收到Focus(from_app_focusFalse)Widget._on_focus置has_focusTrue、刷新自身、向父部件發(fā)送DescendantFocus(B)可冒泡_update_focus_styles更新:focus/:focus-within樣式并重繪refresh_bindings刷新底部綁定提示。若整個(gè)終端窗口失焦再切回則額外發(fā)生AppBlur→記錄_last_focused_on_app_blur→AppFocus→Screen.set_focus(之前部件, from_app_focusTrue)→ 部件收到Focus(from_app_focusTrue)其from_app_focus屬性即為True。實(shí)踐要點(diǎn)小結(jié)自身感知焦點(diǎn)重寫on_focus/on_blur或使用on(Focus)/on(Blur)祖先感知子樹焦點(diǎn)監(jiān)聽on_descendant_focus/on_descendant_blur通過event.widgetevent.control判斷具體部件應(yīng)用整體失焦/聚焦在 App 上監(jiān)聽on_app_focus/on_app_blur注意可用性取決于終端是否上報(bào)FocusIn/FocusOut或是否運(yùn)行于 textual-web區(qū)分焦點(diǎn)來源Focus.from_app_focus為True表示應(yīng)用失而復(fù)得時(shí)自動(dòng)恢復(fù)的焦點(diǎn)可用于抑制不希望的聚焦副作用參考 tests/input/test_select_on_focus.py 的設(shè)計(jì)程序化控制widget.focus()、widget.blur()、screen.set_focus(...)與query().focus()/blur()均可按需觸發(fā)焦點(diǎn)流轉(zhuǎn)?!久赓M(fèi)下載鏈接】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.項(xiàng)目地址: https://gitcode.com/gh_mirrors/te/textual創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考