Jump to content

Fix for unreadable '?' text from WinGetText, ControlGetText

Recommended Posts

_WinGetTextFixed & _StringFixANSIInWide


Update: Text problems no longer exist as of v3.3.9.11


Sick of getting rows of '?' from WinGetText, ControlGetText, and other AutoIt functions? The problem lies in the way the text is encoded - AutoIt 'sees' a Windows wide string (UTF-16), but in reality it grabs a string of 8-bit ANSI characters and shoves them into a 16-bit wide string. Here's a small example:

Original string "x4" with null-term as an ANSI string sequence:

0x78 0x34 0x00

Same string interpreted as Unicode (reversal is due to little-endian encoding):

0x3478 0x00

The latter string produces the Unicode character "㑸" (hopefully that shows up properly as an asian character)

So, as you can see, from the software side, "x4" compacted into a UTF-16 string still produces an acceptable string, even though it wasn't intended to be an asian character.

Now, where the '?'s show up is during conversion of the same string back to ANSI. You can see the result of this by doing a ConsoleWrite(), which itself causes an implicit Wide-ANSI conversion to occur. Also, pasting text into another text editor that's using an ANSI code page will give the same results. (A workaround for the conversion is to set the encoding to Unicode)

Of course, even if you can display the wide characters correctly, it's not the intended result - you want what the window in question produces. Plus, there are many invalid UTF-16 code points that can be produced, and certain values will be interpreted as a pair of UTF-16 code points.. which just gets messy.

So how do we fix this?

The easiest workaround, when the format of the string is known to be ANSI beforehand is:

$sStr = BinaryToString(StringToBinary($sStr, 2), 1)

However, determining whether a window or control will give you ANSI or Wide characters is another issue.. I had written the _StringFixANSIInWide function below to try and detect ANSI-in-Wide situations, and return the corrected string.. Unfortunately its a bit naive in its implementation (see update next paragraph), but it works okay for most situations. I can see it failing only when a Wide string contains nothing but >8-bit characters (usually there is a lot of 7-bit ASCII characters encoded in the lower part of true Wide strings). In theory that is an extremely unlikely situation (unless there are no ASCII characters in a string).

Update - _WinGetTextFixed() alternative:

Now I've identified the core issue is with the call to SendMessage (using WM_GETTEXT): The function will return either ANSI or Unicode (wide) characters, and its up to our code to determine, based on the length, whether it is an ANSI or wide string. We can then return the correct string. See _WinGetTextFixed() below.

Note that AutoIt's WinGetText() function returns text for more than just the window - it returns text for various controls also. While this can be beneficial, it also introduces issues when different controls return Wide or ANSI characters. The result is basically a contaminated string, meaning it packs ANSI characters into UTF-16 sequences and includes legitimate Wide characters in the same string. This is unacceptable and really hard to work with. That's a reason there's now a 'force conversion' parameter in _StringFixANSIInWide. I would really really emphasize that it's best to not use WinGetText() and instead opt for _WinGetTextFixed instead.

I may suggest this behavior be fixed in the AutoIt source code, but it looks like all the Devs are gone :o

Anyway, enjoy!

Example: Notepad 2

$hWnd = WinGetHandle("[CLASS:Notepad2]")
$hControl = ControlGetHandle($hWnd,"","[CLASS:Scintilla]")
; Previous fix:
;~ $sText = _StringFixANSIInWide(ControlGetText($hWnd,"",$hControl))
$sText = _WinGetTextFixed($hControl)

Example: SciTE

$hWnd = WinGetHandle("[CLASS:SciTEWindow]")
$hControl=ControlGetHandle($hWnd,"","[CLASS:Scintilla; INSTANCE:1]")
; Previous fix:
;~ $sText = _StringFixANSIInWide(ControlGetText($hWnd,"",$hControl))
$sText = _WinGetTextFixed($hControl)

Example: Programmer's Notepad 2

$hWnd = WinGetHandle("[REGEXPTITLE:Programmer's Notepad]")
$hControl = ControlGetHandle($hWnd, "", "[CLASS:ScintillaWindowImpl; INSTANCE:1]")
; Previous fix:
;~ $sText = _StringFixANSIInWide(ControlGetText($hWnd,"",$hControl))
$sText = _WinGetTextFixed($hControl)

; ===================================================================================================
; Func Func _WinGetTextFixed($hWnd)
; Function to get Text of a window or control
;  This is an alternative to AutoIt's 'WinGetTitle', 'WinGetText', and 'ControlGetText',
;  which have issues with reading ANSI text from some windows
; Author: Ascend4nt
; ===================================================================================================

