Automating Windows Forms

 

Brian McMaster
Windows Forms
Microsoft Corporation

March 2004

Summary: In this document you will learn how to uniquely identify Microsoft Windows Forms controls using the Name property. You will be shown how to upgrade Visual Test to handle Windows Forms. The document contains source code that you can port over and apply to make similar upgrades to existing automation frameworks. This document also lists some of the Microsoft Win32 APIs that Windows Forms do not intrinsically support. This document does not provide one complete solution for automating Windows Forms, replacement methods for every Visual Test method that does not work on Windows Forms controls, or an interface similar to Visual Test for automating Windows Forms controls for which Visual Test has no equivalent. (23 printed pages)

Applies to:

   Microsoft Visual Studio®
   Windows Forms
   Visual Test
   Active Accessibility

Contents

Introduction
Persistent Identification Problem
Traditional Solutions to the Problem
   Control ID
   Caption and Class Name
   AccName and AccRole
   Windows Hierarchy Order
Recommended Solution for Windows Forms
Formal Specification for WM_GETCONTROLNAME Message
Automating Windows Forms with Visual Test 6.5
WFSupport.inc Source Code
Conclusion
Books

Introduction

Since the initial release of Windows Forms, developers have faced unique challenges with their automation frameworks. Few existing automation frameworks can automate Windows Forms without modification. The dynamic nature of the Windows class names, coupled with lack of support for a few common Win32® APIs can cause difficulties for automation frameworks, in particular Visual Test. To make things easier, Windows Forms exposes the Name property of controls to external processes. This enables you to conveniently identify controls in your tests. In this document, you will learn how to apply your ability to identify controls to Visual Test to enhance the automated testing features of Windows Forms.

Persistent Identification Problem

When you write automated tests for Windows UI, it is important that you can uniquely identify controls on a form so that you can later find those controls to manipulate them at test-case runtime. The identifier has to be persistent across multiple instances of the application under test. Ideally, the identifier should not include a locale-dependent string, and it should not be dependent on something that can frequently change throughout the lifecycle of the product, such as window hierarchy ordering.

Traditional Solutions to the Problem

Control ID

Traditionally, the identifier for UI was the control ID. This was a locale-independent solution, which persisted across multiple instances of the form, and was unique over 90 percent of the time. However, in Windows Forms, the control ID is a mirror image of the HWND for that particular control. Thus, the control ID is different every time you launch the form, so you cannot use this as your UI identifier.

Caption and Class Name

When the control ID failed, the next best persistent identifier for Windows UI was the combination of window caption and class name. This solution worked well in most cases where control ID failed, provided that you could adjust the caption you were looking for on all the different locales in which you were testing. This can add some additional work, and again, it was not a complete solution; some controls have captions that change, or have no captions at all. This required you to get the "Nth" instance of a particular Caption+ClassName combination if there were multiple controls on the dialog with identical caption and class name pairs. Windows Forms complicates this issue by making the trailing portion of the window class name be dynamic. Depending on the framework you are using to automate your UI, this can make searching for a control by its window class name impossible, because it is dynamically generated, for example, WindowsForms10.BUTTON.app3a. The "3a" portion of the class name is generated, and will most likely be different the next time you launch the form with this button on it. So unless your framework has the ability to do substring matching on the class name, this is not an option.

AccName and AccRole

A third option was AccessibleName + AccessibleRole (concepts of MSAA). Similar to Caption+ClassName, the AccName was usually a localized string, and AccRole was far from unique on its own. In addition, searching the MSAA hierarchy was slow, and it was tedious to call WindowFromAccessibleObject to convert to an HWND if you ever needed to call a Windows API to get at some information that was not provided through the MSAA interface. In all, provided your framework will localize the AccessibleNames, and you do not mind resolving duplicates by searching for the "Nth" AccessibleName + Role pair in a situation where there are multiple elements with the same name and role, this is a viable solution for a PersistentID for Windows Forms.

Windows Hierarchy Order

A final option was to use child ordering in the Windows tree hierarchy. That is, if you knew that the button you wanted to click was the third child of the parent main form, you could get the second sibling of the first child by using the appropriate parameters to the GetWindow() API to find the HWND of the button. This certainly works in Windows Forms. However, it is obviously error-prone because a developer can easily add another control to the form to throw off ordering, or even add a new level to the tree hierarchy by moving the button into a GroupBox control or Panel. Moreover, Windows makes no guarantees that child ordering will be consistent across different versions of the operating system, so it is conceivable that some Microsoft operating system could alter the order in the future, making your tests incompatible with the operating system.

Windows Forms internally provides a way to solve the persistent identifier problem. The solution is for you to expose the Name property of the controls on a form to automated tools running out of process. You can do this through a standard SendMessage() API call passing the WM_GETCONTROLNAME message. Sample code is provided in this document, but to summarize the process, you must register the WM_GETCONTROLNAME message, browse the Windows tree hierarchy, and send the message to each HWND you encounter. The internal Name property of the control will be returned to a buffer in the LPARAM of the SendMessage() call. You can then compare this buffer to the name of the control you are searching for. Since the Name property is stored in the code of your application, it does not get localized. In addition, the Visual Studio design-time environment enforces the uniqueness of this property by keeping the Name property in-sync with the actual variable identifier in code (for example Button1, TreeView2, and so on). Thus, in general, if this property were not unique, the code would not compile.

Note    There are exceptions to this rule, such as controls dynamically generated at runtime.

If the developers of your application are not using Visual Studio, or are dynamically generating the controls on the form and adding them to the controls collection, it is possible that your controls do not have their Name property set. This will result in the SendMessage call returning an empty string in the LPARAM. Your options in this case are to force the developers to set the Name property on these controls to a unique string, or to use one of the traditional persistent identification mechanisms mentioned previously in this document.

Formal Specification for WM_GETCONTROLNAME Message

An application sends a WM_GETCONTROLNAME message to copy the name that corresponds to a Windows Forms control into a buffer provided by the caller.

Syntax

To send this message, call the SendMessage function as shown in the following table.

 lResult = SendMessage( 
    // returns LRESULT in lResult 
   (HWND) hWndControl, 
    // handle to destination control 
   (UINT) WM_GETCONTROLNAME, 
    // message ID 
   (WPARAM) wParam, 
    // = (WPARAM) () wParam;
   (LPARAM) lParam 
    // = (LPARAM) () lParam;
); 
 

Parameters

  • WParam: specifies the maximum number of TCHAR characters to be copied, including the terminating null character.
  • LParam: pointer to the buffer that is to receive the control name.

Return Value

The return value is the number of TCHAR characters copied, not including the terminating null character.

Automating Windows Forms with Visual Test 6.5

As stated earlier, it is difficult to automate Windows Forms with a new instance of Visual Test. In addition, at the time of this document's publication, Visual Test does not support the native AMD64 platform. However, by going through the exercise of adapting Visual Test to work with Windows Forms, you can gain some understanding of how to upgrade any existing tools to handle automating Windows Forms.

You can use the WM_GETCONTROLNAME message to search for the controls. The following example shows how to obtain the Windows Forms control name, given a particular HWND.

Function GetWindowsFormsID(wnd As Long) As String 
    ' Define the buffer that will eventually contain the desired
    ' component's name.
    Dim bytearray as String * 65535
    Dim msg as Long
    Dim size As Long 
    ' The amount of memory to be allocated.
    Dim retLength As Long
    Dim retVal as String

    size = 65536 'Len(bytearray)

    msg = RegisterWindowMessage("WM_GETCONTROLNAME")
    ' Send message to the control's HWND for getting the specified
    ' control name.
    retLength = SendMessage(wnd, msg, size, bytearray)

    ' The string comes back as Unicode. Convert to MultiByte and store in
    ' retVal.
    WideCharToMultiByte(CP_ACP, 0, bytearray, -1, retVal, retLength + 1, null, null)

    GetWindowsFormsID = retVal
End Function

**Important **  The actual code you must develop is more complicated than what is shown in the example because Windows does not enable you to marshal strings across processes using SendMessage. The actual code in WFSupport.inc uses shared memory. But once you have this function, writing a recursive routine to browse the Windows tree hierarchy and find the control you want is less complicated. The recursive part of the function is included here for completeness, but the actual code you must develop wraps this function in a timeout loop to support searching multiple times to reduce timing issues in the tests.

