Using ctypes to call additional win32 functions

Introduction

The Windows extensions for Python offer a lot of the Win32 api to the Python developer but there will always be cases where a certain functions will not be available, as the Win32 api is very large. But in that case, the developer needs not to despair, because the ctypes extensions allows to import additional Win32 api functions.

The example given below is a translation of sysmets1.c, found in chapter 4 of Charles Petzold’s “Programming Windows” book. It queries various Windows settings via the GetSystemMetrics() function and displays them in a window. Note that it is based on a french copy of the book, so the strings describing the settings are actually in french.

Problems encountered

Translating C structures

The C source uses a sysmetric structure to define a Windows setting to be displayed to the user. In Python, there are no structures, but we can translate this to a class whose attributes match the structure’s fields:

class sysmetric:
    def __init__(self, iIndex, szLabel, szDesc):
        self.iIndex     = iIndex
        self.szLabel    = szLabel
        self.szDesc     = szDesc

As you can see, __init__(), which is the constructor of the class, initializes the instance’s attributes with the constructor’s arguments.

Translating Win32 API functions

We have to make three Win32 API functions available to Python that don’t exist in the Python for Windows Extensions:

  1. GetTextMetrics()
  2. TextOut()
  3. SetTextAlign()

The pattern chosen to do this is simple: we define a Python wrapper method for each Windows functions. Inside the wrapper, we do the following things(e.g. TextOut):

  1. Get a reference on the native function in the dll.
  2. Define the native function’s argument types by setting the argtypes attribute on the function reference. If the argument types are simple, this is an optional step.
  3. Define the native function’s result type by setting the restype attribute on the function reference. If the result type is simple, this is an optional step.
  4. Call the native function and return the result.
def TextOut(hdc, nXStart, nYStart, lpString):
    TextOut0 = ctypes.windll.gdi32.TextOutA
    TextOut0.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_int]
    TextOut0.restype = ctypes.c_int
    return TextOut0(hdc, nXStart, nYStart, lpString, len(lpString))

Note that this might not be the most efficient way to call the Win32 API, as the reference on the Windows function is re-obtained at each call of the wrapper function.

Translating Windows structures

For GetTextMetrics(), there is an additional difficulty in that one of the arguments is a pointer to the C TEXTMETRIC structure. To map this in Python, we have to define a class that extends the ctypes Structure class. Then on this class we set the _fields_ attribute with a list of tuples describing each field of the C structure. The tuples are field name/field type pairs:

class TEXTMETRIC(ctypes.Structure):    
    _fields_ = [
        ("tmHeight", ctypes.c_long),
        ("tmAscent", ctypes.c_long),
	...
	("tmFirstChar", ctypes.c_char),
	...
    ]

Note that as we use the ANSI version of the Windows functions, the TCHAR fields in the C structure simply translate to the c_char type in Python.

Handling WM_CREATE

The original source uses the WM_CREATE message in the window procedure to initialize the static cxChar, cxCaps and cyChar variables. However, in Python there are two problems with this:

  1. The concept of static variables doesn’t exist
  2. The way that the Python Windows Extensions work make it impossible to receive the WM_CREATE message.

To work around this, I used a little trick: remember, everything in Python is an object, so you can define attributes on the window procedure object. These attributes can then be used exactly like static variables would. Additionally, I only want the WM_CREATE code to be called once, so I check if the cxChar attribute already exists on the window procedure object. If it doesn’t exist, I initialize these attributes then call the code to be executed only once:

def wndProc(hwnd, message, wParam, lParam):    
    
    if not hasattr(wndProc, 'cxChar'):
        # First execution of wndProc. The way win32gui works, WM_CREATE has
        # already been called and cannot be catched...
        wndProc.cxChar         = 0
        wndProc.cxCaps         = 0
        wndProc.cyChar         = 0
		
        hdc = win32gui.GetDC(hwnd)
        ...

The source code

Here is the source code which you can also download as a standalone file. It was developed with Python 2.4, the Python Windows Extensions build 207 and ctypes 0.9.6 (note 14 April 2015: ctypes is now included in Python, starting with Python Version 2.5):

# -*- coding: latin-1 -*-

import ctypes
import win32api
import win32con
import win32gui


