简体   繁体   中英

How to get text out of a ListView or similar control using UI Automation?

I am trying to scrape a ListView like control from an external application. Now I am using System.Windows.Automation . With AutoIt v3 I've extracted the following information about the exact control I want to scrape text from:

>>>> 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

Now, I have noticed ID = 1510520 and by using it I would be able to get the control

AutomationElement element = AutomationElement.FromHandle(1510520);

The control looks like a ListView or similar to that, but I can't do anything else with it.

LisrView 类似控件

How I can now get the content of this control?

UPDATE:
Thanks to Jimi's recommendation inspect.exe from Windows 10 SDK worked the best! I was able to drill down to DataGridView.

在此处输入图片说明

I assume you can find the Window that contains the DataGridView from which to extract the data. The GeDataGridViewDataTable() method expects the Handle of that Window.

Let's break down these methods:

To get the AutomationElement of a Window of interest when its handle is already known, we could just use window = AutomationElement.FromHandle([Window Handle]) .

▶ Here I'm using an AndCodition because you may have ProcessID and Window Title, so instead of filtering using AutomationElement.ControlTypeProperty and AutomationElement.NativeWindowHandleProperty , you could use the AutomationElement.ProcessIdProperty and AutomationElement.NameProperty as the conditions.

If the Window is found, the first child elements in the TreeScope.SubTree scope (all the UI elements in that Window) are parsed to find the first element of type Table ( ControlType.Table ).

▶ Of course that Window might host more than one DataGridView: in this case, we can use FindAll() instead of FindFirst() , then determine which is which using some other condition (the number of Columns, the text of Headers, content of Cells, location, size, parent container etc.).

When the DataGridView of interest is found, we can extract the content of its Cells.
Here comes the second method, GetDataGridViewRowsCollection() :

  • The first OrCondition filters out the DataGridView Scrollbars and, possibly, other child Controls of the grid (a customization may include some).
  • After, we check whether the DGV has a Header: if it does, the first child Row element name is Top Row . We can then use the header text to name the Columns of the DataTable that will store the data extracted. Otherwise, just add some default names.
  • Then, for each Row element, we enumerate its child elements, representing the Cells. I've added a NotCondition filter to exclude the Row Header Cell, ControlType.Header , if any.
  • We then iterate the Cell's collection, extract their value using the GetCurrentPropertyValue() method, setting the Property Type to ValuePattern.ValueProperty , add these values to a List which will provide the param array argument of 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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM