如何使用 UI 自动化从 ListView 或类似控件中获取文本?
How to get text out of a ListView or similar control using UI Automation?
我正在尝试从外部应用程序中抓取类似 ListView 的控件。现在我正在使用 System.Windows.Automation
。使用 AutoIt v3,我提取了以下关于我想从中抓取文本的确切控件的信息:
>>>> Control <<<<
Class: WindowsForms10.Window.8.app.0.34f5582_r6_ad1
Instance: 20
ClassnameNN: WindowsForms10.Window.8.app.0.34f5582_r6_ad120
Name:
Advanced (Class): [CLASS:WindowsForms10.Window.8.app.0.34f5582_r6_ad1; INSTANCE:20]
ID: 1510520
Text:
Position: 182, 164
Size: 1411, 639
ControlClick Coords: 300, 202
Style: 0x56010000
ExStyle: 0x00000000
Handle: 0x0000000000170C78
现在,我注意到了ID = 1510520
并且通过使用它我将能够获得控制权
AutomationElement element = AutomationElement.FromHandle(1510520);
该控件看起来像一个 ListView 或类似的,但我不能用它做任何其他事情。
现在如何获取此控件的内容?
更新:
感谢 Jimi inspect.exe 来自 Windows 的推荐 10 SDK 效果最好!我能够深入到 DataGridView。
我假设您可以找到包含要从中提取数据的 DataGridView 的 Window。 GeDataGridViewDataTable()
方法需要 Window.
的句柄
让我们分解这些方法:
要在其句柄已知时获取感兴趣的 Window 的 AutomationElement,我们可以只使用 window =
AutomationElement.FromHandle([Window Handle])
.
▶ 我在这里使用 AndCodition 因为您可能有 ProcessID 和 Window 标题,所以您可以使用 AutomationElement.ControlTypeProperty
和 AutomationElement.NativeWindowHandleProperty
而不是过滤AutomationElement.ProcessIdProperty
和 AutomationElement.NameProperty
作为条件。
如果找到 Window,则 TreeScope.SubTree
范围中的第一个 child 元素(所有 UI 元素其中 Window) 被解析以找到类型 Table (ControlType.Table).
的第一个元素
▶ 当然 Window 可能承载多个 DataGridView:在这种情况下,我们可以使用 FindAll()
而不是 FindFirst()
,然后使用其他条件确定哪个是哪个(列数、Header 的文本、单元格内容、位置、大小、parent 容器等)。
找到感兴趣的DataGridView后,我们就可以提取其Cells的内容了。
第二种方法来了,GetDataGridViewRowsCollection()
:
- 第一个 OrCondition 过滤掉 DataGridView 滚动条和可能的其他 child 网格控件(自定义可能包括一些)。
- 之后,我们检查DGV是否有Header:如果有,第一个child行元素名称是
Top Row
.然后我们可以使用 header 文本来命名将存储提取数据的数据列 Table。否则,只需添加一些默认名称即可。
- 然后,对于每个 Row 元素,我们枚举其 child 个元素,代表 Cells。我添加了一个 NotCondition 过滤器以排除行 Header 单元格,
ControlType.Header
,如果有的话。
- 然后我们迭代 Cell 的 collection,使用 GetCurrentPropertyValue() method, setting the Property Type to ValuePattern.ValueProperty 提取它们的值,将这些值添加到列表中,该列表将提供 的
param
数组参数DataTable.Rows.Add()
.
private DataTable GeDataGridViewDataTable(IntPtr windowHwnd)
{
var condition = new AndCondition(
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window),
new PropertyCondition(AutomationElement.NativeWindowHandleProperty, windowHwnd.ToInt32())
);
var window = AutomationElement.RootElement.FindFirst(TreeScope.Children, condition);
if (window == null) return null;
var dgv = window.FindFirst(TreeScope.Subtree,
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Table));
if (dgv == null) return null;
var dt = GetDataGridViewRowsCollection(dgv);
return dt;
}
private DataTable GetDataGridViewRowsCollection(AutomationElement dgv)
{
var dt = new DataTable();
// Skips ScrollBars and other child elements
var condition = new OrCondition(
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Custom),
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Header)
);
var rows = dgv.FindAll(TreeScope.Children, condition).OfType<AutomationElement>().ToList();
bool hasColumnHeader = (rows[0].Current.Name == "Top Row");
// First element is the Header (if there's one)
var dgvHeaderColumns = rows[0].FindAll(TreeScope.Children, Condition.TrueCondition);
// Skip the Top/Left header
for (int i = 1; i < dgvHeaderColumns.Count; i++) {
dt.Columns.Add(hasColumnHeader ? dgvHeaderColumns[i].Current.Name : "Column"+i);
}
// Skips the Row Header, if any
var notCondition = new NotCondition(new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Header));
foreach (AutomationElement row in rows) {
var cells = row.FindAll(TreeScope.Children, notCondition);
var values = new List<object>();
foreach (AutomationElement cell in cells) {
values.Add(cell.GetCurrentPropertyValue(ValuePattern.ValueProperty));
}
dt.Rows.Add(values.ToArray());
}
return dt;
}
我正在尝试从外部应用程序中抓取类似 ListView 的控件。现在我正在使用 System.Windows.Automation
。使用 AutoIt v3,我提取了以下关于我想从中抓取文本的确切控件的信息:
>>>> Control <<<<
Class: WindowsForms10.Window.8.app.0.34f5582_r6_ad1
Instance: 20
ClassnameNN: WindowsForms10.Window.8.app.0.34f5582_r6_ad120
Name:
Advanced (Class): [CLASS:WindowsForms10.Window.8.app.0.34f5582_r6_ad1; INSTANCE:20]
ID: 1510520
Text:
Position: 182, 164
Size: 1411, 639
ControlClick Coords: 300, 202
Style: 0x56010000
ExStyle: 0x00000000
Handle: 0x0000000000170C78
现在,我注意到了ID = 1510520
并且通过使用它我将能够获得控制权
AutomationElement element = AutomationElement.FromHandle(1510520);
该控件看起来像一个 ListView 或类似的,但我不能用它做任何其他事情。
现在如何获取此控件的内容?
更新:
感谢 Jimi inspect.exe 来自 Windows 的推荐 10 SDK 效果最好!我能够深入到 DataGridView。
我假设您可以找到包含要从中提取数据的 DataGridView 的 Window。 GeDataGridViewDataTable()
方法需要 Window.
让我们分解这些方法:
要在其句柄已知时获取感兴趣的 Window 的 AutomationElement,我们可以只使用 window =
AutomationElement.FromHandle([Window Handle])
.
▶ 我在这里使用 AndCodition 因为您可能有 ProcessID 和 Window 标题,所以您可以使用 AutomationElement.ControlTypeProperty
和 AutomationElement.NativeWindowHandleProperty
而不是过滤AutomationElement.ProcessIdProperty
和 AutomationElement.NameProperty
作为条件。
如果找到 Window,则 TreeScope.SubTree
范围中的第一个 child 元素(所有 UI 元素其中 Window) 被解析以找到类型 Table (ControlType.Table).
▶ 当然 Window 可能承载多个 DataGridView:在这种情况下,我们可以使用 FindAll()
而不是 FindFirst()
,然后使用其他条件确定哪个是哪个(列数、Header 的文本、单元格内容、位置、大小、parent 容器等)。
找到感兴趣的DataGridView后,我们就可以提取其Cells的内容了。
第二种方法来了,GetDataGridViewRowsCollection()
:
- 第一个 OrCondition 过滤掉 DataGridView 滚动条和可能的其他 child 网格控件(自定义可能包括一些)。
- 之后,我们检查DGV是否有Header:如果有,第一个child行元素名称是
Top Row
.然后我们可以使用 header 文本来命名将存储提取数据的数据列 Table。否则,只需添加一些默认名称即可。 - 然后,对于每个 Row 元素,我们枚举其 child 个元素,代表 Cells。我添加了一个 NotCondition 过滤器以排除行 Header 单元格,
ControlType.Header
,如果有的话。 - 然后我们迭代 Cell 的 collection,使用 GetCurrentPropertyValue() method, setting the Property Type to ValuePattern.ValueProperty 提取它们的值,将这些值添加到列表中,该列表将提供 的
param
数组参数DataTable.Rows.Add()
.
private DataTable GeDataGridViewDataTable(IntPtr windowHwnd)
{
var condition = new AndCondition(
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window),
new PropertyCondition(AutomationElement.NativeWindowHandleProperty, windowHwnd.ToInt32())
);
var window = AutomationElement.RootElement.FindFirst(TreeScope.Children, condition);
if (window == null) return null;
var dgv = window.FindFirst(TreeScope.Subtree,
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Table));
if (dgv == null) return null;
var dt = GetDataGridViewRowsCollection(dgv);
return dt;
}
private DataTable GetDataGridViewRowsCollection(AutomationElement dgv)
{
var dt = new DataTable();
// Skips ScrollBars and other child elements
var condition = new OrCondition(
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Custom),
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Header)
);
var rows = dgv.FindAll(TreeScope.Children, condition).OfType<AutomationElement>().ToList();
bool hasColumnHeader = (rows[0].Current.Name == "Top Row");
// First element is the Header (if there's one)
var dgvHeaderColumns = rows[0].FindAll(TreeScope.Children, Condition.TrueCondition);
// Skip the Top/Left header
for (int i = 1; i < dgvHeaderColumns.Count; i++) {
dt.Columns.Add(hasColumnHeader ? dgvHeaderColumns[i].Current.Name : "Column"+i);
}
// Skips the Row Header, if any
var notCondition = new NotCondition(new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Header));
foreach (AutomationElement row in rows) {
var cells = row.FindAll(TreeScope.Children, notCondition);
var values = new List<object>();
foreach (AutomationElement cell in cells) {
values.Add(cell.GetCurrentPropertyValue(ValuePattern.ValueProperty));
}
dt.Rows.Add(values.ToArray());
}
return dt;
}