pywinauto: DataGrid not wrapped correctly, can't access any cells

I have an ancient app that is using DataGrid. Listing all other controls in the app works (using both win32 and UIA backends), but i can’t get any children from DataGrid (can’t even locate this control using win32 backend). “Inspect.exe” is able to list all details in MSAA mode, even traverse over siblings (cells) in any direction and read it’s content

pywinauto 0.6.4 on python3.6.4 / x64, using UIA backend.

Reading through the sources it looks like the DataGrid should be wrapped in uia_controls.ListView, but here it is wrapped just in the generic UIAWrapper.

>>> from pprint import pprint as pp
>>> pp(win.children()[0].children()[0].children())  # subwindow of a main window
[<uia_controls.ButtonWrapper - 'OK', Button, 3713039909540564731>,
 <uia_controls.ButtonWrapper - 'X', Button, 3713039909750574581>,
 <uia_controls.EditWrapper - '', Edit, 3713039909791710531>,
 <uiawrapper.UIAWrapper - 'DataGrid', Table, 3713039980558534831>,
 <uia_controls.ComboBoxWrapper - '', ComboBox, 3713040619033944881>]
>>> dg = win.children()[0].children()[0].children()[4]
>>> dg
<uiawrapper.UIAWrapper - 'DataGrid', Table, 3713039980558534831>
>>> dg.texts()
['DataGrid']
>>> dg.children()  # this should return quite a lot of items
[]
>>> dg.iface_grid
Traceback (most recent call last):
  File "C:\Users\User\AppData\Local\Programs\Python\Python36\lib\site-packages\pywinauto\uia_defines.py", line 232, in get_elem_interface
    iface = cur_ptrn.QueryInterface(cls_name)
  File "C:\Users\User\AppData\Local\Programs\Python\Python36\lib\site-packages\comtypes\__init__.py", line 1158, in QueryInterface
    self.__com_QueryInterface(byref(iid), byref(p))
ValueError: NULL COM pointer access

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\User\AppData\Local\Programs\Python\Python36\lib\site-packages\pywinauto\controls\uiawrapper.py", line 131, in __get__
    value = self.fget(obj)
  File "C:\Users\User\AppData\Local\Programs\Python\Python36\lib\site-packages\pywinauto\controls\uiawrapper.py", line 265, in iface_grid
    return uia_defs.get_elem_interface(elem, "Grid")
  File "C:\Users\User\AppData\Local\Programs\Python\Python36\lib\site-packages\pywinauto\uia_defines.py", line 234, in get_elem_interface
    raise NoPatternInterfaceError()
pywinauto.uia_defines.NoPatternInterfaceError
>>>

when using win32 backend, this is all i get from the control:

>>> pp(win.children())
[<hwndwrapper.HwndWrapper - '', WindowsForms10.Window.8.app.0.2780b98, 524706>,
 <win32_controls.ButtonWrapper - 'OK', Button, 590198>,
 <win32_controls.ButtonWrapper - 'X', Button, 590260>,
 <win32_controls.EditWrapper - '', Edit, 590238>,
 <hwndwrapper.HwndWrapper - '', WindowsForms10.Window.8.app.0.2780b98, 524594>,
 <hwndwrapper.HwndWrapper - '', WindowsForms10.SCROLLBAR.app.0.2780b98, 524670>,
 <hwndwrapper.HwndWrapper - '', WindowsForms10.SCROLLBAR.app.0.2780b98, 917850>,
 <win32_controls.ComboBoxWrapper - 'Company', ComboBox, 1245528>,
 <win32_controls.EditWrapper - 'Company', Edit, 524672>,
 <win32_controls.ComboBoxWrapper - '', ComboBox, 1376596>,
 <win32_controls.EditWrapper - '', Edit, 852300>,
 <hwndwrapper.HwndWrapper - '', WindowsForms10.Window.8.app.0.2780b98, 590184>,
 <win32_controls.StaticWrapper - 'Infopanel', Static, 1180004>]

DataGrid should be located in the ‘WindowsForms10.Window.8.app.0.2780b98, 524594’ window, but it contains only two scrollbars and nothing else.

Any way to get the data out of the DataGrid?

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 1
  • Comments: 25 (10 by maintainers)

Most upvoted comments

Hi @vasily-v-ryabov , Do we have a solution for this?

I need to get the number of rows in a table and click a particular row to open the details.

@vasily-v-ryabov I was able the click and do the next action.

resultsTable.Custom3.click_input(double=True)

Hi all. When I try implementing this functionality all I get printed out is: uiawrapper.UIAWrapper - ‘’, Pane uiawrapper.UIAWrapper - ‘’, Pane uiawrapper.UIAWrapper - ‘Graphical’, Pane uiawrapper.UIAWrapper - ‘Panel1’, Pane uiawrapper.UIAWrapper - ‘’, Pane

what exactly is this output attempting to tell me? I expected to see cell and identifiable properties.

any help would be great. Thanks Heaps Karl

Here is patch with added .children_generator(). No changes to base_wrapper.py as this is so far limited to UIA backend. It should support same conditions as generic .children() method (caching option is silently discarded)

