Search code examples
pythonpywinauto

pywinauto find_elements() returns ElementNotFoundError


I'm trying to automate a simple app using pywinauto & Python 3.6. The app has a Windows "Open" dialog like this and I want to click on the "Cancel" button: enter image description here

I used SWAPY to get the class_name and control_id attributes for the button. enter image description here

Now the problem is that when I call the find_element() method with these parameters it raises an ElementNotFoundError. Here's my code:

cancel_button = pywinauto.findwindows.find_element(class_name="button", control_id=2)

I've tried (class_name="button", control_id="2"), (class_name="Button", control_id=2) but they all give the same error. The same problem occurs for any other element I try to find on this dialog.

So how do I use the attributes read from SWAPY? I didn't find the official pywinauto documentation to be very useful. It doesn't explain a lot of things clearly.

EDIT: I decided not to use the find_elements method and instead used find_windows() to get a handle to the Open dialog.

w_open_handle = pywinauto.findwindows.find_windows(title=u'Open', class_name='#32770')[0]

I then get a WindowSpecification object using this handle:

w_open = app.window_(handle=w_open_handle)

I then call:

w_open['Cancel'].click()

and this works. Now I want to enter a file name in the "File name:" edit box and click on Open button to open that file. So I do this:

w_open['File name:'].type_keys("abc.txt")

This works. I printed out the control identifiers using print_control_identifiers() and got the name for Open button. So using draw_outline() I draw a boundary outside it and it shows the correct button.

w_open['SplitButton6'].draw_outline()

But calling .click() method on 'SplitButton6' throws a WindowSpecification class has no 'click' method error. Any idea what's causing this? The error seems to be misleading since WindowSpec class does have a .click method.


Solution

  • Correct answer is that you missed top_level_only=False (it's True by default because higher level API calls it at least twice). Then you may have 2 controls matching this criterion (maybe from different applications). find_element is a low level function. I wouldn't recommend its direct usage (the code is too long, there are many pitfalls that were taken into account on a higher level API).

    >>> pywinauto.findwindows.find_element(class_name="Button", control_id=2, top_level_only=False)
    Traceback (most recent call last):
      File "<interactive input>", line 1, in <module>
      File "...\pywinauto\findwindows.py", line 98, in find_element
        raise exception
    ElementAmbiguousError: There are 2 elements that match the criteria {'class_name': 'Button', 'control_id': 2, 'top_level_only': False}
    
    >>> pywinauto.findwindows.find_element(class_name="Button", title='Cancel', top_level_only=False)
    <win32_element_info.HwndElementInfo - 'Cancel', Button, 395554>
    

    Using higher level API (Application object and WindowSpecifications described in the Guide) you shouldn't care about passing process id, backend name and other things to find_element every time.

    P.S. In my mind SWAPY could be significantly improved, but it's not maintained last year. I hope to re-write it in the future with smaller code base and MS UI Automation support. But currently fully automatic script generator is a higher priority.


    EDIT:

    This button w_open['SplitButton6'].draw_outline() could be detected as general HwndWrapper object instead of ButtonWrapper. You can check it using this:

    w_open['SplitButton6'].wrapper_object()
    

    And this is what exactly written in the Getting Started Guide (which you said you've read).

    Fortunately you can use method .click_input() for any control:

    w_open['SplitButton6'].click_input()
    

    I can say more: WindowSpecification does NOT have click method. It's a method of ButtonWrapper which is instantiated dynamically. For example these statements work the same way (but Python can hide .wrapper_object() call):

    w_open['SplitButton6'].wrapper_object().click_input()
    w_open['SplitButton6'].click_input()
    

    And again this is all described in the Getting Started Guide. Please read the whole guide. You will find many useful high level things. I can advise for some corner cases if something is still not clear.