Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ctypes.ArgumentError when using kivy with pywinauto

I have a kivy application that can interact with other windows using the pywinauto module. The application works fine in Linux (where pywinauto isn't used) but in Windows I get the following error, the application won't even start up:

C:\Program Files (x86)\Python36_64\lib\site-packages\pywinauto\__init__.py:80: UserWarning: Revert to STA COM threading mode
    warnings.warn("Revert to STA COM threading mode", UserWarning)
[INFO   ] [GL          ] NPOT texture support is available
[INFO   ] [Base        ] Start application main loop
Traceback (most recent call last):
File ".\application.py", line 368, in <module>
    Application().run()
File "C:\Program Files (x86)\Python36_64\lib\site-packages\kivy\app.py", line 826, in run
    runTouchApp()
File "C:\Program Files (x86)\Python36_64\lib\site-packages\kivy\base.py", line 477, in runTouchApp
    EventLoop.start()
File "C:\Program Files (x86)\Python36_64\lib\site-packages\kivy\base.py", line 164, in start
    provider.start()
File "C:\Program Files (x86)\Python36_64\lib\site-packages\kivy\input\providers\wm_touch.py", line 68, in start
    self.hwnd, GWL_WNDPROC, self.new_windProc)
ctypes.ArgumentError: argument 3: <class 'TypeError'>: wrong type

The reason I think it's an issue with pywinauto, is that I have the following lines and it works fine in Linux:

if SYSTEM == "Windows":
    import win32gui
    import win32process
    import wmi

    from pywinauto import application
    import pywinauto

Also I comment out the pywinauto import lines and it starts. It could be linked to this issue. I don't really know what code to include, as it works in other operating systems.... I'm assuming pywinauto is changing something that stops kivy from working.

My question is this: how can I have both the functionality of kivy and pywinauto in the same application?

like image 476
qr7NmUTjF6vbA4n8V3J9 Avatar asked Apr 30 '19 20:04

qr7NmUTjF6vbA4n8V3J9


1 Answers

I was able to reproduce the behavior using:

  • Python 3.7.3 x64
  • Kivy 1.10.1
  • Pywinauto 0.6.6

As a side note, I didn't work with any of the 2 packages before, I pip installed them specifically for this task.

Since I had no idea how to reproduce the behavior, I just copied the MCVE from [GitHub]: pywinauto/pywinauto - ctypes.ArgumentError @ click_input (that you also shared in the question), and slightly modified it (only to hit the error, no style, improvements, ... and so on).

code.py:

import random
from kivy.app           import App
from kivy.lang          import Builder
from kivy.core.window   import Window
from kivy.uix.boxlayout import BoxLayout

import pywinauto  # @TODO - cfati: moved after Kivy import(s), as it works otherwise (https://github.com/pywinauto/pywinauto/issues/419#issuecomment-488258224)


class DemoLayout(BoxLayout): pass
Builder.load_string("""
#: import datetime  datetime.datetime
<DemoLayout>:
  padding: 75

  Button:
    on_press: print(f"PRESSED @ {datetime.now()}")
""")


class Demo(App):

  def build(self):
    self.root = DemoLayout()

  def on_start(self):
    title = f"__KIVY_APP__{random.getrandbits(128)}"
    Window.set_title(title)
    hwnd = pywinauto.findwindows.find_window(title=title)
    app = pywinauto.Application()
    app.connect(handle=hwnd)
    window = app.window(handle=hwnd).wrapper_object()
    window.click_input(button="left", pressed="", coords=(100, 100), double=False, absolute=False)


Demo().run()

Output:

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q055928463]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code.py
[INFO   ] [Logger      ] Record log in C:\Users\cfati\.kivy\logs\kivy_19-05-01_83.txt
[INFO   ] [Kivy        ] v1.10.1
[INFO   ] [Python      ] v3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)]
[INFO   ] [Factory     ] 194 symbols loaded
[INFO   ] [Image       ] Providers: img_tex, img_dds, img_sdl2, img_pil, img_gif (img_ffpyplayer ignored)
[INFO   ] [Window      ] Provider: sdl2
[INFO   ] [GL          ] Using the "OpenGL" graphics system
[INFO   ] [GL          ] GLEW initialization succeeded
[INFO   ] [GL          ] Backend used <glew>
[INFO   ] [GL          ] OpenGL version <b'4.5.0 - Build 23.20.16.4973'>
[INFO   ] [GL          ] OpenGL vendor <b'Intel'>
[INFO   ] [GL          ] OpenGL renderer <b'Intel(R) HD Graphics 530'>
[INFO   ] [GL          ] OpenGL parsed version: 4, 5
[INFO   ] [GL          ] Shading version <b'4.50 - Build 23.20.16.4973'>
[INFO   ] [GL          ] Texture max size <16384>
[INFO   ] [GL          ] Texture max units <32>
[INFO   ] [Window      ] auto add sdl2 input provider
[INFO   ] [Window      ] virtual keyboard not allowed, single mode, not docked
 e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pywinauto\__init__.py:80: UserWarning: Revert to STA COM threading mode
   warnings.warn("Revert to STA COM threading mode", UserWarning)