diff --git a/pywinauto/controls/uiawrapper.py b/pywinauto/controls/uiawrapper.py
index 808724c..2424fbf 100644
--- a/pywinauto/controls/uiawrapper.py
+++ b/pywinauto/controls/uiawrapper.py
@@ -382,6 +382,17 @@ class UIAWrapper(BaseWrapper):
         return self.friendlyclassname
 
     # -----------------------------------------------------------
+    def children_generator(self, **kwargs):
+        """
+        Return the children of this element as a list
+
+        It returns a generator of BaseWrapper (or subclass) instances.
+        """
+        child_elements = self.element_info.children_generator(**kwargs)
+        for element_info in child_elements:
+            yield self.backend.generic_wrapper_class(element_info)
+
+    # -----------------------------------------------------------
     def is_keyboard_focusable(self):
         """Return True if the element can be focused with keyboard"""
         return self.element_info.element.CurrentIsKeyboardFocusable == 1
diff --git a/pywinauto/uia_element_info.py b/pywinauto/uia_element_info.py
index b77e02c..5b83a67 100644
--- a/pywinauto/uia_element_info.py
+++ b/pywinauto/uia_element_info.py
@@ -278,6 +278,26 @@ class UIAElementInfo(ElementInfo):
         cond = IUIA().build_condition(**kwargs)
         return self._get_elements(IUIA().tree_scope["children"], cond, cache_enable)
 