Func _WinGetTextFixed($hWnd)
    If Not IsHWnd($hWnd) Then Return SetError(1,0,'')

    Local $aRet, $stWideText, $stANSIText, $sText
    Local $nGetTextLen, $nHalfLen

    $aRet = DllCall("user32.dll", "long", "SendMessageW", "hwnd", $hWnd, "uint", 0x0E, "wparam", 0, "lparam", 0)
    If @error Then Return SetError(2, @error, '')
    If Not $aRet[0] Then Return SetError(3, 0, '')

    $nGetTextLen = $aRet[0]

;~     ConsoleWrite("WM_GETTEXTLENGTH return:"&$nGetTextLen&@CRLF)

    ; Create a union structure, add 2 characters - 1 for null-term, 1 to handle odd-count cases
    $stWideText = DllStructCreate("wchar["&$nGetTextLen + 2&"]")
    If @error Then Return SetError(4, 0, '')
    $stANSIText = DllStructCreate("char["&($nGetTextLen+2)*2&"]", DllStructGetPtr($stWideText))

    $aRet = DllCall("user32.dll", "long", "SendMessageW", "hwnd", $hWnd, "uint", 0x0D, "wparam", $nGetTextLen + 1, "ptr", DllStructGetPtr($stWideText))
    If @error Then Return SetError(2, @error, '')
    If Not $aRet[0] Then Return SetError(3, 0, '')

    $nGetTextLen = $aRet[0]

    ; Get text as WIDE characters 1st
    $sText = DllStructGetData($stWideText, 1)

;~     ConsoleWrite("$nGetTextLen = "&$nGetTextLen&", $nHalfLen = "&$nHalfLen&", StringLen() = "&StringLen($sText)&@CRLF)

    ; Determine if the wide string length is half the supposed returned text length
    ; - If so, it's an ANSI string
    $nHalfLen = ($nGetTextLen + BitAND($nGetTextLen, 1) ) / 2
    If (StringLen($sText) - $nHalfLen < 2) Then
        ; Retrieve text correctly as ANSI
        $sText = DllStructGetData($stANSIText, 1)

    Return $sText
; ======================================================================================================
; Func _StringFixANSIInWide($sStr, $bForceCnvt = False)
; Function to fix a common issue where ANSI text is embedded in UTF-16 strings
; Problem occurs in 'WinGetText', 'ControlGetText', 'WinGetTitle'
; and some COM functions using 'bstr' types
; Easiest method, when you *know* the text is ANSI:
; BinaryToString(StringToBinary($sStr, 2), 1)
; *However*, if it is unknown what the string holds, we need to look
; for null characters (0's) in the string
; Alternatives:'WideCharToMultiByte' API call, which does the same replacements as below
; However, on Vista+, WC_ERR_INVALID_CHARS can be used to error-out on illegal characters
; Author: Ascend4nt
; ======================================================================================================

Func _StringFixANSIInWide($sStr, $bForceCnvt = False)
    If $sStr = '' Then Return ''
    Local $nLen, $stStrVer, $stBinVer, $sTmp, $nReplacements

    ; This fails to work in many mixed-ANSI/UTF-16 scenarios (as seen in WinGetText):
;~ If $bForceCnvt Then
;~ Return BinaryToString(StringToBinary($sStr, 2), 1)
;~ EndIf

    $nLen = StringLen($sStr)

    ; Struct for string (+1 for null-term)
    $stStrVer = DllStructCreate("wchar [" & $nLen + 1 & "]")
    ; Create a union, granting us binary 1-byte access to the wide chars
    $stBinVer = DllStructCreate("byte [" & $nLen * 2 & "]", DllStructGetPtr($stStrVer))

    ; Set String in structure
    DllStructSetData($stStrVer, 1, $sStr)

    ; Load string as binary data, convert to ANSI string
    ; AND Replace 0's with 0xFFFD (the Unicode 'REPLACEMENT CHARACTER')
    $sTmp = StringReplace(BinaryToString(DllStructGetData($stBinVer, 1)), ChrW(0), ChrW(0xFFFD), 0, 2)
    $nReplacements = @extended

    ; Trim off null-terminator and any other trailing 0's at the end (all converted to 0xFFFD's)
    While (StringRight($sTmp, 1) = ChrW(0xFFFD))
        $sTmp = StringTrimRight($sTmp, 1)
        $nReplacements -= 1

    ; If no replacements remaining, then every byte contains data, so its a safe bet the string is ANSI
    ; Also, in mixed-ANSI/UTF-16 situations (sometimes seen in WinGetText), allow a force
    If ($nReplacements = 0 Or $bForceCnvt) Then
        Return $sTmp
        ; Same result as:
        ;Return BinaryToString(StringToBinary($sStr, 2), 1)
        Return $sStr
EndFunc   ;==>_StringFixANSIInWide


- Added a 'force' parameter for scenarios where WinGetText() will return a mix of ANSI and Unicode text in the same string. The result will contain some '?'s in these scenarios, but there's really nothing you can do without modifying the AutoIt source code.

