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');
            }
        }
    }
}

Comments

  • Anonymous
    July 13, 2007
    First of all, I'd like to say thanks for the great code, as it was really helpful. However, with a recent .net 2.0 security patch (KB928365) appears to break this code (although I'm using a VB.net translation and the bug could be on my side).  When I use this code, I still get the control name, but the method "ByteArrayToString" seems to return a string of 32768 characters in length.  I tried my VB version of the code in a Windows XP VM and it worked, and then when I installed KB928365, it again broke the code.I was wondering if there is any known solutions or workarounds for his problem or if it was even known?
  • Anonymous
    July 16, 2007
    The comment has been removed
  • Anonymous
    January 21, 2008
    Where can i find NativeMethods ?
  • Anonymous
    January 22, 2008
    I posted it in a follow-up post here.  http://blogs.msdn.com/brianmcm/archive/2006/01/23/516418.aspxSorry again for the inconvenience.
  • Anonymous
    March 23, 2009
    Hi Brian,Thanks for the code, it has really helped my understanding of windows messages. I have used the above code (C#)and a VB.Net translation to no avail. I believe it is because the application I am interrogating is not written in .NET.It is a unique situation where I am interrogating an exe (written in VB6 I believe) that is in the same process. (I have written an add-on in VB.Net and want to populate a combobox in the .exe (in the same process because the .exe is calling my DLL??).I used SetWindowText to populate Textboxes after iterating through the controls. However I cannot uniquely identify some custom controls (that look very much like ComboBoxes class 'SSDataWidgetsEdit'.I had hoped to uniquely identify these controls via a name, howver cannot seem to do so. Any suggestions from a guru?Kind Regards,David
  • Anonymous
    March 24, 2009
    The comment has been removed
  • Anonymous
    March 25, 2009
    I'm doing some application on WinXP in VS2008 and I have problems with GetWindowsFormsID(). Function SendMessage() always returns 0.I've checked which error is returned by calling GetLastError() after SendMessage() and it is described in MSDN as:ERROR_MOD_NOT_FOUND 126 (0x7E) The specified module could not be found.Code is as follows:Process p = new Process();p.StartInfo.FileName = "calc";p.Start();Thread.Sleep(1000);IntPtr hWnd = p.MainWindowHandle;string tmp = WinApi.GetWindowsFormsID(hWnd);Am I forgetting something or doing something wrong?
  • Anonymous
    March 25, 2009
    I have tested this on .NET and VB6 applications also. It does not seem to work on VB6 apps. It does work on .NET aps.I am having real troubles updating the Custom Controls in the VB6 app. The standard SendMessage CB_FINDSTRING, CB_SELECTSTRING do not seem to work. Right now it is in the too hard basket. I will look for another solution.
  • Anonymous
    March 27, 2009
    I got the reason why WM_GETCONTROLNAME does not work with VB6 applications and the solution to the problem too..!!Reason:WM_GETCONTROLNAME is not a standard function. Only Managed windows knows to handle them. Un managed windows (VB6) does not recognize these messages.Solution:Registering process for this message is a two way process. That means VB6 Application also has to register for this message. Kernel makes it sure that both subscribers will get the same ID.Once both register to this message any one can send message to each other.This solution will help to send the message to VB6 app window. But how to get the response i.e "control name" I am still finding the way out.If some body knows how to write to the memory buffer address after receiving message in VB6, sent though SendMessage() funciton. please help me.
  • Anonymous
    March 27, 2009
    Sorry for the late reply.  There's been a lot of traffic over the last few days.  You are correct that WM_GETCONTROLNAME is .NET-specific.  Only Windows Forms knows how to handle this.  There is no way solely through Windows messages to get VB6 controls to return their names.However, there is a technology called MSAA (microsoft active accessibility), which you can use to identify and manipulate most controls.  In David's post above, he's querying Sheridan controls with the class name 'SSDataWidgetsEdit'.  As he discovered, these controls do not support Win32 messages.  But Sheridan is a good, smart company, and I'm betting they support MSAA to allow their customers to use screen readers and other accessible tools to interact with their controls.The best way to find out if your UI supports MSAA is to download a tool called "AccExplorer32.exe" from microsoft.com and play around with pointing it at your UI to see what information is available.  Maybe I'll have time to write up a real blog post about this in between shipping Dev10 Beta1.-Bri
  • Anonymous
    March 30, 2009
    Hello Brian,I started my research with AccExplorer32.exe. I am working on a very old VB6 applicaiton and there is no MSAA support from this UI.Any way, Now I am able to send the "GET_CONTROLNAME" message to VB6 control. But I am struck at a point where I have write the memory buffer with the control's name. This is the buffer whose address I am passing while calling SendMessage. Can you help me to write that buffer?I have used one more tool Named "Test Complete". Amazingly, while recording it fishes out the names of Controls. That means there is atleast one more way to get that name property. Is there some Window message, to which unmanaged windows also respond?? or some other way out?Please show me the way. Coz i am totally directionless at this point of time.
  • Anonymous
    March 31, 2009
    I can't comment on how test complete finds the control names.  'tis a mystery to me.  As for writing to the buffer you're referring to, do a live.com search for "CopyMemory API VB6" and you should find lots of help.  Sorry I can't be of more assistance.-Bri
  • Anonymous
    April 03, 2009
    Hello BrianFinally, I am able to do it. I am able to get the control name.Thanks for leads..!! :-)