class sysmetric:
    def __init__(self, iIndex, szLabel, szDesc):
        self.iIndex     = iIndex
        self.szLabel    = szLabel
        self.szDesc     = szDesc
        


sysmetrics = [
    sysmetric(win32con.SM_CXSCREEN,             'SM_CXSCREEN', 'Largeur écran en pixels'),
    sysmetric(win32con.SM_CYSCREEN,             'SM_CYSCREEN', 'Hauteur écran en pixels'),
    sysmetric(win32con.SM_CXVSCROLL,            'SM_CXVSCROLL', 'Largeur flèche défilement vertical'),
    sysmetric(win32con.SM_CYHSCROLL,            'SM_CYHSCROLL', 'Hauteur flèche défilement horizontal'),
    sysmetric(win32con.SM_CYCAPTION,            'SM_CYCAPTION', 'Hauteur barre de titre'),
    sysmetric(win32con.SM_CXBORDER,             'SM_CXBORDER', 'Largeur bordure fenêtre'),
    sysmetric(win32con.SM_CYBORDER,             'SM_CYBORDER', 'Hauteur bordure fenêtre'),
    sysmetric(win32con.SM_CXFIXEDFRAME,         'SM_CXFIXEDFRAME', 'Largeur cadre fenêtre dialogue'),
    sysmetric(win32con.SM_CYFIXEDFRAME,         'SM_CYFIXEDFRAME', 'Hauteur cadre fenêtre dialogue'),
    sysmetric(win32con.SM_CYVTHUMB,             'SM_CYVTHUMB', 'Hauteur curseur défilement V'),
    sysmetric(win32con.SM_CXHTHUMB,             'SM_CXHTHUMB', 'Largeur curseur défilement H'),
    sysmetric(win32con.SM_CXICON,               'SM_CXICON', 'Largeur icône'),
    sysmetric(win32con.SM_CYICON,               'SM_CYICON', 'Hauteur icône'),
    sysmetric(win32con.SM_CXCURSOR,             'SM_CXCURSOR', 'Largeur curseur'),
    sysmetric(win32con.SM_CYCURSOR,             'SM_CYCURSOR', 'Hauteur curseur'),
    sysmetric(win32con.SM_CYMENU,               'SM_CYMENU', 'Hauteur barre menus'),
    sysmetric(win32con.SM_CXFULLSCREEN,         'SM_CXFULLSCREEN', 'Largeur zone client plein écran'),
    sysmetric(win32con.SM_CYFULLSCREEN,         'SM_CYFULLSCREEN', 'Hauteur zone client plein écran'),
    sysmetric(win32con.SM_CYKANJIWINDOW,        'SM_CYKANJIWINDOW', 'Hauteur fenêtre Kanji'),
    sysmetric(win32con.SM_MOUSEPRESENT,         'SM_MOUSEPRESENT', 'Indicateur de présence souris'),
    sysmetric(win32con.SM_CYVSCROLL,            'SM_CYVSCROLL', 'Hauteur flèche défilement V'),
    sysmetric(win32con.SM_CXHSCROLL,            'SM_CXHSCROLL', 'Largeur flèche défilement H'),
    sysmetric(win32con.SM_DEBUG,                'SM_DEBUG', 'Indicateur version débogage'),
    sysmetric(win32con.SM_SWAPBUTTON,           'SM_SWAPBUTTON', 'Indicateur inversion boutons'),
    sysmetric(win32con.SM_CXMIN,                'SM_CXMIN', 'Largeur fenêtre minimum'),
    sysmetric(win32con.SM_CYMIN,                'SM_CYMIN', 'Hauteur fenêtre minimum'),
    sysmetric(win32con.SM_CXSIZE,               'SM_CXSIZE', 'Largeur boutons'),
    sysmetric(win32con.SM_CYSIZE,               'SM_CYSIZE', 'Hauteur boutons'),
    sysmetric(win32con.SM_CXSIZEFRAME,          'SM_CXSIZEFRAME', 'Largeur bordure fenêtre'),
    sysmetric(win32con.SM_CYSIZEFRAME,          'SM_CYSIZEFRAME', 'Hauteur bordure fenêtre'),
    sysmetric(win32con.SM_CXMINTRACK,           'SM_CXMINTRACK', 'Déplacement H minimum'),
    sysmetric(win32con.SM_CYMINTRACK,           'SM_CYMINTRACK', 'Déplacement V minimum'),
    sysmetric(win32con.SM_CXDOUBLECLK,          'SM_CXDOUBLECLK', 'Tolérance double clic H'),
    sysmetric(win32con.SM_CYDOUBLECLK,          'SM_CYDOUBLECLK', 'Tolérance double clic V'),
    sysmetric(win32con.SM_CXICONSPACING,        'SM_CXICONSPACING', 'Espacement icônes H'),
    sysmetric(win32con.SM_CYICONSPACING,        'SM_CYICONSPACING', 'Espacement icônes V'),
    sysmetric(win32con.SM_MENUDROPALIGNMENT,    'SM_MENUDROPALIGNMENT', 'Déroulement menu G ou D'),
    sysmetric(win32con.SM_PENWINDOWS,           'SM_PENWINDOWS', 'Extensions stylo installées'),
    sysmetric(win32con.SM_DBCSENABLED,          'SM_DBCSENABLED', 'Caractères 16 bits autorisés'),
    sysmetric(win32con.SM_CMOUSEBUTTONS,        'SM_CMOUSEBUTTONS', 'Nombre de boutons souris'),
    sysmetric(win32con.SM_SECURE,               'SM_SECURE', 'Identificateur de sécurité'),
    sysmetric(win32con.SM_CXEDGE,               'SM_CXEDGE', 'Largeur bordure 3D'),
    sysmetric(win32con.SM_CYEDGE,               'SM_CYEDGE', 'Hauteur bordure 3D'),
    sysmetric(win32con.SM_CXMINSPACING,         'SM_CXMINSPACING', 'Déplacement H minimum réduction'),
    sysmetric(win32con.SM_CYMINSPACING,         'SM_CYMINSPACING', 'Déplacement V minimum réduction'),
    sysmetric(win32con.SM_CXSMICON,             'SM_CXSMICON', 'Largeur petites icônes'),
    sysmetric(win32con.SM_CYSMICON,             'SM_CYSMICON', 'Hauteur petites icônes'),
    sysmetric(win32con.SM_CYSMCAPTION,          'SM_CYSMCAPTION', 'Hauteur petites barres'),
    sysmetric(win32con.SM_CXSMSIZE,             'SM_CXSMSIZE', 'Largeurs petits boutons'),
    sysmetric(win32con.SM_CYSMSIZE,             'SM_CYSMSIZE', 'Hauteur petits boutons'),
    sysmetric(win32con.SM_CXMENUSIZE,           'SM_CXMENUSIZE', 'Largeur boutons menus'),
    sysmetric(win32con.SM_CYMENUSIZE,           'SM_CYMENUSIZE', 'Hauteur boutons menus'),
    sysmetric(win32con.SM_ARRANGE,              'SM_ARRANGE', 'Agencer boutons réduction'),
    sysmetric(win32con.SM_CXMINIMIZED,          'SM_CXMINIMIZED', 'Largeur fenêtre réduite'),
    sysmetric(win32con.SM_CYMINIMIZED,          'SM_CYMINIMIZED', 'Hauteur fenêtre réduite'),
    sysmetric(win32con.SM_CXMAXTRACK,           'SM_CXMAXTRACK', 'Largeur déplacement maximum'),
    sysmetric(win32con.SM_CYMAXTRACK,           'SM_CYMAXTRACK', 'Hauteur déplacement minimum'),
    sysmetric(win32con.SM_CXMAXIMIZED,          'SM_CXMAXIMIZED', 'Largeur fenêtre agrandie'),
    sysmetric(win32con.SM_CYMAXIMIZED,          'SM_CYMAXIMIZED', 'Hauteur fenêtre agrandie'),
    sysmetric(win32con.SM_NETWORK,              'SM_NETWORK', 'Indicateur présence réseau'),
    sysmetric(win32con.SM_CLEANBOOT,            'SM_CLEANBOOT', 'Boot système'),
    sysmetric(win32con.SM_CXDRAG,               'SM_CXDRAG', 'Inhiber tolérance déplacement x'),
    sysmetric(win32con.SM_CYDRAG,               'SM_CYDRAG', 'Inhiber tolérance déplacement y'),
    sysmetric(win32con.SM_SHOWSOUNDS,           'SM_SHOWSOUNDS', 'Sons visuels'),
    sysmetric(win32con.SM_CXMENUCHECK,          'SM_CXMENUCHECK', 'Largeur coche menu'),
    sysmetric(win32con.SM_CYMENUCHECK,          'SM_CYMENUCHECK', 'Hauteur coche menu'),
    sysmetric(win32con.SM_SLOWMACHINE,          'SM_SLOWMACHINE', 'Identificateur processeur lent'),
    sysmetric(win32con.SM_MIDEASTENABLED,       'SM_MIDEASTENABLED', 'Identificateur hébreu arabe'),
    sysmetric(win32con.SM_MOUSEWHEELPRESENT,    'SM_MOUSEWHEELPRESENT', 'Identificateur molette souris'),
    sysmetric(win32con.SM_XVIRTUALSCREEN,       'SM_XVIRTUALSCREEN', 'Origine x écran virtuel'),
    sysmetric(win32con.SM_YVIRTUALSCREEN,       'SM_YVIRTUALSCREEN', 'Origine y écran virtuel'),
    sysmetric(win32con.SM_CXVIRTUALSCREEN,      'SM_CXVIRTUALSCREEN', 'Largeur écran virtuel'),
    sysmetric(win32con.SM_CYVIRTUALSCREEN,      'SM_CYVIRTUALSCREEN', 'Hauteur écran virtuel'),
    sysmetric(win32con.SM_CMONITORS,            'SM_CMONITORS', 'Nombre de moniteurs'),
    sysmetric(win32con.SM_SAMEDISPLAYFORMAT,    'SM_SAMEDISPLAYFORMAT', 'Identificateur couleur identique')
]



