VBA - Office 365 x64 bit - Completely crashing - vba

This is my first time asking for any help on stack overflow, let alone commenting so please be gentle with me :)
I am at a loss with this one, I will give as much information as possible.
Issue
I would like to preface, this code does not cause any crashes on the latest update of 0365, only on Version 1807 & earlier. It also does not crash on the 32 bit version at all which makes me think it's a 64 bit issue. My client cannot update from this version either so simply asking them to update is not going to be able to happen.
I have narrowed the crashing down to this particular section.
Public Function GetSpecialFolder(CSIDL As Long) As String
'*******************************************************************************
'* Function: GetSpecialFolder
'* Purpose: Wraps the apis to retrieve folders such as My Docs etc.
'*******************************************************************************
Dim idlstr As Long
Dim sPath As String
Dim IDL As ITEMIDLIST
Const MAX_LENGTH = 260
'Fill the IDL structure with the specified folder item.
On Error GoTo GetSpecialFolder_Error
idlstr = SHGetSpecialFolderLocation _
(0, CSIDL, IDL)
If idlstr = 0 Then
'Get the path from the IDL list, and return the folder adding final "\".
sPath = Space$(MAX_LENGTH)
**idlstr = SHGetPathFromIDList(ByVal IDL.mkid.cb, ByVal sPath)**
If idlstr Then
GetSpecialFolder = Left$(sPath, InStr(sPath, Chr$(0)) _
- 1) & "\"
End If
End If
procExit:
On Error Resume Next
Exit Function
GetSpecialFolder_Error:
CommonErrorHandler lngErrNum:=Err.Number, strErrDesc:=Err.Description, _
strProc:="GetSpecialFolder", strModule:="modWinAPI", lngLineNum:=Erl
Resume procExit
End Function
And here is the declaration
'File system
Public Declare PtrSafe Function SHGetSpecialFolderLocation Lib "shell32.dll" _
(ByVal hwndOwner As Long, ByVal nFolder As Long, pidl As ITEMIDLIST) As Long
Public Declare PtrSafe Function SHGetPathFromIDList Lib "shell32.dll" _
Alias "SHGetPathFromIDListA" (ByVal pidl As Long, ByVal pszPath As String) As Long
Private Declare PtrSafe Function GetTempPath Lib "kernel32" _
Alias "GetTempPathA" (ByVal nBufferLength As Long, ByVal lpBuffer As String) As Long
Private Declare PtrSafe Function ShellExecute Lib "shell32.dll" _
Alias "ShellExecuteA" (ByVal hwnd As LongPtr, ByVal lpOperation As String, _
ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String, _
ByVal nShowCmd As Long) As LongPtr
Private Type ITEMIDLIST
mkid As ShortItemId
End Type
Private Type ShortItemId
cb As Long
abID As Byte
End Type
I have tried adding LongPtr as suggested in documents I've found online but it hasn't helped.
Can anyone help me?
Thanks!

SHGetSpecialFolderLocation does not fill in the memory you allocate for ITEMIDLIST like Declared function usually do, it allocates a new piece of memory that you are later required to free with CoTaskMemFree. That makes it pointless to declare ITEMIDLIST as a structure in VBA to begin with (and your declaration is wrong anyway, cb must be Integer, and abID is a variable-length byte array, not a single byte).
If you needed to do something with individual members of a structure allocated in this way, you would have to copy them out of the returned pointer with CopyMemory. Luckily, you don't need to do any of that because SHGetSpecialFolderLocation returns a pointer to PIDLIST_ABSOLUTE, and SHGetPathFromIDList accepts PCIDLIST_ABSOLUTE:
Public Declare PtrSafe Function SHGetSpecialFolderLocation Lib "shell32.dll" _
(ByVal hwndOwner As LongPtr, ByVal nFolder As Long, ByRef pIdl As LongPtr) As Long
Public Declare PtrSafe Function SHGetPathFromIDList Lib "shell32.dll" _
Alias "SHGetPathFromIDListA" (ByVal pIdl As LongPtr, ByVal pszPath As String) As Long
Public Declare PtrSafe Sub CoTaskMemFree Lib "ole32.dll" (pv As Any)
Public Function GetSpecialFolder(ByVal CSIDL As Long) As String
Dim retval As Long
Dim pIdl As LongPtr
Dim sPath As String
Const MAX_LENGTH = 260
retval = SHGetSpecialFolderLocation(0, CSIDL, pIdl)
If retval = 0 Then
sPath = Space$(MAX_LENGTH)
retval = SHGetPathFromIDList(pIdl, sPath)
If retval <> 0 Then
GetSpecialFolder = Left$(sPath, InStr(sPath, Chr$(0)) - 1) & "\"
End If
CoTaskMemFree ByVal pIdl
End If
End Function
Note that it's pointless to have an On Error Goto in such function because Windows API generally do not raise exceptions, they return error codes. It would make sense if you used Err.Raise ... after finding out a return value indicates an error.

