tags:

views:

87

answers:

4

I have a method set that uses pinvoke to call WM_GETTEXT on another program's textbox - and it works fairly well, but frequently I just get back total junk text appended to the end of it. (The ORIGINAL text is always intact.)

This is random, I cannot reproduce it on demand, but it is frequent enough to be stopship.

Here is the text to get the information.

System.Text.StringBuilder strBuffer = new System.Text.StringBuilder();

int nLen = 0;

bool nUpdated = false;

try
{
    this.isOpen = false;

    if (ptrHandle == null)
        return;

    if (ptrHandle == IntPtr.Zero)
        return;

    nLen =
        Converter.SendMessage(ptrHandle, Converter.WM_GETTEXTLENGTH, 0, 0);

    if (nLen <= 0)
        return;

    if (nPreviousLen != nLen)
        nUpdated = true;

    if (nUpdated)
    {
        System.Diagnostics.Debug.WriteLine("nLen:\t{0}", nLen);

        strBuffer = new System.Text.StringBuilder(null, nLen + 1);

        System.Diagnostics.Debug.WriteLine("strBuffer:\t{0}", strBuffer.ToString());

        int sLen = Converter.SendMessageByString(ptrHandle, Converter.WM_GETTEXT, nLen
            , strBuffer);

        System.Diagnostics.Debug.WriteLine("sLen:\t{0}", sLen);

        System.Diagnostics.Debug.WriteLine("\n\nstrBuffern\n\n{0}", strBuffer.ToString());

        strBuffer = new System.Text.StringBuilder(strBuffer.ToString().Left(sLen));

        System.Diagnostics.Debug.WriteLine("\n\nsLenBuffer\n\n{0}", strBuffer.ToString());

source = new Special.IO.TextReader( 
                    new System.IO.MemoryStream(  System.Text.Encoding.Default.GetBytes(strBuffer.ToString() ) ), nUpdated );
        }
    }
}


    /// <summary>
    /// Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
    /// <br />
    /// To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.
    /// </summary>
    /// <param name="hWnd">
    /// Handle to the window whose window procedure will receive the message. 
    /// If this parameter is HWND_BROADCAST, the message is sent to all top-level windows in the system, including disabled or invisible unowned windows, overlapped windows, and pop-up windows; but the message is not sent to child windows.
    /// </param>
    /// <param name="Msg">
    /// [in] Specifies the message to be sent.
    /// </param>
    /// <param name="wParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <param name="lParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <returns>
    /// The return value specifies the result of the message processing; it depends on the message sent.
    /// </returns>
    [DllImport("user32.dll", EntryPoint = "SendMessageA", CharSet = CharSet.Ansi, SetLastError = false)]
    internal static extern int SendMessageByString(IntPtr hWnd, uint Msg, int wParam, StringBuilder lParam);

    /// <summary>
    /// Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
    /// <br />
    /// To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.
    /// </summary>
    /// <param name="hWnd">
    /// Handle to the window whose window procedure will receive the message. 
    /// If this parameter is HWND_BROADCAST, the message is sent to all top-level windows in the system, including disabled or invisible unowned windows, overlapped windows, and pop-up windows; but the message is not sent to child windows.
    /// </param>
    /// <param name="Msg">
    /// [in] Specifies the message to be sent.
    /// </param>
    /// <param name="wParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <param name="lParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <returns>
    /// The return value specifies the result of the message processing; it depends on the message sent.
    /// </returns>
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
+1  A: 

You shouldn't ignore the return value when you send WM_GETTEXT. From MSDN:

The return value is the number of characters copied, not including the terminating null character.

If the other app changes the control's text (to something shorter) between your WM_GETTEXTLENGTH and your WM_GETTEXT, then that would explain what you're seeing: WM_GETTEXT fills the first (let's say) 5 characters of your 20-character StringBuilder, and the rest is undefined. It might have null characters or it might have garbage (depends on things like whether you're calling the ANSI version of SendMessage, which would force the OS to allocate a probably-garbage-filled temporary buffer on your behalf), but either way, you need to strip them off before you use the string.

You need to read the return value of your SendMessageByString call, and truncate the StringBuilder to that length before using it.