class TEXTMETRIC(ctypes.Structure):
    """TEXTMETRIC structure defined with ctypes"""
    
    _fields_ = [
        ("tmHeight", ctypes.c_long),
        ("tmAscent", ctypes.c_long),
        ("tmDescent", ctypes.c_long),
        ("tmInternalLeading", ctypes.c_long),
        ("tmExternalLeading", ctypes.c_long),
        ("tmAveCharWidth", ctypes.c_long),
        ("tmMaxCharWidth", ctypes.c_long),
        ("tmWeight", ctypes.c_long),
        ("tmOverhang", ctypes.c_long),
        ("tmDigitizedAspectX", ctypes.c_long),
        ("tmDigitizedAspectY", ctypes.c_long),
        ("tmFirstChar", ctypes.c_char), #TCHAR is a c_char as we call GetTextMetricsA
        ("tmLastChar", ctypes.c_char),
        ("tmDefaultChar", ctypes.c_char),
        ("tmBreakChar", ctypes.c_char),
        ("tmItalic", ctypes.c_byte),
        ("tmUnderlined", ctypes.c_byte),
        ("tmStruckOut", ctypes.c_byte),
        ("tmPitchAndFamily", ctypes.c_byte),
        ("tmCharSet", ctypes.c_byte)]



def GetTextMetrics(hdc):
    GetTextMetrics0 = ctypes.windll.gdi32.GetTextMetricsA
    # Note how we define the TEXTMETRIC structure is defined in the argtypes list
    GetTextMetrics0.argtypes = [ctypes.c_int, ctypes.POINTER(TEXTMETRIC)]
    GetTextMetrics0.restype = ctypes.c_int    
    textMetric = TEXTMETRIC()
    result = GetTextMetrics0(hdc, ctypes.byref(textMetric))    
    return result, textMetric
    
    
    