Function FindWindowsFormsControlRecursive(startWnd As Long, controlName As String) as Long
    Dim childWnd As Long
    Dim tmpWnd As Long
    Dim retVal As Long

    ' Start with the first child.
    childWnd = GetWindow(startWnd, GW_CHILD)
    While childWnd <> 0 And retVal = 0
        ' Compare the WindowsFormsID and see if this is the control.
        If GetWindowsFormsID(childWnd) <> controlName Then
            tmpWnd = childWnd
            ' Do depth-first recursion on the children.
            retVal = FindWindowsFormsControlRecursive(tmpWnd, controlName)
            childWnd = GetWindow(childWnd, GW_HWNDNEXT)
        Else
            ' Found it.
            retVal = childWnd
            Exit While
        End if

    Wend
    
    FindWindowsFormsControlRecursive = retVal
End Function

You can then use this routine to find the HWND of any Windows Forms control given a starting HWND and a control name, such as shown in the following example.

Dim controlHandle As Long
controlHandle = FindWindowsFormsControlRecursive( myStartWnd, "Button1")

The next big issue is the dynamic nature of the window class names for Windows Forms controls. Visual Test APIs for controls perform validation to ensure that the HWND you pass them actually refer to a known control of that type. For example, if you call WButtonClick() on an HWND that refers to a SysTreeView32, it will not work. You first have to register the class name as a valid type of button using WButtonSetClass().

If Windows Forms button controls had static control names, you could just call WButtonSetClass("WindowsForms10.BUTTON") and all the WButton* API's would work fine. However, because the button controls have dynamic class names and because WButtonSetClass() doesn't support prefix matching, you must figure out what the class name is exactly at the runtime of your test. You can do this by wrapping the FindWindowsFormsControlRecursive call with a method called FindControlAndClassName() that returns both the HWND and the class name of the button using reference parameters. When you get back the class name, simply pass it to WButtonSetClass() and Visual Test is ready to interact with the Windows Forms button. The following example shows what WFndWFButton() looks like:

Function WFndWFButton(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WButtonSetClass(className)
    WFndWFButton = controlWnd
End Function

Caution   In the previous example, timeout support was added so the routine will not fail right away if the control takes a few seconds to appear.

Some of the standard Windows Forms controls do not support all the Win32 APIs. This causes problems for Visual Test because Win32 API's are the primary drivers behind the Visual Test libraries. For example, BM_GETSTATE is an unsupported Win32 API used to get the state of buttons, check boxes, radio buttons, and so on. In such cases, additional methods are provided in WFSupport.inc to replace the Visual Test equivalents. Most of these methods use MSAA to extract the necessary information. The following example shows the source for a replacement for Visual Test's WCheckState().

Function WFCheckState(controlHwnd As String, timeout% = -1) as Integer
    Dim state as String
    
    state = AAGetState(controlHWnd, timeout)
    If Instr(state,"checked") > 0 Then
        WFCheckState = CHECKED
    ElseIf Instr(state, "mixed") > 0 Then
        WFCheckState = GRAYED
        Else
        WFCheckState = UNCHECKED
    End If

End Function

For a list of the known Visual Test functions that do not work, see the replacement functions in the bottom section of WFSupport.inc Source Code.

WFSupport.inc Source Code

The following example contains the support routines to help Visual Test automate Windows Forms. You can include these routines in any project and call the methods to search for and manipulate Windows Forms controls.

'=======================================================
' File name: WFSupport.inc
'$Include 'winapi.inc'

Declare Function VirtualAllocEx Lib "kernel32" Alias "VirtualAllocEx" 
(hProcess As Long, lpAddress As Any, dwSize As Long, flAllocationType As 
Long, flProtect As Long) As Long
Declare Function VirtualFreeEx Lib "kernel32" Alias "VirtualFreeEx" 
(hProcess As Long, lpAddress As Any, dwSize As Long, dwFreeType As Long) 
As Long
Declare Function WriteProcessMemoryEx Lib "kernel32" Alias 
"WriteProcessMemory" (hProcess As Long, lpBaseAddress As Any, lpBuffer As 
Long, nSize As Long, lpNumberOfBytesWritten As Long) As Long
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDst As Any, 
pSrc As Any, ByteLen As Long)
Declare Sub CopyMemoryA Lib "kernel32" Alias "RtlMoveMemory" (lpvDest As 
Any, lpvSource As Any, cbCopy As Long)
Declare Function WideCharToMultiByte Lib "kernel32" Alias 
"WideCharToMultiByte" (CodePage As Long, dwFlags As Long, lpWideCharStr As 
String, cchWideChar As Long, lpMultiByteStr As String, cchMultiByte As 
Long, lpDefaultChar As String, lpUsedDefaultChar As Long) As Long


