iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0

Notepad++ 的編輯區是 Scintilla——一個被大量第三方編輯器內嵌的控制項。它在 UIA 樹裡長這樣:

<Pane class='Scintilla' name='' patterns=[]>

patterns=[]。沒有 Value、沒有 Text、什麼都沒有。所以 find_text_input() 的 UIA 控制項型別階梯完全碰不到它——它會跑完 20 秒的階梯然後失敗,而編輯器就在那裡。

Scintilla 有自己的訊息介面:SCI_GETLENGTHSCI_GETCURRENTPOSSCI_GETTEXT……幾百個。所以我以為這一題的答案顯然是「送 SCI_* 訊息」。

我連錯三次。


錯誤一:以為需要 SCI_GETTEXT

SCI_GETTEXT 要一個緩衝區:

SCI_GETTEXT(position length, char *text) → position

char *text——一個指標。我送過去,回來的是 0,緩衝區沒有被填。

原因很直接:我的指標指向我自己的位址空間。接收方——Notepad++ 的處理程序——拿到那個數值之後,把它當成它自己位址空間裡的位址去解參照。那是兩個不同的地方。

錯誤二:以為 VirtualAllocEx 可以救

標準解法應該是這樣:在目標處理程序裡配置記憶體,把那個位址送過去,再用 ReadProcessMemory 讀回來。

remote = kernel32.VirtualAllocEx(handle, None, size, MEM_COMMIT, PAGE_READWRITE)
user32.SendMessageW(hwnd, SCI_GETTEXT, size, remote)
kernel32.ReadProcessMemory(handle, remote, buffer, size, None)

也不行。 緩衝區還是空的。

這一步花了我最久,因為技術本身是對的——那確實是跨處理程序傳緩衝區的正規做法。錯的是我對「誰在做 marshalling」的理解。

那道分界線

WM_GETTEXT 的用法跟 SCI_GETTEXT 幾乎一樣,也要一個指標。而 WM_GETTEXT 跨處理程序完全正常。差別在這裡:

USER32 認識系統訊息。 它知道 WM_GETTEXTlParam 是一個指向緩衝區的指標,所以當 SendMessage 跨越處理程序邊界時,它會幫你把緩衝區複製過去、再把結果複製回來

USER32 不認識 SCI_* 那些是 WM_USER + n,對 USER32 來說只是兩個整數。它把數值原封不動送過去,不做任何 marshalling。

所以 VirtualAllocEx 為什麼沒用也清楚了:問題從來不是「目標處理程序裡沒有可用的記憶體」。問題是接收端的程式碼(Scintilla 自己)拿到指標之後直接解參照——它沒有任何理由去 ReadProcessMemory,因為在正常使用情境下,呼叫它的人跟它在同一個處理程序裡。

要真正做到,得把程式碼注入到目標處理程序裡執行。那超出一個測試函式庫該做的事的邊界很遠。

錯誤三:以為 SCI_GETCHARAT 是答案

SCI_GETCHARAT(position) 回傳單一個字元的位元組值——不用指標。所以它跨處理程序可以用。逐字元讀完整份文件,理論上可行。

我甚至開始寫了。然後才回頭做一件應該最早做的事:試試 get_value()

edit = win.find_text_input(timeout=30.0)
print(edit.get_value())     # 'alpha\r\nbeta\r\ngamma\r\n'

它一直都可以。 get_value() 在沒有 Value pattern 的時候會退回 WM_GETTEXT,而 WM_GETTEXT 是系統訊息,USER32 幫我 marshal 了。

我花了三輪去解一個不存在的問題。真正的問題只有一個:階梯找不到 Scintilla。


真正的修法:階梯裡一行

DEFAULT_TEXT_INPUT_LADDER: tuple[dict, ...] = (
    {"class_name": "RichEditD2DPT"},  # Win11 tabbed Notepad
    {"class_name": "Edit"},           # classic Win32 edit control
    {"class_name": "Scintilla"},      # ← 這一行
    {"control_type_id": 50030},       # UIA Document
    {"control_type_id": 50004},       # UIA Edit
)

find_text_input() 在 Notepad++ 上從「跑完 20 秒階梯後失敗」變成 718ms 成功

一個 class name。 而我在那之前寫了兩份被丟掉的實驗程式碼。


SCI_* 到底有沒有用

有,但只有回傳純量的那些。它們不需要指標,所以跨處理程序沒問題,而且它們回答的是 WM_GETTEXT 答不出來的問題:

view = ScintillaView.from_element(edit)
view.length              # 位元組數,不是字元數
view.line_count
view.current_position    # 游標在哪
view.selection_range     # (7, 11)
view.eol_mode            # EolMode.CRLF
view.tab_width
view.code_page
view.is_modified         # 有沒有未儲存的變更

is_modified 特別有用:「打了字但還沒存」是一個沒有其他方法問得到的狀態。 讀檔案問不到(檔案還是舊的),讀緩衝區也問不到(內容看起來就是新的)。

十四個常數,每一個都是實際送過去、確認回傳值合理才寫進去的。wintegrate 刻意提供逐字元讀取器——get_value() 已經能讀,而 SCI_GETCHARAT 迴圈只會是一條比較慢、比較容易錯的第二條路。


這一課可以推廣

分界線是「這個訊息 USER32 認不認識」,而它比 Scintilla 廣得多:

訊息類型 跨處理程序帶指標時 例子
系統訊息(< WM_USER USER32 幫你 marshal WM_GETTEXTWM_SETTEXT
自訂訊息(>= WM_USER 原封不動送數值 SCI_*、任何 app 自訂的

所以下次看到一個控制項有豐富的訊息介面,先問一句:這些訊息裡有沒有指標?如果有,它們是 WM_USER 之後的嗎? 是的話,跨處理程序那條路就不通,不要在 VirtualAllocEx 上花時間。


順帶一提:Notepad++ 的按鈕

主視窗有 35 個 Button,全部沒有 automation_id——工具列是 MFC 的,只剩在地化的名稱可用(新增(N) / New)。位置也不是備案:依 (top, left) 排序之後最前面的是兩個 0x0 的幽靈按鈕,點下去什麼都不會發生。

但它的 Win32 對話框控制項帶數字 control id。按 Ctrl+F 開啟尋找對話框:

Button aid='1'    name='尋找下一個'     ← IDOK
Button aid='1614' name='計數'
Button aid='2'    name='關閉'           ← IDCANCEL

數字不會被翻譯。 所以在這個應用程式裡,唯一與語言無關的按鈕測試路徑,是走它的 Win32 對話框而不是它的工具列。


小結

  • Scintilla 在 UIA 裡是一個 patterns=[] 的 Pane,控制項型別階梯碰不到它
  • SCI_*WM_USER + n,USER32 不會 marshal 它們的指標
  • VirtualAllocEx 不能救,因為接收端的程式碼直接解參照那個指標
  • WM_GETTEXT 可以,因為它是系統訊息——而 get_value() 本來就會退回它
  • 真正的修法是階梯裡加一行 class name,find_text_input() 變成 718ms
  • 純量的 SCI_* 仍有價值,尤其 is_modified
  • MFC 工具列按鈕沒有 id,但 Win32 對話框控制項有數字 control ID

明天換 WinMerge,而它的編輯窗格連 WM_GETTEXT 都不回應。那時候唯一剩下的證據是像素


上一篇
Day 21:四個標的,四代技術
下一篇
Day 23:UIA 完全看不到的控制項,像素是唯一的證據
系列文
Windows 桌面軟體 CI 實戰:從系統工具開發到 Zero-RDP 自動化測試錄影24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言