[INFO   ] [Text        ] Provider: sdl2
[INFO   ] [Base        ] Start application main loop
 Traceback (most recent call last):
   File "code.py", line 36, in <module>
     Demo().run()
   File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\kivy\app.py", line 826, in run
     runTouchApp()
   File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\kivy\base.py", line 477, in runTouchApp
     EventLoop.start()
   File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\kivy\base.py", line 164, in start
     provider.start()
   File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\kivy\input\providers\wm_touch.py", line 68, in start
     self.hwnd, GWL_WNDPROC, self.new_windProc)
 ctypes.ArgumentError: argument 3: <class 'TypeError'>: wrong type

Before going further, I want to point out:

  1. [Python 3.Docs]: ctypes - A foreign function library for Python
  2. [MS.Docs]: SetWindowLongPtrW function

As seen from the latter, SetWindowLongPtrW's 3rd argument can be a DWORD, a HANDLE, a function pointer, depending on the 2nd argument value: basically it's a void* that can be mapped to anything.
Both modules call this function via ctypes:

  • Pywinauto ([GitHub]: pywinauto/pywinauto - (0.6.6) pywinauto/pywinauto/win32functions.py):

    try:
        SetWindowLongPtr    =   ctypes.windll.user32.SetWindowLongPtrW
        SetWindowLongPtr.argtypes = [win32structures.HWND, ctypes.c_int, win32structures.LONG_PTR]
        SetWindowLongPtr.restype = win32structures.LONG_PTR
    except AttributeError:
        SetWindowLongPtr = SetWindowLong
    
  • Kivy ([GitHub]: kivy/kivy - (1.10.1) kivy/kivy/input/providers/wm_common.py):

    try:
        windll.user32.SetWindowLongPtrW.restype = WNDPROC
        windll.user32.SetWindowLongPtrW.argtypes = [HANDLE, c_int, WNDPROC]
        SetWindowLong_wrapper = windll.user32.SetWindowLongPtrW
    except AttributeError:
        windll.user32.SetWindowLongW.restype = WNDPROC
        windll.user32.SetWindowLongW.argtypes = [HANDLE, c_int, WNDPROC]
        SetWindowLong_wrapper = windll.user32.SetWindowLongW
    

Explanation:

  • user32.dll is only loaded once in the current Python process
  • Code as above initializes the functions and typically is only executed once, at module import time (it can be executed as many times as desired, but it would decrease performance)
  • Both modules specify the function (windll.user32.SetWindowLongPtrW as we are on 64bit) prototype, but they do it differently
  • From the 3 above, results that the module that gets imported last, decides how the function prototype will look like
  • When the module that was imported 1st tries to use the prototype, it doesn't match the arguments passed, hence the error

That's why I had to move Pywinauto import after Kivy, so that Kivy tried to call the function using Pywinauto's prototype, otherwise it would have worked.
It's possible to go the other way around, but I didn't bother to find a scenario where Pywinauto would call the function, as it's not relevant.

Looking at the 2 ctypes prototypes and the C one (from MS URL), it turns out that:

  • Pywinauto is doing things correctly (I'm curious how it would work on 32bit, though)
  • Kivy only uses a SetWindowLongPtrW use-case (the one with the function pointer), and to make things easier, they adapted the prototype for their scenario. However, it doesn't quite match the C prototype, and this from my PoV, looks like a lame workaround (gainarie)

I modified my Kivy installation, and tadaa! (it's the Easter Bunny! :) ):

Working

Note: A (simpler) variant encountered here: [SO]: How to keep pynput and ctypes from clashing?



Update #0

I've submitted [GitHub]: kivy/kivy - SetWindowLongPtrW ctypes prototype bug, which was merged. Not sure when it will be available on the market (PyPI, so you can simply pip install it), though.

As an alternative, you could download the patch, and apply the changes locally. Check [SO]: Run/Debug a Django application's UnitTests from the mouse right click context menu in PyCharm Community Edition? (@CristiFati's answer) (Patching utrunner section) for how to apply patches on Win (basically, every line that starts with one "+" sign goes in, and every line that starts with one "-" sign goes out). I am using Cygwin, btw.
Or you could download the 3 modified files and overwrite your existing ones.
In any case, back them up first! Also, I don't know how the changes fit into older Kivy versions.

like image 195
CristiFati Avatar answered Sep 29 '22 13:09

CristiFati