5

Is it possible to scrape the text from a textbox that is contained within a separate executable? I have an application that has a debug window. The debug window generates a verbose log. However, the log never is saved anywhere and can only be viewed within the app. If the app generates an exception, I'd like to email myself knowing that an exception has been generated so I can hop in and check things out. There is also a button to copy the textbox so I was thinking of using Spy++ to get the command information. However, I don't know where to go from there. Any pointers are greatly appreciated.

I'd prefer to use C# in .NET, but if I need to use C++, I will.

UPDATE:

Based on the comments, I've tried doing the following:

Private Declare Function GETWINDOWTEXT Lib "user32" Alias "GetWindowTextA" (ByVal hwnd As Integer, ByVal lpString As String, ByVal cch As Integer) As Integer
Private Declare Function SetForegroundWindow Lib "user32.dll" (ByVal hwnd As Integer) As Integer
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindow As String) As IntPtr
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As Integer, ByVal hWnd2 As Integer, ByVal lpsz1 As String, ByVal lpsz2 As String) As Integer
Private Declare Ansi Function SendMessage Lib "user32.dll" Alias "SendMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As String) As Integer
Private Const WM_GETTEXT As Short = &HDS
Private Const WM_GETTEXTLENGTH As Short = &HES

Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    Dim hwnd As Integer = FindWindowEx(0, 0, "MyAppForm", "Hello World")

    If Not hwnd = 0 Then
        SetForegroundWindow(hwnd)

        'Dim LabelEx As Integer = FindWindowEx()
        Dim TextBoxEx As Integer = FindWindowEx(hwnd, 0, "MyAppTextBox", vbNullString)
        Dim txtLength As Long = SendMessage(TextBoxEx, WM_GETTEXTLENGTH, CInt(0), CInt(0)) + 1
        Dim txtBuff As String = Space(txtLength)
        Dim txtValue As Long = SendMessage(TextBoxEx, WM_GETTEXT, txtLength, txtBuff)

        MsgBox(txtBuff)
    End If
End Sub

However, I can't seem to find the handle of the textbox control. When I enumerate all of the windows, I only see one TextBox object, but I see the parent multiple times throughout the enumeration. How can I get the pointers to the controls within the window?

UPDATE 2:

I've uploaded a sample Windows app to show the type of app I'm trying to get access to. I'm trying to get the values of both labels in addition to the textbox. The textbox is the most important. The sample Win app is here: http://www.mediafire.com/file/172r2xapj7p4f2f/StatusSimulator.zip

Jason N. Gaylord
  • 7,910
  • 15
  • 56
  • 95

3 Answers3

4

Text scraping is done by accessibility interfaces. For managed code, you can use the classes in the System.Windows.Automation namespace. If you already have a window handle, then extracting the text is simple:

AutomationElement.FromHandle(hwnd)
                 .GetCurrentPropertyValue(ValuePattern.ValueProperty) as string;

(Kind of confused since the question is tagged C# and you ask for a C# solution, but your code sample is in VB.)

Raymond Chen
  • 44,448
  • 11
  • 96
  • 135
  • Sorry. Just whipped up a VB sample as I pulled it from elsewhere. – Jason N. Gaylord Dec 08 '11 at 20:13
  • 1
    What is the "it" you're referring to? The program doing the scraping is in managed code because you wrote it in C#. It doesn't matter whether the program being scraped from is managed or unmanaged. (The raw accessibility interfaces are unmanaged, but the `System.Windows.Automation` namespace provides a handy managed wrapper around it.) – Raymond Chen Dec 08 '11 at 22:50
  • I'm getting an empty result when I use: Dim hwnd As Integer = FindWindowEx(0, 0, "MyAppForm", vbNullString) If Not hwnd = 0 Then Dim TextBoxEx As Integer = FindWindowEx(hwnd, 0, "MyTextBox", vbNullString) Dim value As String = AutomationElement.FromHandle(TextBoxEx).GetCurrentPropertyValue(ValuePattern.ValueProperty).ToString() – Jason N. Gaylord Dec 09 '11 at 03:31
  • Is there a tool that would allow me to enumerate the handles to make sure I'm getting the proper window handle? I've tried enumerating using VB6 and see about 100 classes including several of the same. That's why I'm thinking I'm getting incorrect results. – Jason N. Gaylord Dec 09 '11 at 03:32
  • What is the class of your MyTextBox? I was assuming it is an edit control, but maybe it is static text, in which case you probably want the Name property. I'm not going to try to teach UI Automation in an SO comment. You can learn more on MSDN. The part about searching for an element may be helpful. – Raymond Chen Dec 09 '11 at 04:10
  • So, I'm assuming that the form I'm trying to access has multiple Labels and a multi-line textbox control. I can't be certain, because it's not my apps. – Jason N. Gaylord Dec 09 '11 at 05:48
  • But, I also can't seem to get anywhere with the AutomationElement class. I'm not too familiar with UI Automation. I'm assuming that this will work and will look into it further. – Jason N. Gaylord Dec 09 '11 at 05:49
  • 1
    You can use Spy++ to see the window hierarchy and use that to figure out how to get the window. Or you can use `AutomationElement.FindFirst` and pass a condition that describes the target you're looking for. See [Obtaining UI Automation Elements](http://msdn.microsoft.com/en-us/library/ms752331.aspx#Finding_Elements_in_a_Subtree); the "Finding Elements in a Subtree" section looks like what you're after. – Raymond Chen Dec 09 '11 at 13:17
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/5717/discussion-between-jason-n-gaylord-and-raymond-chen) – Jason N. Gaylord Dec 09 '11 at 14:43
2