'============NT Shared memory constant======================
Const PROCESS_VM_OPERATION = &H8
Const PROCESS_VM_READ = &H10
Const PROCESS_VM_WRITE = &H20
Const PROCESS_ALL_ACCESS = 0

Const VER_PLATFORM_WIN32_WINDOWS = 1


Function FindWindowsFormsControlRecursive(startWnd As Long, controlName As String) as Long
    Dim childWnd As Long
    Dim tmpWnd As Long
    Dim retVal As Long

    ' Start with the first child.
    childWnd = GetWindow(startWnd, GW_CHILD)
    While childWnd <> 0 And retVal = 0
        ' Compare the WindowsFormsID and see if this is the control
        ' we are after.
        If GetWindowsFormsID(childWnd) <> controlName Then
            tmpWnd = childWnd
            ' Do depth-first recursion on the children.
            retVal = FindWindowsFormsControlRecursive(tmpWnd, controlName)
            childWnd = GetWindow(childWnd, GW_HWNDNEXT)
        Else
            ' Found it.
            retVal = childWnd
            Exit While
        End if

    Wend
    
    FindWindowsFormsControlRecursive = retVal
End Function

Function FindWindowsFormsControl(startWnd As Long, controlName As String, timeout% = 50) as Long
    Dim retVal As Long
    Dim tmpWnd As Long
    Dim originalTimeout as Integer


    If timeout = -1 Then 
        ' Why does  Visual Test not have a
        ' GetDefaultWaitTimeout method?
        originalTimeout = SetDefaultWaitTimeout(5)
        timeout = originalTimeout
        ' Reset the timeout.
        SetDefaultWaitTimeout(originalTimeout)
    End If
    
    While retVal = 0 And timeout > 0
        retVal = FindWindowsFormsControlRecursive(startWnd, controlName)
        
        ' If we did not find it, sleep and try again.
        If retVal = 0 Then
            Sleep 1
            timeout = timeout - 1
        End if
    Wend

    If retVal = 0 Then
        ' Need to search one more time (this covers the case where
        ' timeout is 0).
        retVal = FindWindowsFormsControlRecursive(startWnd, controlName)
    End If
    
    FindWindowsFormsControl = retVal
End function

Function ByteArrayToString(bytes As String, length As Long) As String
    Dim retVal as String

    If IsWin9x() Then
        retVal = Left(bytes, Instr(1, bytes, Chr(0)) - 1)
    Else
        retVal = String$(length + 1, Chr(0))

        WideCharToMultiByte(CP_ACP, 0, bytes, -1, retVal, length + 1, null, null)
    End If

    ByteArrayToString = retVal

End Function


'''-----------------------------------------------------------------------------
''' <summary>
''' Determine if we are on a Win9x machine or not
''' </summary>
'''    <returns>True if this is a flavor of Win9x</returns>
'''-----------------------------------------------------------------------------
Function IsWin9x() as Bool

    Dim osVerInfo as OSVERSIONINFO

    osVerInfo.dwOSVersionInfoSize = 128 + 4 * 5

    GetVersionEx(osVerInfo)

    IsWin9x = osVerInfo.dwPlatformId = VER_PLATFORM_WIN32_WINDOWS

End Function

