Getting the WinForms ID of a Control

Happy New Year and stuff! =)

If you've read my article on issues related to automating Windows Forms with traditional Win32-like UI Automation tools, then you know about what we refer to as the Windows Forms ID.  If you're not familiar with the issue, here's a quick intro.  Basically, when automating Win32 UI in the past, the typical way that you located a control through code was by using the Control ID.  You probably had some method in your automation framework called "FindControlById()" or something similar that took a window handle and an integer as parameters, and then searched the descendants of that window handle for a window who's Control ID matched the one you passed in.  Well that's all great, except this method is useless in automating Windows Forms.  That's because the Control ID for Windows Forms UI is a mirror of the HWND of the control.  Thus, it will not be the same on subsequent launches of the app.

The replacement for Control ID's in Windows Forms is "Windows Forms ID."  When you develop a Windows Forms application in Visual Studio using the designer, it will automatically set the ".Name" property for your controls to default values (usually "button1," "button2," "listBox1," etc.).  So what you really want to use in your automation is to be able to ask a particular control what it's Name property is set to.  Windows Forms supports just that.  If you send a WM_GETCONTROLNAME message using a standard SendMessage Windows API to any Windows Forms control, it will respond in the LPARAM with it's Name property.  The code in my article is written in Rational Visual Test, which is kind of pseudo-Basic I would say.  However, I often get asked for C# code to get the WinformsId, so I'm going to paste it in below.

As you can see, it has a public static method called GetWinFormsId() that takes an HWND as a parameter and returns a string representing the Name.  So all you have to do is add this code to a C# project, and then write some code to walk the Windows Hierarchy using the GetWindow() Function or similar API's to find the HWNDs of the controls on your form.  Then you can call WinFormsUtilities.GetWinFormsId() on those HWNDS to see if you've found the HWND of the control you're looking for.

 using System;
using System.Text;
using System.ComponentModel;

namespace GetWinFormsId
{
    /// <summary>
    /// Summary description for WinFormsUtilities.
    /// </summary>
    public class WinFormsUtilities
    {
        private static int GetControlNameMessage = 0;

        static WinFormsUtilities()
        {
            GetControlNameMessage = NativeMethods.RegisterWindowMessage("WM_GETCONTROLNAME");
        }

        public static string GetWinFormsId(IntPtr hWnd)
        {
            return XProcGetControlName(hWnd, GetControlNameMessage);
        }

        protected static string XProcGetControlName(IntPtr hwnd, int msg)
        {
            //define the buffer that will eventually contain the desired window's WinFormsId

            byte[] bytearray = new byte[65536];

            //allocate space in the target process for the buffer as shared memory
            IntPtr bufferMem = IntPtr.Zero; //base address of the allocated region for the buffer
            IntPtr written= IntPtr.Zero;  //number of bytes written to memory
            IntPtr retHandle= IntPtr.Zero;
            bool retVal;

            
            //creating and reading from a shared memory region is done differently in Win9x then in newer OSs
            IntPtr processHandle= IntPtr.Zero;
            IntPtr fileHandle= IntPtr.Zero;

            if(!(Environment.OSVersion.Platform == PlatformID.Win32Windows))
            {   
                try
                {
                    uint size; //the amount of memory to be allocated
                    size = 65536;

                    processHandle = NativeMethods.OpenProcess(NativeMethods.PROCESS_VM_OPERATION | NativeMethods.PROCESS_VM_READ | NativeMethods.PROCESS_VM_WRITE, false, GetProcessIdFromHWnd(hwnd));

                    if(processHandle.ToInt64() == 0)
                    {
                        throw new Win32Exception();
                    }

                    bufferMem = NativeMethods.VirtualAllocEx(processHandle, IntPtr.Zero, new UIntPtr(size), NativeMethods.MEM_RESERVE | NativeMethods.MEM_COMMIT, PageProtection.ReadWrite);

                    if(bufferMem.ToInt64() == 0)
                    {
                        throw new Win32Exception();
                    }

                    //send message to the control's hWnd for getting the specified control name
                    retHandle = NativeMethods.SendMessage(hwnd, msg, new IntPtr(size), bufferMem);

                    //now read the TVITEM's info from the shared memory location
                    retVal = NativeMethods.ReadProcessMemory(processHandle, bufferMem, bytearray, new UIntPtr(size), written);
                    if(!retVal)
                    {
                        throw new Win32Exception();
                    }
                }
                finally
                {
                    //free the memory that was allocated
                    retVal = NativeMethods.VirtualFreeEx(processHandle, bufferMem, new UIntPtr(0), NativeMethods.MEM_RELEASE);
                    if(!retVal)
                    {
                        throw new Win32Exception();
                    }
                    NativeMethods.CloseHandle(processHandle);
                }
            }
            else
            {
                try
                {
                    int size2; //the amount of memory to be allocated
                    size2 = 65536;

                    fileHandle = NativeMethods.CreateFileMapping(new IntPtr(NativeMethods.INVALID_HANDLE_VALUE), IntPtr.Zero, PageProtection.ReadWrite, 0, size2, null);
                    if(fileHandle.ToInt64() == 0)
                    {
                        throw new Win32Exception();
                    }
                    bufferMem = NativeMethods.MapViewOfFile(fileHandle, NativeMethods.FILE_MAP_ALL_ACCESS, 0, 0, new UIntPtr(0));
                    if(bufferMem.ToInt64() == 0)
                    {
                        throw new Win32Exception();
                    }
                    NativeMethods.MoveMemoryFromByte(bufferMem, ref bytearray[0], size2);

                    retHandle = NativeMethods.SendMessage(hwnd, msg, new IntPtr(size2), bufferMem);

                    //read the control's name from the specific shared memory for the buffer
                    NativeMethods.MoveMemoryToByte(ref bytearray[0], bufferMem, 1024);

                }
                finally
                {
                    //unmap and close the file
                    NativeMethods.UnmapViewOfFile(bufferMem);
                    NativeMethods.CloseHandle(fileHandle);
                }
            }

            //get the string value for the Control name
            return ByteArrayToString(bytearray);

        }

        private static uint GetProcessIdFromHWnd(IntPtr hwnd)
        {
            uint pid;
            
            NativeMethods.GetWindowThreadProcessId(hwnd, out pid);

            return pid;
        }

        private static string ByteArrayToString(byte[] bytes)
        {
            if(Environment.OSVersion.Platform == PlatformID.Win32Windows)
            {
                // Use the Ansii encoder
                return Encoding.Default.GetString(bytes).TrimEnd('\0');
            }
            else
            {
                // use Unicode
                return Encoding.Unicode.GetString(bytes).TrimEnd('\0');
            }
        }
    }
}