Joe White
I thought this had fixed it - but no, it still does the same thing.
Stacey
Any other ideas? =/
Stacey
It would be helpful if you posted all your code, including both this fix and your P/Invoke declarations.
Joe White
I have posted it as you asked.
Stacey
Are you running this on a 64-bit OS? If so, the problem might be your P/Invoke declaration for SendMessageByString. According to http://pinvoke.net/default.aspx/user32/SendMessage.html, both the wParam and the return value should be IntPtr, not int.
Joe White
Also, is there a reason you're working so hard to force this to use the ANSI version of SendMessage? If you run this on an NT-based OS (i.e., anything newer than Windows 95 or 98), you're forcing the OS to do extra work to convert the control's Unicode text into ANSI -- and then you're turning around and changing the ANSI text right back to Unicode (since all .NET strings are Unicode). That conversion (and the temp buffer the OS has to allocate to do it) might be part of the reason for the garbage. Try changing SendMessageByString's EntryPoint to "SendMessage" and CharSet to CharSet.Auto.
Joe White
I am using a 64-bit OS, but some of my users will be on 32-bit. Does this make a difference? I am not trying to 'force' anything. I am an amateur at best with this, and I am copying what I have found online and read in books. Let me try what you have suggested, and get back to you.
Stacey
I have updated my code as you suggested and posted it in a new 'answer' to this thread.
Stacey
Alright, even with the updated code, I am experiencing the same problem.
Stacey
I really don't know what to do. This is extremely important. It just isn't working... It still produces junk at random.
Stacey
A: 

Alright, per your suggestion, I have modified my pinvokes to look as follows.

I think the most frustrating part of all of this is that it is completely random. I have trouble reproducing the problem. Some of my clients will be on 64-bit, some on 32-bit. What should I do about this? I am testing it now and I still have the same problem. I get junk back.

    /// <summary>
    /// Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
    /// <br />
    /// To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.
    /// </summary>
    /// <param name="hWnd">
    /// Handle to the window whose window procedure will receive the message. 
    /// If this parameter is HWND_BROADCAST, the message is sent to all top-level windows in the system, including disabled or invisible unowned windows, overlapped windows, and pop-up windows; but the message is not sent to child windows.
    /// </param>
    /// <param name="Msg">
    /// [in] Specifies the message to be sent.
    /// </param>
    /// <param name="wParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <param name="lParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <returns>
    /// The return value specifies the result of the message processing; it depends on the message sent.
    /// <br />
    /// If you use '[Out] StringBuilder', initialize the string builder with proper length first.
    /// </returns>
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, StringBuilder lParam);

    /// <summary>
    /// Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
    /// <br />
    /// To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.
    /// </summary>
    /// <param name="hWnd">
    /// Handle to the window whose window procedure will receive the message. 
    /// If this parameter is HWND_BROADCAST, the message is sent to all top-level windows in the system, including disabled or invisible unowned windows, overlapped windows, and pop-up windows; but the message is not sent to child windows.
    /// </param>
    /// <param name="Msg">
    /// [in] Specifies the message to be sent.
    /// </param>
    /// <param name="wParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <param name="lParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <returns>
    /// The return value specifies the result of the message processing; it depends on the message sent.
    /// <br />
    /// If you use '[Out] StringBuilder', initialize the string builder with proper length first.
    /// </returns>
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

    /// <summary>
    /// Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
    /// <br />
    /// To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.
    /// </summary>
    /// <param name="hWnd">
    /// Handle to the window whose window procedure will receive the message. 
    /// If this parameter is HWND_BROADCAST, the message is sent to all top-level windows in the system, including disabled or invisible unowned windows, overlapped windows, and pop-up windows; but the message is not sent to child windows.
    /// </param>
    /// <param name="Msg">
    /// [in] Specifies the message to be sent.
    /// </param>
    /// <param name="wParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <param name="lParam">
    /// [in] Specifies additional message-specific information.
    /// </param>
    /// <returns>
    /// The return value specifies the result of the message processing; it depends on the message sent.
    /// </returns>
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

Now then, my access code looks such as this.

    private void CreateStream(object sender, EventArgs e)
    {
        System.Text.StringBuilder strBuffer = new System.Text.StringBuilder();

        System.Text.StringBuilder strDebug = new System.Text.StringBuilder();

        // nLen is the length of the text recovered
        int nLen = 0;

        // nUpdated determines whether the text has changed since we last looked at it
        bool nUpdated = false;

        try
        {
            //
            // close the stream so that it can be processed
            //
            this.isOpen = false;

            if (ptrHandle == null)
                return;

            if (ptrHandle == IntPtr.Zero)
                return;

            nLen =
                Converter.SendMessage(ptrHandle, Converter.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero).ToInt32();

            // if the length is 0 or negative, just exit. Something went wrong.
            if (nLen <= 0)
                return;

            if (nPreviousLen != nLen)
                nUpdated = true;

            if (nUpdated)
            {
                strDebug.AppendFormat("nLen:\t{0}", nLen);

                strBuffer = new System.Text.StringBuilder(null, nLen + 1);

                strBuffer.Length = Converter.SendMessage(ptrHandle, Converter.WM_GETTEXT, new IntPtr(nLen)
                    , strBuffer).ToInt32();

                strDebug.AppendFormat("\nsLen:\t{0}", strBuffer.Length);
            }
        }
        finally
        {
            source = new SPECIAL.IO.TextReader(
                strBuffer.ToString(), nUpdated);

            source.Debugging = strDebug.ToString() ;
            //
            // open the stream so that it can be processed
            //
            this.isOpen = true;
            //
            // if we get this far, specify this length as the 'previous length'
            //
            nPreviousLen = nLen;
        }
    }
