🎙️ Lecture 16: 【中階專案】特定軟體專屬守衛:記事本焦點音效提示 (AppModule & Events)

NOTE作者資訊 (Author Info)

🎧 錄音室開場:別讓 AI 寫的外掛把 NVDA 弄啞了!


🎯 專案三目標:記事本焦點音效守衛 (Notepad Audio Guard)

當我們在使用「Windows 記事本 (Notepad)」打字時,只要焦點移動到「文字編輯區」,外掛會自動發出「滴」的一聲高音;若是移到其他地方(如選單或按鈕),則發出低音。在其他軟體中則完全不發聲。


🧠 大導演的思維課:事件監聽與軟體特攻隊的 Prompt 設計

要寫出一個「只在特定軟體生效、且安全不卡死」的外掛,我們需要掌握兩大需求設計心法:

📋 步驟一:拆解你的「事件攔截需求」

  1. 發動範圍(在哪裡生效?):這不是全域外掛,我們只在「記事本」生效。
    • 行話翻譯:不要寫 globalPlugins。請要求 AI 寫在 appModules 目錄下,且檔案名稱必須命名為 notepad.py(對應 notepad.exe)。
  2. 監聽事件(要監視什麼動作?):當游標獲得焦點時。
    • 行話翻譯:必須攔截 event_gainFocus 事件。
  3. 條件與動作(如何判斷並發聲?)
    • 如果焦點是「可編輯文字區」,發出 800Hz 嗶聲。
    • 如果是其他區,發出 300Hz 嗶聲。
    • 行話翻譯:使用 obj.rolecontrolTypes.Role.EDITABLETEXT 進行比對。發聲則呼叫 tones.beep()
  4. 安全生命線(如何防止 NVDA 卡死變啞巴?)
    • 超級核心行話:在事件函式的最後,絕對必須呼叫 nextHandler()。這代表「我們攔截完、播完聲音後,要把控制權交還給 NVDA 原本的神經鏈,讓它正常朗讀」。

📝 實戰演練:大導演的 Prompt 派工單

請複製以下 Prompt 貼給你的 AI:

「你現在是專業的 NVDA 視障輔助軟體外掛工程師。請幫我寫一個應用程式專屬模組(AppModule),檔案名為 notepad.py,需放置於 appModules 目錄下。

1. 開發需求描述: - 目標軟體:Windows 記事本(Notepad)。 - 攔截事件:請繼承 appModules.AppModule,並覆寫焦點獲取事件:def event_gainFocus(self, obj, nextHandler):。 - 判斷與發聲邏輯: - 檢查目前焦點物件 obj 的角色。如果 obj.role 等於 controlTypes.Role.EDITABLETEXT(文字編輯區),請呼叫 tones.beep(800, 50) 發出高音。 - 如果是其他角色(如選單或按鈕),請呼叫 tones.beep(300, 50) 發出低音。 - 安全控制鏈(最重要):不論是否播放音效,在 event_gainFocus 結束前,必須呼叫 nextHandler(),將焦點事件傳遞下去,絕不可中斷。

2. 規範限制: - 請確保程式碼結構乾淨,且必須附有詳細的中文註釋,解釋每一行程式碼的運作原理,讓完全不懂寫程式代碼的人也能一目了然看懂。

請直接輸出完整程式碼。」


🛠️ 看懂 AI 寫的代碼:逐行白話拆解

AI 產出的 notepad.py 結構如下,請跟著 Ray 導讀,看懂它是怎麼保護系統的:

# 引入應用程式模組的基礎類別
import appModules
# 引入控制類型定義庫,用來判斷元件角色(如編輯區、按鈕)
import controlTypes
# 引入發聲嗶嗶聲的模組
import tones

