Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 12/26/2020 in all areas

  1. I have had a need for quite a while to redact sensitive information in screenshots before sending them off to 3rd party auditors. Previously I have done this by using HP UFT which has built in ocr to find the text, and then pass that to my AutoIt script to redact that information and take the screenshot. Using Tesseract and the UDF provided by seangriffin as a great starting point I was able to finally incorporate all of this into just AutoIt. This requires the Tesseract v3.0.5+ as it uses the tsv option. I wrote this using v4.0.0 which is available here I’ve never needed to use OCR to find text to click on but if you just want to use this for that, then the array returned by _TesseractFindCoords() will provide that. For some reason ScITE refreshes the screen a lot while running which can delete the redaction so it’s best to have ScITE on a different screen than what Explorer will open on when running this example. Run Script, Pick 1 or more files in a single folder then click Open. The script will then ShellExecute to open that selected folder and a screenshot will be taken. OCR will be performed and the selected files will be redacted, then a screenshot will be taken of that redacted screen. The redacted screenshot will then be displayed. The main functions that I’d love some feedback on are the _TesseractCaptureText() and _TesseractFindCoords(). Also, If you know of a way to prevent refreshes from deleting the _WinAPI_DrawLine in the Redaction I’d love to hear it, though it doesn’t seem to be a problem when I run as compiled. #include <WindowsConstants.au3> #include <File.au3> ;Required to create a unique ~temp file #include <ScreenCapture.au3> ;Required to capture image Global $aOCR_IMG_ORI ;Global Var that will store the location of the image that was taken to perform OCR on Global $tesseract_temp_path = @TempDir & "\" ;Location of the image that is taken, and the .tsv file that is generated from that Global $tesseract_path = @ScriptDir & "\Tesseract-OCR\" ;Need to have the Tesseract-OCR folder in the sub-folder of the script thats running Global $sPath, $aFileName ;For this example, you will browse to a folder and select a file. that files name will be the text to search for ; That files location will then be opened in Windows Explorer and OCR will be performed on that to find the file within that folder ; Note: file name must be visible to find so pick a folder without too many files or file names that are longer than can be displayed ;Pick file(s) to OCR find within Explorer $sFile = FileOpenDialog("Pick a file", @ScriptDir, "All (*.*)", 4) $aFiles = StringSplit($sFile, "|") If $aFiles[0] > 1 Then ;Multiple files were selected. ;[1] will contain the folder ;[2]+ will contain the individual files Global $aFileName[$aFiles[0] - 1] $sPath = $aFiles[1] & "\" For $i = 2 to $aFiles[0] $aFileName[$i - 2] = $aFiles[$i] Next Else ;Only 1 file was selected, split it up into parts so the array will be setup same as if multiple files were selected Local $sDrive, $sDir, $sFileName, $sExtension $aPathSplit = _PathSplit($sFile, $sDrive, $sDir, $sFileName, $sExtension) $sPath = $sDrive & $sDir Global $aFileName[1] = [$sFileName & $sExtension] EndIf SplashTextOn("MyOCR_Example", "This can take a few minutes..." & @CRLF & "Opening Location", 500, 40, 0, 0) ShellExecute($sPath, "open") WinWaitActive("[CLASS:CabinetWClass]", StringLeft($sPath, StringLen($sPath) - 1), 15) ;Wait until WinExplorer is opened ControlSetText("MyOCR_Example", "", "Static1", "Found WinExplorer") ;Capture all text from the main area of Windows Explorer ControlSetText("MyOCR_Example", "", "Static1", "Getting all text") $aTSV = _TesseractCaptureText("[CLASS:CabinetWClass]", "", "[CLASS:DirectUIHWND; INSTANCE:3]", 2, 0, 0, 0, 0, 1, 1) ;Capture text from the explorer item within windows explorer ;From the captured text, find what we are looking for ;Must not do anything that would cause screen refreshes in the loop or before taking screenshot as that can erase redaction, ControlSetText in SplashText can break it too... For $i = 0 to UBound($aFileName) - 1 $sFileName = StringLeft($aFileName[$i], StringInStr($aFileName[$i], ".", 0, -1) - 1) ;Strip off the Extension since some systems hide extensions ConsoleWrite("Looking for " & $sFileName) $aCoords = _TesseractFindCoords($aTSV, $sFileName, 0, 1, 2) ;Returns x,y,w,h of requested text ConsoleWrite(@TAB & "Drawing a black line from " & $aOCR_IMG_ORI[0] + $aCoords[0] & "," & $aOCR_IMG_ORI[1] + $aCoords[1] & " to " & $aCoords[2] & "," & $aCoords[3] & @CRLF) _Redact($aOCR_IMG_ORI[0] + $aCoords[0], $aOCR_IMG_ORI[1] + $aCoords[1], $aCoords[2], $aCoords[3], 0x000000) ;Black out/Redact the selected text Next ;Now that everything is redacted, take the screenshot $hwnd2 = ControlGetHandle("[CLASS:CabinetWClass]", "", "[CLASS:DirectUIHWND; INSTANCE:3]") _ScreenCapture_CaptureWnd(@ScriptDir & "\Redacted.jpg", $hwnd2) MsgBox(0, "", "Redacted" & @CRLF & "Click OK to display screenshot") ShellExecute(@ScriptDir & "\Redacted.jpg") ;=============================================================================== ; Name...........: _TesseractFindCoords() ; Description ...: Retreives the Coordinates for specific text from the text returned by a Tesseract OCR TSV function ; Syntax.........: _TesseractFindCoords($aTSV, $sFind, $iFindType = 0, $iMatch = 1) ; Parameters ....: $aTSV - Array returned from one of the _Tesseract-CapturePOS() functions ; $sFind - String value of the text to find, spacing will be ignored as Tesseract seperates array by words ignoring spaces. ; $iFindType - How to search for the text ; 0 = Contains anywhere (StringInStr) (default) ; 1 = Starts with (StringLeft) ; 2 = Ends with (StringRight) ; 3 = Exact Match (=) ; $iMatch - If multiple matches may exist, which of them do we want to return ; 1 = Return the first one we find (default) ; -1 = Return the last one we find ; # = Return the # item that we find ; $scale - This must be the same scale as used in the _TesseractCaptureText() function as it will be used to adjust the coordinates ; ; 2nd to last Column = conf. Confidence % that it's actually the word, may want to incorporate this as an additional option ; ; Return values .: On Success - Returns an array of coordinates for the matched text. ; On Failure - Sets @error, returns empty array ; ; Author ........: BigDaddyO ; Modified.......: ; ; Remarks .......: Array returned from the _TesseractCaptureText() functions contains the text and their location ; This function finds the $sFind text within that array. ; Note: All text from the image is broken down into individual words, so if the $sFind contains any spaces then it will ; be split up into an array itself and will be matched as concurrent items in the array. ; The Coordinates of each word are then added together to make coords for a single box including calculated space width ; ; Related .......: Requires Tesseract 3.05+ to use TSV commandline option ; Link ..........: ; Example .......: ;========================================================================================== Func _TesseractFindCoords($aTSV, $sTextToFind, $iFindType = 0, $iMatch = 1, $scale = 2) If $iMatch = 0 Then ConsoleWrite("Error, $iMatch 0 is not valid" & @CRLF) Return SetError(1) EndIf local $aCoords[4] ;Create the array that will store the coordinates Local $iFoundCount = 0 $sTextToFind = StringStripWS($sTextToFind, 7) ;Remove any Trailing, leading, or double spaces so it will split and match up properly If $sTextToFind = "" Then ConsoleWrite("Error, the $sTextToFind is empty" & @CRLF) Return SetError(1, 0, $aCoords) EndIf If IsArray($aTSV) = 0 Then ConsoleWrite("Error, $aTSV is not an array" & @CRLF) Return SetError(1, 0, $aCoords) EndIf If UBound($aTSV, 2) <> 12 Then ConsoleWrite("OCR Text array size = ([" & UBound($aTSV) & "][" & UBound($aTSV, 2) & "]), does not match the expected [#][12]. Ensure you have the $tsv flag set to 1 in the _TesseractCaptureText() Function" & @CRLF) Return SetError(2, 0, $aCoords) EndIf $aSearchText = StringSplit($sTextToFind, " ") ;Split up the $sTextToFind into individual words as Tesseract stores them Switch $iFindType Case 0 ;Looks anywhere for the word/sentence to start and end, words within a sentence must be exact For $i = 1 to UBound($aTSV) - 1 ;Search the contents of the OCR text array If $aSearchText[0] > 1 Then ;This is a sentence, not just a word, so ensure this ends with the first word, not just InStr If StringRight($aTSV[$i][11], StringLen($aSearchText[1])) <> $aSearchText[1] Then ContinueLoop ;This is not the text we are looking for EndIf Else ;Looking for just a single word, so it could be anywhere If StringInStr($aTSV[$i][11], $aSearchText[1]) = 0 Then ContinueLoop ;This is not the text we are looking for EndIf EndIf Local $aTmpCoords[4] ;Create array to store the coords until we are sure it's a full match Local $iFoundAt = $i ;Save the # we found the first word at, incase this isn't the whole sentence Local $iCount = 0 ;Used to count the current word in the search text array we are on For $t = $i to $i + ($aSearchText[0] - 1) ;Looks like this is what we want, verify everything matches. $iCount += 1 Select Case $iCount = $aSearchText[0] ;This is the last word in the text we are looking for If $iCount = 1 Then ;If this is actually the first & last then we need to save the Coords directly If StringInStr($aTSV[$t][11], $aSearchText[($t - $iFoundAt) + 1]) Then ;This is the first & Last word to find, so do an InStr $aTmpCoords[0] = $aTSV[$t][6] ;Save the left value into the Temp Coords array $aTmpCoords[1] = $aTSV[$t][7] ;Save the Top value into the Temp Coords array $aTmpCoords[2] = $aTSV[$t][8] ;Save the Width value into the Temp Coords array $aTmpCoords[3] = $aTSV[$t][9] ;Save the Height value into the Temp Coords array Else ContinueLoop(2) ;not found, so continue looking for full match EndIf Else ;This is the last but not the first, so we need to do a StringLeft instead of InStr to ensure the sentence is continues. If StringLeft($aTSV[$t][11], StringLen($aSearchText[($t - $iFoundAt) + 1])) = $aSearchText[($t - $iFoundAt) + 1] Then If $aTmpCoords[1] > $aTSV[$t][7] Then $aTmpCoords[1] = $aTSV[$t][7] ;Use the top most coord avail $aTmpCoords[2] += $aTSV[$t][8] ;Add this words width to what we already have If $aTmpCoords[3] < $aTSV[$t][9] Then $aTmpCoords[3] = $aTSV[$t][9] ;Use the value with the greatest height Else ContinueLoop(2) EndIf EndIf $iFoundCount += 1 ;This is a full match, so Increase the successful match by 1 $aCoords[0] = $aTmpCoords[0] ;Save the Temp Coordinates $aCoords[1] = $aTmpCoords[1] $aCoords[2] = $aTmpCoords[2] $aCoords[3] = $aTmpCoords[3] If $iMatch = $iFoundCount Then ;See if this is the match we want, defaults to the first match found ;If there are multiple words, need to identify how many pixels to include where the spaces should have been If $aSearchText[0] > 1 Then $iSpace = $aTSV[$t][8] / StringLen($aTSV[$t][11]) ;Get width of this word, then divide by the character length to get pixels per character $sTxtOnly = StringStripWS($sTextToFind, 8) $iSpace = $aCoords[2] / StringLen($sTxtOnly) ;Divide the total width by the number of characters to get Pixels per character $aCoords[2] += ($iSpace * ($aSearchText[0] - 1)) EndIf ;Convert the coordinates to onscreen coords according to the scale $aCoords[0] = $aCoords[0] / $scale $aCoords[1] = $aCoords[1] / $scale $aCoords[2] = $aCoords[2] / $scale $aCoords[3] = $aCoords[3] / $scale Return $aCoords Else ContinueLoop(2) ;We found the full search text, but we don't want this one. EndIf Case $iCount = 1 If $aSearchText[0] > 1 Then ;This is a sentence, not just a word, so ensure this ends with the first word, not just InStr If StringRight($aTSV[$t][11], StringLen($aSearchText[($t - $iFoundAt) + 1])) <> $aSearchText[($t - $iFoundAt) + 1] Then ContinueLoop(2) ;This is not the text we are looking for EndIf Else If StringInStr($aTSV[$t][11], $aSearchText[($t - $iFoundAt) + 1]) = 0 Then ContinueLoop(2) ;This is not the text we are looking for EndIf EndIf $aTmpCoords[0] = $aTSV[$t][6] ;Save the left value into the Temp Coords array $aTmpCoords[1] = $aTSV[$t][7] ;Save the top value into the Temp Coords array $aTmpCoords[2] = $aTSV[$t][8] ;Save the width value into the Temp Coords array $aTmpCoords[3] = $aTSV[$t][9] ;Save the height value into the Temp Coords array ContinueLoop ;Look for the next word Case Else ;This is a middle word in the search text, so do an = If $aTSV[$t][11] = $aSearchText[$t - $iFoundAt + 1] Then ;Leave the [0] coord as-is If $aTmpCoords[1] > $aTSV[$t][7] Then $aTmpCoords[1] = $aTSV[$t][7] ;Use the top most coord avail $aTmpCoords[2] += $aTSV[$t][8] ;Add this words width to what we already have If $aTmpCoords[3] < $aTSV[$t][9] Then $aTmpCoords[3] = $aTSV[$t][9] ;Use the value with the greatest height ContinueLoop ;Possible that there is only 1 search word, so need to continue loop so the Start and End don't get triggered Else $i = $iFoundAt ContinueLoop(2) ;if we didn't find the last word in the search text, start the search over EndIf EndSelect Next Next Case 1 ;The word/sentence starts with the specified text For $i = 1 to UBound($aTSV) - 1 ;Search the contents of the OCR text array If $aSearchText[0] > 1 Then ;This is a sentence, not just a word, so ensure the entire first word matches If $aTSV[$i][11] <> $aSearchText[1] Then ContinueLoop EndIf Else ;Looking for just a single word, so ensure it starts with this If StringLeft($aTSV[$i][11], StringLen($aSearchText[1])) <> $aSearchText[1] Then ContinueLoop ;This is not the text we are looking for EndIf EndIf Local $aTmpCoords[4] ;Create array to store the coords until we are sure it's a full match Local $iFoundAt = $i ;Save the # we found the first word at, incase this isn't the whole sentence Local $iCount = 0 ;Used to count the current word in the search text array we are on For $t = $i to $i + ($aSearchText[0] - 1) ;Looks like this is what we want, verify everything matches. $iCount += 1 Select Case $iCount = $aSearchText[0] ;This is the last word in the text we are looking for If $iCount = 1 Then ;If this is actually the first & last then we need to save the Coords directly If StringLeft($aTSV[$i][11], StringLen($aSearchText[1])) = $aSearchText[($t - $iFoundAt) + 1] Then ;This is the first & Last word to find, so ensure it starts with $aTmpCoords[0] = $aTSV[$t][6] ;Save the left value into the Temp Coords array $aTmpCoords[1] = $aTSV[$t][7] ;Save the Top value into the Temp Coords array $aTmpCoords[2] = $aTSV[$t][8] ;Save the Width value into the Temp Coords array $aTmpCoords[3] = $aTSV[$t][9] ;Save the Height value into the Temp Coords array Else ContinueLoop(2) ;not found, so continue looking for full match EndIf Else ;This is the last but not the first, so we need to do a StringLeft to ensure the sentence is continues. If StringLeft($aTSV[$t][11], StringLen($aSearchText[($t - $iFoundAt) + 1])) = $aSearchText[($t - $iFoundAt) + 1] Then If $aTmpCoords[1] > $aTSV[$t][7] Then $aTmpCoords[1] = $aTSV[$t][7] ;Use the top most coord avail $aTmpCoords[2] += $aTSV[$t][8] ;Add this words width to what we already have If $aTmpCoords[3] < $aTSV[$t][9] Then $aTmpCoords[3] = $aTSV[$t][9] ;Use the value with the greatest height Else ContinueLoop(2) EndIf EndIf $iFoundCount += 1 ;This is a full match, so Increase the successful match by 1 $aCoords[0] = $aTmpCoords[0] ;Save the Temp Coordinates $aCoords[1] = $aTmpCoords[1] $aCoords[2] = $aTmpCoords[2] $aCoords[3] = $aTmpCoords[3] If $iMatch = $iFoundCount Then ;See if this is the match we want, defaults to the first match found ;If there are multiple words, need to identify how many pixels to include where the spaces should have been If $aSearchText[0] > 1 Then $iSpace = $aTSV[$t][8] / StringLen($aTSV[$t][11]) ;Get width of this word, then divide by the character length to get pixels per character $sTxtOnly = StringStripWS($sTextToFind, 8) $iSpace = $aCoords[2] / StringLen($sTxtOnly) ;Divide the total width by the number of characters to get Pixels per character $aCoords[2] += ($iSpace * ($aSearchText[0] - 1)) EndIf ;Convert the coordinates to onscreen coords according to the scale $aCoords[0] = $aCoords[0] / $scale $aCoords[1] = $aCoords[1] / $scale $aCoords[2] = $aCoords[2] / $scale $aCoords[3] = $aCoords[3] / $scale Return $aCoords Else ContinueLoop(2) ;We found the full search text, but we don't want this one. EndIf Case $iCount = 1 If $aSearchText[0] > 1 Then ;This is a sentence, not just a word, so ensure this matches the first word exactly If $aTSV[$t][11] <> $aSearchText[($t - $iFoundAt) + 1] Then ContinueLoop(2) ;This is not the text we are looking for EndIf Else If StringLeft($aTSV[$t][11], StringLen($aSearchText[($t - $iFoundAt) + 1])) <> $aSearchText[($t - $iFoundAt) + 1] Then ContinueLoop(2) ;This is not the text we are looking for EndIf EndIf $aTmpCoords[0] = $aTSV[$t][6] ;Save the left value into the Temp Coords array $aTmpCoords[1] = $aTSV[$t][7] ;Save the top value into the Temp Coords array $aTmpCoords[2] = $aTSV[$t][8] ;Save the width value into the Temp Coords array $aTmpCoords[3] = $aTSV[$t][9] ;Save the height value into the Temp Coords array ContinueLoop ;Look for the next word Case Else ;This is a middle word in the search text, so do an = If $aTSV[$t][11] = $aSearchText[$t - $iFoundAt + 1] Then ;Leave the [0] coord as-is If $aTmpCoords[1] > $aTSV[$t][7] Then $aTmpCoords[1] = $aTSV[$t][7] ;Use the top most coord avail $aTmpCoords[2] += $aTSV[$t][8] ;Add this words width to what we already have If $aTmpCoords[3] < $aTSV[$t][9] Then $aTmpCoords[3] = $aTSV[$t][9] ;Use the value with the greatest height ContinueLoop ;Possible that there is only 1 search word, so need to continue loop so the Start and End don't get triggered Else $i = $iFoundAt ContinueLoop(2) ;if we didn't find the last word in the search text, start the search over EndIf EndSelect Next Next Case 2 ;The word/sentence ends with the specified text For $i = 1 to UBound($aTSV) - 1 ;Search the contents of the OCR text array If StringRight($aTSV[$i][11], StringLen($aSearchText[1])) <> $aSearchText[1] Then ;Ensure the first word is the last part ContinueLoop ;This is not the text we are looking for EndIf Local $aTmpCoords[4] ;Create array to store the coords until we are sure it's a full match Local $iFoundAt = $i ;Save the # we found the first word at, incase this isn't the whole sentence Local $iCount = 0 ;Used to count the current word in the search text array we are on For $t = $i to $i + ($aSearchText[0] - 1) ;Looks like this is what we want, verify everything matches. $iCount += 1 Select Case $iCount = $aSearchText[0] ;This is the last word in the text we are looking for If $iCount = 1 Then ;If this is actually the first & last then we need to save the Coords directly If StringRight($aTSV[$i][11], StringLen($aSearchText[1])) = $aSearchText[($t - $iFoundAt) + 1] Then ;This is the first & Last word to find, so ensure it Ends with $aTmpCoords[0] = $aTSV[$t][6] ;Save the left value into the Temp Coords array $aTmpCoords[1] = $aTSV[$t][7] ;Save the Top value into the Temp Coords array $aTmpCoords[2] = $aTSV[$t][8] ;Save the Width value into the Temp Coords array $aTmpCoords[3] = $aTSV[$t][9] ;Save the Height value into the Temp Coords array Else ContinueLoop(2) ;not found, so continue looking for full match EndIf Else ;This is the last but not the first, so we need to match exact since nothing should be before or after. If $aTSV[$t][11] = $aSearchText[($t - $iFoundAt) + 1] Then If $aTmpCoords[1] > $aTSV[$t][7] Then $aTmpCoords[1] = $aTSV[$t][7] ;Use the top most coord avail $aTmpCoords[2] += $aTSV[$t][8] ;Add this words width to what we already have If $aTmpCoords[3] < $aTSV[$t][9] Then $aTmpCoords[3] = $aTSV[$t][9] ;Use the value with the greatest height Else ContinueLoop(2) EndIf EndIf $iFoundCount += 1 ;This is a full match, so Increase the successful match by 1 $aCoords[0] = $aTmpCoords[0] ;Save the Temp Coordinates $aCoords[1] = $aTmpCoords[1] $aCoords[2] = $aTmpCoords[2] $aCoords[3] = $aTmpCoords[3] If $iMatch = $iFoundCount Then ;See if this is the match we want, defaults to the first match found ;If there are multiple words, need to identify how many pixels to include where the spaces should have been If $aSearchText[0] > 1 Then $iSpace = $aTSV[$t][8] / StringLen($aTSV[$t][11]) ;Get width of this word, then divide by the character length to get pixels per character $sTxtOnly = StringStripWS($sTextToFind, 8) $iSpace = $aCoords[2] / StringLen($sTxtOnly) ;Divide the total width by the number of characters to get Pixels per character $aCoords[2] += ($iSpace * ($aSearchText[0] - 1)) EndIf ;Convert the coordinates to onscreen coords according to the scale $aCoords[0] = $aCoords[0] / $scale $aCoords[1] = $aCoords[1] / $scale $aCoords[2] = $aCoords[2] / $scale $aCoords[3] = $aCoords[3] / $scale Return $aCoords Else ContinueLoop(2) ;We found the full search text, but we don't want this one. EndIf Case $iCount = 1 If StringRight($aTSV[$i][11], StringLen($aSearchText[($t - $iFoundAt) + 1])) = $aSearchText[($t - $iFoundAt) + 1] Then $aTmpCoords[0] = $aTSV[$t][6] ;Save the left value into the Temp Coords array $aTmpCoords[1] = $aTSV[$t][7] ;Save the top value into the Temp Coords array $aTmpCoords[2] = $aTSV[$t][8] ;Save the width value into the Temp Coords array $aTmpCoords[3] = $aTSV[$t][9] ;Save the height value into the Temp Coords array ContinueLoop ;Look for the next word Else ContinueLoop(2) EndIf Case Else ;This is a middle word in the search text, so do an = If $aTSV[$t][11] = $aSearchText[$t - $iFoundAt + 1] Then ;Leave the [0] coord as-is If $aTmpCoords[1] > $aTSV[$t][7] Then $aTmpCoords[1] = $aTSV[$t][7] ;Use the top most coord avail $aTmpCoords[2] += $aTSV[$t][8] ;Add this words width to what we already have If $aTmpCoords[3] < $aTSV[$t][9] Then $aTmpCoords[3] = $aTSV[$t][9] ;Use the value with the greatest height ContinueLoop ;Possible that there is only 1 search word, so need to continue loop so the Start and End don't get triggered Else ContinueLoop(2) ;if we didn't find the last word in the search text, start the search over EndIf EndSelect Next Next Case 3 ;The word/sentence match exactly with what is specified For $i = 1 to UBound($aTSV) - 1 ;Search the contents of the OCR text array If $aTSV[$i][11] <> $aSearchText[1] Then ;Ensure the first word matches ContinueLoop ;This is not the text we are looking for EndIf Local $aTmpCoords[4] ;Create array to store the coords until we are sure it's a full match Local $iFoundAt = $i ;Save the # we found the first word at, incase this isn't the whole sentence Local $iCount = 0 ;Used to count the current word in the search text array we are on For $t = $i to $i + ($aSearchText[0] - 1) ;Looks like this is what we want, verify everything matches. $iCount += 1 Select Case $iCount = $aSearchText[0] ;This is the last word in the text we are looking for If $iCount = 1 Then ;If this is actually the first & last then we need to save the Coords directly If $aTSV[$i][11] = $aSearchText[($t - $iFoundAt) + 1] Then ;This is the first & Last word to find, so ensure it matches exact $aTmpCoords[0] = $aTSV[$t][6] ;Save the left value into the Temp Coords array $aTmpCoords[1] = $aTSV[$t][7] ;Save the Top value into the Temp Coords array $aTmpCoords[2] = $aTSV[$t][8] ;Save the Width value into the Temp Coords array $aTmpCoords[3] = $aTSV[$t][9] ;Save the Height value into the Temp Coords array Else ContinueLoop(2) ;not found, so continue looking for full match EndIf Else ;This is the last but not the first, so we need to match exact since nothing should be before or after. If $aTSV[$t][11] = $aSearchText[($t - $iFoundAt) + 1] Then If $aTmpCoords[1] > $aTSV[$t][7] Then $aTmpCoords[1] = $aTSV[$t][7] ;Use the top most coord avail $aTmpCoords[2] += $aTSV[$t][8] ;Add this words width to what we already have If $aTmpCoords[3] < $aTSV[$t][9] Then $aTmpCoords[3] = $aTSV[$t][9] ;Use the value with the greatest height Else ContinueLoop(2) EndIf EndIf $iFoundCount += 1 ;This is a full match, so Increase the successful match by 1 $aCoords[0] = $aTmpCoords[0] ;Save the Temp Coordinates $aCoords[1] = $aTmpCoords[1] $aCoords[2] = $aTmpCoords[2] $aCoords[3] = $aTmpCoords[3] If $iMatch = $iFoundCount Then ;See if this is the match we want, defaults to the first match found ;If there are multiple words, need to identify how many pixels to include where the spaces should have been If $aSearchText[0] > 1 Then $iSpace = $aTSV[$t][8] / StringLen($aTSV[$t][11]) ;Get width of this word, then divide by the character length to get pixels per character $sTxtOnly = StringStripWS($sTextToFind, 8) $iSpace = $aCoords[2] / StringLen($sTxtOnly) ;Divide the total width by the number of characters to get Pixels per character $aCoords[2] += ($iSpace * ($aSearchText[0] - 1)) EndIf ;Convert the coordinates to onscreen coords according to the scale $aCoords[0] = $aCoords[0] / $scale $aCoords[1] = $aCoords[1] / $scale $aCoords[2] = $aCoords[2] / $scale $aCoords[3] = $aCoords[3] / $scale Return $aCoords Else ContinueLoop(2) ;We found the full search text, but we don't want this one. EndIf Case $iCount = 1 If $aTSV[$i][11] = $aSearchText[($t - $iFoundAt) + 1] Then $aTmpCoords[0] = $aTSV[$t][6] ;Save the left value into the Temp Coords array $aTmpCoords[1] = $aTSV[$t][7] ;Save the top value into the Temp Coords array $aTmpCoords[2] = $aTSV[$t][8] ;Save the width value into the Temp Coords array $aTmpCoords[3] = $aTSV[$t][9] ;Save the height value into the Temp Coords array ContinueLoop ;Look for the next word Else ContinueLoop(2) EndIf Case Else ;This is a middle word in the search text, so do an = If $aTSV[$t][11] = $aSearchText[$t - $iFoundAt + 1] Then ;Leave the [0] coord as-is If $aTmpCoords[1] > $aTSV[$t][7] Then $aTmpCoords[1] = $aTSV[$t][7] ;Use the top most coord avail $aTmpCoords[2] += $aTSV[$t][8] ;Add this words width to what we already have If $aTmpCoords[3] < $aTSV[$t][9] Then $aTmpCoords[3] = $aTSV[$t][9] ;Use the value with the greatest height ContinueLoop ;Possible that there is only 1 search word, so need to continue loop so the Start and End don't get triggered Else ContinueLoop(2) ;if we didn't find the last word in the search text, start the search over EndIf EndSelect Next Next Case Else ConsoleWrite("Invalid $iFindType value" & @CRLF) Return SetError(1, 0, $aCoords) EndSwitch If $iMatch = -1 Then ;If the $iMatch = -1 then we want the last match found which should now be set to the $aCoords array, so return that $aCoords[0] = $aCoords[0] / $scale $aCoords[1] = $aCoords[1] / $scale $aCoords[2] = $aCoords[2] / $scale $aCoords[3] = $aCoords[3] / $scale Return $aCoords EndIf Return SetError(1, 0, $aCoords) ;If we got this far, then text was not found EndFunc ;_TesseractFindCoords() ; #FUNCTION# =============================================================================== ; ; Name...........: _TesseractCaptureText() ; Description ...: Captures text from a control or Window ; Syntax.........: _TesseractCaptureText($win_title, $win_text = "", $ctrl_id = "", $scale = 2, $left_indent = 0, $top_indent = 0, $right_indent = 0, $bottom_indent = 0, $tsv = 0) ; Parameters ....: $win_title - The title of the window to capture text from. ; $win_text - Optional: The text of the window to capture text from. ; $ctrl_id - Optional: The ID of the control to capture text from. ; The text of the window will be returned if one isn't provided. ; $scale - Optional: The scaling factor of the screenshot prior to text recognition. ; Increase this number to improve accuracy. ; The default is 2. ; $left_indent - A number of pixels to indent the capture from the ; left of the control. ; $top_indent - A number of pixels to indent the capture from the ; top of the control. ; $right_indent - A number of pixels to indent the capture from the ; right of the control. ; $bottom_indent - A number of pixels to indent the capture from the ; bottom of the control. ; $tsv - What type of return you want ; 0 = Return text in a 1D array ; 1 = Returns 2D array showing text and the exact position found within the control ; ; Return values .: On Success - Returns an array of text that was captured. Global Var ($aOCR_IMG_ORI) is the starting coord the OCR image was captured from ; On Failure - Returns an empty array. ; Author ........: seangriffin - Was origionally _TesseractControlCapture() function ; ; Modified.......: BigDaddyO - Included new ability to capture coords using $tsv (requires 3.05+) ; - Removed the dropdown and listbox stuff as they should allow their text to be pulled directly without OCR ; - Removed the Cleanup option as that wasn't compatible with the new tsv options. ; - Removed the scrolling option as it added a lot of complications and could just call this function +1 times to do the same if text not found ; ; Remarks .......: ; Related .......: ; Link ..........: ; Example .......: ; ;========================================================================================== Func _TesseractCaptureText($win_title, $win_text = "", $ctrl_id = "", $scale = 2, $left_indent = 0, $top_indent = 0, $right_indent = 0, $bottom_indent = 0, $tsv = 0, $Debug = 0) if Not IsDeclared($aOCR_IMG_ORI) Then Global $aOCR_IMG_ORI Local $aArray, $hwnd ; if a control ID is specified, then get it's HWND if StringCompare($ctrl_id, "") <> 0 Then $hwnd = ControlGetHandle($win_title, $win_text, $ctrl_id) EndIf ; Perform the text recognition WinActivate($win_title) $capture_filename = _TempFile($tesseract_temp_path, "~", ".tif") $ocr_filename = StringLeft($capture_filename, StringLen($capture_filename) - 4) $Capture = CaptureToTIFF($win_title, $win_text, $hwnd, $capture_filename, $scale, $left_indent, $top_indent, $right_indent, $bottom_indent) If @error Then ConsoleWrite("Error Capturing TIFF for OCR (" & $Capture & ")" & @CRLF) EndIf $aOCR_IMG_ORI = $Capture ConsoleWrite("Area that will be scanned for OCR: " & $Capture[0] & "," & $Capture[1] & "," & $Capture[2] & "," & $Capture[3] & @CRLF) If $tsv = 1 Then $ocr_filename_and_ext = $ocr_filename & ".tsv" ShellExecuteWait($tesseract_path & "tesseract.exe", $capture_filename & " " & $ocr_filename & " tsv", $tesseract_path, "open", @SW_HIDE) ;This will return a tab seperated file Else $ocr_filename_and_ext = $ocr_filename & ".txt" ShellExecuteWait($tesseract_path & "tesseract.exe", $capture_filename & " " & $ocr_filename, $tesseract_path, "open", @SW_HIDE) EndIf If $tsv = 1 Then _FileReadToArray($ocr_filename_and_ext, $aArray, 0, @TAB) ;Build a 2D array Else _FileReadToArray($ocr_filename_and_ext, $aArray, 0) ;This will return each line of text in a 1D array EndIf FileDelete($ocr_filename & ".*") Return $aArray EndFunc ;Draw the black line over the text we found prior to taking a screenshot ; Note, if running from ScITE consolewrites and some other things within the script can refresh the screen and delete the redactions ; Best if you can move ScITE to a different screen than the redaction is running on, or just run as compiled. Func _Redact($iX, $iY, $iWidth, $iHeight, $iColor) Local $hDC = _WinAPI_GetWindowDC(0) ;DC of entire screen (desktop) Local $hPen = _WinAPI_CreatePen($PS_SOLID, $iHeight, $iColor) ;Create a solid pen object with the color provided Local $oSelect = _WinAPI_SelectObject($hDC, $hPen) ;Select the $hDC and use the $hPen for this line _WinAPI_DrawLine($hDC, $iX, $iY + ($iHeight / 2), $iX + $iWidth, $iY + ($iHeight / 2)) ;Draw the line in the middle, Pen size should cover everything _WinAPI_DeleteObject($hPen) _WinAPI_SelectObject($hDC, $oSelect) _WinAPI_ReleaseDC(0, $hDC) EndFunc ;==>_WinAPI_DrawRect ; #FUNCTION# ;=============================================================================== ; ; Name...........: CaptureToTIFF() ; Description ...: Captures an image of the screen, a window or a control, and saves it to a TIFF file. ; Syntax.........: CaptureToTIFF($win_title = "", $win_text = "", $ctrl_id = "", $sOutImage = "", $scale = 1, $left_indent = 0, $top_indent = 0, $right_indent = 0, $bottom_indent = 0) ; Parameters ....: $win_title - The title of the window to capture an image of. ; $win_text - Optional: The text of the window to capture an image of. ; $ctrl_id - Optional: The ID of the control to capture an image of. ; An image of the window will be returned if one isn't provided. ; $sOutImage - The filename to store the image in. ; $scale - Optional: The scaling factor of the capture. ; $left_indent - A number of pixels to indent the screen capture from the ; left of the window or control. ; $top_indent - A number of pixels to indent the screen capture from the ; top of the window or control. ; $right_indent - A number of pixels to indent the screen capture from the ; right of the window or control. ; $bottom_indent - A number of pixels to indent the screen capture from the ; bottom of the window or control. ; Return values .: None ; Author ........: seangriffin ; Modified.......: BigDaddyO - Returns the Position of the object we are scanning for text ; Remarks .......: ; Related .......: ; Link ..........: ; Example .......: No ; ; ;========================================================================================== Func CaptureToTIFF($win_title = "", $win_text = "", $ctrl_id = "", $sOutImage = "", $scale = 1, $left_indent = 0, $top_indent = 0, $right_indent = 0, $bottom_indent = 0) Local $hWnd, $hwnd2, $hDC, $hBMP, $hImage1, $hGraphic, $CLSID, $tParams, $pParams, $tData, $i = 0, $hImage2, $pos[4], $screenpos[4] Local $Ext = StringUpper(StringMid($sOutImage, StringInStr($sOutImage, ".", 0, -1) + 1)) Local $giTIFColorDepth = 24 Local $giTIFCompression = $GDIP_EVTCOMPRESSIONNONE ; If capturing a control if StringCompare($ctrl_id, "") <> 0 Then $hwnd2 = ControlGetHandle($win_title, $win_text, $ctrl_id) $pos = WinGetPos($hwnd2) ;WinGetPos works for controls as well if you pass the handle as the window title Else ; If capturing a window if StringCompare($win_title, "") <> 0 Then $hwnd2 = WinGetHandle($win_title, $win_text) $pos = WinGetPos($win_title, $win_text) Else ; If capturing the desktop $hwnd2 = "" $pos[0] = 0 $pos[1] = 0 $pos[2] = @DesktopWidth $pos[3] = @DesktopHeight EndIf EndIf If $pos[2] < 1 or $pos[3] < 1 Then Return SetError(1, 0, "No Width or Height found for specified item") ; Capture an image of the window / control if IsHWnd($hwnd2) Then WinActivate($win_title, $win_text) $hBitmap2 = _ScreenCapture_CaptureWnd("", $hwnd2, 0, 0, -1, -1, False) Else $hBitmap2 = _ScreenCapture_Capture("", 0, 0, -1, -1, False) EndIf _GDIPlus_Startup () ; Convert the image to a bitmap $hImage2 = _GDIPlus_BitmapCreateFromHBITMAP ($hBitmap2) $hWnd = _WinAPI_GetDesktopWindow() $hDC = _WinAPI_GetDC($hWnd) $hBMP = _WinAPI_CreateCompatibleBitmap($hDC, ($pos[2] * $scale) - ($right_indent * $scale), ($pos[3] * $scale) - ($bottom_indent * $scale)) _WinAPI_ReleaseDC($hWnd, $hDC) $hImage1 = _GDIPlus_BitmapCreateFromHBITMAP ($hBMP) $hGraphic = _GDIPlus_ImageGetGraphicsContext($hImage1) _GDIPLus_GraphicsDrawImageRect($hGraphic, $hImage2, 0 - ($left_indent * $scale), 0 - ($top_indent * $scale), ($pos[2] * $scale) + $left_indent, ($pos[3] * $scale) + $top_indent) $CLSID = _GDIPlus_EncodersGetCLSID($Ext) ; Set TIFF parameters $tParams = _GDIPlus_ParamInit(2) $tData = DllStructCreate("int ColorDepth;int Compression") DllStructSetData($tData, "ColorDepth", $giTIFColorDepth) DllStructSetData($tData, "Compression", $giTIFCompression) _GDIPlus_ParamAdd($tParams, $GDIP_EPGCOLORDEPTH, 1, $GDIP_EPTLONG, DllStructGetPtr($tData, "ColorDepth")) _GDIPlus_ParamAdd($tParams, $GDIP_EPGCOMPRESSION, 1, $GDIP_EPTLONG, DllStructGetPtr($tData, "Compression")) If IsDllStruct($tParams) Then $pParams = DllStructGetPtr($tParams) ; Save TIFF and cleanup _GDIPlus_ImageSaveToFileEx($hImage1, $sOutImage, $CLSID, $pParams) _GDIPlus_ImageDispose($hImage1) _GDIPlus_ImageDispose($hImage2) _GDIPlus_GraphicsDispose ($hGraphic) _WinAPI_DeleteObject($hBMP) _GDIPlus_Shutdown() $pos[0] += $left_indent $pos[1] += $top_indent $pos[2] -= $left_indent - $right_indent ;Subtract the left and right indent from the overall width $pos[3] -= $top_indent - $bottom_indent ;Subtract the top and bottom indent from the overall height Return $pos ;Return the expected size of the control/window that was captured EndFunc
    1 point
  2. Yes, the last example from mikell does not bring any improvement. But mikell's ideas have probably put me on the right track and I have now found a possible workaround. Here is my demo 6, which shows once the unwanted behavior of the nasty functions and once with the workaround the desired behavior. Un/comment simply the line: GUIRegisterMsg($WM_MOUSEACTIVATE, 'WM_EVENTS') ; Non-focusable GUI does not give focus after calling "evil" functions ; ; Demo 6, 2020-12-26, Professor Bernd. #include <ButtonConstants.au3> #include <GuiConstants.au3> #include <WinAPIConv.au3> #include <WinAPISysWin.au3> Opt("MustDeclareVars", 1) ; First start the script with commented out line ; "GUIRegisterMsg($WM_MOUSEACTIVATE, 'WM_EVENTS')". ; Click on the non-focusable GUI and press character or arrow keys on the keyboard. ; No other window will get the input. ; ; Workaround: Exit the script, uncomment the line ; "GUIRegisterMsg($WM_MOUSEACTIVATE, 'WM_EVENTS')" and start the script again. ; Click on the non-focusable GUI and press character or arrow keys on the keyboard. ; All input goes to the editor (or to other windows if you click on them). GUIRegisterMsg($WM_MOUSEACTIVATE, 'WM_EVENTS') Const $MA_NOACTIVATEANDEAT = 4 ; ToolWindow, frameless, topmost, which never gets focus. Global $hMy_GUI = GUICreate("TEST", 200, 120, -1, -1, _ BitOR($WS_POPUP, $WS_BORDER), BitOR($WS_EX_TOOLWINDOW, $WS_EX_TOPMOST, $WS_EX_NOACTIVATE)) Global $btnClose = GUICtrlCreateButton("X", 10, 10, 24, 24) Global $lblLabel_1 = GUICtrlCreateLabel("Label 1", 10, 60, 300) Global $lblLabel_2 = GUICtrlCreateLabel("Label 1", 10, 80) Global $lblCoords = GUICtrlCreateLabel("Coords", 80, 10, 80) GUISetState(@SW_SHOWNOACTIVATE, $hMy_GUI) Example_1() Func Example_1() Local $aCaretPos, $iCnt = 0 While 1 Switch GUIGetMsg() Case $GUI_EVENT_CLOSE Exit Case $btnClose Exit Case $lblLabel_1 $iCnt += 1 GUICtrlSetData($lblLabel_2, $iCnt) Case $lblLabel_2 $iCnt += 1 GUICtrlSetData($lblLabel_1, $iCnt) EndSwitch $aCaretPos = _WinGetCaretPos() If IsArray($aCaretPos) Then GUICtrlSetData($lblCoords, "X: " & $aCaretPos[0] & " Y: " & $aCaretPos[1]) WinMove($hMy_GUI, "", $aCaretPos[0], $aCaretPos[1]) EndIf WEnd ; Affected (= "evil") functions are: "WinGetCaretPos()", "ControlGetFocus()" and "ControlGetPos()", ... EndFunc ;==>Example_1 ; ------------------------------------------------------------------------------ ; This is the example function from the AutoIt help for "WinGetCaretPos". ; ------------------------------------------------------------------------------ ; A more reliable method to retrieve the caret coordinates in MDI text editors. Func _WinGetCaretPos() Local $iXAdjust = 0 Local $iYAdjust = 20 Local $iOpt = Opt("CaretCoordMode", 0) ; Set "CaretCoordMode" to relative mode and store the previous option. Local $aGetCaretPos = WinGetCaretPos() ; Retrieve the relative caret coordinates. Local $aGetPos = WinGetPos("[ACTIVE]") ; Retrieve the position as well as height and width of the active window. Local $sControl = ControlGetFocus("[ACTIVE]") ; Retrieve the control name that has keyboard focus. Local $aControlPos = ControlGetPos("[ACTIVE]", "", $sControl) ; Retrieve the position as well as the size of the control. $iOpt = Opt("CaretCoordMode", $iOpt) ; Reset "CaretCoordMode" to the previous option. Local $aReturn[2] = [0, 0] ; Create an array to store the x, y position. If IsArray($aGetCaretPos) And IsArray($aGetPos) And IsArray($aControlPos) Then $aReturn[0] = $aGetCaretPos[0] + $aGetPos[0] + $aControlPos[0] + $iXAdjust $aReturn[1] = $aGetCaretPos[1] + $aGetPos[1] + $aControlPos[1] + $iYAdjust Return $aReturn ; Return the array. Else Return SetError(1, 0, $aReturn) ; Return the array and set @error to 1. EndIf EndFunc ;==>_WinGetCaretPos Func WM_EVENTS($hWndGUI, $MsgID, $WParam, $LParam) Switch $hWndGUI Case $hMy_GUI Switch $MsgID Case $WM_MOUSEACTIVATE ; Check mouse position Local $aMouse_Pos = GUIGetCursorInfo($hMy_GUI) If $aMouse_Pos[4] <> 0 Then Local $word = _WinAPI_MakeLong($aMouse_Pos[4], $BN_CLICKED) _SendMessage($hMy_GUI, $WM_COMMAND, $word, GUICtrlGetHandle($aMouse_Pos[4])) EndIf Return $MA_NOACTIVATEANDEAT EndSwitch EndSwitch Return $GUI_RUNDEFMSG EndFunc ;==>WM_EVENTS This workaround works great in Scite, but in PSPad double clicks in the editor get eaten, probably by the nasty functions. Does anyone know a solution for this?
    1 point
  3. ptrex

    Hex Editor COM Control

    Dan_555 updated the frst post. Thanks
    1 point
  4. Subz

    Can't read listview !!

    You can use the _GuiCtrlListView functions for example: Local $idListView_rd = _GUICtrlListView_GetItemTextString ($idListView, Number(_GUICtrlListView_GetSelectedIndices($idListView)))
    1 point
  5. caramen, Start from the bottom and work up: For $i = $iRows To 0 Step -1 That way you never try to access a non-existent row. M23
    1 point
  6. UsageAn obvious use of ROT objects is in connection with tasks such as inter-process communication (IPC) and job processing. The advantage of using ROT objects for these tasks is partly that you can handle large amounts of data and partly that you can handle the vast majority of AutoIt data types. Using a ROT object it's very easy to transfer arrays between two different processes. At the same time, the internal data types of the array elements are preserved. If an array containing integers, floats, and strings is passed from a sender to a recipient, then it's still integers, floats, and strings at the recipient. This means that the recipient can sort the array with exactly the same result as a sort of the array at the sender. Not just simple data types are preserved. If an array element contains another array (embedded array) or an object (embedded object), then this embedded array or object is still valid at the recipient. ROT objects are based on COM techniques. How is it possible that proprietary AutoIt data types such as arrays, can be transferred between a sender process and a receiver process using a technique based on standard COM data types? This is possible because the AutoIt array is converted to a safearray of variants (default COM array) at the sender and converted back to an AutoIt array at the receiver. But en route between sender and recipient it's a safearray. These conversions are performed automatically by internal AutoIt code (compiled C/C++ code, very fast). If you are interested you can find more information on these topics in Accessing AutoIt Variables. Passing array between AutoIt programsExamples\2) IPC Demo\Array\ contains two files that very simply demonstrate the exchange of arrays between programs running in two different processes. The example is a copy of the example in this thread but implemented with the new IRunningObjectTable UDF. 1) Server.au3: #include <Array.au3> #include "..\..\..\Includes\IRunningObjectTable.au3" Example() Func Example() MsgBox( 0, "Server", "This is server" ) Local $sDataTransferObject = "DataTransferObject" & ROT_CreateGUID() ROT_RegisterObject( Default, $sDataTransferObject ) ; Default => Object = Dictionary object Local $oDataTransferObject = ObjGet( $sDataTransferObject ) ; Dictionary object ; Create array Local $aArray[1000][10] For $i = 0 To 1000 - 1 For $j = 0 To 9 $aArray[$i][$j] = $i & "/" & $j Next Next _ArrayDisplay( $aArray, "Array on server" ) ; Transfer data $oDataTransferObject.Add( "$aArray", $aArray ) ; Start client RunWait( @AutoItExe & " /AutoIt3ExecuteScript " & '"2) Client.au3" ' & $sDataTransferObject ) ; ------- Server waiting while client is executing ------- MsgBox( 0, "Server", "This is server again" ) ; Receive data on server $aArray = $oDataTransferObject.Item( "$aArray" ) _ArrayDisplay( $aArray, "Modified array on server" ) EndFunc 2) Client.au3: #include <Array.au3> Example() Func Example() MsgBox( 0, "Client", "This is client" ) ; Data transfer object Local $sDataTransferObject = $CmdLine[1] Local $oDataTransferObject = ObjGet( $sDataTransferObject ) ; Receive data on client Local $aArray = $oDataTransferObject.Item( "$aArray" ) _ArrayDisplay( $aArray, "Array on client" ) ; Modify array on client For $i = 0 To 100 - 1 $aArray[$i][0] = "Modified" $aArray[$i][1] = "on" $aArray[$i][2] = "client" Next _ArrayDisplay( $aArray, "Modified array on client" ) ; Transfer data $oDataTransferObject.Item( "$aArray" ) = $aArray EndFunc Passing array between AutoIt and VBScriptBecause ROT objects are based on standard COM code, it's possible to exchange data between an AutoIt program and other COM compatible programs e.g. VBScript. The files in Examples\3) IPC Demo2\VBScript\ demonstrate this: 1) Server.au3: #include <GUIConstantsEx.au3> #include "..\..\..\Includes\IRunningObjectTable.au3" Example() Func Example() Local $sArray = "ArrayData" ROT_RegisterObject( Default, $sArray ) ; Default => Object = Dictionary object Local $oArray = ObjGet( $sArray ) ; Dictionary object Local $aArray = [ 123, 123.123, "This is data from AutoIt" ] $oArray( "Array" ) = $aArray Local $sText = "Data exchange between AutoIt and VBScript" Local $hGui = GUICreate( $sText, 400, 300 ) $sText = "Run Client.vbs:" & @CRLF & _ "Open a Command Prompt in the current folder." & @CRLF & _ "Key in ""wscript Client.vbs"" and hit the Enter key." & @CRLF & @CRLF & _ "When you have seen the information in the MsgBox, press the button below." GUICtrlCreateLabel( $sText, 20, 60, 360, 80 ) $sText = "Press Button to continue" Local $idButton = GUICtrlCreateButton( $sText, 20, 180, 360, 30 ) GUISetState( @SW_SHOW, $hGui ) While 1 Switch GUIGetMsg() Case $idButton $aArray = $oArray( "Array" ) MsgBox( 0, "AutoIt: Array from VBScript", "Int: " & $aArray[0] & " (" & VarGetType( $aArray[0] ) & ")" & @CRLF & _ "Flt: " & $aArray[1] & " (" & VarGetType( $aArray[1] ) & ")" & @CRLF & _ "Str: " & $aArray[2] & " (" & VarGetType( $aArray[2] ) & ")" ) Case $GUI_EVENT_CLOSE ExitLoop EndSwitch WEnd GUIDelete( $hGui ) EndFunc Client.vbs: Dim oArray, iAnswer Set oArray = GetObject( "ArrayData" ) iAnswer = MsgBox( "Int: " & oArray( "Array" )(0) & vbCrLf & _ "Flt: " & oArray( "Array" )(1) & vbCrLf & _ "Str: " & oArray( "Array" )(2), 0, "VBScript: Array from AutoIt" ) Dim aArray aArray = Array( 234, 234.234, "This is data from VBScript" ) oArray( "Array" ) = aArray Examples with programs like C# and VB.NET are probably more interesting than VBScript. This way, you can transfer (large) arrays from AutoIt to C# and VB.NET, perform array calculations in compiled code, and return arrays or results back to AutoIt. Here, the AutoIt and C#/VB.NET code are standalone programs that run in their own processes and are connected only through the ROT object. It's a new option to execute code sections that are bottlenecks in interpreted AutoIt code as compiled and possibly multithreaded C#/VB.NET code. Note that such a technique is very different from the technique in Using C# and VB Code in AutoIt through the .NET Framework, where all code runs in the same process. Passing data through ROT Objects (2020-07-11)In IPC Techniques through ROT Objects (Data types section) the AutoIt data types have been tested based on the example for the VarGetType() function in the help file. The data types are sent from Sender to Receiver with these results: $aArray : Array variable type. $dBinary : Binary variable type. $bBoolean : Bool variable type. $pPtr : Int32 variable type. $hWnd : Int32 variable type. $iInt : Int32 variable type. $fFloat : Double variable type. $oObject : Object variable type. $sString : String variable type. $tStruct : Not recognized as a valid variable type. $vKeyword : Keyword variable type. fuMsgBox : Not recognized as a valid variable type. $fuFunc : Not recognized as a valid variable type. $fuUserFunc : Not recognized as a valid variable type. Only the $tStruct and the function data types are not received correctly. The Int32 types for $pPtr and $hWnd can easily be converted to pointer and window handles with the Ptr() and HWnd() functions. In the following section (DllStruct section) it's shown how the $tStruct can be sent by converting the DllStruct to Binary data. In the Large array section (still in the IPC Techniques example) it's shown that even very large arrays can be passed with ROT objects. AutoIt/Perl integration is an example showing how to pass data back and forth between AutoIt and Perl scripts. Also between different programming languages, the data types are preserved with this technique.
    1 point
  7. Is it possible to pass a native AutoIt array as a parameter to a function coded in assembler, C, C++, C# or FreeBasic? And how is this possible? That's what this example is about. If possible, it may increase the performance of array manipulations significantly through fast functions of compiled code. The very, very short answers to the two questions above are: Yes. And through COM objects. Here is a small example that shows what it's all about. You can find the example in zip file in bottom of post (goto top of second post and scroll up a little bit). ;#AutoIt3Wrapper_UseX64=y #include "Includes\AccVarsUtilities.au3" #include "Includes\InspectVariable.au3" #include "Includes\ArrayDisplayEx.au3" #include "Includes\FasmUtils.au3" Opt( "MustDeclareVars", 1 ) Example() Func Example() ; === UDF and flat assembler (fasm) code === Local $hTimer1 = TimerInit() ; --- Create and fill safearray --- ; Create safearray Local $tSafeArrayBound = DllStructCreate( $tagSAFEARRAYBOUND ) DllStructSetData( $tSafeArrayBound, "cElements", 2^24 ) ; Number of elements in array DllStructSetData( $tSafeArrayBound, "lLbound", 0 ) ; Lower bound of array index Local $pSafeArray = SafeArrayCreate( $VT_I4, 1, $tSafeArrayBound ) ConsoleWrite( @CRLF & "--- Inspect safearray ---" & @CRLF ) InspectSafeArray( $pSafeArray ) ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; AutoIt code ;For $i = 0 To 2^24 - 1 ; DllStructSetData( $tSafeArrayData, 1, $i, $i + 1 ) ;Next ; <<<< Execute your assembler, C, C++, C# or FreeBasic code at this point >>>> ; Get fasm code Local $sFasmCode = @AutoItX64 ? "0xB80000000089024883C204FFC0E2F6C3" _ ; Example-x64.asm : "0x5589E58B4D088B550CB800000000890283C20440E2F85DC20800" ; Example-x86.asm Local $pFasmCode = FasmGetBinaryString( $sFasmCode, 64 ) If Not $pFasmCode Then Return ConsoleWrite( "$pFasmCode ERR" & @CRLF ) ; Execute fasm code to fill safearray DllCallAddress( "int", $pFasmCode, "int", 2^24, "ptr", $pSafeArrayData ) SafeArrayUnaccessData( $pSafeArray ) ; --- Convert safearray to native AutoIt array --- Local $aArray1 AccVars_SafeArrayToArray( $pSafeArray, $aArray1 ) ; <<<< The UDF >>>> ConsoleWrite( @CRLF & "Applied time to create and fill array (UDF and fasm): " & TimerDiff( $hTimer1 ) & @CRLF ) ; ------------------------------------------------ _ArrayDisplayEx( $aArray1, Default, "", 0x0010, "", "", "", 75 ) ConsoleWrite( @CRLF & "--- Inspect $aArray1 ---" & @CRLF ) InspectArray( $aArray1 ) $aArray1 = 0 ; === Pure AutoIt code === ConsoleWrite( @CRLF & "Executing pure AutoIt code to create and fill array (~10 seconds) ..." & @CRLF ) Local $hTimer2 = TimerInit() Local $aArray2[2^24] For $i = 0 To 2^24 - 1 $aArray2[$i] = $i Next ConsoleWrite( @CRLF & "Applied time to create and fill array (pure AutoIt): " & TimerDiff( $hTimer2 ) & @CRLF ) _ArrayDisplayEx( $aArray2, Default, "", 0x0010, "", "", "", 75 ) ConsoleWrite( @CRLF & "--- Inspect $aArray2 ---" & @CRLF ) InspectArray( $aArray2 ) $aArray2 = 0 EndFunc Line 53 is the crucial line of code: AccVars_SafeArrayToArray( $pSafeArray, $aArray1 ) ; <<<< The UDF >>>> Output in SciTE console should look like this: --- Inspect safearray --- Number of dimensions = 1 Features flags = 0x00000080 ($FADF_HAVEVARTYPE, array of variant type) Variant type = 0x0003 (VT_I4, 4 bytes signed integer) Size of array element (bytes) = 4 (size of the element structure) Number of locks = 0 Pointer to data = 0x02B80020 (pvData) Dimension 1: Elements in dimension = 16777216 Lower bound of dimension = 0 Applied time to create and fill array (UDF and fasm): 797.855906880413 --- Inspect $aArray1 --- Number of dimensions = 1 Features flags = 0x00000880 ($FADF_VARIANT+$FADF_HAVEVARTYPE, array of variants) Variant type = 0x000C (VT_VARIANT, variant data type) Size of array element (bytes) = 16 (size of the variant structure) Number of locks = 0 Pointer to data = 0x7FFF0020 (pvData) Dimension 1: Elements in dimension = 16777216 Lower bound of dimension = 0 Executing pure AutoIt code to create and fill array (~10 seconds) ... Applied time to create and fill array (pure AutoIt): 8670.46279987079 --- Inspect $aArray2 --- Number of dimensions = 1 Features flags = 0x00000880 ($FADF_VARIANT+$FADF_HAVEVARTYPE, array of variants) Variant type = 0x000C (VT_VARIANT, variant data type) Size of array element (bytes) = 16 (size of the variant structure) Number of locks = 0 Pointer to data = 0x7FFF0020 (pvData) Dimension 1: Elements in dimension = 16777216 Lower bound of dimension = 0 The UDF and fasm code is about 10 times faster than the pure AutoIt code. The code to populate the array is very simple. That's why the AutoIt code is doing relatively well compared to the UDF and fasm code. The technique implemented here can also be used to pass simple AutoIt variables to a function coded in a another language. This makes it possible to test on simple variables, rather than more complex arrays. And simple variables are needed in the final UDF. Post 7 contains a brief description of how the idea for this project arose. You'll find the following sections below: Accessing variables COM objects - COM objects can handle AutoIt arrays. Can this be exploited? AccessingVariablesTest.au3 - Test with UIAutomation::RectToVariant (Examples - Tests\Examples\0) Accessing variables\) Simple variables Variants - Introduction Basic strings - Introduction Examples - Tests\Examples\1) Simple variables\ Numeric variables (post 11) Numeric variants (post 11) Array variables Safearrays - Introduction Examples - Tests\Examples\2) Array variables\ Assembler code SafeArrayDisplay.au3 (post 24) Safearrays of integers (post 24) Internal conversions Exploiting conversions Avoiding conversions (Examples - Tests\Examples\3) Internal conversions\) Limitations (post 13) Final UDF - AccessingVariables.au3 AccessVariablesXY - 30 functions Restrictions - No literal params, no nested funcs Utility funcs - AccVarsUtilities.au3 InspectVariable - InspectVariable.au3 Using the UDF - Examples\Demo examples\4) Other demo examples\6) Using the UDF\ Subclassing (post 21) - Examples\Subclassing\ Examples Demo examples - Examples\Demo examples\ Assembler code - Tools to create fasm code not included Other demo examples - Examples\Demo examples\4) Other demo examples\ Real examples - Examples\Real examples\ sqlite3_get_table (post 25) What's next (post 26) Zip file For tests and for implementing the final UDF I've copied code written by monoceres, I've copied code from AutoItObject.au3 by the AutoItObject-Team: monoceres, trancexx, Kip, ProgAndy, and from CUIAutomation2.au3 by junkew. Lots of credit to these guys. Accessing variables AutoIt is a BASIC-like language. In BASIC-like languages simple variables are internally stored as variants, and arrays are internally stored as safearrays contained in variants. Assuming that an AutoIt variable is internally stored as a variant, is it possible to get a pointer to this variant? Assuming that an AutoIt array is internally stored as a safearray contained in a variant, then it should be possible to get a pointer to the safearray through the pointer to the variant. Why is a pointer to a safearray interesting? Because such a pointer can be passed as a parameter to a function coded in assembler, C, C++, C# or FreeBasic. We can thus access an AutoIt array directly from a function coded in another language, without the need to convert the array to a structure (DllStructCreate) or similar. In this way it's possible to code very fast array manipulation functions in a real compiled language. The crucial step is to get a pointer to the variant that contains the variable or array. COM objects If you have been using COM objects like the Dictionary ("Scripting.Dictionary") object, you know that this object can return AutoIt arrays in this way: $aKeys = $oDict.Keys() $aItems = $oDict.Items() In an example in "Obj/COM Reference" chapter in AutoIt Help file you can find this code line: $oExcel.activesheet.range("A1:O16").value = $aArray ; Fill cells with example numbers The Excel object seems to know how to handle a native AutoIt array. For objects created with ObjCreateInterface it's also easy to find examples where these objects understands how to handle native AutoIt arrays. An example is RectToVariant method of the UIAutomation object. This method converts a rectangle structure to an array: ; Create UIAutomation object Local $oUIAutomation = ObjCreateInterface( $sCLSID_CUIAutomation, $sIID_IUIAutomation, $stag_IUIAutomation ) If Not IsObj( $oUIAutomation ) Then Return ConsoleWrite( "$oUIAutomation ERR" & @CRLF ) ConsoleWrite( "$oUIAutomation OK" & @CRLF ) ; Create rectangle structure Local $tRect = DllStructCreate( $tagRECT ) DllStructSetData( $tRect, "Left", 100 ) DllStructSetData( $tRect, "Top", 200 ) DllStructSetData( $tRect, "Right", 3000 ) DllStructSetData( $tRect, "Bottom", 4000 ) Local $aArray $oUIAutomation.RectToVariant( $tRect, $aArray ) ; Variant array: VT_ARRAY + VT_R8 If Not IsArray( $aArray ) Then Return ConsoleWrite( "$aArray ERR" & @CRLF ) ConsoleWrite( "$aArray OK" & @CRLF ) _ArrayDisplay( $aArray ) You can find the example (Tests\Examples\0) Accessing variables\Example1.au3) in the zip below. AccessingVariablesTest.au3 I've been playing with RectToVariant method of the UIAutomation object. And I have studied how the output array ($aArray in the code above) looks like in C++ (see also last code box in post 15). The description for RectToVariant in $stag_IUIAutomation looks like this: "RectToVariant hresult(" & ( @AutoItX64 ? "struct*;" : "struct;" ) & "variant*);" Note the last parameter "variant*". What's that? That's a pointer to a variant. Exactly what we need. You can get a pointer to the variant that contains $aArray in the example above in this way: Replace the RectToVariant method with your own function. Inside the function the pointer to the variant that contains $aArray is simply the last parameter. It's necessary to replace RectToVariant with our own function to be able to add code inside the function. And it's very important that the parameter type in the function (or method) description string is "variant*". Exactly this parameter type ensures that the parameter coming from the AutoIt code is converted to a pointer to a variant. Inside the RectToVariant method or our own function the last parameter is not a native AutoIt array. It's a pointer to a variant. This conversion between different data types is performed by internal AutoIt code. And the conversion is only performed in relation to objects. There is no such conversion in relation to eg. the DllCall function. That's why we have to deal with objects. The technique of replacing an object method with our own function has been seen many times before. Eg. in this old example by monoceres. Now when we have ObjCreateInterface it's much easier. You don't need much code: #include-once #include "..\Includes\Variant.au3" #include "..\Includes\SafeArray.au3" #include "..\Includes\Utilities.au3" Global $hAccessVariableFunction Func AccessVariable( $hAccessVariableFunc, ByRef $vVariable ) Static $oAccessVariable = AccessVariableInit() ; Init $oAccessVariable (only once) $hAccessVariableFunction = $hAccessVariableFunc ; Code to execute in VariableToVariant $oAccessVariable.VariableToVariant( $vVariable ) ; Execute VariableToVariant method EndFunc Func AccessVariableInit() ; Three locals copied from "IUIAutomation MS framework automate chrome, FF, IE, ...." by junkew ; https://www.autoitscript.com/forum/index.php?showtopic=153520 Local $sCLSID_CUIAutomation = "{FF48DBA4-60EF-4201-AA87-54103EEF594E}" Local $sIID_IUIAutomation = "{30CBE57D-D9D0-452A-AB13-7AC5AC4825EE}" Local $stag_IUIAutomation = _ "f01 hresult();f02 hresult();f03 hresult();f04 hresult();f05 hresult();f06 hresult();f07 hresult();" & _ "f08 hresult();f09 hresult();f10 hresult();f11 hresult();f12 hresult();f13 hresult();f14 hresult();" & _ "f15 hresult();f16 hresult();f17 hresult();f18 hresult();f19 hresult();f20 hresult();f21 hresult();" & _ "f22 hresult();f23 hresult();f24 hresult();f25 hresult();f26 hresult();f27 hresult();f28 hresult();" & _ "f29 hresult();f30 hresult();f31 hresult();f32 hresult();f33 hresult();f34 hresult();f35 hresult();" & _ "f36 hresult();f37 hresult();f38 hresult();f39 hresult();f40 hresult();f41 hresult();" & _ "VariableToVariant hresult(variant*);" & _ ; "RectToVariant hresult(" & ( @AutoItX64 ? "struct*;" : "struct;" ) & "variant*);" "f43 hresult();f44 hresult();f45 hresult();f46 hresult();f47 hresult();f48 hresult();f49 hresult();" & _ "f50 hresult();f51 hresult();f52 hresult();f53 hresult();f54 hresult();f55 hresult();" ; Create AccessVariable object (Automation object) Local $oAccessVariable = ObjCreateInterface( $sCLSID_CUIAutomation, $sIID_IUIAutomation, $stag_IUIAutomation ) If Not IsObj( $oAccessVariable ) Then Return SetError(1,0,1) ; Replace RectToVariant method with VariableToVariant method Local $pVariableToVariant = DllCallbackGetPtr( DllCallbackRegister( "VariableToVariant", "long", "ptr;ptr*" ) ) ReplaceVTableFuncPtr( Ptr( $oAccessVariable() ), ( 3 + 42 - 1 ) * ( @AutoItX64 ? 8 : 4 ), $pVariableToVariant ) Return $oAccessVariable EndFunc Func VariableToVariant( $pSelf, $pVariant ) $hAccessVariableFunction( $pVariant ) Return 0 ; $S_OK (COM constant) #forceref $pSelf EndFunc The code is saved in Tests\AccessingVariablesTest.au3 in the zip. To use the UDF you call AccessVariable function in the top. Note the ByRef keyword in the function. The ByRef keyword is very important. The function will not work without this keyword. Note also that the pointer to the variant in VariableToVariant function in bottom of the UDF is created by some internal AutoIt conversion code. And it's a local function parameter. The pointer is only valid within VariableToVariant and in the function you specify as a parameter when you call AccessVariable. As soon as VariableToVariant returns, the pointer is invalid. Let's try some small examples with simple variables and array variables. Simple variables Before we go to the examples let's take a quick look at variants and basic strings (BSTRs). Variants and basic strings are needed in our assembler, C, C++, C# or FreeBasic functions. Variants A variant is defined by this structure: Global Const $tagVARIANT = "word vt;word r1;word r2;word r3;ptr data; ptr" Only vt and data elements are used. The structure takes up 16/24 bytes when you're running 32/64 bit. Space for the data element at the end represents 2 pointers. This is 8/16 bytes when you're running 32/64 bit. If $pVariant is a pointer to a variant you can get vt and data elements in this way: Local $vt = DllStructGetData( DllStructCreate( "word", $pVariant ), 1 ) Local $data = DllStructGetData( DllStructCreate( "<vt>", $pVariant + 8 ), 1 ) The four word (word = 2 bytes) elements before the data element takes up 8 bytes. Common values of vt in AutoIt are: $VT_I4 = 3 ; Integer, "<vt>" = "int" $VT_R8 = 5 ; Double, "<vt>" = "double" $VT_BSTR = 8 ; Basic string, "<vt>" = "ptr" $VT_UI4 = 19 ; Pointer running 32 bit, "<vt>" = "ptr" $VT_UI8 = 21 ; Pointer running 64 bit, "<vt>" = "ptr" If $pVariant is a pointer to a variant which contains an array you'll always get this value for vt: $VT_ARRAY + $VT_VARIANT = 0x200C ; Pointer, "<vt>" = "ptr" A native AutoIt array is stored as a safearray ($VT_ARRAY = 0x2000) contained in a variant ($VT_VARIANT = 0x000C). The pointer to the safearray is stored in the data element of the variant. Variant constants and functions are defined in Includes\Variant.au3. Most of the code is shamelessly copied from AutoItObject.au3 by the AutoItObject-Team: monoceres, trancexx, Kip, ProgAndy. Basic strings Internally AutoIt strings are stored as basic strings or BSTRs. The pointer to the BSTR is stored in a variant. A BSTR is defined by this structure: Global Const $tagBSTR = & _ "dword Length;" & _ ; Length in bytes (2 * $iLen), does not include the Terminator "wchar String[" & $iLen & "];" & _ ; $iLen is wchars, $pBSTR = DllStructGetPtr( $tBSTR, "String" ) "word Terminator;" ; Two null characters Use this code to get the pointer to the BSTR (the pointer which is stored in a variant): Local $pBSTR = DllStructGetPtr( $tBSTR, "String" ) Note that the BSTR pointer is the start of the "String" element and not the start of the structure. Normally you do not handle BSTRs directly through this structure. You use the BSTR functions in Variant.au3. Also copied from AutoItObject.au3. You can find information about variant and BSTR conversion and manipulation functions here. Examples The examples are stored in "Tests\Examples\1) Simple variables\". There are three small examples. These examples are just AutoIt code. This is a part of Example2.au3: Local $sStr = "AutoIt" ConsoleWrite( "$sStr = " & $sStr & @CRLF ) AccessVariable( InspectVariableMtd, $sStr ) ; InspectVariableMtd is coded in Includes\InspectVariable.au3 AccessVariable( SetString, $sStr ) ; Shows how to use the AccessVariable function ConsoleWrite( "$sStr = " & $sStr & @CRLF ) Func SetString( $pVariant ) Local $pData = $pVariant + 8 ; See InspectVariable.au3 Local $tData = DllStructCreate( "ptr", $pData ) Local $pBStr = DllStructGetData( $tData, 1 ) SysFreeString( $pBStr ) ; See Variant.au3 $pBStr = SysAllocString( "Hello world" ) DllStructSetData( $tData, 1, $pBStr ) EndFunc The output i SciTE console should look like this: $sStr = AutoIt ptr = 0x006F9630 ($pVariant) vt = 0x0008 (VT_BSTR, basic string) data = AutoIt $sStr = Hello world Example3.au3 is similar to the example for VarGetType in the Help file. It prints the variant vt-values for the corresponding AutoIt data types. Array variables Before we go to the examples let's take a quick look at safearrays. Safearrays A safearray is defined by these structures: Global Const $tagSAFEARRAYBOUND = _ "ulong cElements;" & _ ; The number of elements in the dimension. "long lLbound;" ; The lower bound of the dimension. Global Const $tagSAFEARRAY = _ "ushort cDims;" & _ ; The number of dimensions. "ushort fFeatures;" & _ ; Flags, see below. "ulong cbElements;" & _ ; The size of an array element. "ulong cLocks;" & _ ; The number of times the array has been locked without a corresponding unlock. "ptr pvData;" & _ ; The data. $tagSAFEARRAYBOUND ; One $tagSAFEARRAYBOUND for each dimension. ; Examples ; 1D, 2D and 3D safearrays: Local $tagSAFEARRAY1D = $tagSAFEARRAY Local $tagSAFEARRAY2D = $tagSAFEARRAY & $tagSAFEARRAYBOUND Local $tagSAFEARRAY3D = $tagSAFEARRAY & $tagSAFEARRAYBOUND & $tagSAFEARRAYBOUND In AutoIt an array is stored as a safearray contained in a variant. The safearray is always an array of variants. That the safearray is contained in a variant means that the pointer to the safearray is stored in the data element of a variant. That the safearray is an array of variants means that the pvData element of the safearray points to a memory area which contains a continuous row of variant structures. If we're running 32 bit a variant takes up 16 bytes. For a 1D-array with three elements: Local $aArray[3] = [ 1, 2, 3 ] The pvData element of the safearray points to a memory area which takes up 48 bytes and consists of three variant structures. Normally you do not handle safearrays directly through these structures. You use the safearray functions in Includes\SafeArray.au3. Copied from AutoItObject.au3. You can find information about safearray conversion and manipulation functions here. Examples The examples are stored in "Tests\Examples\2) Array variables\". The examples are still just AutoIt code. Example1.au3 prints information for 1D-arrays of integers, floats and strings. Information for the integer array should look like this: $aInts = [ 0, 1, 2 ] --- InspectVariable $aInts --- ptr = 0x00990378 ($pVariant) vt = 0x200C (VT_ARRAY+VT_VARIANT, array of variants, safearray) data = 0x009981E8 (pointer to safearray) --- InspectVariable $aInts[0] --- ptr = 0x0098FEC8 ($pVariant) vt = 0x0003 (VT_I4, 4 bytes signed integer) data = 0 --- InspectArray $aInts --- Number of dimensions = 1 Features flags = 0x00000880 ($FADF_VARIANT+$FADF_HAVEVARTYPE, array of variants) Variant type = 0x000C (VT_VARIANT, variant data type) Size of array element (bytes) = 16 (size of the variant structure) Number of locks = 0 Pointer to data = 0x00998710 (pvData) Dimension 1: Elements in dimension = 3 Lower bound of dimension = 0 Example2.au3 prints information for 2D-arrays of integers, floats and strings, and prints the contents of the arrays through the data area of the safearray. pvData element of $tagSAFEARRAY structure is a pointer to the data area. Example3.au3 fills an existing array, $aArray[50000], with integers. Example4.au3 creates and fills an array with 50000 integers and assigns it to an uninitialized variable: $aArray (empty string). Assembler code Example5.au3 is the first small example with fasm code (fasm = flat assembler, more info about fasm in one of the next sections). A 1D-array with 2^24 (16,777,216) elements is filled with integers from zero to 2^24 - 1. The array is first filled through AutoIt code. Then AccessVariable is used to fill the corresponding safearray through fasm code. There are two versions of the fasm code: A 32 bit version and a 64 bit version. AutoIt code (Example5.au3): ;#AutoIt3Wrapper_UseX64=y #include "..\..\AccessingVariablesTest.au3" #include "..\..\..\Includes\ArrayDisplayEx.au3" #include "..\..\..\Includes\FasmUtils.au3" Opt( "MustDeclareVars", 1 ) Example5() Func Example5() ConsoleWrite( "Executing AutoIt code to fill array (~10 seconds) ..." & @CRLF ) Local $aArray1[2^24] ; 2^24 = 16,777,216, maximum number of elements for an array Local $hTimer1 = TimerInit() For $i = 0 To 2^24 - 1 $aArray1[$i] = $i Next ConsoleWrite( "Time for AutoIt code to fill array: " & TimerDiff( $hTimer1 ) & @CRLF & @CRLF ) _ArrayDisplayEx( $aArray1 ) $aArray1 = 0 ConsoleWrite( "Executing FillArray to fill array ..." & @CRLF ) Local $aArray2[2^24] ; 2^24 = 16,777,216, maximum number of elements for an array Local $hTimer4 = TimerInit() AccessVariable( FillArray, $aArray2 ) Local $fTime4 = TimerDiff( $hTimer4 ) ConsoleWrite( "Time for FillArray to fill array (outside FillArray): " & $fTime4 & @CRLF ) ConsoleWrite( "Time for FillArray to fill array: " & $fTime4 & @CRLF ) _ArrayDisplayEx( $aArray2 ) $aArray2 = 0 EndFunc Func FillArray( $pVariant ) Local $hTimer3 = TimerInit() ; Pointer to safearray Local $pData = $pVariant + 8 Local $tData = DllStructCreate( "ptr", $pData ) Local $pSafeArray = DllStructGetData( $tData, 1 ) ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; Get fasm code Static $sFasmCode = @AutoItX64 ? "0xB80000000066C70203008942084883C218FFC0E2F0C3" _ ; Example5-x64.asm : "0x5589E58B4D088B550CB80000000066C702030089420883C21040E2F25DC20800" ; Example5-x86.asm Static $pFasmCode = FasmGetBinaryString( $sFasmCode, 64 ) If Not $pFasmCode Then Exit ConsoleWrite( "$pFasmCode ERR" & @CRLF ) ; Execute fasm code Local $hTimer2 = TimerInit() DllCallAddress( "int", $pFasmCode, "int", 2^24, "ptr", $pSafeArrayData ) ConsoleWrite( "Time for FillArray to fill array (fasm code only): " & TimerDiff( $hTimer2 ) & @CRLF ) SafeArrayUnaccessData( $pSafeArray ) ConsoleWrite( "Time for FillArray to fill array (inside FillArray): " & TimerDiff( $hTimer3 ) & @CRLF ) EndFunc Note that the fasm code is executed inside FillArray function. And FillArray function is executed inside the $oAccessVariable.VariableToVariant method (in AccessingVariablesTest.au3). The pointer to the safearray data ($pSafeArrayData) is valid only inside this method. $pSafeArrayData is a pointer that points to the data area that contains the data of $aArray2. $pSafeArrayData however does not point directly to the same data as contained in $aArray2. Internal AutoIt conversions on function entry copies data from $aArray2 to the data area. Internal AutoIt conversions on function exit copies data from the data area to $aArray2. See next section. 32 bit fasm code (Example5-x86.asm): ; flat assembler code ; Translate AutoIt code to fasm code: ; For $i = 0 To 2^24 - 1 ; $aArray[$i] = $i ; Next ; Parameters: ; [ebp + 08] : iRows ; First parameter ; [ebp + 12] : pSafeArrayData ; Second parameter ; Init directive use32 ; 32 bit code ; Entry code push ebp ; Store base pointer on stack mov ebp, esp ; Use stack pointer as base pointer ; Function code mov ecx, [ebp + 08] ; ecx corresponds to 2^24 in "For $i = 0 To 2^24 - 1" mov edx, [ebp + 12] ; edx is pointer in safearray data area mov eax, 0 ; eax = 0, eax corresponds to $i iLoop: mov [edx], word 3 ; Set vt element in variant to 3 (VT_I4, integer) mov [edx + 08], eax ; Set data element in variant to eax ($i) add edx, 16 ; Add size of variant structure to edx inc eax ; eax += 1, corresponds to $i += 1 loop iLoop ; ecx -= 1, jump to iLoop if not zero ; Exit code pop ebp ; Restore base pointer from stack ret 08 ; Return and cleanup stack 64 bit fasm code (Example5-x64.asm): ; flat assembler code ; Translate AutoIt code to fasm code: ; For $i = 0 To 2^24 - 1 ; $aArray[$i] = $i ; Next ; Parameters: ; rcx : iRows ; First parameter, ecx corresponds to 2^24 in "For $i = 0 To 2^24 - 1" ; rdx : pSafeArrayData ; Second parameter, rdx is pointer in safearray data area ; Init directive use64 ; 64 bit code ; Function code mov eax, 0 ; eax = 0, eax corresponds to $i iLoop: mov [rdx], word 3 ; Set vt element in variant to 3 (VT_I4, integer) mov [rdx + 08], eax ; Set data element in variant to eax ($i) add rdx, 24 ; Add size of variant structure to rdx inc eax ; eax += 1, corresponds to $i += 1 loop iLoop ; ecx -= 1, jump to iLoop if not zero ; Exit code ret ; Return Output in SciTE console: Executing AutoIt code to fill array (~10 seconds) ... Time for AutoIt code to fill array: 9033.87383563625 Executing FillArray to fill array ... Time for FillArray to fill array (fasm code only): 28.5154490311906 Time for FillArray to fill array (inside FillArray): 28.6592369763861 Time for FillArray to fill array (outside FillArray): 2388.37594979003 Time for FillArray to fill array: 2388.37594979003 Why is there such a big difference in the time it takes to execute FillArray when the time is measured inside and outside the function? Internal conversions In "Obj/COM Reference" chapter, "COM Events" section and "Limitations on COM Events in AutoIt" subsection in AutoIt Help file you can find the following sentence: "... AutoIt uses its own variable scheme, which is not compatible to COM variables. This means that all values from Objects need to be converted into AutoIt variables ...". In the AutoIt website you can find small bits of information like this one. These internal conversions takes place between native AutoIt data types and COM data types, when AutoIt variables are passed to object methods as function parameters, and when COM variables are returned to AutoIt. There are two sets of conversions. Conversions on function entry (object method entry), and conversions on function exit. The previous section ended with this question: Why is there such a big difference in the time it takes to execute FillArray when the time is measured inside and outside the function (FillArray is executed inside the object method)? The large time difference is caused by the conversions. The conversions are performed by internal AutoIt code and Windows API functions. Both consists of compiled C++ code. Judging from the time the conversions takes (about 2.5 seconds on my PC), they seem to perform a complete (by value) copy of the entire array. The array with 16,777,216 integers. Even for compiled C++ code it takes time to copy such a large array. Example1.au3 ("Tests\Examples\3) Internal conversions\") shows how long time the conversions takes: ;#AutoIt3Wrapper_UseX64=y #include "..\..\AccessingVariablesTest.au3" #include "..\..\..\Includes\ArrayDisplayEx.au3" Opt( "MustDeclareVars", 1 ) Example1() Func Example1() ConsoleWrite( "Filling array of 16,777,216 integers (~10 seconds) ..." & @CRLF ) Local $aArray1[2^24] ; 2^24 = 16,777,216, maximum number of elements for an array For $i = 0 To 2^24 - 1 $aArray1[$i] = 1234 Next Local $hTimer = TimerInit() AccessVariable( ConversionTime, $aArray1 ) ConsoleWrite( "Time for conversion code to execute: " & TimerDiff( $hTimer ) & @CRLF & @CRLF ) _ArrayDisplayEx( $aArray1 ) $aArray1 = 0 ConsoleWrite( "Filling array of 16,777,216 doubles (~10 seconds) ..." & @CRLF ) Local $aArray2[2^24] ; 2^24 = 16,777,216, maximum number of elements for an array For $i = 0 To 2^24 - 1 $aArray2[$i] = 1234.5678 Next $hTimer = TimerInit() AccessVariable( ConversionTime, $aArray2 ) ConsoleWrite( "Time for conversion code to execute: " & TimerDiff( $hTimer ) & @CRLF & @CRLF ) _ArrayDisplayEx( $aArray2 ) $aArray2 = 0 ConsoleWrite( "Filling array of 1,048,576 100-chars strings (~5 seconds) ..." & @CRLF ) Local $aArray3[2^20] ; 2^20 = 1,048,576 For $i = 0 To 2^20 - 1 $aArray3[$i] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; 100 chars Next $hTimer = TimerInit() AccessVariable( ConversionTime, $aArray3 ) ConsoleWrite( "Time for conversion code to execute: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray3 ) $aArray3 = 0 EndFunc ; Because this is an empty function the time measured above ; is the total time for all conversions of one array parameter. Func ConversionTime( $pVariant ) EndFunc Output in SciTE console: Filling array of 16,777,216 integers (~10 seconds) ... Time for conversion code to execute: 4303.24786930952 Filling array of 16,777,216 doubles (~10 seconds) ... Time for conversion code to execute: 4341.52716013949 Filling array of 1,048,576 100-chars strings (~5 seconds) ... Time for conversion code to execute: 2416.14925416137 Because 16,777,216 elements is the maximum number of elements in an array, the conversions will never take longer than 4.5 seconds on my PC for an array of integers or doubles. The time is about the same for integers and doubles because they are both stored as variants. A variant takes up 16 bytes when you're running 32 bit whether the variant contains a 4 bytes integer or an 8 bytes double. Why did the conversions in the previous section only take 2.5 seconds on my PC? Only about half as much time. I can only guess that this must be caused by the fact that this was an uninitialized array. And for an uninitialized array the conversions on function entry seems to be much faster. Conversions of an array of strings takes longer time. The variants which contains pointers to the strings has to be copied. And the strings (BSTRs) themselves has to be copied. That's the reason why the number of strings is limited to about 1,000,000. Still a decent number of strings. Although the assembler, C, C++, C# or FreeBasic code is lightning fast, the conversions especially for very large arrays means that the total execution time for the entire function (FillArray in the example in the previous section) will be increased with a few seconds. On the other hand we need the conversions. We don't want the array to be returned as a safearray contained in a variant. We want the array to be returned as a native AutoIt array. Exploiting conversions As I wrote in first section I've been playing with RectToVariant method of the UIAutomation object. In Remarks section in the link you can read this sentence: "The returned VARIANT has a data type of VT_ARRAY | VT_R8." (= VT_ARRAY + VT_R8). But this does not match the internal implementation of an AutoIt array which is VT_ARRAY + VT_VARIANT. Because RectToVariant returns a perfect AutoIt array this must mean that the conversion code also inspects the variant array type and converts it to a VT_ARRAY + VT_VARIANT type if necessary. I've tested that variants of types VT_ARRAY+VT_I4 (integers), VT_ARRAY+VT_R8 (doubles) and VT_ARRAY+VT_BSTR (strings) are properly converted to variants of type VT_ARRAY+VT_VARIANT. Rememeber that a variant of type VT_ARRAY+VT_I4 is a variant which contains a safearray (VT_ARRAY), where the pvData element of the safearray structure points to a memory area which contains a continuous row of integers (VT_I4). What is a continuous row of integers? Well, in AutoIt you create a continuous row of integers with DllStructCreate like this: Local $tIntegers = DllStructCreate( "int[50000]" ) This is a continuous row of 50000 integers. This means that if you are manipulating an array of integers in your assembler, C, C++, C# or FreeBasic code, you don't have to mess around with variant structures containing integers. You can simply use an array of integers. When you've finished the array manipulations you can store the integers as a fairly simple VT_ARRAY+VT_I4 variant (safearray of integers). And then you can leave it to the conversion code on function exit to convert the variant to a VT_ARRAY+VT_VARIANT variant (safearray of variants) which is understandable by AutoIt. And in fact, all these safearray data types are correctly converted to safearrays of variants: $VT_I2, $VT_I4, $VT_R4, $VT_R8, $VT_BSTR, $VT_BOOL, $VT_UI4, $VT_UI8 Example2a/b/c.au3, Example3a/b/c.au3 and Example4a/b/c.au3 demonstrates this technique in three slightly different ways for integers, doubles and strings. These techniques are needed in the assembler, C, C++, C# or FreeBasic code. Note that the examples are based on the final UDF and not the test UDF. See next section. This is Example2a.au3: ;#AutoIt3Wrapper_UseX64=y #include "..\..\..\Includes\AccessingVariables.au3" ; <<<< Final UDF (not test UDF) >>>> #include "..\..\..\Includes\InspectVariable.au3" #include <Array.au3> Opt( "MustDeclareVars", 1 ) Example2() Func Example2() Local $aArray ; Empty string ConsoleWrite( "--- InspectVariable ---" & @CRLF ) InspectVariable( $aArray ) ; $aArray is an empty string AccessVariables01( CreateArray, $aArray ) ConsoleWrite( "--- InspectVariable ---" & @CRLF ) InspectVariable( $aArray ) ; $aArray is an array ConsoleWrite( "--- InspectArray ---" & @CRLF ) InspectArray( $aArray ) _ArrayDisplay( $aArray ) EndFunc Func CreateArray( $pVariant ) ; --- Create and fill structure of integers --- ; Create structure Local $tIntegers = DllStructCreate( "int[50000]" ) Local $pIntegers = DllStructGetPtr( $tIntegers ) ; Fill structure ; Array manipulation For $i = 0 To 50000 - 1 DllStructSetData( $tIntegers, 1, $i, $i + 1 ) Next ; --- Create and fill safearray --- ; Create safearray Local $tSafeArrayBound = DllStructCreate( $tagSAFEARRAYBOUND ) DllStructSetData( $tSafeArrayBound, "cElements", 50000 ) ; Number of elements in array DllStructSetData( $tSafeArrayBound, "lLbound", 0 ) ; Lower bound of array index Local $pSafeArray = SafeArrayCreate( $VT_I4, 1, $tSafeArrayBound ) ; <<<< Not a proper AutoIt safearray >>>> ; This is a safearray of integers and not variants as a usual AutoIt array ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; Create structure from safearray data area Local $tSafeArrayBytes = DllStructCreate( "byte[200000]", $pSafeArrayData ) ; Fill safearray data area with data from $tIntegers DllStructSetData( $tSafeArrayBytes, 1, DllStructGetData( DllStructCreate( "byte[200000]", $pIntegers ), 1 ) ) ; This technique only works with byte's, char's and wchar's SafeArrayUnaccessData( $pSafeArray ) ; --- Set variant to match an array of integers --- ; Set vt element to $VT_ARRAY + $VT_I4 Local $tvt = DllStructCreate( "word", $pVariant ) DllStructSetData( $tvt, 1, $VT_ARRAY + $VT_I4 ) ; <<<< Not a proper AutoIt array >>>> ; This is an array of integers and not variants as a usual AutoIt array ; Set data element to safearray pointer Local $pData = $pVariant + 8 Local $tData = DllStructCreate( "ptr", $pData ) DllStructSetData( $tData, 1, $pSafeArray ) ; <<<< On function exit the safearray contained in a variant is converted to a native AutoIt array >>>> EndFunc This is output in SciTE console. The array is displayed with _ArrayDisplay. --- InspectVariable --- ptr = 0x00D0E698 ($pVariant) vt = 0x0008 (VT_BSTR, basic string) data = --- InspectVariable --- ptr = 0x02B78028 ($pVariant) vt = 0x200C (VT_ARRAY+VT_VARIANT, array of variants, safearray) data = 0x00CAB170 (pointer to safearray) --- InspectArray --- Number of dimensions = 1 Features flags = 0x00000880 ($FADF_VARIANT+$FADF_HAVEVARTYPE, array of variants) Variant type = 0x000C (VT_VARIANT, variant data type) Size of array element (bytes) = 16 (size of the variant structure) Number of locks = 0 Pointer to data = 0x02EF0020 (pvData) Dimension 1: Elements in dimension = 50000 Lower bound of dimension = 0 Avoiding conversions For large arrays conversions may take quite some time. The conversions cannot be avoided, but in some situations they can be limited. Eg. a large array with 10 columns to sort by four columns (like the details view in Windows Explorer can be sorted by name, date, type and size). In this situation four indexes can be used to implement the sorting. And because it's a large array the indexes should be created with compiled code. Instead of converting the large array four times (once for each index), it would be much better to get a pointer to the safearray, and then reuse this pointer for each index. It'll only require one conversion of the large array to get a pointer to the safearray. For this purpose two functions in Includes\AccVarsUtilities.au3 can be used: AccVars_ArrayToSafeArray which creates a pointer to a safearray from a native AutoIt array, and AccVars_SafeArrayToArray which creates a native AutoIt array from a pointer to a safearray. More about these functions i a later section. Example5.au3 shows how long time it takes for the two functions to create a safearray and an array: ;#AutoIt3Wrapper_UseX64=y #include "..\..\..\Includes\AccessingVariables.au3" ; <<<< Final UDF (not test UDF) >>>> #include "..\..\..\Includes\AccVarsUtilities.au3" #include "..\..\..\Includes\ArrayDisplayEx.au3" Opt( "MustDeclareVars", 1 ) Example5() Func Example5() ; --- Array of integers --- ConsoleWrite( "Filling array of 16,777,216 integers (~10 seconds) ..." & @CRLF ) Local $aArray1[2^24] ; 2^24 = 16,777,216, maximum number of elements for an array For $i = 0 To 2^24 - 1 $aArray1[$i] = 1234 Next Local $pSafeArray1 Local $hTimer = TimerInit() AccVars_ArrayToSafeArray( $aArray1, $pSafeArray1 ) ConsoleWrite( "Time for AccVars_ArrayToSafeArray to execute: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray1 ) $aArray1 = 0 Local $aArray2 $hTimer = TimerInit() AccVars_SafeArrayToArray( $pSafeArray1, $aArray2 ) ConsoleWrite( "Time for AccVars_SafeArrayToArray to execute: " & TimerDiff( $hTimer ) & @CRLF & @CRLF ) _ArrayDisplayEx( $aArray2 ) $aArray2 = 0 ; --- Array of doubles --- ConsoleWrite( "Filling array of 16,777,216 doubles (~10 seconds) ..." & @CRLF ) Local $aArray3[2^24] ; 2^24 = 16,777,216, maximum number of elements for an array For $i = 0 To 2^24 - 1 $aArray3[$i] = 1234.5678 Next Local $pSafeArray3 $hTimer = TimerInit() AccVars_ArrayToSafeArray( $aArray3, $pSafeArray3 ) ConsoleWrite( "Time for AccVars_ArrayToSafeArray to execute: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray3 ) $aArray3 = 0 Local $aArray4 $hTimer = TimerInit() AccVars_SafeArrayToArray( $pSafeArray3, $aArray4 ) ConsoleWrite( "Time for AccVars_SafeArrayToArray to execute: " & TimerDiff( $hTimer ) & @CRLF & @CRLF ) _ArrayDisplayEx( $aArray4 ) $aArray4 = 0 ; --- Array of strings --- ConsoleWrite( "Filling array of 1,048,576 100-chars strings (~5 seconds) ..." & @CRLF ) Local $aArray5[2^20] ; 2^20 = 1,048,576 For $i = 0 To 2^20 - 1 $aArray5[$i] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; 100 chars Next Local $pSafeArray5 $hTimer = TimerInit() AccVars_ArrayToSafeArray( $aArray5, $pSafeArray5 ) ConsoleWrite( "Time for AccVars_ArrayToSafeArray to execute: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray5 ) $aArray5 = 0 Local $aArray6 $hTimer = TimerInit() AccVars_SafeArrayToArray( $pSafeArray5, $aArray6 ) ConsoleWrite( "Time for AccVars_SafeArrayToArray to execute: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray6 ) $aArray6 = 0 EndFunc Output in SciTE console: Filling array of 16,777,216 integers (~10 seconds) ... Time for AccVars_ArrayToSafeArray to execute: 4493.62536677101 Time for AccVars_SafeArrayToArray to execute: 1164.59904458766 Filling array of 16,777,216 doubles (~10 seconds) ... Time for AccVars_ArrayToSafeArray to execute: 4549.6914981711 Time for AccVars_SafeArrayToArray to execute: 1159.73609940987 Filling array of 1,048,576 100-chars strings (~5 seconds) ... Time for AccVars_ArrayToSafeArray to execute: 2419.86058615209 Time for AccVars_SafeArrayToArray to execute: 675.048988223156 Note that a consequence of the conversions is that the input safearray for AccVars_SafeArrayToArray is deleted. If you need to use the same safearray several times you must make a copy of the safearray with SafeArrayCopy function in Includes\SafeArray.au3. In the example with fasm code in previous section we got this result: Time for FillArray to fill array: 2388.37594979003 In top of post we got this result for more or less the same code: Applied time to create and fill array (UDF and fasm): 797.855906880413 Three times as fast. This is a consequence of limiting conversions. In a few situations it may be possible to completely avoid conversions. Eg. if the data will be used to fill the rows in a virtual listview. In a virtual listview rows are filled through $LVN_GETDISPINFOW notifications. And for $LVN_GETDISPINFOW notifications it doesn't matter whether the data source is a native AutoIt array or the data structure in a safearray. Final UDF The whole idea of this UDF is to utilize the information we already know: We know that COM objects and methods are working very well in AutoIt. We also know that the only variable type as COM object methods are familiar with is variants (only completely true for IDispatch based objects and objects of Automation compatible types). But how is it possible for an object method to handle variables that are passed to the method as native AutoIt variables, when the only variable type as COM object methods are familiar with is variants? The only explanation is that native AutoIt input parameters must be converted to variants immediately before the method code is executed, and that variant output parameters must be converted to native AutoIt variables immediately after the method code has finished. These conversions must be carried out by internal AutoIt code. The final UDF (Includes\AccessingVariables.au3) makes it possible to pass native AutoIt arrays and simple variables as parameters to a function coded in another language eg. assembler, C, C++, C# or FreeBasic. This is done by executing the function inside an object method and passing the array and variable parameters as variant pointers. Internal AutoIt conversions ensures that AutoIt variables outside the method are properly converted to COM variants inside the method. And the other way around. In the test UDF (Tests\AccessingVariablesTest.au3) only one AutoIt variable is passed to the object method by AccessVariable function. The final UDF contains 30 functions named AccessVariables01 - AccessVariables30 where the number indicates the number of AutoIt variables passed to the object method. Manipulating AutoIt variables in another language (a compiled language) is especially relevant for arrays with a large number of elements, or smaller arrays where complex calculations are performed on the elements. AccessVariablesXY To use the UDF you call one of the AccessVariablesXY functions eg. AccessVariables01. AccessVariables01 is coded in this way: Func AccessVariables01( $hAccVars_Method, ByRef $vVariable01 ) $hAccVars_MethodFunc = $hAccVars_Method $oAccVars_Object.AccVars_VariableToVariant01( $vVariable01 ) EndFunc The first parameter is a function type parameter (the name of an AutoIt function). You must code this function yourself. The function is assigned to the global variable $hAccVars_MethodFunc. The second parameter is an AutoIt variable. Typically an array. This parameter is passed to the AccVars_VariableToVariant01 object method. AccessVariables01 is just a wrapper function to make it easier to call the object method. The description string for the object method looks like this: "AccVars_VariableToVariant01 hresult(variant*);" The AutoIt variable is passed to the method as a variant pointer. And the method is coded in this way: Func AccVars_VariableToVariant01( $pSelf, $pVariant01 ) $hAccVars_MethodFunc( $pVariant01 ) Return 0 ; $S_OK (COM constant) #forceref $pSelf EndFunc The first parameter $pSelf must be a pointer to the object $oAccVars_Object. This is a COM rule. The second parameter is the AutoIt variable you passed to the AccessVariables01 function. But inside AccVars_VariableToVariant01 this is not an AutoIt variable any more. Inside AccVars_VariableToVariant01 it's a pointer to a variant. Inside AccVars_VariableToVariant01 the $hAccVars_MethodFunc is called and the variant pointer is passed as a parameter. $hAccVars_MethodFunc is the function you passed to AccessVariables01. If you passed a native AutoIt array to AccessVariables01, you can extract this array as a pointer to a safearray inside $hAccVars_MethodFunc. And this pointer or a pointer directly to the safearray data area you can pass to a function coded in assembler, C, C++, C# or FreeBasic. When $hAccVars_MethodFunc is finished zero is returned to indicate that everything is OK. Restrictions Because the $vVariableXY parameters in AccessVariablesXY functions are ByRef parameters you cannot pass literal values as parameters. You must store the literal value in a variable and pass the variable. You cannot call an AccessVariablesXY function inside another AccessVariablesXY function. No nested function calls. This also means that you cannot call InspectVariable or InspectArray inside an AccessVariablesXY function. But you can call InspectSafeArray. See InspectVariable section below for info about Inspect-functions. Utility funcs Typically arrays are passed to the object methods. But this does not exclude the need to pass simple variables to the object methods. Eg. the number of rows and columns in the arrays. Simple variables are also passed to object methods as variant pointers. In most cases you probably want to treat simple variables as native AutoIt variables inside object methods and not as variant pointers. AccVars_VariantToVariable (Includes\AccVarsUtilities.au3) converts variant pointers to native AutoIt variables. Arrays are not converted. See "Examples\Demo examples\4) Other demo examples\2) Multiple parameters\Example1.au3". AccVars_VariableToVariant converts native AutoIt variables to variant pointers. Arrays are not converted. See "Examples\Demo examples\4) Other demo examples\6) Using the UDF\Example6.au3" AccVars_ArrayToSafeArray and AccVars_SafeArrayToArray are already mentioned above. AccVars_ArrayToSafeArray creates a pointer to a safearray from a native AutoIt array. AccVars_SafeArrayToArray creates a native AutoIt array from a pointer to a safearray. "Examples\Demo examples\4) Other demo examples\4) ArrayToSafearray\" includes a few examples of AccVars_ArrayToSafeArray. "Examples\Demo examples\4) Other demo examples\5) SafearrayToArray\" includes a few examples of AccVars_SafeArrayToArray. In both folders Example1.au3 shows how everything can be done manually without using the two functions. And Example2.au3 shows how it can be done using the two functions. Note that a consequence of the conversions is that the input safearray for AccVars_SafeArrayToArray is deleted. If you need to use the same safearray several times you must make a copy of the safearray with SafeArrayCopy function in Includes\SafeArray.au3. InspectVariable Includes\InspectVariable.au3 contains three functions: InspectVariable (corresponds to InspectVariableMtd), InspectArray (corresponds to InspectVariableMtd) and InspectSafeArray. The first two are used in many of the test examples. The last is used in the examples for AccVars_ArrayToSafeArray and AccVars_SafeArrayToArray. InspectVariable and InspectArray takes AutoIt variables as input parameters and prints information about the variables in SciTE console after they have been converted to variants. InspectSafeArray prints information about safearrays in SciTE console. Using the UDF Here are six small scripts which shows how to use the UDF. You can use these scripts as templates for your own code. The scripts are saved in "Examples\Demo examples\4) Other demo examples\6) Using the UDF\". The first four scripts is about creating and filling a new array. The array is in all cases a 1D-array with 2^24 (16,777,216) integer elements. In Example1.au3 an AutoIt integer structure is filled with data, the data in the structure is copied into a safearray which is converted to a native AutoIt array: ;#AutoIt3Wrapper_UseX64=y #include "..\..\..\..\Includes\AccVarsUtilities.au3" #include "..\..\..\..\Includes\ArrayDisplayEx.au3" #include "..\..\..\..\Includes\FasmUtils.au3" Opt( "MustDeclareVars", 1 ) Example1() Func Example1() Local $hTimer = TimerInit() ; --- Create and fill structure of integers --- ; Create structure of integers Local $tIntegers = DllStructCreate( "int[" & 2^24 & "]" ) ; 2^24 = 16,777,216 elements Local $pIntegers = DllStructGetPtr( $tIntegers ) ; AutoIt code ;For $i = 0 To 2^24 - 1 ; DllStructSetData( $tIntegers, 1, $i, $i + 1 ) ;Next ; <<<< Execute your assembler, C, C++, C# or FreeBasic code at this point >>>> ; Get fasm code Local $sFasmCode = @AutoItX64 ? "0xB80000000089024883C204FFC0E2F6C3" _ ; Example1-x64.asm : "0x5589E58B4D088B550CB800000000890283C20440E2F85DC20800" ; Example1-x86.asm Local $pFasmCode = FasmGetBinaryString( $sFasmCode, 64 ) If Not $pFasmCode Then Return ConsoleWrite( "$pFasmCode ERR" & @CRLF ) ; Execute fasm code to fill structure DllCallAddress( "int", $pFasmCode, "int", 2^24, "ptr", $pIntegers ) ; --- Create and fill safearray --- ; Create safearray Local $tSafeArrayBound = DllStructCreate( $tagSAFEARRAYBOUND ) DllStructSetData( $tSafeArrayBound, "cElements", 2^24 ) ; Number of elements in array DllStructSetData( $tSafeArrayBound, "lLbound", 0 ) ; Lower bound of array index Local $pSafeArray = SafeArrayCreate( $VT_I4, 1, $tSafeArrayBound ) ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; Create structure from safearray data area Local $tSafeArrayBytes = DllStructCreate( "byte[" & 4 * 2^24 & "]", $pSafeArrayData ) ; Fill safearray data area with data from $tIntegers DllStructSetData( $tSafeArrayBytes, 1, DllStructGetData( DllStructCreate( "byte[" & 4 * 2^24 & "]", $pIntegers ), 1 ) ) ; This technique only works with byte's, char's and wchar's SafeArrayUnaccessData( $pSafeArray ) $tIntegers = 0 ; --- Convert safearray to native AutoIt array --- Local $aArray AccVars_SafeArrayToArray( $pSafeArray, $aArray ) ConsoleWrite( "Time to fill array: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray ) $aArray = 0 EndFunc ; Time to fill array: 939.174260660843 This example is simple because you're not dealing with variants or safearrays at all in your assembler, C, C++, C# or FreeBasic code. In Example2.au3 the data area of a safearray is directly filled with integers and the safearray is converted to a native AutoIt array: ;#AutoIt3Wrapper_UseX64=y #include "..\..\..\..\Includes\AccVarsUtilities.au3" #include "..\..\..\..\Includes\ArrayDisplayEx.au3" #include "..\..\..\..\Includes\FasmUtils.au3" Opt( "MustDeclareVars", 1 ) Example2() Func Example2() Local $hTimer = TimerInit() ; --- Create and fill safearray --- ; Create safearray Local $tSafeArrayBound = DllStructCreate( $tagSAFEARRAYBOUND ) DllStructSetData( $tSafeArrayBound, "cElements", 2^24 ) ; Number of elements in array DllStructSetData( $tSafeArrayBound, "lLbound", 0 ) ; Lower bound of array index Local $pSafeArray = SafeArrayCreate( $VT_I4, 1, $tSafeArrayBound ) ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; AutoIt code ;For $i = 0 To 2^24 - 1 ; DllStructSetData( $tSafeArrayData, 1, $i, $i + 1 ) ;Next ; <<<< Execute your assembler, C, C++, C# or FreeBasic code at this point >>>> ; Get fasm code Local $sFasmCode = @AutoItX64 ? "0xB80000000089024883C204FFC0E2F6C3" _ ; Example1-x64.asm : "0x5589E58B4D088B550CB800000000890283C20440E2F85DC20800" ; Example1-x86.asm Local $pFasmCode = FasmGetBinaryString( $sFasmCode, 64 ) If Not $pFasmCode Then Exit ConsoleWrite( "$pFasmCode ERR" & @CRLF ) ; Execute fasm code to fill safearray DllCallAddress( "int", $pFasmCode, "int", 2^24, "ptr", $pSafeArrayData ) SafeArrayUnaccessData( $pSafeArray ) ; --- Convert safearray to native AutoIt array --- Local $aArray AccVars_SafeArrayToArray( $pSafeArray, $aArray ) ConsoleWrite( "Time to fill array: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray ) $aArray = 0 EndFunc ; Time to fill array: 805.347826773543 Example2 is more efficient than Example1 because you're avoiding the integer structure. The conversion in Example3.au3 is not performed with AccVars_SafeArrayToArray but with your own SafeArrayToArray method function which is executed through AccessVariables02: ;#AutoIt3Wrapper_UseX64=y #include "..\..\..\..\Includes\AccVarsUtilities.au3" #include "..\..\..\..\Includes\ArrayDisplayEx.au3" #include "..\..\..\..\Includes\FasmUtils.au3" Opt( "MustDeclareVars", 1 ) Example3() Func Example3() Local $hTimer = TimerInit() ; --- Create and fill safearray --- ; Create safearray Local $tSafeArrayBound = DllStructCreate( $tagSAFEARRAYBOUND ) DllStructSetData( $tSafeArrayBound, "cElements", 2^24 ) ; Number of elements in array DllStructSetData( $tSafeArrayBound, "lLbound", 0 ) ; Lower bound of array index Local $pSafeArray = SafeArrayCreate( $VT_I4, 1, $tSafeArrayBound ) ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; AutoIt code ;For $i = 0 To 2^24 - 1 ; DllStructSetData( $tSafeArrayData, 1, $i, $i + 1 ) ;Next ; <<<< Execute your assembler, C, C++, C# or FreeBasic code at this point >>>> ; Get fasm code Local $sFasmCode = @AutoItX64 ? "0xB80000000089024883C204FFC0E2F6C3" _ ; Example1-x64.asm : "0x5589E58B4D088B550CB800000000890283C20440E2F85DC20800" ; Example1-x86.asm Local $pFasmCode = FasmGetBinaryString( $sFasmCode, 64 ) If Not $pFasmCode Then Exit ConsoleWrite( "$pFasmCode ERR" & @CRLF ) ; Execute fasm code to fill safearray DllCallAddress( "int", $pFasmCode, "int", 2^24, "ptr", $pSafeArrayData ) SafeArrayUnaccessData( $pSafeArray ) ; --- Convert safearray to native AutoIt array --- Local $aArray AccessVariables02( SafeArrayToArray, $pSafeArray, $aArray ) ConsoleWrite( "Time to fill array: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray ) $aArray = 0 EndFunc Func SafeArrayToArray( $pSafeArray, $pArray ) ; --- Set $pArray to match an AutoIt array --- ; Set vt element to $VT_ARRAY + $VT_VARIANT DllStructSetData( DllStructCreate( "word", $pArray ), 1, $VT_ARRAY + $VT_VARIANT ) ; Set data element to safearray pointer DllStructSetData( DllStructCreate( "ptr", $pArray + 8 ), 1, AccVars_VariantToVariable( $pSafeArray ) ) ; <<<< On function exit $pArray (safearray contained in a variant) is converted to an AutoIt array >>>> EndFunc ; Time to fill array: 800.813455228795 Example3 is a more general example than Example2. Creation of the safearray is moved from AutoIt code to your assembler, C, C++, C# or FreeBasic code in Example4.au3: ;#AutoIt3Wrapper_UseX64=y #include "..\..\..\..\Includes\AccessingVariables.au3" #include "..\..\..\..\Includes\ArrayDisplayEx.au3" #include "..\..\..\..\Includes\FasmUtils.au3" Opt( "MustDeclareVars", 1 ) Example4() Func Example4() ; --- Create and fill array --- Local $aArray Local $hTimer = TimerInit() AccessVariables01( CreateArray, $aArray ) ConsoleWrite( "Time to fill array: " & TimerDiff( $hTimer ) & @CRLF ) _ArrayDisplayEx( $aArray ) $aArray = 0 EndFunc Func CreateArray( $pArray ) ; --- Create and fill safearray --- ; <<<< Execute your assembler, C, C++, C# or FreeBasic code at this point >>>> ; Create safearray Local $tSafeArrayBound = DllStructCreate( $tagSAFEARRAYBOUND ) DllStructSetData( $tSafeArrayBound, "cElements", 2^24 ) ; Number of elements in array DllStructSetData( $tSafeArrayBound, "lLbound", 0 ) ; Lower bound of array index Local $pSafeArray = SafeArrayCreate( $VT_I4, 1, $tSafeArrayBound ) ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; AutoIt code ;For $i = 0 To 2^24 - 1 ; DllStructSetData( $tSafeArrayData, 1, $i, $i + 1 ) ;Next ; Get fasm code Local $sFasmCode = @AutoItX64 ? "0xB80000000089024883C204FFC0E2F6C3" _ ; Example1-x64.asm : "0x5589E58B4D088B550CB800000000890283C20440E2F85DC20800" ; Example1-x86.asm Local $pFasmCode = FasmGetBinaryString( $sFasmCode, 64 ) If Not $pFasmCode Then Exit ConsoleWrite( "$pFasmCode ERR" & @CRLF ) ; Execute fasm code to fill safearray DllCallAddress( "int", $pFasmCode, "int", 2^24, "ptr", $pSafeArrayData ) SafeArrayUnaccessData( $pSafeArray ) ; --- Set $pArray to match an AutoIt array --- ; Set vt element to $VT_ARRAY + $VT_VARIANT DllStructSetData( DllStructCreate( "word", $pArray ), 1, $VT_ARRAY + $VT_VARIANT ) ; Set data element to safearray pointer DllStructSetData( DllStructCreate( "ptr", $pArray + 8 ), 1, $pSafeArray ) ; <<<< On function exit $pArray (safearray contained in a variant) is converted to an AutoIt array >>>> EndFunc ; Time to fill array: 799.20854202792 If you code in assembler it's more advantageous to create the safearray in AutoIt code. The last two scripts is about manipulating an existing array. In both scripts a 1D-array with 2^23 (8,388,608) random integers is searched to find the minimum and maximum value. In Example5.au3 AccVars_ArrayToSafeArray is used to convert the AutoIt array to a safearray: ;#AutoIt3Wrapper_UseX64=y #include "..\..\..\..\Includes\AccVarsUtilities.au3" ;#include "..\..\..\..\Includes\InspectVariable.au3" #include "..\..\..\..\Includes\ArrayDisplayEx.au3" #include "..\..\..\..\Includes\FasmUtils.au3" Opt( "MustDeclareVars", 1 ) Example5() Func Example5() ; --- Create test array of random integers --- ConsoleWrite( "Create test array (~10 seconds) ..." & @CRLF ) Local $aArray[2^23] ; 2^23 = 8,388,608 For $i = 0 To 2^23 - 1 $aArray[$i] = Random( 0, 2^31-1, 1 ) Next _ArrayDisplayEx( $aArray ) ; --- Convert native AutoIt array to safearray --- Local $hTimer = TimerInit() Local $pSafeArray AccVars_ArrayToSafeArray( $aArray, $pSafeArray ) ;InspectSafeArray( $pSafeArray ) ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; --- Test safearray for min/max values --- ; AutoIt code ;Local $iMin1 = 2^31-1, $iMax1 = 0 ;For $i = 0 To 2^23 - 1 ; If $aArray[$i] < $iMin1 Then $iMin1 = $aArray[$i] ; If $aArray[$i] > $iMax1 Then $iMax1 = $aArray[$i] ;Next ; <<<< Execute your assembler, C, C++, C# or FreeBasic code at this point >>>> ; $iMin, $iMax storage for fasm code Local $tMinMax = DllStructCreate( "int iMin;int iMax" ) DllStructSetData( $tMinMax, "iMin", 2^31-1 ) DllStructSetData( $tMinMax, "iMax", 0 ) Local $pMinMax = DllStructGetPtr( $tMinMax ) ; iMin = $pMinMax, iMax = $pMinMax + 4 (int = 4 bytes) ; Get fasm code Static $sFasmCode = @AutoItX64 ? "0x4D8B4804418B003B420876038B4208443B4A087304448B4A084883C218E2E841890045894804C3" _ ; Example5-x64.asm : "0x5589E58B4D088B550C8B45108B58048B003B420876038B42083B5A0873038B5A0883C210E2EB8B55108902895A045DC20C00" ; Example5-x86.asm Static $pFasmCode = FasmGetBinaryString( $sFasmCode, 64 ) If Not $pFasmCode Then Exit ConsoleWrite( "$pFasmCode ERR" & @CRLF ) ; Execute fasm code DllCallAddress( "int", $pFasmCode, "int", 2^23, "ptr", $pSafeArrayData, "ptr", $pMinMax ) ConsoleWrite( "$iMin, $iMax = " & DllStructGetData( $tMinMax, "iMin" ) & ", " & DllStructGetData( $tMinMax, "iMax" ) & @CRLF ) ConsoleWrite( "Time to test array: " & TimerDiff( $hTimer ) & @CRLF ) SafeArrayDestroy( $pSafeArray ) $aArray = 0 EndFunc ; $iMin, $iMax = 262, 2147483599 ; Time to test array: 2293.62874670214 In Example6.au3 the conversion is done in your own TestArray method function which is executed through AccessVariables03: ;#AutoIt3Wrapper_UseX64=y #include "..\..\..\..\Includes\AccVarsUtilities.au3" #include "..\..\..\..\Includes\ArrayDisplayEx.au3" #include "..\..\..\..\Includes\FasmUtils.au3" Opt( "MustDeclareVars", 1 ) Example6() Func Example6() ; --- Create test array of random integers --- ConsoleWrite( "Create test array (~10 seconds) ..." & @CRLF ) Local $aArray[2^23] ; 2^23 = 8,388,608 For $i = 0 To 2^23 - 1 $aArray[$i] = Random( 0, 2^31-1, 1 ) Next _ArrayDisplayEx( $aArray ) ; --- Test array for min/max values --- ; AutoIt code ;Local $iMin1 = 2^31-1, $iMax1 = 0 ;For $i = 0 To 2^23 - 1 ; If $aArray[$i] < $iMin1 Then $iMin1 = $aArray[$i] ; If $aArray[$i] > $iMax1 Then $iMax1 = $aArray[$i] ;Next Local $hTimer = TimerInit() Local $iMin = 2^31-1, $iMax = 0 AccessVariables03( TestArray, $aArray, $iMin, $iMax ) ConsoleWrite( "$iMin, $iMax = " & $iMin & ", " & $iMax & @CRLF ) ConsoleWrite( "Time to test array: " & TimerDiff( $hTimer ) & @CRLF ) $aArray = 0 EndFunc Func TestArray( $pvArray, $pvMin, $pvMax ) ; <<<< On function entry $aArray is converted to $pvArray (safearray contained in a variant) >>>> ; Pointer to safearray Local $pData = $pvArray + 8 Local $tData = DllStructCreate( "ptr", $pData ) Local $pSafeArray = DllStructGetData( $tData, 1 ) ; Pointer to data Local $pSafeArrayData SafeArrayAccessData( $pSafeArray, $pSafeArrayData ) ; <<<< Execute your assembler, C, C++, C# or FreeBasic code at this point >>>> ; $iMin, $iMax storage for fasm code Local $tMinMax = DllStructCreate( "int iMin;int iMax" ) DllStructSetData( $tMinMax, "iMin", AccVars_VariantToVariable( $pvMin ) ) DllStructSetData( $tMinMax, "iMax", AccVars_VariantToVariable( $pvMax ) ) Local $pMinMax = DllStructGetPtr( $tMinMax ) ; iMin = $pMinMax, iMax = $pMinMax + 4 (int = 4 bytes) ; Get fasm code Static $sFasmCode = @AutoItX64 ? "0x4D8B4804418B003B420876038B4208443B4A087304448B4A084883C218E2E841890045894804C3" _ ; Example5-x64.asm : "0x5589E58B4D088B550C8B45108B58048B003B420876038B42083B5A0873038B5A0883C210E2EB8B55108902895A045DC20C00" ; Example5-x86.asm Static $pFasmCode = FasmGetBinaryString( $sFasmCode, 64 ) If Not $pFasmCode Then Exit ConsoleWrite( "$pFasmCode ERR" & @CRLF ) ; Execute fasm code DllCallAddress( "int", $pFasmCode, "int", 2^23, "ptr", $pSafeArrayData, "ptr", $pMinMax ) ; Get $iMin, $iMax AccVars_VariableToVariant( DllStructGetData( $tMinMax, "iMin" ), $pvMin ) AccVars_VariableToVariant( DllStructGetData( $tMinMax, "iMax" ), $pvMax ) SafeArrayUnaccessData( $pSafeArray ) EndFunc ; $iMin, $iMax = 39, 2147483610 ; Time to test array: 2182.54509368366 Examples There are two obvious uses for this: To create and fill a new array. And to manipulate one or more existing arrays. Demo examples "Examples\Demo examples\" contains a few simple demonstration examples. 1) Create and fill new array There are already examples above where a 1D-array is filled with integers. As a demonstration example of creating and filling out an array, a little more complex 2D-array is filled with integers. Example2.au3 is an optimized version of Example1.au3. See post 6. 2) Manipulate existing array As a demonstration example of manipulating a single existing array, minimum and maximum values are determined in a 1D-array of random integers. This is the same as Example5 and Example6 above. See post 6. 3) Concatenate 1D-arrays As a demonstration example of manipulating several existing arrays 2, 8 and 16 1D-arrays of integers with 2^20 (1,048,576) rows are concatenated into a 2D-array with 2, 8 and 16 columns. See post 6. In all three demonstration examples flat assembler (fasm) is used as the fast language to manipulate the arrays. Note that in the three examples the pure AutoIt code is doing relatively well compared to the fasm code. The reason is that the code in the array loops is very simple. For more complex code the AutoIt code will not do so well. Assembler code flat assembler (fasm) is available as a DLL-file. This means that all you need to create fasm code is this DLL and the DllCall command. DllCall is a native AutoIt command. The DLL-file is a 32 bit DLL, but it can generate both 32 and 64 bit fasm code. Furthermore, the assembled code (the executable machine code) is very compact. None of the example programs are larger than 64 bytes. There are several threads in these forums regarding fasm and development of fasm code. And there are threads in the German forums regarding fasm. For the example programs included here I'm just using the DLL-file, DllCall and a few AutoIt functions to make things easier. Nothing of this is included in the zip. Other demo examples These are small examples I used while I developed and tested the UDF. The examples are stored in "Examples\Demo examples\4) Other demo examples\". Create and fill array\ contains the the same examples as "Tests\Examples\3) Internal conversions\". Multiple parameters\ tests all 30 AccessVariablesXY functions. The functions are tested with simple variables where some variables are added and the result is stored in another variable. Example1.au3. Inside the AccessVariablesXY functions the variables are true variants. This is demonstrated by adding the variables with VarAdd API function. Example2.au3. Safearray dimensions\ shows how to handle safearrays of 1, 2 and 3 dimensions. ArrayToSafearray\ and SafearrayToArray\ is about the AccVars_ArrayToSafeArray and AccVars_SafeArrayToArray utility functions. Using the UDF\ contains the six examples in the previous section. Real examples The examples of fasm code that I've used for tests and demonstrations are almost too small to be realistic examples. But so far I've not finalized any more realistic examples. I'll add some examples when they are completed. Zip file At the top level the zip contains the following folders and files: Examples\ - Examples main section Includes\ - Final UDF main section Tests\ - The first four sections Example.au3 - Example in top of post Example-x64.asm - 64 bit fasm code Example-x86.asm - 32 bit fasm code You need AutoIt 3.3.10 or later. Tested on Windows 10/7 32/64 bit and Windows XP 32 bit. If you have less than 4GB of RAM in your PC, you should reduce the number of array rows by a factor of four (change 2^24, 2^23, 2^20 to 2^22, 2^21, 2^18 in .au3-files) in examples with fasm code. Comments are welcome. Let me know if there are any issues. AccessingVariables.7z
    1 point
×
×
  • Create New...