Stacey
You're making things *way* too complicated. It feels like you're gluing code snippets together without understanding them. Don't convert your StringBuilder to a string, then do a substring, then create another StringBuilder out of that string. That whole line can be replaced with `strBuffer.Length = sLen;`. Then your next chain (StringBuilder to string to bytes to stream to TextReader to string) is crazy complicated and FAIK could be introducing problems. Try doing a `Debug.WriteLine(strBuffer.ToString());` right after you set the Length, and see if it's got the garbage yet.
Joe White
I get the same results. Sometimes it works fine. Sometimes it has junk. This is really driving me mad...
Stacey
The classname of the object being read is "RICHCNTLREADONLY". Does this give you any further information?
Stacey
A: 

It seems to me that your error is in the wrong usage of one parameter of WM_GETTEXT message. You should use nLen + 1 instead of nLen as the wParam.

At the beginning you use WM_GETTEXTLENGTH to get nLen, which will be the number of TCHARs copied, not including the terminating null character. Then you allocate the buffer of the size nLen + 1 characters. There steps are absolutely correctly, but then you send WM_GETTEXT with nLen as the wParam which is wrong, because corresponds to http://msdn.microsoft.com/en-us/library/ms632627.aspx wParam must contain the maximum number of characters to be copied, including the terminating null character. So the correct parameter of WM_GETTEXT message must be nLen + 1 instead of nLen.

The usage of the buffer which are larger as nLen I find for the best way. I'll recommend you to allocate buffer at least 2 characters longer as the nLen value returned by WM_GETTEXTLENGTH and use nLen + 2 as the parameter of WM_GETTEXT (exactly how large are you buffer size). If the returned value of the WM_GETTEXT are nLen or less, then you can be sure that the returned string contain full text which you want to read. If the result of WM_GETTEXT will be nLen + 1, then the text are changed between sending of WM_GETTEXTLENGTH and WM_GETTEXT messages and you should repeat all the steps starting with WM_GETTEXTLENGTH one more time to know the new text size.

Oleg
I have tried this, with the same results.
Stacey
If you find an example how your results can be reproduced your problem will be quickly solved. Independent on this you should agree: 1) you should use at least `nLen+1` as a `wParam` instead of `nLen` for `WM_GETTEXT` 2) only if the value returned by `WM_GETTEXT` is more as one less then `wParam` (the buffer size) which you send, then you can be sure that returned data contain the full text which you try read by `WM_GETTEXT` message.
Oleg
+1  A: 

There seems to be some-thing weird going on with this, it looks like the control you are targetting, when using P/Invoke via WM_GETTEXT is returning the junk... I suggest the following, instead of returning the whole buffer, return back the current line which would make things a bit more snappier...

try{
    int nLineCount = Converter.SendMessage(ptrHandle, Converter.EM_GETLINECOUNT, 0, 0);
    int nIndex = Converter.SendMessage(ptrHandle, Converter.EM_LINEINDEX, nLineCount, 0);
    int nLineLen = Converter.SendMessage(ptrHandle, Converter.EM_LINELENGTH, nIndex, 0);
    //
    strBuffer = new System.Text.StringBuilder(nLineLen);
    strBuffer.Append(Convert.ToChar(nLineLen));
    strBuffer.Length = nLineLen;
    int nCharCnt = Converter.SendMessage(ptrHandle, Converter.EM_GETLINE, new IntPtr(nLineCount),     strBuffer).ToInt32();
    nLen = nCharCnt;
    if (nLen <= 0) return;
    if (nPreviousLen != nLen) nUpdated = true;
}finally{
    source = new TextReader(strBuffer.ToString(), nUpdated, isOpen ? true : false);
    this.isOpen = true;
    nPreviousLen = nLen;
}

In that way, we obtain:

  • the line count in the control - nLineCount
  • Obtain the character index that is the start of the line nLineCount - nIndex
  • Finally, obtain the line length using the nIndex - nLineLen

Using nLineLen, then we can set up the StringBuilder buffer, the tricky part in using EM_GETLINE is, the zero'th position of the buffer MUST contain the length in char's - hence the usage of strBuffer.Appent(Convert.ToChar(nLineLen)), and the stringbuilder's Length property specified.

Here are the constants required for the above P/Invoke

  • const int EM_GETLINECOUNT = 0xBA;
  • const int EM_LINEINDEX = 0xBB;
  • const int EM_LINELENGTH = 0xC1;
tommieb75