Search code examples
c#.netvb.netcursorinterop

.NET - Capturing cursor bitmap at specific coordinates


DISCLAIMER

This question is somewhat similar to another on StackOverflow, C# - Capturing the Mouse cursor image - but with a slightly different requirement.

BACKGROUND

  • I am writing a scriptable automation client that scraps data from 3 legacy Win32 systems.
  • Two of these systems may indicate the presence of finished tasks via a change in cursor bitmap when the cursor is hovered over some specific areas. No other hints (color change, status message) are offered.
  • My own code is derived from the original post mentioned on the disclaimer.

REQUIREMENTS

  • While I an able to capture the cursor bitmaps by programatically moving the cursor to a specific coordinate and capturing it via CURSORINFO, the idea was to allow an interactive user to continue using the computer. As it is, the forced positioning disrupts the process.

QUESTION

  • Is there a way to capture the cursor bitmap by parametrized position (e.g., request the CURSORINFO as if the focus was in window W at coordinates X, Y)?

Solution

  • A solution fulfilling the specifics of this question was implemented using the information provided by Hans Passant, so all credit must go to him.

    The current setup is as shown:

    Environment definition

    It runs on a machine with two displays. Not shown in the picture is a small application that is actually responsible for the event monitoring and data scraping - it runs minimized and unattended.

    Solution

    • Obtain the Window handle for the application to be tested (in this case, I cycled through all processes returned by Process.GetProcesses():

          IntPtr _probeHwnd;
          var _procs = Process.GetProcesses();
      
          foreach (var item in _procs)
          {
              if (item.MainWindowTitle == "WinApp#1")
              {
                  _probeHwnd= item.MainWindowHandle;
                  break;
              }
          }
      
    • With the window handle for the target application, we are now able to craft specific messages and send to it via SendMessage.

    • In order to pass coordinates to SendMessage we need to serialize both X and Y coordinates into a single long value:

      public int MakeLong(short lowPart, short highPart)
      {
          return (int)(((ushort)lowPart) | (uint)(highPart << 16)); 
      }
      
    • Knowing the specific coordinates we want to probe (_probeX,_probeY), now we can issue a WM_NCHITTEST message:

      SendMessage(_probeHwnd, WM_NCHITTEST, NULL, (LPARAM)MakeLong(_probeX, _probeY));
      
    • We need GetCursorInfo to obtain the Bitmap:

      Win32Stuff.CURSORINFO ci = new Win32Stuff.CURSORINFO();
      Win32Stuff.GetCursorInfo(ci);
      
    • Check if the return flag from GetCursorInfo indicates that the cursor is showing (pco.flags == CURSOR_SHOWING):

    • Use CopyIcon in order to obtain a valid handle for the cursor bitmap:

      IntPtr hicon = default(IntPtr);
      hicon = Win32Stuff.CopyIcon(ci.hCursor);
      
    • Use GetIconInfo to extract the information from the handler:

      Win32Stuff.ICONINFO icInfo = default(Win32Stuff.ICONINFO);
      Win32Stuff.GetIconInfo(hicon, icInfo);
      
    • Use the System.Drawing.Icon class to obtain a manageable copy using Icon.FromHandle, passing the value returned by CopyIcon;

      Icon ic = Icon.FromHandle(hicon);
      
    • Extract the bitmap via Icon.ToBitmap method.

      Bitmap bmp = ic.ToBitmap();
      

    Limitations

    • This solution was tested on two different OSes: Windows XP and Windows 8. It only worked on Windows XP. On Windows 8 the cursor would flicker and return to the 'correct' format immediately, and the the captured CURSORINFO reflected that.
    • The test point areas must be visible (i.e., application must not be minimized, and test points can't be under an overlapping window. Tested window may be partially overlapped, though - and it doesn't need to have focus.)
    • When WM_NCHITTEST is issued, the current physical cursor over WebApp changes to whatever cursor bitmap is set by the probed application. CURSORINFO contains the cursor bitmap set by the probed application, but the coordinates always indicate the 'physical' location.