'''-----------------------------------------------------------------------------
''' <summary>
''' This method extracts the Windows Forms Name property from the given HWND.
''' </summary>
'''    <param name="wnd">target window</param>
'''    <returns>The name of control as a string.</returns>
'''-----------------------------------------------------------------------------
Function GetWindowsFormsID(wnd As Long) As String
    Dim PID As Long 'pid of the process that contains the control
    Dim msg as Long

    ' Define the buffer that will eventually contain the desired
    ' component's name.
    Dim bytearray as String * 65535

    ' Allocate space in the target process for the buffer as shared
    ' memory.
    Dim bufferMem As Long 
    ' Base address of the allocated region for the buffer.
    Dim size As Long 
    ' The amount of memory to be allocated.
    Dim written As Long 
    ' Number of bytes written to memory.
    Dim retLength As Long
    Dim retVal As Long
    Dim errNum As Integer
    Dim errDescription As String


    size = 65536 'Len(bytearray)

    ' Creating and reading from a shared memory region is done
    ' differently in Win9x than in newer Oss.
    Dim processHandle As Long
    Dim fileHandle As Long

    msg = RegisterWindowMessage("WM_GETCONTROLNAME")

    If Not IsWin9x() Then
        On Local Error Goto Error_Handler_NT
            GetWindowThreadProcessId(wnd, VarPtr(PID))
            
            processHandle = OpenProcess(PROCESS_VM_OPERATION Or 
            PROCESS_VM_READ Or PROCESS_VM_WRITE, 0, PID)
            If processHandle = 0 Then
                Error Err, "OpenProcess API Failed"
            End If

            bufferMem = VirtualAllocEx(processHandle, 0, size, 
            MEM_RESERVE Or MEM_COMMIT, PAGE_READWRITE)
            If bufferMem = 0 Then
                Error Err, "VirtualAllocEx API Failed"
            End If

            ' Send message to the control's HWND for getting the
         ' Specified control name.
            retLength = SendMessage(wnd, msg, size, bufferMem)

            
            ' Now read the component's name from the shared memory location.
            retVal = ReadProcessMemory(processHandle, bufferMem, bytearray, size, VarPtr(written))
            If retVal = 0 Then
                Error Err, "ReadProcessMemory API Failed"
            End If
        Error_Handler_NT:
            errNum = Err
            errDescription = Error$
            ' Free the memory that was allocated.
            retVal = VirtualFreeEx(processHandle, bufferMem, 0, MEM_RELEASE)
            If retVal = 0 Then
                Error Err, "VirtualFreeEx API Failed"
            End If
            CloseHandle(processHandle)
            If errNum <> 0 Then
                On Local Error Goto 0
                Error errNum, errDescription
            End If
        On Local Error Goto 0

    
    Else
       On Local Error Goto Error_Handler_9x

            fileHandle = CreateFileMapping(INVALID_HANDLE_VALUE, null, 
            PAGE_READWRITE, 0, size, null)
            If fileHandle = 0 Then
                Error Err, "CreateFileMapping API Failed"
            End If
            bufferMem = MapViewOfFile(fileHandle, FILE_MAP_ALL_ACCESS, 0, 0, 0)
            If bufferMem = 0 Then
                Error Err, "MapViewOfFile API Failed"
            End If
            
            CopyMemory(bufferMem, bytearray, size)

            ' Send message to the treeview control's HWND for 
            ' getting the specified control's name.
            retLength = SendMessage(wnd, msg, size, bufferMem) 

            ' Read the control's name from the specific shared memory
            ' for the buffer.
            CopyMemoryA(bytearray, bufferMem, 1024)

        Error_Handler_9x:
            errNum = Err
            errDescription = Error$

            ' Unmap and close the file.
            UnmapViewOfFile(bufferMem)
            CloseHandle(fileHandle)

            If errNum <> 0 Then
                On Local Error Goto 0
                Error errNum, errDescription
            End If
        On Local Error Goto 0

    End If

    ' Get the string value for the Control name.
    GetWindowsFormsID = ByteArrayToString(bytearray, retLength)

End Function


Sub FindControlAndClassName(startWnd As Long, controlName As String, 
controlWnd As Long, className As String, timeout% = 1)
    Dim controlHandle As Long
    Dim info As INFO
    
    controlHandle = FindWindowsFormsControl(startWnd, controlName, timeout)
    WGetInfo controlHandle, info
    className = info.Class

    controlWnd = controlHandle

End Function



'''-----------------------------------------------------------------------------
'''    <name> WFndWF*</name>
''' <summary>
''' These are the functions you use to find the HWnds of Windows Forms controls.
''' </summary>
'''    <param name="startWnd">window handle of where you want to start your search
'''            NOTE: this window is not included in the search, only the descendants </param>
'''    <param name="controlName">This is the WindowsFormsID of the control.
'''                Use the Windows Forms Spy tool to get the ID.  Note that this is also
'''                the "Name" property of the Windows Forms control in code.</param>
'''    <returns>The window handle of the control</returns>
'''-----------------------------------------------------------------------------
Function WFndWFCheck(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WCheckSetClass(className)
    WFndWFCheck = controlWnd
End Function

Function WFndWFCombo(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WComboSetClass(className)
    WFndWFCombo = controlWnd
End Function

Function WFndWFButton(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WButtonSetClass(className)
    WFndWFButton = controlWnd
End Function

Function WFndWFEdit(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WEditSetClass(className)
    WFndWFEdit = controlWnd
End Function

Function WFndWFHeader(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WHeaderSetClass(className)
    WFndWFHeader = controlWnd
End Function

Function WFndWFList(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WListSetClass(className)
    WFndWFList = controlWnd
End Function

Function WFndWFView(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WViewSetClass(className)
    WFndWFView = controlWnd
End Function

Function WFndWFMonthCal(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WMonthCalSetClass(className)
    WFndWFMonthCal = controlWnd
End Function

Function WFndWFOption(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WOptionSetClass(className)
    WFndWFOption = controlWnd
End Function

Function WFndWFPicker(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WPickerSetClass(className)
    WFndWFPicker = controlWnd
End Function

Function WFndWFProgress(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WProgressSetClass(className)
    WFndWFProgress = controlWnd
End Function

Function WFndWFScroll(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WScrollSetClass(className)
    WFndWFScroll = controlWnd
End Function

Function WFndWFSlider(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WSliderSetClass(className)
    WFndWFSlider = controlWnd
End Function

Function WFndWFSpin(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WSpinSetClass(className)
    WFndWFSpin = controlWnd
End Function

Function WFndWFStatic(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WStaticSetClass(className)
    WFndWFStatic = controlWnd
End Function

Function WFndWFStatus(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WStatusSetClass(className)
    WFndWFStatus = controlWnd
End Function

Function WFndWFTab(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WTabSetClass(className)
    WFndWFTab = controlWnd
End Function

Function WFndWFToolbar(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WToolbarSetClass(className)
    WFndWFToolbar = controlWnd
End Function

Function WFndWFTips(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WTipsSetClass(className)
    WFndWFTips = controlWnd
End Function

Function WFndWFTree(startWnd As Long, controlName As String, timeout% = -1) As Long
    Dim controlWnd as Long
    Dim className As String
    FindControlAndClassName(startWnd, controlName, controlWnd, className, timeout)
    WTreeSetClass(className)
    WFndWFTree = controlWnd
End Function




'''-----------------------------------------------------------------------------
''' <summary>
''' Windows Forms replacement for WCheckState function
''' </summary>
'''    <param name="controlHwnd">The HWnd of this control in traditional  
       Visual Test representation (e.g. "=1234")</param>