When you use the WM_GETTEXT message you provide a pointer to a buffer which receives the text. If you're sending this message to a window in another process then the pointer you provide will be in your process address space not the other process.

I've done something similar to this (scraping windows in another process) and what I did which worked was to use DLL injection. Basically you use SetWindowsHook and provide a callback inside a DLL. The OS then loads the DLL into other processes and you figure out when you've loaded into the desired target process. At that point your code is in that other process and you can get the window text.

Then there's the matter of getting it back to your app. I used a shared memory block to do it myself. Maybe there's a simpler way to do all this, though, but this worked for me in the past.

Nerdtron
  • 1,486
  • 19
  • 32
  • I'm not concerned about the window text as much as I'm concerned about the controls contained within the window. For example, if the window has 3 textboxes, I may be concerned with 1 or 2 of them. How do I read their contents? – Jason N. Gaylord Dec 08 '11 at 04:54
  • 2
    @Nerdtron: Windows will perform marshaling for system messages (0 to WM_USER-1). See http://msdn.microsoft.com/en-us/library/windows/desktop/ms644950%28v=vs.85%29.aspx and http://blogs.msdn.com/b/oldnewthing/archive/2003/08/21/54675.aspx so that isn't a problem – shf301 Dec 08 '11 at 05:21
  • @shf301 interesting. For the specific case where I was doing what I described, I was trying to read item text from a tree control and found it didn't work across process boundaries. So perhaps that doesn't apply to the tree control messaging. Thanks for the links! – Nerdtron Dec 08 '11 at 12:10
  • @JasonN.Gaylord see shf301's links. Looks like for some messages you don't have to get as involved as what I was saying. – Nerdtron Dec 08 '11 at 12:10
  • @Nerdtron - You guys lost me. I guess I can't use long words. So, I've tried using the following (again in VB since I started a quick sample app using VB): Dim szBuf As Char() Dim status As Integer = GETWINDOWTEXT(hwnd, szBuf, 1000) But that isn't getting anything. szBuf has a value of Nothing and status has a value of 26. It's not really helping me get my textbox text. – Jason N. Gaylord Dec 08 '11 at 20:36
  • @shf301 See my comments above. – Jason N. Gaylord Dec 08 '11 at 20:36
  • @Nerdtron I've added a sample win app that has 3 controls just to show you what I'm trying to get access to. – Jason N. Gaylord Dec 08 '11 at 20:49
1

Your P/Invoke definition for GetWindowText isn't right. With P/Invoke String types are only marshalled in, and not out which is why you aren't getting any text back. If you change your lpString parameter to a StringBuffer it will work.

Note you also will need to set the capacity of the StringBuffer to 1000 before passing it to GetWindowText because it requires that an allocated buffer be passed to it - it won't size the StringBuffer itself.

See the definition and example and pinvoke.net.

<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function GetWindowText(ByVal hwnd As IntPtr, ByVal lpString As StringBuilder, ByVal cch As Integer) As Integer
End Function

Private Declare Function SetForegroundWindow Lib "user32.dll" (ByVal hwnd As Integer) 

As Integer
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindow As String) As IntPtr
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As Integer, ByVal hWnd2 As Integer, ByVal lpsz1 As String, ByVal lpsz2 As String) As Integer
Private Declare Ansi Function SendMessage Lib "user32.dll" Alias "SendMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As String) As Integer
Private Const WM_GETTEXT As Short = &HDS
Private Const WM_GETTEXTLENGTH As Short = &HES

Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    Dim hwnd As Integer = FindWindowEx(0, 0, "MyAppForm", "Hello World")

    If Not hwnd = 0 Then
        SetForegroundWindow(hwnd)

        'Dim LabelEx As Integer = FindWindowEx()
        Dim TextBoxEx As Integer = FindWindowEx(hwnd, 0, "MyAppTextBox", vbNullString)
        Dim txtLength As Long = SendMessage(TextBoxEx, WM_GETTEXTLENGTH, CInt(0), CInt(0)) + 1
        Dim txtBuff As New System.Text.StringBuilder(txtLength + 1)
        GetWindowText(hWnd, txtBuff , txtBuff .Capacity)
        Dim txtValue As Long = SendMessage(TextBoxEx, WM_GETTEXT, txtLength, txtBuff)
        MsgBox(txtBuff.ToString())
    End If
End Sub
shf301
  • 31,086
  • 2
  • 52
  • 86
  • Is there an easy way to enumerate the handlers within a container? – Jason N. Gaylord Dec 09 '11 at 03:09
  • 1
    The way to enumerate windows is to user `EnumChildWindows` if you have a window handle to the application. There is also `EnumWindows` for all top level windows and `EnumThreadWindows` to enumerate all windows owned by a thread. Take a look at the Window functions listed at MSDN and see what's possible, then take a look at pinvoke.net to get their definitions. http://msdn.microsoft.com/en-us/library/windows/desktop/ff468919%28v=VS.85%29.aspx – shf301 Dec 09 '11 at 03:39
  • So, I should be able to grab the parent windows by name (or title text), then enumerate the children to find the InPtr values of the labels and textbox, correct? – Jason N. Gaylord Dec 09 '11 at 05:47
  • 1
    @JasonN.Gaylord - yes that is correct. You will need to recursively enumerate all of the child windows to see all them all. That is you will need to call `EnumChildWindows` on the handles you receive in the callback from `EnumChildWindows` – shf301 Dec 09 '11 at 15:43