def TextOut(hdc, nXStart, nYStart, lpString):
    TextOut0 = ctypes.windll.gdi32.TextOutA
    TextOut0.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_int]
    TextOut0.restype = ctypes.c_int
    return TextOut0(hdc, nXStart, nYStart, lpString, len(lpString))
    
    
    
def SetTextAlign(hdc, fMode):
    SetTextAlign0 = ctypes.windll.gdi32.SetTextAlign
    SetTextAlign0.argtypes = [ctypes.c_int, ctypes.c_uint]
    SetTextAlign0.restype = ctypes.c_uint
    return SetTextAlign0(hdc, fMode)    




def main():
    """main function"""
    
    hInstance = win32api.GetModuleHandle()
    
    appName = 'SysMets1'    
    
    wndClass                = win32gui.WNDCLASS()
    wndClass.style          = win32con.CS_HREDRAW | win32con.CS_VREDRAW
    wndClass.lpfnWndProc    = wndProc
    wndClass.hInstance      = hInstance
    wndClass.hIcon          = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
    wndClass.hCursor        = win32gui.LoadCursor(0, win32con.IDC_ARROW)
    wndClass.hbrBackground  = win32gui.GetStockObject(win32con.WHITE_BRUSH)
    wndClass.lpszClassName  = appName
    
    wndClassAtom = None
    try:
        wndClassAtom = win32gui.RegisterClass(wndClass)
    except Exception, e:
        print e
        raise e
        
    hWindow = win32gui.CreateWindow(
        wndClassAtom,
        'Taille des objets - Version 1',
        win32con.WS_OVERLAPPEDWINDOW,
        win32con.CW_USEDEFAULT,
        win32con.CW_USEDEFAULT,
        win32con.CW_USEDEFAULT,
        win32con.CW_USEDEFAULT,
        0,
        0,
        hInstance,
        None)
    
    win32gui.ShowWindow(hWindow, win32con.SW_SHOWNORMAL)
    win32gui.UpdateWindow(hWindow)    
    win32gui.PumpMessages()
    
    
    
