1

I'm using PInvoke in C#, trying to read tooltips visible in a window with a known handler, but the apps who's windows I try to inspect in this manner crash with memory access violation errors, or simply don't reveal the tooltip text in the lpszText TOOLINFO member.

I'm calling EnumWindows with a callback and then sending a message to the tooltip window in that function:

public delegate bool CallBackPtr(IntPtr hwnd, IntPtr lParam);

static void Main(string[] args)
{
  callBackPtr = new CallBackPtr(Report);

  IntPtr hWnd = WindowFromPoint(<mouse coordinates point>);

  if (hWnd != IntPtr.Zero)
  {
        Console.Out.WriteLine("Window with handle " + hWnd +
                              " and class name " +
                              getWindowClassName(hWnd));

        EnumWindows(callBackPtr, hWnd);

        Console.Out.WriteLine();
  }


  public static bool Report(IntPtr hWnd, IntPtr lParam)
  {
        String windowClassName = getWindowClassName(hWnd);

        if (windowClassName.Contains("tool") &&
             GetParent(hWnd) == lParam)
        {
            string szToolText = new string(' ', 250);

            TOOLINFO ti = new TOOLINFO();
            ti.cbSize = Marshal.SizeOf(typeof(TOOLINFO));
            ti.hwnd = GetParent(hWnd);
            ti.uId = hWnd;
            ti.lpszText = szToolText;

            SendMessage(hWnd, TTM_GETTEXT, (IntPtr)250, ref ti);

            Console.WriteLine("Child window handle is " + hWnd + " and class name " + getWindowClassName(hWnd) + " and value " + ti.lpszText);
        }

        return true;
    }

Here's how I defined the TOOLINFO structure:

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    private int _Left;
    private int _Top;
    private int _Right;
    private int _Bottom;
}


struct TOOLINFO
{
    public int cbSize;
    public int uFlags;
    public IntPtr hwnd;
    public IntPtr uId;
    public RECT rect;
    public IntPtr hinst;

    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpszText;

    public IntPtr lParam;
}

the TTM_GETTEXT value

private static UInt32 WM_USER = 0x0400;
private static UInt32 TTM_GETTEXT = (WM_USER + 56);

and the SendMessage overload

[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, ref TOOLINFO lParam);

So, is there any obvious error that I'm missing in my code, what should I change so that this situation is resolved?

Edit: Here is the whole code, so you could test.

luvieere
  • 37,065
  • 18
  • 127
  • 179

2 Answers2

5

You are sending a private message across processes, which requires manual marshaling. Here's another stackoverflow question on the same topic. Better would be to change direction entirely and use Active Accessibility and/or UI Automation, which are designed for this sort of thing.

Community
  • 1
  • 1
Raymond Chen
  • 44,448
  • 11
  • 96
  • 135
  • UI Automation requires the cooperation of the targeted app, that, unfortunately, I do not have. Active Accessibility seems to be a precursor of the current UI Automation framework in Windows, therefore it might not help me either. – luvieere Sep 22 '11 at 15:20
  • Besides this, the code I wrote is based on an alleged functioning solution written in C++, that I found in an answer to a question here: http://stackoverflow.com/questions/1333770/how-to-get-tooltip-text-for-a-given-hwnd – luvieere Sep 22 '11 at 15:24
  • That solution assumes that the target window is one that belongs to the same process. (Which is "obvious" if you realize how the buffer needs to be marshaled.) And it's not clear what scenario requires you to read other applications' tooltips. – Raymond Chen Sep 23 '11 at 07:06
  • You were right about using UI Automation, see my answer for the resulting code. Low level details about the marshaling process are indeed an unfamiliar topic for me, that for the moment no longer constitute a barrier to overcome in order to get this working. Thanks for your help. – luvieere Sep 23 '11 at 07:16
1

I ended up using UI Automation, as Raymond suggested. AutomationElement, who's Name property value contains the text in case of tooltips, proved to be exactly what the code required. I'm cycling through all the Desktop's child windows, where all the tooltips reside and I only display those that belong to the process that owns the window under the mouse:

    public static bool Report(IntPtr hWnd, IntPtr lParam)
    {
        if (getWindowClassName(hWnd).Contains("tool"))
        {
            AutomationElement element = AutomationElement.FromHandle(hWnd);
            string value = element.Current.Name;

            if (value.Length > 0)
            {
                uint currentWindowProcessId = 0;
                GetWindowThreadProcessId(currentWindowHWnd, out currentWindowProcessId);

                if (element.Current.ProcessId == currentWindowProcessId)
                    Console.WriteLine(value);
            }
        }

        return true;
    }


    static void Main(string[] args)
    {
        callBackPtr = new CallBackPtr(Report);

        do
        {
            System.Drawing.Point mouse = System.Windows.Forms.Cursor.Position; // use Windows forms mouse code instead of WPF

            currentWindowHWnd = WindowFromPoint(mouse);
            if (currentWindowHWnd != IntPtr.Zero)
                EnumChildWindows((IntPtr)0, callBackPtr, (IntPtr)0);

            Thread.Sleep(1000);
        }
        while (true);
    }
luvieere
  • 37,065
  • 18
  • 127
  • 179