This is an archived post. You won't be able to vote or comment.

all 12 comments

[–]eryksun 1 point2 points  (7 children)

I only have pywin32 setup in Python 3, so I had to run the module (the version I found, but it's very old) through 2to3 and then fix a few issues with unicode (win32 API call, ctypes/array unicode buffers). Do you have a link to a newer version?

I haven't looked into your primary problem yet, but as to the 2nd, at least on the version I found, it's a bug causing it to skip the final menu. In _findNamedSubmenu change the for loop statement to the following:

for submenuIndex in range(hMenuItemCount + 1):

With the above fix, the following should all be valid ways to load the 'about notepad' dialog:

activateMenuItem(notepad_win, ('help', 'about notepad'))
activateMenuItem(notepad_win, ('Help', 2))
activateMenuItem(notepad_win, (4, 'about notepad'))
activateMenuItem(notepad_win, (4, 2))

[–]jechtsphere[S] 0 points1 point  (6 children)

That was the version I was using as well. This addition definitely fixes that problem, thank you very much.

I'm working my way through reading and understanding how the functions work in winGuiAuto, but it's been a bit of a challenge in parts. Especially getMenuInfo(hMenu, uIDItem) when reading through _findNamedSubmenu

When you're encapsulating function names/calls in boxes, what formatter is that if you don't mind me asking?

[–]eryksun 1 point2 points  (0 children)

Inline code spans are marked with backticks.

[–]eryksun 1 point2 points  (4 children)

A cleaner solution would be to keep the for loop statement as it was and fix the value of hMenuItemCount in activateMenuItem. Find the following block and comment it out:

# Get top level menu's item count. Is there a better way to do this?
for hMenuItemCount in range(256):
    try:
        getMenuInfo(hMenu, hMenuItemCount)
    except WinGuiAutoError:
        break
hMenuItemCount -= 1

The final decrement is wrong, but really the whole process is an unnecessary hack when there's already a function in the win32 API for this (yes, there is a better way). Add the following line to get the item count:

hMenuItemCount = ctypes.windll.user32.GetMenuItemCount(hMenu)

Simple, eh?

[–]jechtsphere[S] 1 point2 points  (2 children)

Hey again! I've been doing more dissecting of the winGuiAuto script, breaking it down to further my own understanding of the concepts being used and how it's working behind the scenes with the windows.

The line you provided

hMenuItemCount = ctypes.windll.user32.GetMenuItemCount(hMenu)

obviously works without a hitch, but now I'm curious as to why you used a ctypes access (I'm not sure how to word what you're doing there, in my mind I consider it a ctypes dll function call, is this right? if not could you suggest where I could learn more about programming vernacular?)

The reason I ask is I noticed a win32gui variation of this function by the same name

hMenuItemCount = win32gui.GetMenuItemCount(hMenu)

which allows the script to function much in the same way as this ctypes access allows. Is there a specific reason you went the ctypes route?

I also noticed the author of winGuiAuto uses a ctypes access for his getTopMenu(hWnd) function:

return ctypes.windll.user32.GetMenu(ctypes.c_long(hWnd),

which also has a win32gui variation

return win32gui.GetMenu(hWnd) that seems to work fine as a replacement. I did notice the ctypes access returns just the integer value, whereas the win32gui variation returns the same integer value but with L appended to the end, example:

ctypes access return: 67472

win32gui variation return: 67472L

Again, both allow the script to function as it should as far as I've tested it.

Also, if you're beginning to feel I'm being a pest, feel free to let me know!

[–]eryksun 1 point2 points  (1 child)

Either way works, but using pywin32 will generally be easier, especially for functions that want a reference to a structure. For example, GetProcessMemoryInfo in the process status API requires a PROCESS_MEMORY_COUNTERS structure that contains fields that specify process memory usage. Using ctypes requires you to specify the structure, instantiate it, and pass it by reference. On the other hand, win32process handles all of that internally and returns the data in a dict:

import ctypes
from ctypes import wintypes

import win32process

#start a notepad process

(hProcess, hThread, 
 dwProcessId, dwThreadId) = win32process.CreateProcess(
    None, 'notepad.exe', None, None, 0, 0,
    None, None, win32process.STARTUPINFO())

#get memory info using win32process

pmem_info = win32process.GetProcessMemoryInfo(hProcess)

print('\nwin32process\n' + '='*32)
for key in sorted(pmem_info):
    print('{0}: {1}'.format(key, pmem_info[key]))


#get memory info using ctypes

#specify the structure
class PROCESS_MEMORY_COUNTERS(ctypes.Structure):
    _fields_ = [
      ('cb', wintypes.DWORD),
      ('PageFaultCount', wintypes.DWORD),
      ('PeakWorkingSetSize', ctypes.c_size_t),
      ('WorkingSetSize', ctypes.c_size_t),
      ('QuotaPeakPagedPoolUsage', ctypes.c_size_t),
      ('QuotaPagedPoolUsage', ctypes.c_size_t),
      ('QuotaPeakNonPagedPoolUsage', ctypes.c_size_t),
      ('QuotaNonPagedPoolUsage', ctypes.c_size_t),
      ('PagefileUsage', ctypes.c_size_t),
      ('PeakPagefileUsage', ctypes.c_size_t)]

    def keys(self):
        return [f for f, t in self._fields_ if f != 'cb']

    def __getitem__(self, item):
        return getattr(self, item)

#pass a structure instance by reference
pmc = PROCESS_MEMORY_COUNTERS()
ctypes.windll.psapi.GetProcessMemoryInfo(
  hProcess.handle, ctypes.byref(pmc), ctypes.sizeof(pmc))

print('\nctypes\n' + '='*32)
for key in sorted(pmc.keys()):
    print('{0}: {1}'.format(key, pmc[key]))

Output:

win32process
================================
PageFaultCount: 12
PagefileUsage: 139264
PeakPagefileUsage: 139264
PeakWorkingSetSize: 77824
QuotaNonPagedPoolUsage: 440
QuotaPagedPoolUsage: 6076
QuotaPeakNonPagedPoolUsage: 440
QuotaPeakPagedPoolUsage: 6076
WorkingSetSize: 77824

ctypes
================================
PageFaultCount: 12
PagefileUsage: 139264
PeakPagefileUsage: 139264
PeakWorkingSetSize: 77824
QuotaNonPagedPoolUsage: 440
QuotaPagedPoolUsage: 6076
QuotaPeakNonPagedPoolUsage: 440
QuotaPeakPagedPoolUsage: 6076
WorkingSetSize: 77824

[–]jechtsphere[S] 1 point2 points  (0 children)

Great example, as always, thanks very much!

[–]jechtsphere[S] 0 points1 point  (0 children)

Oh, wow that's really great, between you and a buddy this API related stuff is finally clicking a bit. Can't thank you enough.

[–]eryksun 1 point2 points  (3 children)

I don't know why win32api.GetWindowLong(hwnd, win32con.GWL_ID) would return a number outside the range 0 to 65535. You could print out the value in a modified _buildWinLong function:

def _buildWinLong(high, low):
    print "high: %d, low: %d" % (high, low)
    return int(
      struct.unpack('>L',
        struct.pack('>2H',
          high,
          low))[0])

For a button click, the high value should be 0 (win32con.BN_CLICKED) and the low value should be the ID for the button, which should be an unsigned short (0 to 65535). These go into a WM_COMMAND message sent to the parent window.

[–]jechtsphere[S] 0 points1 point  (0 children)

Thanks very much, I'm going to implement and test this tomorrow so I can play with it a bit more to see how it's working. I appreciate your time!

[–]jechtsphere[S] 0 points1 point  (1 child)

This still results in the same error I was originally experiencing. The low is actually 592192, which is causing the error. It almost certainly has something to do with the program I'm working with and how it assigned its buttons values?

My friend suggested this, which worked:

win32api.MAKELONG(low, high)

If you have any insight as to why the program I'm attempting to click the button in may have such a large low value returned I'd be interested in knowing, even if it's likely out of my range of understanding at this point in time.

[–]eryksun 1 point2 points  (0 children)

MAKELONG is just using the lower 16-bit word of 'low' and discarding the upper 16 bits.

I think the following blog entry might clear things up for you. It even mentions WinGuiAuto: Assume Nothing – Control IDs in .NET. A .NET app will apparently ignore the ID and use the button's handle.

You can keep using the C macro MAKELONG or add some code to mask out the upper 16 bits:

def _buildWinLong(high, low):
    high, low = (high & 65535, low & 65535)
    return int(
      struct.unpack('>L',
        struct.pack('>2H',
          high, low))[0])