Hi all, Hi Jaafar
I am working on a tool manipulating the VBProject of other Excel files, which can have their VBProject locked with 2 different passwords. But I have no way to know which one it will be in advanced, so I must attempt them one after the other. I managed to write some code that runs in a main Excel.Application instance so that I can work on the VBE windows of the other Excel.Application instance (containing the locked project) to input a password. I sucessfully unlocked some projects this way (I found this solution more elegant and 100% working compared to the SendKeys method that gives random results and would not work in my particular case)
One can verify I do end up with 2 different Excel processes, because I have 2 VBE Window in my Windows Task Bar:
But when the password is incorrect, a "Project Locked / Invalid password" dialog appears, and it happens to even stop execution in my main running instance using the Win32 APIs. I think it must somehow throw an exception to my main running instance, which requires that the user clicks the "OK" button in the dialog for the execution to continue.
I would like to by-pass this interaction with the user to have something fully automated.
I came with a solution that I have not yet fully carried through: generate a worker file that I would run in a third Excel.Application instance just before confirming the password in the previous dialog, which would detect the "Project Locked / Invalid password" dialog and close it, so the execution can continue in my main running instance, allowing me to try the second password. This workaround seems very heavy and unnatural...
Another quick workaround is to use the Sendkeys method of the locked project's Application to send an {ESC} key just before confirming the password. This works to close the error dialog, giving back control to the main VBE. But the whole point of my tool is to avoid using SendKeys...
Would there be another way? One that would prevent the VBE from idling in my main running instance (making the dialog modeless, etc. I really don't know...)
Here is the code to illustrate my problem. This is not exactly simple, but as minimal as it gets. I think it is meaningless for anybody capable of understanding the problem with major principles, and able of giving me hints to workaround this issue...
NOTE: look for my 3 comments in CAPITAL LETTERS
I am working on a tool manipulating the VBProject of other Excel files, which can have their VBProject locked with 2 different passwords. But I have no way to know which one it will be in advanced, so I must attempt them one after the other. I managed to write some code that runs in a main Excel.Application instance so that I can work on the VBE windows of the other Excel.Application instance (containing the locked project) to input a password. I sucessfully unlocked some projects this way (I found this solution more elegant and 100% working compared to the SendKeys method that gives random results and would not work in my particular case)
One can verify I do end up with 2 different Excel processes, because I have 2 VBE Window in my Windows Task Bar:
But when the password is incorrect, a "Project Locked / Invalid password" dialog appears, and it happens to even stop execution in my main running instance using the Win32 APIs. I think it must somehow throw an exception to my main running instance, which requires that the user clicks the "OK" button in the dialog for the execution to continue.
I would like to by-pass this interaction with the user to have something fully automated.
I came with a solution that I have not yet fully carried through: generate a worker file that I would run in a third Excel.Application instance just before confirming the password in the previous dialog, which would detect the "Project Locked / Invalid password" dialog and close it, so the execution can continue in my main running instance, allowing me to try the second password. This workaround seems very heavy and unnatural...
Another quick workaround is to use the Sendkeys method of the locked project's Application to send an {ESC} key just before confirming the password. This works to close the error dialog, giving back control to the main VBE. But the whole point of my tool is to avoid using SendKeys...
Would there be another way? One that would prevent the VBE from idling in my main running instance (making the dialog modeless, etc. I really don't know...)
Here is the code to illustrate my problem. This is not exactly simple, but as minimal as it gets. I think it is meaningless for anybody capable of understanding the problem with major principles, and able of giving me hints to workaround this issue...
NOTE: look for my 3 comments in CAPITAL LETTERS
VBA Code:
#If VBA7 Then
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
Private Declare PtrSafe Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As LongPtr, ByVal hwnd2 As LongPtr, ByVal lpsz1 As String, ByVal lpsz2 As String) As LongPtr
Private Declare PtrSafe Function GetWindowText Lib "user32" Alias "GetWindowTextA" (ByVal hWnd As LongPtr, ByVal lpString As String, ByVal cch As LongPtr) As Long
Private Declare PtrSafe Function GetWindowTextLength Lib "user32" Alias "GetWindowTextLengthA" (ByVal hWnd As LongPtr) As Long
Private Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hWnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, lParam As Any) As LongPtr
Dim Ret As LongPtr, ChildRet As LongPtr, OpenRet As LongPtr
#Else
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As Long, ByVal hwnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long
Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (ByVal hWnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
Private Declare Function GetWindowTextLength Lib "user32" Alias "GetWindowTextLengthA" (ByVal hWnd As Long) As Long
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long
Dim Ret As Long, ChildRet As Long, OpenRet As Long
#End If
Dim strBuff As String, ButCap As String
Const WM_SETTEXT = &HC
Const BM_CLICK = &HF5
Const WORKAROUND1_SENDKEYS_ESC = True
Sub Main()
Call UnlockVBA("password1")
End Sub
Sub UnlockVBA(ByVal pwd As String)
Set xlApp = CreateObject("Excel.Application")
xlApp.Visible = -1
Set Wb = xlApp.Workbooks.Open(Filename:=ThisWorkbook.Path & "\" & "DUMMY_LOCKED_VBPROJECT.xlsm", UpdateLinks:=False)
Set vbProj = Wb.VBProject
Set xlApp.VBE.ActiveVBProject = vbProj
DoEvents
xlApp.VBE.CommandBars(1).FindControl(ID:=2578, recursive:=True).Execute
DoEvents
Ret = FindWindow(vbNullString, vbProj.Name & " Password")
If Ret <> 0 Then
'~~> Get the handle of the TextBox Window where we need to type the password
ChildRet = FindWindowEx(Ret, ByVal 0&, "Edit", vbNullString)
If ChildRet <> 0 Then
'~~> Type the password in the TextBox Window
SendMess pwd, ChildRet
DoEvents
'~~> Search for the "OK" Button among siblings of the TextBox Child Window
'~~> Get the handle of the first Child Window of class "Button"
ChildRet = FindWindowEx(Ret, ByVal 0&, "Button", vbNullString)
If ChildRet <> 0 Then
'~~> Get the caption of the child window
strBuff = String(GetWindowTextLength(ChildRet) + 1, Chr$(0))
GetWindowText ChildRet, strBuff, Len(strBuff)
ButCap = strBuff
'~~> Loop through all child windows
Do While ChildRet <> 0
'~~> Check if the caption has the word "OK"
If InStr(1, ButCap, "OK") Then
'~~> If this is the button we are looking for then exit
OKRet = ChildRet
Exit Do
End If
'~~> Get the handle of the next child window
ChildRet = FindWindowEx(Ret, ChildRet, "Button", vbNullString)
'~~> Get the caption of the child window
strBuff = String(GetWindowTextLength(ChildRet) + 1, Chr$(0))
GetWindowText ChildRet, strBuff, Len(strBuff)
ButCap = strBuff
Loop
'~~> Check if we found it or not
If OKRet <> 0 Then
'~~> Click the OK Button
If Not WORKAROUND1_SENDKEYS_ESC Then
MsgBox "before user click"
ElseIf WORKAROUND1_SENDKEYS_ESC Then
xlApp.SendKeys "{ESC}"
End If
SendMessage OKRet, BM_CLICK, 0, vbNullString
' >>> IF INCORRECT PASSWORD, MAIN VBE IDLES HERE UNTIL USER CANCELS THE ERROR IN THE OTHER VBE <<<
MsgBox "after user click"
' >>> AT THIS POINT, THE ERROR DIALOG HAS BEEN DISMISSED BY THE USER, I WAS NOT EXPECTING THIS INTERACTION, NEITHER THAT THE ERROR DIALOG WOULD IDLE THE MAIN VBE, SO THE FOLLOWING CODE DOES NOT WORK PROPERLY
'~~> Check if password has worked and VBProject Properties Window is displayed
Ret = FindWindow(vbNullString, vbProj.Name & " - Project Properties")
If Ret <> 0 Then
'~~> Get the handle of the first Child Window of class "Button"
ChildRet = FindWindowEx(Ret, ByVal 0&, "Button", vbNullString)
'~~> Check if we found it or not
If ChildRet <> 0 Then
'~~> Get the caption of the child window
strBuff = String(GetWindowTextLength(ChildRet) + 1, Chr$(0))
GetWindowText ChildRet, strBuff, Len(strBuff)
ButCap = strBuff
'~~> Loop through all next child windows
Do While ChildRet <> 0
'~~> Check if the caption has the word "OK"
If InStr(1, ButCap, "OK") Then
'~~> If this is the button we are looking for then exit
OK2Ret = ChildRet
Exit Do
End If
'~~> Get the handle of the next child window
ChildRet = FindWindowEx(Ret, ChildRet, "Button", vbNullString)
'~~> Get the caption of the child window
strBuff = String(GetWindowTextLength(ChildRet) + 1, Chr$(0))
GetWindowText ChildRet, strBuff, Len(strBuff)
ButCap = strBuff
Loop
'~~> Check if we found it or not
If OK2Ret <> 0 Then
'~~> Click the OK again Button to close VBProject Properties
SendMessage OK2Ret, BM_CLICK, 0, vbNullString
DoEvents
'~~> AT THIS POINT, the VBProject should be unlocked
End If
End If
Else
'~~> "Project Locked / Invalid password" Window might be displayed
' #32769 (Desktop Window) "EXCEL.EXE"
' #32770 (Dialog) "Project Locked" (top-level window owned by the top-level dialog window "VBAProject Password", itself owned by the top-level window of class wndclass_desked_gsk "VBE")
' Button "OK"
' Static ""
' Static "Invalid password"
'Project Locked
'/!\ Invalid password
Ret = FindWindow(vbNullString, "Project Locked")
If Ret <> 0 Then
ChildRet = FindWindowEx(Ret, ByVal 0&, "Button", vbNullString)
If ChildRet <> 0 Then
strBuff = String(GetWindowTextLength(ChildRet) + 1, Chr$(0))
GetWindowText ChildRet, strBuff, Len(strBuff)
ButCap = strBuff
Do While ChildRet <> 0
If InStr(1, ButCap, "OK") Then
OK3Ret = ChildRet
Exit Do
End If
ChildRet = FindWindowEx(Ret, ChildRet, "Button", vbNullString)
strBuff = String(GetWindowTextLength(ChildRet) + 1, Chr$(0))
GetWindowText ChildRet, strBuff, Len(strBuff)
ButCap = strBuff
Loop
If OK3Ret <> 0 Then
SendMessage OK3Ret, BM_CLICK, 0, vbNullString
DoEvents
'~~> Close VBAProject Password Window
' #32769 (Desktop Window) "EXCEL.EXE"
' #32770 (Dialog) "vbProj.Name Password"
' Button "OK"
' Button "Cancel"
' Static "&Password"
' Edit ""
Ret = FindWindow(vbNullString, vbProj.Name & " Password")
If Ret <> 0 Then
ChildRet = FindWindowEx(Ret, ByVal 0&, "Button", vbNullString)
If ChildRet <> 0 Then
strBuff = String(GetWindowTextLength(ChildRet) + 1, Chr$(0))
GetWindowText ChildRet, strBuff, Len(strBuff)
ButCap = strBuff
Do While ChildRet <> 0
If InStr(1, ButCap, "Cancel") Then
CancelRet = ChildRet
Exit Do
End If
ChildRet = FindWindowEx(Ret, ChildRet, "Button", vbNullString)
strBuff = String(GetWindowTextLength(ChildRet) + 1, Chr$(0))
GetWindowText ChildRet, strBuff, Len(strBuff)
ButCap = strBuff
Loop
If CancelRet <> 0 Then
SendMessage CancelRet, BM_CLICK, 0, vbNullString
DoEvents
'~~> Check if VBProject Properties is NOT displayed
Ret = FindWindow(vbNullString, vbProj.Name & " - Project Properties")
If Ret = 0 Then
'~~> ok! ready to try to call the sub with another password
Else
'~~> hum...
End If
End If
End If
End If
End If
End If
End If ' >>> SUB GOES OUT HERE, THE TEXT BOX DIALOG IN THE OTHER VBE IS STILL VISIBLE
End If
End If
End If
End If
End If
End Sub
#If VBA7 Then
Sub SendMess(Message As String, hWnd As LongPtr)
Call SendMessage(hWnd, WM_SETTEXT, False, ByVal Message)
End Sub
#Else
Sub SendMess(Message As String, hWnd As Long)
Call SendMessage(hWnd, WM_SETTEXT, False, ByVal Message)
End Sub
#End If