# 建立我們的記事本特攻隊類別,繼承自官方的 AppModule
class AppModule(appModules.AppModule):

    # 當記事本內部有任何元件獲得焦點時,NVDA 會自動呼叫這個事件函數
    def event_gainFocus(self, obj, nextHandler):

        # 1. 檢查這個獲得焦點的元件 (obj),它的角色 (role) 是否為「可編輯文字區」
        if obj.role == controlTypes.Role.EDITABLETEXT:
            # 如果是編輯區,發出 800Hz(較清脆高亢)的嗶聲,持續 50 毫秒
            tones.beep(800, 50)
        else:
            # 如果是其他元件(如選單、按鈕),發出 300Hz(低沉)的嗶聲,持續 50 毫秒
            tones.beep(300, 50)

        # 2. 【生命線】:呼叫 nextHandler(),把焦點事件傳還給 NVDA 核心
        # 這樣 NVDA 才會繼續朗讀出這個元件的名字,才不會讓記事本變成啞巴!
        nextHandler()

💡 核心代碼導讀:


🧪 測試與驗收

  1. 將檔案命名為 notepad.py,放進 appModules 資料夾。
  2. 重啟 NVDA。打開記事本,移動游標,你會聽到清脆的嗶聲隨游標跳動。打開其他軟體,則完全安靜。測試成功!

👉 下一堂課:Lecture 17 【高階專案】網頁無障礙探索者:解析 Web ARIA 與 NVDAObject
👉 回課程大綱:學習地圖

🔥 專家級深度思考與實戰挑戰 (Expert Challenge)

以下題目專為尋求真正硬核觀念與專家級實戰能力的學員設計。題目無法從講義表面抄寫解答,請嘗試獨立思考後再點擊展開解析。

❓ 挑戰 1:`event_gainFocus` vs `event_nameChange` 事件的頻率暴風雨防禦 👉 點擊展開專家解析答案

【實戰情境問題】:在某些動態網頁(如即時跳動的股票看板或聊天室)中,`event_nameChange` 事件每秒可能被觸發 50 次。如果外掛在 `event_nameChange` 中直接進行複雜的語法分析,會引發什麼災難?如何設計防抖 (Debounce) 與節流 (Throttle)?

【鑑別點說明】:考驗學員對高效能 GUI 事件處理與事件防暴風雨(Event Flooding Mitigation)的實戰能力。

【專家級解答與深度剖析】: 每秒 50 次的事件觸發會瞬間擠爆 NVDA 的 Python 事件佇列,導致 CPU 使用率飆升至 100%,NVDA 語音報讀嚴重延遲或卡死。防禦作法:使用時間差節流 (Throttle) 策略,記錄 `lastEventTime`;或者設定一個 100 毫秒的 `wx.CallLater` 防抖定時器。如果在 100 毫秒內再次收到新事件,取消上一次的處理任務,僅處理最後一次穩定下來的事件狀態。

❓ 挑戰 2:`chooseNVDAObjectOverlayClasses` 動態類別注入機制 👉 點擊展開專家解析答案

【實戰情境問題】:NVDA 如何透過 `chooseNVDAObjectOverlayClasses` 方法,在不修改 NVDA 核心程式碼的前提下,動態將我們自訂的行為注入到特定視窗(如 Chrome 瀏覽器或特定軟體)的 `NVDAObject` 中?

【鑑別點說明】:深剖 Python 動態多重繼承 (Dynamic Multiple Inheritance / Monkey Patching) 在 NVDA 架構中的應用。

【專家級解答與深度剖析】: NVDA 在實实例化任何 `NVDAObject` 時,都會遍歷所有已載入外掛的 `chooseNVDAObjectOverlayClasses(obj, clsList)` 方法。外掛可以檢查 `obj.appModule.appName` 或 `obj.windowClassName`。若匹配到目標軟體(如 `appName == "chrome"`),外掛將自訂的 Mixin 類別插入到 `clsList` 的首位。NVDA 隨後會動態建構一個融合了外掛行為的新類別,實現了對特定軟體無障礙行為的無侵入式擴充。