'''    <returns>CHECKED, UNCHECKED, or GRAYED</returns>
'''-----------------------------------------------------------------------------
Function WFCheckState(controlHwnd As String, timeout% = -1) as Integer
    Dim state as String
    
    state = AAGetState(controlHWnd, timeout)
    If Instr(state,"checked") > 0 Then
        WFCheckState = CHECKED
    ElseIf Instr(state, "mixed") > 0 Then
        WFCheckState = GRAYED
    Else
        WFCheckState = UNCHECKED
    End If

End Function



'''-----------------------------------------------------------------------------
''' <summary>
''' Windows Forms replacement for WOptionState function
''' </summary>
'''    <param name="controlHwnd">The HWnd of this control in traditional 
       Visual Test representation (e.g. "=1234")</param>
'''    <returns>CHECKED or UNCHECKED</returns>
'''-----------------------------------------------------------------------------
Function WFOptionState(controlHwnd As String, timeout% = -1) as Integer
    Dim state as String
    
    state = AAGetState(controlHWnd, timeout)
    If Instr(state,"checked") > 0 Then
        WFOptionState = CHECKED
    Else
        WFOptionState = UNCHECKED
    End If

End Function

Conclusion

Traditional means of identifying Windows controls on a dialog box through automation do not work well for Windows Forms. However, the Name property on controls, accessed through the WM_GETCONTROLNAME message, gives you a locale-independent persistent identifier that is almost always unique. The code snippet in this article shows how to adapt Visual Test to make use of this message, but you can probably do something similar to adapt any other Windows UI automation framework to test Windows Forms.

If you have any questions or concerns about automating Windows Forms, see the community forums at http://www.windowsforms.net/.

Books