def wndProc(hwnd, message, wParam, lParam):    
    
    if not hasattr(wndProc, 'cxChar'):
        # First execution of wndProc. The way win32gui works, WM_CREATE has
        # already been called and cannot be catched...
        wndProc.cxChar         = 0
        wndProc.cxCaps         = 0
        wndProc.cyChar         = 0        
        
        hdc = win32gui.GetDC(hwnd)
        
        result, tm = GetTextMetrics(hdc)
        wndProc.cxChar = tm.tmAveCharWidth
        # Note use of ... and ... or ... idiom
        wndProc.cxCaps = (
            ((tm.tmPitchAndFamily & win32con.TMPF_FIXED_PITCH) and 3 or 2) 
                * wndProc.cxChar / 2)
        wndProc.cyChar = tm.tmHeight + tm.tmExternalLeading
        
        win32gui.ReleaseDC(hwnd, hdc)        
        
        
    if message == win32con.WM_PAINT:
        hdc, paintStruct = win32gui.BeginPaint(hwnd)        
        
        for i in range(len(sysmetrics)):
            TextOut(hdc, 0, wndProc.cyChar * i, sysmetrics[i].szLabel)            
            TextOut(hdc, 22 * wndProc.cxCaps, wndProc.cyChar * i, sysmetrics[i].szDesc)            
            SetTextAlign(hdc, win32con.TA_RIGHT | win32con.TA_TOP)            
            TextOut(
                hdc, 
                22 * wndProc.cxCaps + 40 * wndProc.cxChar, 
                wndProc.cyChar * i,
                # We use directly python's string formatting
                '%5d' % win32api.GetSystemMetrics(sysmetrics[i].iIndex))            
            SetTextAlign(hdc, win32con.TA_LEFT | win32con.TA_TOP)            
        
        win32gui.EndPaint(hwnd, paintStruct)        
        return 0
    
    elif message == win32con.WM_DESTROY:        
        win32gui.PostQuitMessage(0)
        return 0               
    
    else:
        return win32gui.DefWindowProc(hwnd, message, wParam, lParam)
    
    
    
if __name__ == '__main__':    
    main()

Conclusion

The ctypes extensions are a powerfull addition to Python that allow to call pretty much any Win32 API function. As this article has shown, mapping functions is rather easy and works very well. The best is that ctypes is not limited to Windows functions, as any function exported via a dll is callable by Python.

If you are interested on how the two other examples of the 4th chapter in Charles Petzold’s “Programming Windows” book translate to Python, I have made them available here: sysmets2.py and sysmets3.py.