🎙️ Lecture 17: 【高階專案】網頁無障礙探索者:解析 Web ARIA 與 NVDAObject

NOTE作者資訊 (Author Info)

🎧 錄音室開場:打通網頁虛擬世界的任督二脈!


🎯 專案四目標:網頁核取方塊掃描雷達 (Web Checkbox Scanner)

當我們在 Google Chrome 瀏覽網頁時,按下 NVDA+Shift+C,外掛會立刻自動掃描整個網頁,用語音和點字告訴你:「這個網頁總共有幾個核取方塊」。


🧠 大導演的思維課:解析 Web ARIA 與網頁虛擬樹的 Prompt 設計

要在網頁中順暢導航,我們需要懂得將 MDN 的 Web ARIA 標準與 NVDA 的底層虛擬緩衝區相結合。

📋 步驟一:拆解你的「網頁掃描需求」

  1. 發動時機:按下快捷鍵 NVDA+Shift+C
  2. 範圍限制(網頁專用):只在瀏覽器的「瀏覽模式(Browse Mode)」下執行。
    • 行話翻譯:必須檢查當前焦點物件是否擁有 treeInterceptor (VirtualBuffer),且該虛擬緩衝區必須是活著的 (isAlive)。如果沒有,代表使用者不在網頁上,必須結束並提示。
  3. 目標特徵(我要找什麼?):尋找網頁上的核取方塊。
    • 行話翻譯:網頁上的 <input type="checkbox"> 或帶有 role="checkbox" 的 ARIA 標籤,在 NVDA 底層都會被翻譯成 controlTypes.Role.CHECKBOX。這就是我們要找的身分證!
  4. 遍歷演算法(怎麼找最快?)
    • 行話翻譯:請 AI 使用 NVDA 內建的遍歷方法(例如對 treeInterceptor.rootNVDAObject 進行樹狀搜尋,或者使用 textInfos 進行快速查找),去計算符合條件的物件數量。

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

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

「你現在是深諳 NVDA 官方架構與 Web ARIA 無障礙規範的頂尖工程師。請幫我寫一個全域外掛(GlobalPlugin),檔案名為 webScanner.py

1. 開發需求描述: - 功能目標:按下快捷鍵 kb:NVDA+shift+c 時,掃描當前網頁中有幾個『核取方塊』並朗讀數量。 - 環境判定:請先使用 api.getFocusObject() 獲取當前焦點。接著,請檢查該焦點是否具有 treeInterceptor 屬性。如果沒有,表示不在網頁瀏覽模式,請呼叫 ui.message("請在網頁瀏覽模式下使用此功能") 並結束。 - 搜尋邏輯:當確認在網頁虛擬樹中後,請遍歷 treeInterceptor.rootNVDAObject 及其所有子節點(使用遞迴搜尋或樹狀迭代器)。只要節點的 role 等於 controlTypes.Role.CHECKBOX,就將計數器加一。 - 播報提示:最後使用 ui.message() 朗讀出『此網頁共有 X 個核取方塊』。

2. 規範限制: - 程式碼必須結構清晰,且必須包含給一般人看得懂的詳細中文註釋,解釋每一行程式碼的運作原理與遍歷邏輯,讓不懂程式碼的人也能輕鬆讀懂。

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


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

AI 產出的 webScanner.py 結構如下,我們一起來看看它是怎麼在網頁虛擬樹裡「抓鬼」的:

# 引入全域外掛基礎類別
import globalPluginHandler
# 引入快捷鍵綁定裝飾器
import scriptHandler
# 引入 NVDA 核心 API
import api
# 引入控制控制項類型的定義(Role)
import controlTypes
# 引入語音/點字播報模組
import ui

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

    # 綁定快捷鍵 NVDA+Shift+C
    @scriptHandler.script(
        description="Scans the web page for checkboxes",
        gestures=["kb:NVDA+shift+c"]
    )
    def script_scanCheckboxes(self, gesture):
        # 1. 抓取目前使用者的焦點物件
        focusObj = api.getFocusObject()

        # 2. 檢查這個焦點物件身上,有沒有「網頁虛擬樹(treeInterceptor)」這個構造
        # 這是判斷我們是不是在瀏覽網頁、看 PDF 的關鍵
        vbuf = getattr(focusObj, 'treeInterceptor', None)

        # 如果找不到,或是這個虛擬樹已經失效了
        if not vbuf or not vbuf.isAlive:
            # 朗讀警告訊息,並結束外掛
            ui.message("請在網頁瀏覽模式下使用此功能")
            return

        # 3. 抓取整張網頁的最頂端根節點
        root = vbuf.rootNVDAObject

        # 4. 呼叫我們自己寫的樹狀搜尋工具,傳入根節點開始數數
        count = self._count_checkboxes(root)

        # 5. 將最終數出來的數量報讀給使用者聽
        ui.message(f"此網頁共有 {count} 個核取方塊")

    # 這是一個遞迴輔助函數,用來深入每一個分支節點數數
    def _count_checkboxes(self, obj):
        count = 0

        # 檢查當前節點的角色是不是「核取方塊」
        if obj.role == controlTypes.Role.CHECKBOX:
            count += 1

        # 接著,把這個節點底下所有的子節點(children)通通拉出來,重複執行一樣的檢查
        for child in obj.children:
            count += self._count_checkboxes(child)

        return count

💡 核心代碼導讀:


🧪 測試與驗收

  1. 將檔案存為 webScanner.py 放進 globalPlugins 並重啟。
  2. 打開 Google Chrome,進入有大量勾選框的設定頁面。
  3. 按下 NVDA+Shift+C,聽取語音報出的數量。大功告成!

👉 下一堂課:Lecture 18 【大師專案】商用級隱私防護網:語音過濾器與 GUI 設定面板
👉 回課程大綱:學習地圖

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

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

❓ 挑戰 1:`TreeInterceptor` (樹狀攔截器/瀏覽模式) 的 Virtual Buffer 機制 👉 點擊展開專家解析答案

【實戰情境問題】:當 NVDA 使用者在 Chrome 中閱讀網頁時,按 `NVDA + Space` 可以切換「瀏覽模式 (Browse Mode)」與「焦點模式 (Focus Mode)」。這背後的 `TreeInterceptor` 如何在背景建立網頁的虛擬 DOM 緩衝區?

【鑑別點說明】:測試學員對 NVDA 瀏覽模式與網頁互動底層 Virtual Buffer 運作原理的理解。

【專家級解答與深度剖析】: 在「焦點模式」下,鍵盤按鍵會直接傳送給瀏覽器(如在輸入框打字);而「瀏覽模式」下,NVDA 的 `TreeInterceptor` 會透過 IAccessible2 / UIA 介面,將網頁的 DOM 樹抽取出文字與結構,在 NVDA 記憶體中建立一份「虛擬文件緩衝區 (Virtual Buffer)」。此時使用者的上下方向鍵與單鍵導航(如 H 鍵)會被 `TreeInterceptor` 攔截,用來在虛擬緩衝區中移動審閱游標並播報,從而解鎖了極速網頁導航的能力。

❓ 挑戰 2:`aria-live="polite"` vs `aria-live="assertive"` 的播報佇列打斷機制 👉 點擊展開專家解析答案

【實戰情境問題】:在開發無障礙 Web 應用時,如果將一個每 3 秒更新一次的聊天訊息設定為 `aria-live="assertive"`,會對視障學員造成什麼毀滅性的閱讀體驗?

【鑑別點說明】:深剖 WAI-ARIA 動態區域 (Live Regions) 在螢幕報讀器 Speech Queue 中的搶占優先級。

【專家級解答與深度剖析】: `aria-live="assertive"` 的優先級是最高搶占式(Preemptive)。一旦有新內容更新,NVDA 會**立刻中斷目前正在朗讀的任何句子**並強制播報新訊息。如果更新頻率高(如每 3 秒),學員試圖閱讀講義文章時,話講到一半就會被強行打斷,永遠無法讀完一段完整的句子。正解:非緊急訊息必須使用 `aria-live="polite"`,它會靜靜等待 NVDA 講完當前句子後,才禮貌地播報新動態。