TBH, I have no clue how this was functioning correctly on a 32 bit build. The declarations for the two structures are incorrect. This one...
Private Type ShortItemId
cb As Long
abID As Byte
End Type
...is defined in the MS documentation as this:
typedef struct _SHITEMID {
USHORT cb;
BYTE abID[1];
} SHITEMID;
Note that abID is an array, and cb is an unsigned short (you can use an Integer for that in VBA, but it definitely is not a Long).
In addition, this structure (wrapped in the ITEMIDLIST) is not even supposed to be allocated by the caller, but must be freed by the caller:
It is the responsibility of the calling application to free the returned IDList by using CoTaskMemFree.
Re the pointers, the only pointers (that aren't being marshaled from String) are the
pidl parameter of SHGetSpecialFolderLocation and the pointer to ppidl in SHGetPathFromIDList. Note that you can't use a VBA defined struct, because you need to free the memory when you're done. Something like this will work:
Private Declare PtrSafe Function SHGetSpecialFolderLocation Lib "shell32.dll" _
(ByVal hwndOwner As Long, ByVal nFolder As Long, pidl As LongPtr) As Long
Private Declare PtrSafe Function SHGetPathFromIDList Lib "shell32.dll" _
Alias "SHGetPathFromIDListA" (ByVal pidl As LongPtr, ByVal pszPath As String) As Boolean
Private Declare PtrSafe Sub CoTaskMemFree Lib "ole32.dll" (ByVal pv As LongPtr)
Private Const S_OK As Long = 0
Private Const MAX_LENGTH = 260
Public Function GetSpecialFolder(ByVal CSIDL As Integer) As String
Dim result As Long
Dim path As String
Dim idl_ptr As LongPtr
'Fill the IDL structure with the specified folder item.
result = SHGetSpecialFolderLocation(0, CSIDL, idl_ptr)
If result = S_OK Then
'Get the path from the IDL list, and return the folder adding final "\".
path = Space$(MAX_LENGTH)
If SHGetPathFromIDList(idl_ptr, path) Then
GetSpecialFolder = Left$(path, InStr(path, vbNullChar) - 1) & "\"
End If
CoTaskMemFree idl_ptr
End If
End Function
Note that per the discussion in the comments, you could technically declare hwndOwner as LongPtr as well, but it shouldn't make any difference.

Related

VBA 64 Bit Forms Image Control and loading an image from byte array

I have an Access 365 64-bit problem I am trying to solve and I need some guidance.
I have a byte array with image data, retrieved from a Base64 encoded string. The decoding is working just fine and I can produce the array as expected. I have had this code from an older 32 bit version of VBA applications and it remains fully functional. So far so good.
The issue comes into play when I am trying to place the image data into a forms image control directly - no saving to a file. I have had this working in the 32 bit applications but now that the office 365 subscription I am working with is 64 bit (as is the rest of the company) I am having trouble converting the API calls and subsequent code across to 64-bit compatible versions. The images are largely PNG and JPG images (when extracted).
The main issue seems to be coming from the need to replace OLEPRO32 with OLEAUT32. The code runs (seemingly) without error however I am not getting any output. I am sure that I am missing something simple but I just cannot see it. My code is below as well as the API declarations I am using, along with the old OLEPRO32 declaration which is commented out.
Option Explicit
Option Compare Database
Declare PtrSafe Function CreateStreamOnHGlobal Lib "ole32" (ByVal hGlobal As LongPtr, ByVal fDeleteOnRelease As Long, ppstm As Any) As LongPtr
Declare PtrSafe Function GlobalAlloc Lib "kernel32" (ByVal uFlags As Long, ByVal dwBytes As Long) As LongPtr
Declare PtrSafe Function GlobalLock Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
Declare PtrSafe Function GlobalUnlock Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
'Declare PtrSafe Function OleLoadPicture Lib "olepro32" (pStream As Any, ByVal lSize As Long, ByVal fRunmode As Long, riid As Any, ppvObj As Any) As LongPtr
Declare PtrSafe Function OleLoadPicture Lib "oleaut32" (pStream As Any, ByVal lSize As Long, ByVal fRunmode As Long, riid As Any, ppvObj As Any) As LongPtr
Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
And here is the routine that uses the declarations:
Public Function ArrayToPicture(inArray() As Byte, Offset As Long, Size As Long) As IPicture
' function creates a stdPicture from the passed array
' Offset is first item in array: 0 for 0 bound arrays
' Size is how many bytes comprise the image
Dim o_hMem As LongPtr
Dim o_lpMem As LongPtr
Dim aGUID(0 To 3) As Long
Dim IIStream As IUnknown
aGUID(0) = &H7BF80980 ' GUID for stdPicture
aGUID(1) = &H101ABF32
aGUID(2) = &HAA00BB8B
aGUID(3) = &HAB0C3000
o_hMem = GlobalAlloc(&H2&, Size)
If Not o_hMem = 0& Then
o_lpMem = GlobalLock(o_hMem)
If Not o_lpMem = 0& Then
CopyMemory ByVal o_lpMem, inArray(Offset), Size
Call GlobalUnlock(o_hMem)
If CreateStreamOnHGlobal(o_hMem, 1&, IIStream) = 0& Then
Call OleLoadPicture(ByVal ObjPtr(IIStream), 0&, 0&, aGUID(0), ArrayToPicture)
End If
End If
End If
End Function
If anyone has any ideas please let me know. The output of this function seems to be nothing at all whereas in the past I could expect a valid iPicture object that could be assigned directly to the form image controls .PictureData.
Any guidance greatly appreciated.
Cheers
The Frog
UPDATE:
I have worked through a large portion of the code and can now specifically isolate the locations where the crashes are happening. Code is below
Option Compare Database
' API declarations
Private Declare PtrSafe Function CreateStreamOnHGlobal Lib "Ole32.dll" (ByRef hGlobal As LongPtr, ByVal fDeleteOnRelease As Long, ByRef ppstm As LongPtr) As Long
Private Declare PtrSafe Function CLSIDFromString Lib "Ole32" (ByVal lpsz As LongPtr, pclsid As Any) As Long
Private Declare PtrSafe Function OLELoadPicture Lib "OleAut32.lib" (ByRef lpStream As LongPtr, ByVal lSize As Long, ByVal fRunMode As Long, ByRef RIID As GUID, ByRef lplpObj As LongPtr) As Long
Private Declare PtrSafe Function GetObject Lib "gdi32" Alias "GetObjectA" (ByVal hObject As LongPtr, ByVal nCount As Long, lpObject As Any) As Long
Private Declare PtrSafe Function CreateCompatibleBitmap Lib "gdi32" (ByVal hdc As LongPtr, ByVal nWidth As Long, ByVal nHeight As Long) As LongPtr
Private Declare PtrSafe Function SelectObject Lib "gdi32" (ByVal hdc As LongPtr, ByVal hObject As LongPtr) As LongPtr
Private Declare PtrSafe Function GetDIBits Lib "gdi32" (ByVal aHDC As LongPtr, ByVal hBitmap As LongPtr, ByVal nStartScan As Long, ByVal nNumScans As Long, lpBits As Any, lpBI As BITMAPINFO, ByVal wUsage As Long) As Long
Private Declare PtrSafe Function CreateCompatibleDC Lib "gdi32" (ByVal hdc As LongPtr) As LongPtr
Private Declare PtrSafe Function DeleteDC Lib "gdi32" (ByVal hdc As LongPtr) As Long
Private Declare PtrSafe Function GetWindowDC Lib "user32" (ByVal hWnd As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalAlloc Lib "kernel32" (ByVal wFlags As Long, ByVal dwBytes As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalLock Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalUnlock Lib "kernel32" (ByVal hMem As LongPtr) As Long
Private Declare PtrSafe Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
'Necessary Types
Private Type BITMAPINFOHEADER '40 Bytes
biSize As Long
biWidth As Long
biHeight As Long
biPlanes As Integer
biBitCount As Integer
biCompression As Long
biSizeImage As Long
biXPelsPerMeter As Long
biYPelsPerMeter As Long
biClrUsed As Long
biClrImportant As Long
End Type
Private Type RGBQUAD
rgbBlue As Byte
rgbGreen As Byte
rgbRed As Byte
rgbReserved As Byte
End Type
Private Type BITMAPINFO
bmiHeader As BITMAPINFOHEADER
bmiColors As RGBQUAD
End Type
Private Type DIBHEADER '14 magical bytes
BmpIdentification(1) As Byte
BmpSize(3) As Byte
BmpCreator As Integer
BmpCreator2 As Integer
BmpDataOffset(3) As Byte
End Type
Private Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
Public Function StdPictureToDibImage(ByRef Picture As StdPicture, Optional PadColor As Integer = -1) As Byte()
' Performs StdPicture to DIB compliant Byte Array
' Adopted based on : http://www.vbforums.com/showthread.php?833125-How-to-convert-StdPicture-into-pixel-array
' The Byte Arrays to Hold the Initial PictureData along with the Final One
Dim ImageData() As Byte
Dim buffer() As Byte
Dim tmp() As Byte
' Type Instances
Dim BMI As BITMAPINFO
Dim DIB As DIBHEADER
Dim PaddingColor As Integer
' API handle
Dim hdc As LongPtr
Dim hpic As LongPtr
hdc = CreateCompatibleDC(0) 'Create a temporary in-memory device context
BMI.bmiHeader.biSize = Len(BMI.bmiHeader) 'Initialize BitmapInfoHeader with header size
'Get the header Info of the Image based on the StdPicture handle provided
GetDIBits hdc, Picture.handle, 0, 0, ByVal 0&, BMI, 0 'Get Information about the image
'Normally here we would setup the header for BMI header but i found out that simply is not working as it should
' Only the DIB header needs manual handling
With DIB
.BmpIdentification(0) = 66
.BmpIdentification(1) = 77
tmp = LongToByteArray(BMI.bmiHeader.biSizeImage + Len(DIB) + Len(BMI.bmiHeader))
.BmpSize(0) = tmp(0)
.BmpSize(1) = tmp(1)
.BmpSize(2) = tmp(2)
.BmpSize(3) = tmp(3)
.BmpCreator = 0
.BmpCreator2 = 0
tmp = LongToByteArray(Len(DIB) + Len(BMI.bmiHeader))
.BmpDataOffset(0) = tmp(0)
.BmpDataOffset(1) = tmp(1)
.BmpDataOffset(2) = tmp(2)
.BmpDataOffset(3) = tmp(3)
End With
'Byte Arrays Initialization
ReDim ImageData(3, BMI.bmiHeader.biWidth - 1, BMI.bmiHeader.biHeight - 1) 'Initialize array for holding pixel data
ReDim buffer(0 To BMI.bmiHeader.biSizeImage + (Len(DIB) + Len(BMI.bmiHeader)) - 1)
'Here we get the actual Image Data from the StdPicture
'This was the most troubled part of the whole process as it kept truncating the image to around 3/4
' no matter what....until i noticed that the info "feeded" to the BMI header was wrong
hpic = CLngPtr(Picture.handle)
GetDIBits hdc, hpic, 0, Abs(BMI.bmiHeader.biHeight), ImageData(0, 0, 0), BMI, 0 'Get pixel data
'GetDIBits hdc, Picture.handle, 0, Abs(BMI.bmiHeader.biHeight), ImageData(0, 0, 0), BMI, 0 'Get pixel data
'Constructing the Final Image Data
'1st the DIB header ***** CRUCIAL ******* , without this everything fails and burns
CopyMemory buffer(0), DIB, Len(DIB)
'2nd the BMP header, this was done in all other cases
CopyMemory buffer(Len(DIB)), BMI.bmiHeader, Len(BMI.bmiHeader)
'3rd the actual image data
CopyMemory buffer(Len(DIB) + Len(BMI.bmiHeader)), ImageData(0, 0, 0), 3 * (BMI.bmiHeader.biWidth - 1) * (BMI.bmiHeader.biHeight - 1)
'Cleaning up
DeleteDC hdc 'Get rid of temporary in-memory device context
'Some Padding to remove the "dead" space because Images Dimensions are "resized" to multiple of 4s
'so if the either of the dimensions is not exactly a multiple of 4 then padding is applied which
'results is "dead" pixels
If PadColor < 0 Then
PaddingColor = 255
Else
PaddingColor = PadColor
End If
For I = UBound(buffer) To LBound(buffer) Step -1
If buffer(I) = 0 Then
buffer(I) = PaddingColor
Else
Exit For
End If
Next
'The final magical byte array...no more temp files,links,extra controls...whatever..everything in memory
StdPictureToDibImage = buffer()
End Function
Public Function PictureFromByteStream(ByRef B() As Byte) As IPicture
Dim LowerBound As Long
Dim ByteCount As Long
Dim hMem As Long
Dim lpMem As Long
Dim IID_IPicture As GUID
Dim istm As stdole.IUnknown
On Error GoTo Err_Init
If UBound(B, 1) < 0 Then
Exit Function
End If
LowerBound = LBound(B)
ByteCount = (UBound(B) - LowerBound) + 1
hMem = GlobalAlloc(&H2, ByteCount)
If hMem <> 0 Then
lpMem = GlobalLock(hMem)
If lpMem <> 0 Then
MoveMemory ByVal lpMem, B(LowerBound), ByteCount
Call GlobalUnlock(hMem)
If CreateStreamOnHGlobal(hMem, 1, istm) = 0 Then
If CLSIDFromString(StrPtr("{7BF80980-BF32-101A-8BBB-00AA00300CAB}"), IID_IPicture) = 0 Then
Call OLELoadPicture(ByVal ObjPtr(istm), ByteCount, 0, IID_IPicture, PictureFromByteStream)
End If
End If
End If
End If
Exit Function
Err_Init:
If err.Number = 9 Then
Debug.Print err.Number & " - " & err.Description
End Function
Function ArrayToStdPicture(imageBytes() As Byte) As StdPicture
Dim W As WIA.Vector
Dim s As StdPicture
Set W = New WIA.Vector
W.BinaryData = imageBytes
Set ArrayToStdPicture = W.Picture
If Not W Is Nothing Then Set W = Nothing
End Function
The process is as follows:
ArrayToStdPicture -> StdPictureToDIBImage
The crash occurs on the line:
GetDIBits hdc, hpic, 0, Abs(BMI.bmiHeader.biHeight), ImageData(0, 0, 0), BMI, 0 'Get pixel data
The crash totally crashes MS Access and kills the application.

vba Find Text box in an application using FindWindowEx

I have an MS Access form that contains Button to open an application. The application is created using c#. I want to get the TextBox in the Form so that I will set a value on it using the MS Access project.
I am using the following code:
hwndParent = FindWindow(vbNullString, "Form1")
If hwndParent <> 0 Then
TextBoxHandle = FindWindowEx(hwndParent, 0&, "WindowsForms10.EDIT.app.0.3cb5890_r6_ad1", vbNullString)
SendMessage TextBoxHandle, WM_SETTEXT, 0&, ByVal strText
End If
Above code is working on my workstation: Windows 10 Pro.
When I open the MS Access in windows 8. it can't find the TextBox.
TextBoxHandle always return 0 in Windows 8. I am sure that the issue is with 3rd parameter in FinWindowEx. I used spy++ from Microsoft to get the value WindowsForms10.EDIT.app.0.3cb5890_r6_ad1 cause when I try to just enter "Edit", it does not work.
Edit: Adjusted answer using information about dynamic name of class from Hans Passant.
First, we're going to declare WinAPI functions to be able to iterate through all windows and get their class name.
Declare PtrSafe Function FindWindowExW Lib "user32" (ByVal hWndParent As LongPtr, Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As LongPtr, Optional ByVal lpszWindow As LongPtr) As LongPtr
Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameW" (ByVal hWnd As LongPtr, ByVal lpClassName As LongPtr, ByVal nMaxCount As Long) As Long
Then, we're going to declare a helper function to get the class name from a hWnd:
Public Function GetWindowClass(hWnd As LongPtr) As String
Dim buf(512) As Byte
GetClassName hWnd, varPtr(buf(0)), 255
GetWindowClass = Replace(CStr(buf), Chr(0), "")
End Function
Then, we're going to iterate through all top-level windows, and return the hWnd from the one matching that class name:
Public Function getThehWnd(hWndParent) As LongPtr
Dim hWnd As LongPtr
hWnd = FindWindowExW(hWndParent)
Do While hWnd <> 0
If GetWindowClass(hWnd) Like "WindowsForms10.EDIT.app.0.*" Then
getThehWnd = hWnd
Exit Function
End If
hWnd = FindWindowExW(hWndParent, hWnd)
Loop
End Function
Old answer:
There are numerous things that can go wrong when calling WinAPI functions from VBA with strings. These include passing a string that's not terminated by a null string, and passing a string that's in the wrong encoding.
For that first case, you get unstable behavior. If the string happens to be stored somewhere where there are a lot of zero's in memory, it works. Else, it continues reading bytes from memory and appending them to the string until it finds two bytes that happen to both be 0.
The first case is easily fixed by appending a null character to the end of your string:
TextBoxHandle = FindWindowEx(hwndParent, 0&, "WindowsForms10.EDIT.app.0.3cb5890_r6_ad1" & Chr(0), vbNullString)
Note that you should probably also make that last argument optional. Entering vbNullString there passes a pointer to a zero-length string, that might also not be delimited by a null character, causing WinAPI to read subsequent characters till it finds 2 null bytes. Setting the type to LongPtr and passing 0 (the default value) passes an actual null pointer, which WinAPI expects when no string gets put in.
The second code is more difficult. I tend to use bytearrays to make sure VBA doesn't do weird things
Dim className As Byte(1024)
className = "WindowsForms10.EDIT.app.0.3cb5890_r6_ad1" 'Yes, this is valid, and assigns the first part of the bytearray to a string
FindWindowExW(hwndParent, 0&, VarPtr(className(0)))
The corresponding declaration of FindWindowExW:
Declare PtrSafe Function FindWindowExW Lib "user32" (ByVal hWndParent As LongPtr, Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As LongPtr, Optional ByVal lpszWindow As String) As LongPtr
To debug problems and identify specific windows, I use the following function to iterate through all top and child windows, instead of Spy++. This one has the advantage of running in VBA, so you can set breakpoints and watches, which means you can very easily determine the class name and parent window of all open windows:
Public Sub IterateAllWindows(Optional hWnd As LongPtr, Optional EnumLevel = 0)
Dim hwndChild As LongPtr
If hWnd <> 0 Then
Debug.Print String(EnumLevel, "-");
Debug.Print hWnd & ":";
Debug.Print GetWindowName(hWnd);
Debug.Print "(" & GetWindowClass(hWnd) & ")"
hwndChild = FindWindowExW(hWnd)
Do While hwndChild <> 0
IterateAllWindows hwndChild, EnumLevel:=EnumLevel + 1
hwndChild = FindWindowExW(hWnd, hwndChild)
Loop
Else
Dim hWndTopLevel As LongPtr
hWndTopLevel = GetTopWindow
Do While hWndTopLevel <> 0
Debug.Print String(EnumLevel, "-");
Debug.Print hWndTopLevel & ":";
Debug.Print GetWindowName(hWndTopLevel);
Debug.Print "(" & GetWindowClass(hWndTopLevel) & ")"
hwndChild = FindWindowExW(hWndTopLevel)
Do While hwndChild <> 0
IterateAllWindows hwndChild, EnumLevel:=EnumLevel + 1
hwndChild = FindWindowExW(hWndTopLevel, hwndChild)
Loop
hWndTopLevel = GetWindow(hWndTopLevel, 2)
Loop
End If
End Sub
This uses the following 2 helper functions:
Public Function GetWindowName(hWnd As LongPtr) As String
Dim buf(512) As Byte
GetWindowText hWnd, varPtr(buf(0)), 255
GetWindowName = Replace(CStr(buf), Chr(0), "")
End Function
Public Function GetWindowClass(hWnd As LongPtr) As String
Dim buf(512) As Byte
GetClassName hWnd, varPtr(buf(0)), 255
GetWindowClass = Replace(CStr(buf), Chr(0), "")
End Function
Corresponding WinAPI declarations for that sub:
Declare PtrSafe Function GetTopWindow Lib "user32" (Optional ByVal hWnd As LongPtr) As LongPtr
Declare PtrSafe Function GetWindow Lib "user32" (ByVal hWnd As LongPtr, ByVal wCmd As Integer) As LongPtr
Declare PtrSafe Function GetWindowText Lib "user32" Alias "GetWindowTextW" (ByVal hWnd As LongPtr, ByVal lpString As Any, ByVal nMaxCount As Long) As Long
Declare PtrSafe Function FindWindowExW Lib "user32" (ByVal hWndParent As LongPtr, Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As LongPtr, Optional ByVal lpszWindow As LongPtr) As LongPtr
Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameW" (ByVal hWnd As LongPtr, ByVal lpClassName As LongPtr, ByVal nMaxCount As Long) As Long
Running this function with a watch on that class name should help you identify if it's top-level or a child window, and if it's a child window, which class it belongs to. You can also modify it to return the hWnd independent of nesting (by using an If getWindowClass = "WindowsForms10.EDIT.app.0.3cb5890_r6_ad1" Then or by checking the title).
I think you should use Spy to conduct the same investigations on Windows 8 as you (presumably) did on Windows 10. Something must be different there, else your code would work.
Sidenote (because it bit me in the past): make sure you run the version of Spy whose 'bitness' (32 bit / 64 bit) matches the application you're interested in, otherwise message logging doesn't work.
Also, sorry for my previous post, it was a load of cr#p.
Edit Ah ha! Hans comments above that the class name is dynamically generated, so that's your problem. So now we know.

Calling On-Screen Keyboard from Excel VBA

I'm trying to pull up the on-screen keyboard.
Here are my attempts so far:
' Only needed for Test3
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" ( _
ByVal hWnd As Long, ByVal lpOperation As String, ByVal lpFile As String, _
ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As Long
Sub Test1()
' Run-time error'53':
' File Not found
Dim RetVal As Variant
RetVal = Shell("C:\WINDOWS\system32\osk.exe", 1)
End Sub
Sub Test2()
' Run-time error '432':
' File name or class name not found during Automation operation
ActiveWorkbook.FollowHyperlink Address:="C:\Windows\System32\osk.exe"
End Sub
Sub Test3()
' No error. Nothing happens at all
ShellExecute 0, vbNullString, "osk.exe", vbNullString, "C:\", 1
End Sub
Test2 from this forum.
Test3 from this forum.
I checked the path to osk.exe is correct.
I have a Surface laptop/tablet, so it has a touch screen and a "touch" keyboard (different from the osk). Is that what's causing the issue or possibly it's a Windows 10 thing?
On a 64-Bit OS try this
Option Explicit
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" (ByVal hwnd As Long, ByVal lpOperation As String, ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As Long
Private Declare Function Wow64EnableWow64FsRedirection Lib "kernel32.dll" (ByVal Enable As Boolean) As Boolean
Private Sub RunOsk_on64Bit()
Const SW_SHOWNORMAL = 1
On Error Resume Next
Wow64EnableWow64FsRedirection False
ShellExecute 0, "open", "osk.exe", "", "C:\windows\system32\osk.exe", SW_SHOWNORMAL
Wow64EnableWow64FsRedirection True
End Sub
Found here, this might the explanation, quote from the link
This is an issue with 64-bit OS, it affects any 64-bit version of Windows.
Basically you are calling osk.exe, but your program you are calling it
from is a 32-bit app. Windows won't allow you to call a 64-bit OSK.exe
from your program. The comments appear to miss your point here, anyone
can start osk.exe from Run, but call it from within a 32-bit
application won't work in 64-bit Windows.
I am developing software that uses the on-screen keyboard, the only
work around is Wow64DisableWow64FsRedirection.
Update: A "nicer" version might look like that
Option Explicit
Type SHELLEXECUTEINFO
cbSize As Long
fMask As Long
hwnd As Long
lpVerb As String
lpFile As String
lpParameters As String
lpDirectory As String
nShow As Long
hInstApp As Long
lpIDList As Long
lpClass As String
hkeyClass As Long
dwHotKey As Long
hIcon As Long
hProcess As Long
End Type
Public Declare Function ShellExecuteEx Lib "shell32.dll" _
(lpExecInfo As SHELLEXECUTEINFO) As Long
Declare Function Wow64DisableWow64FsRedirection Lib "kernel32.dll" (ByRef ptr As Long) As Boolean
Declare Function Wow64RevertWow64FsRedirection Lib "kernel32.dll" (ByRef ptr As Long) As Boolean
Public Function KeyboardOpen()
Dim shInfo As SHELLEXECUTEINFO
Dim lngPtr As Long
With shInfo
.cbSize = Len(shInfo)
.lpFile = "C:\Windows\Sysnative\cmd.exe" 'best to use Known folders here
.lpParameters = "/c start osk.exe"
.lpDirectory = "C:\windows\system32" 'best to use Known folders here
.lpVerb = "open"
.nShow = 0
End With
Call Wow64DisableWow64FsRedirection(lngPtr)
Call ShellExecuteEx(shInfo)
Call Wow64RevertWow64FsRedirection(lngPtr)
End Function
Based on the information in MSDN it might be more reliable to use Wow64DisableWow64FsRedirection and Wow64RevertWow64FsRedirection functions instead.

VBa Compile Error:eXPECTED IDENTIFIER for Private Type ****EMID

I have decleared the following global variables. These will be used by a module.
However on the line "Private Type ****EMID" I am getting a compile error: Expected identifier.
I have never used these declarations before. Can you please suggest what needs to be done?
'- to get Explorer folder
Declare Function SHGetSpecialFolderLocation Lib "shell32.dll" _
(ByVal hwndOwner As Long, ByVal nFolder As Long, pidl As ITEMIDLIST) As Long
Const CSIDL_WINDOWS = &H24
'---------------------------------------------------------------------------------------------------------
Declare Function SHGetPathFromIDList Lib "shell32.dll" Alias "SHGetPathFromIDListA" _
(ByVal pidl As Long, ByVal pszPath As String) As Long
'---------------------------
Private Type ****EMID
cb As Long
abID As Byte
End Type
'---------------------------
Private Type ITEMIDLIST
mkid As ****EMID
End Type
'---------------------------------------------------------------------------------------------------------
You need to remove the asterisks.
EMID and ITEMIDLIST are Custom Data Types not global variables.
Here is what your code is doing:
A variable pidl As type ITEMIDLIST is being passed ByRef as a parameter to the Win32 function SHGetSpecialFolderLocation which is found in the dynamic link library (DLL): shell32.dll. The function will retrieve and/or set values from the pidl variable. In this case it is setting the values of it.
'- to get Explorer folder
Declare Function SHGetSpecialFolderLocation Lib "shell32.dll" _
(ByVal hwndOwner As Long, ByVal nFolder As Long, pidl As ITEMIDLIST) As Long
Const CSIDL_WINDOWS = &H24
'---------------------------------------------------------------------------------------------------------
Declare Function SHGetPathFromIDList Lib "shell32.dll" Alias "SHGetPathFromIDListA" _
(ByVal pidl As Long, ByVal pszPath As String) As Long
'---------------------------
Private Type EMID
cb As Long
abID As Byte
End Type
'---------------------------
Private Type ITEMIDLIST
mkid As EMID
End Type
'---------------
I would use GetTempName Method of the a FileSystemObject instead of the API calls.
Function getTempFileName() As String
Const TemporaryFolder = 2
Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
getTempFileName = fso.GetSpecialFolder(TemporaryFolder).Path & "\" & fso.GetTempName
Set fso = Nothing
End Function

How to get the string from an address specified by a long in VB

In vba, There is an address held by a long type which points to a null-terminated string, but I can't find a way to get the string from this address:
long str_address = ...
string str = ?
Would you please shed some light on this?
I use CopyMemory this way:
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Private Declare Function lstrlenA Lib "kernel32" (ByVal lpString As Long) As Long
Private Function pvToString(ByVal lPtr As Long) As String
If lPtr <> 0 Then
pvToString = String(lstrlenA(lPtr), 0)
Call CopyMemory(ByVal pvToString, ByVal lPtr, Len(pvToString))
End If
End Function
If for some reason you ended up with a pointer to an ANSII zero-terminated string in VBA, you need to copy the contents to a string:
Private Declare Function SysAllocStringByteLen Lib "oleaut32.dll" (ByVal m_pBase As Long, ByVal l As Long) As String
Private Declare Function lstrlen Lib "kernel32.dll" Alias "lstrlenA" (ByVal lpString As Long) As Long
...
dim s as string
s = SysAllocStringByteLen(str_address, lstrlen(str_address))