FlaUI UI 自動化指南
用途:透過 Microsoft UI Automation (UIA) 框架自動化操作 Windows 桌面應用程式
NuGet:
FlaUI.UIA3(推薦)/FlaUI.UIA2版本:5.0.0
授權:MIT
支援:.NET Framework 4.8、.NET 6+、.NET 8+
什麼是 FlaUI
FlaUI 是一個基於 Microsoft UI Automation 的 .NET 測試框架,用來自動化操作 Windows 桌面應用程式。它可以:
- 啟動/附加到應用程式
- 找到視窗上的控件(按鈕、文字框、DataGrid 等)
- 模擬使用者操作(點擊、輸入文字、選擇項目)
- 截取螢幕截圖
- 驗證 UI 狀態
為什麼選 FlaUI
| 工具 | 狀態 | 適用場景 | 備註 |
|---|---|---|---|
| FlaUI | 活躍維護 | Windows 桌面應用 | 純 .NET、UIA3 支援 |
| WinAppDriver | 已停止維護 | — | 微軟已不再更新 |
| Playwright | 活躍 | Web 應用 | 不支援桌面 |
| Appium + WinAppDriver | 部分活躍 | 跨平台測試 | 架構複雜 |
| TestStack.White | 已停止維護 | — | FlaUI 的前身 |
UIA2 vs UIA3
| 項目 | UIA2 | UIA3(推薦) |
|---|---|---|
| API | 基於 COM | 基於 COM,更新版本 |
| 效能 | 較慢 | 較快 |
| WPF 支援 | 部分 | 完整 |
| Win32 支援 | 完整 | 完整 |
| Pattern 支援 | 基本 | 完整(含 ItemContainer、VirtualizedItem) |
結論:永遠用 UIA3,除非目標應用只支援 UIA2。
安裝
dotnet add package FlaUI.UIA3
# FlaUI.Core 會作為相依套件自動安裝
官方測試工具(含 NUnit 基底類別、截圖、錄影):
dotnet add package FlaUI.TestUtilities
測試框架:
dotnet add package NUnit # 或 xUnit
dotnet add package NUnit3TestAdapter
tip
FlaUInspect 是 FlaUI 附帶的 UI 自動化樹檢視工具,可用來查看目標應用程式的 AutomationId、Name、ClassName 等屬性。開發測試時必備。以系統管理員身分執行,並確保與目標應用程式的位元數(32/64-bit)一致。
核心概念
UIA 自動化的對象是一棵 Automation Element Tree。每個應用程式的 UI 都是樹狀結構,從 Application 往下展開到 Window,再到具體控制項。下圖以一個典型的設備控制視窗為例:
樹的特性:
- 每個節點都是
AutomationElement:取得子節點後可進一步轉型為具體控制項(AsButton()、AsTextBox()等) - AutomationId 是黃金識別子:開發 WPF 時主動設定
AutomationProperties.AutomationId,測試端就有穩定的元素定位 - 層級會比視覺看到的深:FlaUInspect 顯示的樹通常含有看不見的中介 Pane / Group,搜尋時優先用
FindFirstDescendant(遞迴搜尋)而非FindFirstChild(只搜直接子節點)
Application — 啟動或附加應用程式
using FlaUI.Core;
using FlaUI.UIA3;
// 啟動新的應用程式
var app = Application.Launch("DeviceControl.exe");
// 附加到已運行的程式
var app = Application.Attach(Process.GetProcessesByName("DeviceControl")[0]);
// 附加到指定 PID
var app = Application.Attach(processId);
Window — 取得視窗
using var automation = new UIA3Automation();
// 取得主視窗(等待最多 10 秒)
var mainWindow = app.GetMainWindow(automation, TimeSpan.FromSeconds(10));
// 取得所有視窗
var allWindows = app.GetAllTopLevelWindows(automation);
AutomationElement — UI 元素
所有 UI 控件都是 AutomationElement,可進一步轉型為具體控件類型。
元素搜尋
搜尋策略優先順序
- AutomationId(最推薦):開發時設定,不受語系影響
- Name:控件的可見文字,多語系會變
- ClassName:控件的類別名稱
- ControlType:控件類型(Button、TextBox 等)
FindFirstDescendant — 找第一個符合條件的子元素
// 用 AutomationId 搜尋(最推薦)
var startButton = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("StartButton"));
// 用 Name 搜尋
var stopButton = mainWindow.FindFirstDescendant(cf =>
cf.ByName("停止"));
// 用 ControlType 搜尋
var firstTextBox = mainWindow.FindFirstDescendant(cf =>
cf.ByControlType(ControlType.Edit));
// 組合條件
var tempInput = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("TemperatureInput")
.And(cf.ByControlType(ControlType.Edit)));
FindAllDescendants — 找所有符合條件的子元素
// 找到所有按鈕
var allButtons = mainWindow.FindAllDescendants(cf =>
cf.ByControlType(ControlType.Button));
// 找到所有 TextBox
var allInputs = mainWindow.FindAllDescendants(cf =>
cf.ByControlType(ControlType.Edit));
FindFirstChild — 只搜尋直接子元素
比 FindFirstDescendant 更快,但只搜一層:
var panel = mainWindow.FindFirstChild(cf =>
cf.ByAutomationId("MainPanel"));
var button = panel.FindFirstChild(cf =>
cf.ByAutomationId("StartButton"));
ConditionFactory (cf) 常用方法
| 方法 | 說明 |
|---|---|
cf.ByAutomationId("id") | 依 AutomationId 搜尋 |
cf.ByName("name") | 依 Name 搜尋 |
cf.ByClassName("class") | 依 ClassName 搜尋 |
cf.ByControlType(type) | 依 ControlType 搜尋 |
cf.ByText("text") | 依文字內容搜尋 |
.And(condition) | 組合條件(AND) |
.Or(condition) | 組合條件(OR) |
基本操作
點擊按鈕
var button = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("StartButton")).AsButton();
button.Invoke(); // 觸發按鈕(推薦,不需要焦點)
// 或
button.Click(); // 模擬滑鼠點擊
輸入文字
var textBox = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("IpAddressInput")).AsTextBox();
textBox.Text = "192.168.1.100"; // 直接設定值
// 或
textBox.Enter("192.168.1.100"); // 模擬鍵盤輸入
選擇 ComboBox
var comboBox = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("DeviceTypeCombo")).AsComboBox();
comboBox.Select("Modbus TCP"); // 依文字選擇
// 或
comboBox.Select(2); // 依索引選擇
ToggleSwitch / CheckBox
var toggle = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("AutoReconnectToggle")).AsToggleButton();
toggle.Toggle(); // 切換狀態
// 或 設定特定狀態
if (!toggle.IsToggled)
toggle.Toggle();
最小完整範例
using FlaUI.Core;
using FlaUI.UIA3;
// 啟動應用程式
var app = Application.Launch(@"C:\Program Files\DeviceControl\DeviceControl.exe");
using var automation = new UIA3Automation();
// 等待主視窗出現
var mainWindow = app.GetMainWindow(automation, TimeSpan.FromSeconds(15));
// 輸入設備 IP
var ipInput = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("DeviceIpInput")).AsTextBox();
ipInput.Text = "192.168.1.100";
// 點擊連線按鈕
var connectButton = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("ConnectButton")).AsButton();
connectButton.Invoke();
// 等待連線成功指示
var statusLabel = mainWindow.FindFirstDescendant(cf =>
cf.ByAutomationId("ConnectionStatus")).AsLabel();
// 等待文字變為「已連線」
Retry.WhileException(() =>
{
Assert.AreEqual("已連線", statusLabel.Text);
}, TimeSpan.FromSeconds(10));
// 結束
app.Close();
自動化操作時序
把上面範例的執行流程畫成 sequenceDiagram,可以看出 Test、FlaUI Application、Window、Element 之間的互動順序:
時序重點:
Launch+GetMainWindow:Launch 返回後不一定能立刻取得視窗,GetMainWindow內建等待邏輯FindFirstDescendant是同步呼叫:UIA 即時掃描目前的元素樹;若控制項尚未渲染需要搭配 Retry / 等待InvokevsClick:Invoke直接呼叫 UIA 的 InvokePattern(不需要焦點/座標),優先使用;Click模擬滑鼠座標點擊,遇到「按鈕無 InvokePattern」時才退回此法- 狀態變化要用
Retry.WhileException:UI 操作後的狀態更新是非同步的,Retry.WhileException會反覆評估直到斷言通過或 timeout
本指南結構
| 頁面 | 內容 |
|---|---|
| 概觀(本頁) | 核心概念、元素搜尋、基本操作 |
| 自動化 Pattern | Page Object、等待策略、DataGrid、Dialog、截圖 |
| 最佳實踐 | AutomationId、等待、測試隔離、CI |