+    def children_generator(self, **kwargs):
+        """Return generator of only immediate children of the element
+
+         * **kwargs** is a criteria to reduce a list by process,
+           class_name, control_type, content_only and/or title.
+        """
+        # it is one-time and one-way walk, caching makes no sense here
+        kwargs.pop('cache_enable', False)
+        cond = IUIA().build_condition(**kwargs)
+        walker = IUIA().iuia.CreateTreeWalker(cond)
+        element = walker.GetFirstChildElement(self._element)
+        if not element:
+            return
+        yield UIAElementInfo(element)
+        while True:
+            element = walker.GetNextSiblingElement(element)
+            if not element:
+                return
+            yield UIAElementInfo(element)
+
     def descendants(self, **kwargs):
         """Return a list of all descendant children of the element
 

Usage:

>>> app = pywinauto.Application(backend="uia").connect(title="Some Application")
>>> gen = app.top_window().children_generator()
>>> for element in gen: print(element)
uiawrapper.UIAWrapper - '', Custom
[ ... ]
>>>

Small update - i’m testing generator walk over large datagrid (4435 rows, 9 columns, that’s 39915 cells + 4435 wrapper rows = total 44350 elements). There is still CPU load spike while iterating over the elements (strangely, it’s quite early, usually within first 300 elements), but otherwise it works and renders all elements. RAM footprint is quite stable (around 119MB in main process).

That <300 hang looks like there is some optimization in the UIAutomation library and only some elements are made available from start. When the “magic number” of elements are exhausted, it hangs for some time with 100% CPU load (probably collecting pointers to remaining elements), then it continues without further delays till the end (more elements = longer cpu spike). But that’s pure speculation.

It’s possible FindAll approach works the same way and would return all children too, if i’d extended it’s timeout, but it probably allocates RAM for element list in process memory before sending it back (i’ve seen 1.4G RAM used and not freed when i tried to use .children() on this datagrid)

I’ll draw complete tree of the app as printed by app.top_window().print_control_identifiers() with one datagrid subwindow opened, just to have something to refer to (in previous message i’ve posted only the row container and it obviously created some confusion). Extra info is stripped, only control_type, auto_id and title (where applicable) is included. Let’s pretend the datatable has three columns:

Window / FormMain / title=AppTitle
  |  Window / FormRoll / title=Orders
  |  |  Pane / panel1
  |  |  |  Button / bnQuickCmd / title=New
  |  |  |  [ .. some extra buttons ..]
  |  |  |  Table / dataGrid1 / title=DataGrid
  |  |  |  |  ScrollBar / <random>
  |  |  |  |  |  Button / title=ColumnLeft
  |  |  |  |  |  Thumb / title=Position
  |  |  |  |  |  Button / title=PageRight
  |  |  |  |  |  Button / title=ColumnRight
  |  |  |  |  Header / title=<col1_title>
  |  |  |  |  Header / title=<col2_title>
  |  |  |  |  Header / title=<col3_title>
  |  |  |  |  Custom / title=N
  |  |  |  |  | Custom / title=<col1_title>
  |  |  |  |  | Custom / title=<col2_title>
  |  |  |  |  | Custom / title=<col3_title>
  |  |  |  |  Custom / title=N
  |  |  |  |  | Custom / title=<col1_title>
  |  |  |  |  | Custom / title=<col2_title>
  |  |  |  |  | Custom / title=<col3_title>
  |  |  |  |  Custom / title=N
  |  |  |  |  | Custom / title=<col1_title>
  |  |  |  |  | Custom / title=<col2_title>
  |  |  |  |  | Custom / title=<col3_title>
  [ .. many other rows .. ]

this is how it should look like, and is consistent with MS .NET docs. However after several rows (row container is the N element) the output is (sometimes) mangled and row elements are not rendered at all, it continues like this:

  |  |  |  |  Custom / title=N
  |  |  |  |  | Custom / title=<col1_title>
  |  |  |  |  | Custom / title=<col2_title>
  |  |  |  |  | Custom / title=<col3_title>
  |  |  |  | Custom / title=<col1_title>   #  <-- mangled from here
  |  |  |  | Custom / title=<col2_title>
  |  |  |  | Custom / title=<col3_title>
  |  |  |  | Custom / title=<col1_title>
  |  |  |  | Custom / title=<col2_title>
  |  |  |  | Custom / title=<col3_title>
  |  |  |  | Custom / title=<col1_title>
  |  |  |  | Custom / title=<col2_title>
  |  |  |  | Custom / title=<col3_title>

But this is not a real problem (at least not for me) as this is probably just some mishap in the printing function.

As you see, every ScrollBar, Header, N and Custom element is descendant of DataGrid element (control_type Table), but only ScrollBar, Header and N should show up in .children(), right? Well, that’s not the case. When i open my smallest DataGrid window with 35 rows / 17 columns, i should get 1 ScrollBar, 17 Header elements and 35 N rows, that’s 53 elements. Let’s see:

>>> # app / subwindow / pane /  datagrid element after some buttons
>>> dg = app.top_window().children()[0].children()[0].children()[6]  
>>> dg
<uiawrapper.UIAWrapper - 'DataGrid', Table, 3713043101523575881>
>>> len(dg.children())
648
>>> len(dg.descendants())
1247

Here, dg.children() contains 1 ScrollBar (but not it’s buttons) and EVERY header and cell (1 + 17 + 35 + 17 * 35). I don’t know why it contains children of N elements, but not children of ScrollBar element. dg.descendants() contains everything and from the counts it looks like each cell is duplicated in the output (1 + 4buttons + 17 + 35 + (17 * 35) * 2).

To reiterate previous findings - ANY .children() call on DataGrid element probably scans each cell and when the table has more than few rows it quickly ends up with one of following errors:

  • Timeout Exception
  • COM Unspecified Error Exception
  • zero children returned (and no exception) In every case (failed or not) it leaks memory in original app.

Using FindAll() is discouraged for elements that can lead to deep tree scans (like calling it on Desktop root element) as it can quickly lead to stack overflow. I guess that’s what happens when zero children are returned.

I’ve switched to TreeWalker wrapped in generator and so far it works like a charm. Memory footprint looks stable, it’s pretty fast (no 100% CPU load for several seconds) and i can bail-out early when searched row was found without the need to scan rest of the table.

Digging deeper, it looks the app does something weird with the DataGrid element. Calling GetCurrentPattern() on the element returns non-null pointer only for pattern id 10018 (LegacyIAccessible) and calling legacy_properties() indeed does return some data.

>>> dg.legacy_properties()
{'ChildId': 0, 'DefaultAction': '', 'Description': '', 'Help': '', 'KeyboardShortcut': '', 
 'Name': 'DataGrid', 'Role': 24, 'State': 1048576, 'Value': ''}
>>> dg.element_info.element.CurrentAutomationId
'dataGrid1'

Still i haven’t found a way to list children. Calling any Find* method with any combination of children/descendants and any condition hangs for some time and ends up with following exception

>>> cond = IUIA().build_condition()
>>> scope = IUIA().tree_scope['children']
>>> dg.element_info.element.FindAll(scope, cond)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
_ctypes.COMError: (-2147467259, 'Unspecified error', (None, None, None, 0, None))

However “Inspect.exe” shows that something is probably broken in the application itself. Getting info about cell element by mouse-hover mode i get something like this (MSAA mode):

How found: 
        Mouse move (243,296) hwnd=0x00080132 64bit class="WindowsForms10.Window.8.app.0.2780b98" style=0x56010000 ex=0x0
ChildId:
        0
Interfaces:
        IEnumVARIANT IOleWindow
Impl:
        Remote native IAccessible
[...]
Other Props:
        Object has no additional properties
Children:
        Container has no children
Ancestors:
        "N" : Row : Focus, Select
        "DataGrid" : Table : Focus
        "DataGrid" : Window : Focus
        none : Client : Focus
        none : Window : Focus
        "[dialog window title]" : Client : Focus
        "[dialog window title]" : Window : Change size,Move,Focus
	"Desktop" : Client : Focus
	"Desktop" : Window : Focus
	[ No Parent ]

In UI Automation mode, Ancestors looks like this:

Ancestors:
        "N" 
        "DataGrid" table
        "" Subwindow
        "[dialog window title]" Window
        "[app window title]" Window
        "Desktop" Subwindow
	[ No Parent ]

In both modes it shows some empty window between DataGrid and window. Stepping up to parent works until it should inspect this empty window, then Inspect.exe hangs for quite a long time (roughly the same time as python before throwing exception)