update: problem no longer exists as of v3.3.9.11 (see BugTracker ticket # 2362)

Edited by Ascend4nt
Link to post
Share on other sites

One of those things I've always noticed and wondered about, but never enough to really ask why :)! Great function, definitely a keeper for the snippet box, much appreciated m8 :)... Despite everything else, this imho might still be worth a bug ticket.

Edited by KaFu
Link to post
Share on other sites

Actually, as it turns out, I was a bit wrong in assuming the '?'s were invalid characters. I was able to see what the output really looked like by changing the encoding in Notepad2 and doing a paste..

So basically, a lot of the 2-byte ANSI characters are perfectly valid UTF-16 combinations, and they generally show up as Eastern language characters in a Unicode viewer. The '?'s are what results when converting from those eastern language codes to an ANSI version, which is what you see in most text editors in their default locale settings (and in SciTE's output).

Soo now I'm thinking MultiByteToWideChar may be a better option.. and just 'fool' AutoIt into thinking it is accepting a "wstr" as the 3rd parameter in the DLLCall. The issue I'm a bit uncertain about is deciding what to put as the Locale info.. I'd need to find out the encoding the given window uses - which in most cases would be the system (or user?) code page. Hmm.. I'll have to see if there's a Windows API function for detecting the encoding of a window. If anyone knows it, let me know (otherwise I'll be poking around the API for a bit)

Also, another problem is that MultiByteToWideChar will either drop illegal code points or fail completely (depending on the MB_ERR_INVALID_CHARS flag.. Oh, and of course any mixed ANSI/Wide strings would give some funky results for the Wide string portion.. which is why I'm leaning towards telling people just to avoid WinGetText on problem windows, since you're basically getting a contaminated string by doing that.

Hmm.. for now I suppose I'll leave the function intact, as it works relatively well for Controls, but there's still a minimal chance that embedded ANSI can produce a completely valid UTF-16 string..

Link to post
Share on other sites

Maybe it's related to the code page and language defined in the resources?

FileGetVersion($sFile, "DefaultLangCodepage")

From Help-File for FileGetVersion():

"Another special stringname is "DefaultLangCodepage" can be used to retrieve the default language and codepage.

The language and codepage can be used if needed to differentiate the "stringname" i.e. "080904b0Comments" (see MSDN StringFileInfo in VerQueryValue function)."

Link to post
Share on other sites

Thanks for the idea KaFu, but I don't know that that would work well with multilanguage apps or text editors that let you change the encoding..

However, I've identified the problem - its the call to SendMessage (with WM_GETTEXT). It returns either an ANSI or Wide string, and the length of the returned string. I've found its pretty easy once we know the string length - we can just compare the wide string length to this and determine if it should be read as ANSI if its approximately half that returned length.

I've updated the first post with the new _WinGetTextFixed() function.

Link to post
Share on other sites
  • 5 months later...
  • Administrators

Edit: I'll try some debugging with those examples.  I see that I made some notes in the source about scite being bugged and returning weird stuff.

Edited by Jon
Link to post
Share on other sites
  • Administrators

Hopefully this is all fixed in

I've fixed my window handling class so that all calls for WM_GETTEXT use it (there were a few places I'd missed).  I've checked WinGetText() and ControlGetText() with it and Scite.

Same fixes for Au3Info too.

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By MrKm
      This tiny yet powerful UDF will help you to convert Images to text with the help of  OCRSpace API version 3.50 .
      Detect text from a local file.
      ; ========================================================= ; Example 2 : Gets text from an image from a local path reference ; : Searchable PDF is not requested by default. ; : Processes it using a basic OCR logic. ; ========================================================= $b_Create_Searchable_PDF = True ; Use a table logic for receipt OCR $b_Table = True ; Set your key here. $v_OCRSpaceAPIKey = "" $OCROptions = _OCRSpace_SetUpOCR($v_OCRSpaceAPIKey, 1, $b_Table, True, "eng", True, Default, Default, $b_Create_Searchable_PDF) $sText_Detected = _OCRSpace_ImageGetText($OCROptions, @scriptdir & "\receipt.jpg", 0, "SEARCHABLE_URL") ConsoleWrite( _ " Detected text : " & $sText_Detected & @CRLF & _ " Error Returned : " & @error & @CRLF & _ " PDF URL : " & Eval("SEARCHABLE_URL") & @CRLF)  
      Detect text from a URL reference.
      ; ========================================================= ; Example 1 : Gets text from an image using a url reference ; : Searchable PDF is not requested. ; : Processes it using a basic OCR logic. ; ========================================================= $v_OCRSpaceAPIKey = "" ; SetUp some preferences.. $OCROptions = _OCRSpace_SetUpOCR($v_OCRSpaceAPIKey, 1, False, True, "eng", True, Default, Default, False) ; Make the request.. $sText_Detected = _OCRSpace_ImageGetText($OCROptions, "https://i.imgur.com/vbYXwJm.png", 0) ConsoleWrite( _ " Detected text : " & $sText_Detected & @CRLF & _ " Error Returned : " & @error & @CRLF)    
      Detect text from a URL reference to an array
      #include "OCRSpaceUDF\_OCRSpace_UDF.au3" #include <array.au3> ; Set your key here. $v_OCRSpaceAPIKey = "" $OCROptions = _OCRSpace_SetUpOCR($v_OCRSpaceAPIKey, 1, $b_Table, True, "eng", True, Default, Default, False) ; Below, the return type is set to 1 to return an array containing the coordinates of the bounding boxes for each word detected, ; in the format : #WordDetected , #Left , #Top , 3Height, #Width $aText_Detected = _OCRSpace_ImageGetText($OCROptions, "https://i.imgur.com/Z1enogD.jpeg", 1) _ArrayDisplay($aText_Detected, "")  
      Download Latest Version : 
    • By nacerbaaziz
      hello guys, please i need your help
      am trying to work with CreateWindowEx api, i created the window with it controls, also i setup the call back function
      i'am using WinMSGLoop to focus with the keyboard.
      here i have a problem, i hope that you can help me.
      on the controls i used the UDF that comme with the autoit, such as _GUIButton_Create, _GUIListBox_Create....
      but i can't find a STATIC control UDF, for that i used this

      local $h_ssrvlbl = _WinAPI_CreateWindowEx(0, "STATIC", "الخادم", BitOr($WS_VISIBLE, $WS_CHILD, $WS_CLIPSIBLINGS, $WS_CLIPCHILDREN), 250, 10, 100, 20, $hWnd)
      as you can see here, there is an arabic text, so here is the problem, the arabic text isn't show normally, what is the problem here?
      also i have  an other question about keyboard focus, when i used WinMSGLoop, it worked, but if i press alt+tab to switch windows or focus an other window and return back to my window, the focus of control is kill.
      can any one help me to solve that please?
      my code will be as file here with the include files
      i hope can any one help me here
      thanks in advance
      speed Test win.zip
    • By nacerbaaziz
      hello evrybody
      here is an example about how to split your texts using a delimiter with the ability to select how much of delimiters shows in each colum  with $i_number
      you have a long text and you want to split it in an array
      that evry colum have a number (n) of lines
      i made a function that do that for you
      just call it with a three params
      your text
      the number that you want to put in each col
      the siparator
      default is "|"
      here is the function with example
      i hope that it will be useful for you
      #include <Array.au3> $s_txt = "some text1some text2|some text3|some text4|some text5|some text6" $array = splitText($s_txt, 2) _ArrayDisplay($array) Func splitText($s_text, $i_number, $s_siparator = "|") Local $a_TXT = StringSplit($s_text, $s_siparator) Local $a_Return[$a_TXT[0] + 1] If ($a_TXT[0] <= $i_number) Or ($i_number <= 0) Then ReDim $a_Return[2] $a_Return[0] = 1 $a_Return[1] = $s_text Return $a_Return EndIf Local $i_Processed = 1, $i_arrayProcessed = 1 Do For $i = $i_Processed To ($i_Processed + $i_number) - 1 If ($a_TXT[0] < $i) Then ExitLoop If Not ($a_Return[$i_arrayProcessed]) Then $a_Return[$i_arrayProcessed] = $a_TXT[$i] Else $a_Return[$i_arrayProcessed] &= $s_siparator & $a_TXT[$i] EndIf $i_Processed += 1 Next $i_arrayProcessed += 1 Until ($a_TXT[0] < $i_Processed) ReDim $a_Return[$i_arrayProcessed] $a_Return[0] = $i_arrayProcessed - 1 Return $a_Return EndFunc ;==>splitText
      accept my greetings
      thanks to
      for his notes
    • By cruisepandey
      I have a string like this : 
      Global $Msga = "urrent directory is /send.  (Submission of file with log number 29381077284 is confirmed)";
      I want to extract the number 29381077284  from the string. I did StringSplit to split based on "(" and then use space to reach there, But it's not a good choice. 
      Can anyone help me with regular expression to find the number from String using AutoIT. TIA
    • By DannyJ
      I have this example string:
      2020-06-08 09:23:33 : abcdefghifjklm How to trim just the right part? Or how to trim this text right to left?
      I want to trim this string right to left until ' : '.
      In ohter words: I need the 'abcdefghifklm' text.
      I have tried StringTrimRight, but that requires 
      The number of characters to trim. Unfortunately StringSplit trims the left part of the strings.
  • Create New...