How to wait for a 'save as' dialog box to completely finish loading before posting to it with SendMessage

velcrow
velcrow used Ask the Experts™
on
Upon getting a handle to the 'save as' dialog box using FindWindow("#32770", "Save As") , messages are sent to it to fill the file name text box and click save using SendMessage WM_SETTEXT and SendMessage BM_CLICK.  However, the dialog that actually appears on the screen it does not have the file name that was sent to it, and upon SendMessage BM_CLICK, apparently before the dialog has finished loading, it crashes and shuts down the browser that opened it.  When I step through the code (in effect manually waiting for the dialog to appear on the screen) it works fine. In my code, how can I wait for the dialog to completely load before using SendMessage on it?
Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®
Top Expert 2010

Commented:
Hey bud,
Here is some example that might help.
Create new class module named: SaveAsDlg.cls
Create a timer on form set interval between 1-3 seconds or (1000,3000) the rate at which to check for the dialog.
The event will fire once when the dialog is found on the screen.

'// 
'// SaveAsDlg.cls
'//



Option Explicit

Enum DialogButton
[Save] = &H1 ' Save
[Cancel] = &H2 ' Cancel
End Enum

Private Const DlgTitle As String = "Save As"

Private Const WM_GETTEXT = &HD&
Private Const WM_SETTEXT = &HC&
Private Const BM_CLICK = &HF5&

Private Declare Function FindWindowW Lib "user32" ( _
  ByVal lpClassName As Long, _
  ByVal lpWindowName As Long) As Long
  
Private Declare Function SendDlgItemMessageW Lib "user32" ( _
  ByVal hDlg As Long, _
  ByVal nIDDlgItem As Long, _
  ByVal Msg As Long, _
  ByVal wParam As Long, _
  ByVal lParam As Long) As Long

Private Declare Sub Sleep Lib "kernel32" ( _
  ByVal dwmilliseconds As Long)

Public Event OnSaveAsDialog()

Dim hDlg As Long
Dim hDlgPrev As Long
 
Public Sub QuerySaveAsDialog()
  hDlg = FindWindowW(0, StrPtr(DlgTitle))
  If hDlg <> 0 Then
    If hDlg <> hDlgPrev Then
      hDlgPrev = hDlg
      RaiseEvent OnSaveAsDialog
    End If
  End If
End Sub

Public Sub Filename(ByVal szData As String)
  Call SendDlgItemMessageW(hDlg, &H47C, WM_SETTEXT, 0, StrPtr(szData))
End Sub

Public Sub Click(ByVal BtnId As DialogButton)
  Sleep 1000
  Call SendDlgItemMessageW(hDlg, BtnId, BM_CLICK, 0, 0)
End Sub








'//
'// Form/Userform
'//


Option Explicit

Dim WithEvents Dialog As SaveAsDlg

Private Sub Dialog_OnSaveAsDialog()
  
  Dialog.Filename "testing"
  Dialog.Click Save
  
End Sub

Private Sub Form_Load()
  
  Set Dialog = New SaveAsDlg
  
End Sub

Private Sub Timer1_Timer()
  
  Dialog.QuerySaveAsDialog
  
End Sub

Open in new window

Author

Commented:
egl1044,

I'm trying this now, but I may have some questions to understand the concept…  Is there not an easier way?  One would think that the FindWindow function would not return a handle until the window has actually finished loading or that there would be an easy way (such as another user32.dll function) to determine when it had finished loading.

Author

Commented:
egl1044,

In your Public Sub QuerySaveAsDialog() your using FindWindowW.  What is the difference between FindWindowW and FindWindowA?  Also, if you’re using FindWindow to identify when the window is loaded, won't the OnSaveAsDialog be raised before the window is actually ready to be posted to using SendMessage just as it is now?  Currently I'm using FindWindow to demine when the window exists, but upon getting the handle apparently that is still too soon to begin posting to the window.  What am I missing?
Success in ‘20 With a Profitable Pricing Strategy

Do you wonder if your IT business is truly profitable or if you should raise your prices? Learn how to calculate your overhead burden using our free interactive tool and use it to determine the right price for your IT services. Start calculating Now!

Top Expert 2010

Commented:
I don't understand what your concerns are with the "loading" of the control. You just need to make sure the window is visible the controls are rendered or created before the dialog is visible on the screen. The approach uses a class for a couple of reasons one is that if you need to work with another dialog one can create another class for that dialog for example if I use Save As from internet explorer and choose (Save) another dialog appears that ask me if I would like to Open,Open Folder,Close (Download Complete Dialog) in this case. If you needed to automate this dialog the easiest approach is to use the same concept.

Author

Commented:
egl1044,

Regarding: 'You just need to make sure the window is visible the controls are rendered or created before the dialog is visible on the screen.'  Yes, I would think that would be the case, but when I send a file name to the 'save as' immediately after receiving a handle using FindWindow, it fails.  It doesn’t indicate that it failed (ie sendmessage does not return 0), but the filename does not appear in the box when the dialog actually becomes visible.  Also when I click save immediately after getting the handle and posting the filename, it crashes and closes the dialog and the browser that opened it.
Top Expert 2010

Commented:
I think it's a coding issue you have and not directly related to the dialog itself. Have you tried the example supplied in the first snippet? It should work as intended. If my code works on your machine then we know that it's not a dialog issue but a coding issue. :)

Author

Commented:

Function savetheFile(Filename)
    Dim hwndDialog As Long  ' handle to the dialog box
    Dim hwndButton As Long  ' handle to the save button
    Dim retval As Variant      ' return value
    
    'wait until dialog box titled "File Download" is open.
    Do
        hwndDialog = FindWindow("#32770", "File Download")
    Loop While hwndDialog = 0
    
    'Now get a handle to the "Save" button in the dialog...
    'the dialog box will not have completely loaded at the moment it is found so loop until the save button is found
    Do
        hwndButton = FindWindowEx(hwndDialog, 0, "Button", "&Save")
    Loop While hwndButton = 0
    
    'the save button is not accessable the moment it's handle becomes avialable
    'keep clicking save until the 'Save As' dialog appears
    Do
        retval = SetActiveWindow(hwndDialog)
        retval = SendMessage(hwndButton, BM_CLICK, ByVal CLng(0), ByVal CLng(0))
        SaveAsDialog = FindWindow("#32770", "Save As")
    Loop While SaveAsDialog = 0
    
    comboBox32win = FindWindowEx(SaveAsDialog, 0, "ComboBoxEx32", vbNullString)
    ComboBoxwin = FindWindowEx(comboBox32win, 0, "ComboBox", vbNullString)
    EditBox = FindWindowEx(ComboBoxwin, 0, "Edit", vbNullString)
    SaveButton = FindWindowEx(SaveAsDialog, 0, "Button", "&Save")
    
    retval = SetActiveWindow(hwndDialog)
    retval = SendMessage(EditBox, WM_SETTEXT, vbNullString, Filename)
''''if i break my code here the dialog is showing without the file name i posted to it even though SendMessage(EditBox, WM_SETTEXT, vbNullString, Filename) did not fail.  In fact if I get the text using the methods you helped me with yesterday it does return the filename I posted even though it is not showing in the dialog
    retval = SendMessage(SaveButton, BM_CLICK, ByVal CLng(0), ByVal CLng(0))
    
    'wait until the 'Save As' dialog has disappeared
    Do While FindWindow("#32770", "Save As") <> 0
    Loop
    
    'loop while the % of dowload dialog is still visible, which is the same class of dialog as the 'Save As' dialog
    Do While FindWindow("#32770", "") <> 0
    Loop

End Function

Open in new window

Top Expert 2010

Commented:
velcrow,
Your code has alot issues it's not reasonable to have any type of loops all you have to do is ensure the main dialog window is present this is why a timer is a better idea. The example I provided checks for the dialog and then fires the event this lets you know the dialog is ready to send messages.
You don't need to use FindWindowEx() to find the button use the approach I have showed you because it's easier to use. The dialogs for windows have static ID's associated with the control so all you need is the main handle to the dialog. Using SendDlgItemMessage() accepts the dialog window handle followed by an ID this value is what is used to send the messag directly to that control item removing extra walking you have to do find the handle of the control itself.

Author

Commented:
If it comes down to using a timer then I can insert a 1 second delay in my code right before sending the file name and clicking save and everything works just fine the way I have it.  However, I was trying to avoid using timers or delays because the delay necessary can be different on every machine and also because the code may then run slower than it might have to.

Despite the different methods we are using, essentially we are doing the same thing, which is waiting until FindWindow returns a handle to the window being sought before posting to that window, right?

But if I post the window immediately upon getting the handle to the window, it does not work, but if I wait until the window is actually visible then it does work.  I will try using SendDlgItemMessage as you have done and see if that makes a difference.
Top Expert 2010

Commented:
If you want it to work fast you can use a Timer with interval 500 milliseconds and change Sleep 1000.. to Sleep 100
You just need to put your thread to sleep before sending the next message BM_CLICK.
Look at the code snippet in first post you will see I put the thread to sleep before clicking the button this gives the control time process the message otherwise you will have 2 messages being processed at the same time.
Make sense?

Author

Commented:
Sorry I can't try your solution exactly has you have provided it.  I'm building a MS Access application using VBA and I don’t know how to duplicate exactly what you have done using Access and VBA (at least not without spending significant time).  
However, I have a hunch that I would work because of the 1 to 3 second delay for the same reason my code works if I put a 1 second delay before posting the dialog.

Author

Commented:
I appreciate your patience.  This comment thread may be getting a little wordy, so let me just simplify a little.  If you use SendMessage to post the filename immediately after receiving the handle to the 'Save as' dialog box, with no timers involved at all, does it work?

In other words if you loop until FindWindowW(0, StrPtr(DlgTitle)) gets a handle and then the very next line of code is posting the filename, after which you break to see the results, does the dialog showing contain what you posted?
Top Expert 2010

Commented:
Yes it does.
Option Explicit

Private Const DlgTitle As String = "Save As"

Private Const WM_SETTEXT = &HC&
Private Const BM_CLICK = &HF5&

Private Declare Function FindWindowW Lib "user32" (ByVal lpClassName As Long, ByVal lpWindowName As Long) As Long
Private Declare Function SendDlgItemMessageW Lib "user32" (ByVal hDlg As Long, ByVal nIDDlgItem As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Sub Sleep Lib "kernel32" (ByVal dwmilliseconds As Long)

Dim hDlg As Long

Public Sub QuerySaveAsDialog(ByVal szData As String)

Do
  hDlg = FindWindowW(0, StrPtr(DlgTitle))
  Sleep 10
  DoEvents
Loop Until hDlg <> 0
Call SendDlgItemMessageW(hDlg, &H47C, WM_SETTEXT, 0, StrPtr(szData))

End Sub

Open in new window

Author

Commented:
I pasted your last code snippet in my project exactly and it will not work.  However, if I break at the Call SendDlgItemMessageW(hDlg, &H47C, WM_SETTEXT, 0, StrPtr(szData)) and give the dialog time to ‘finish loading’ then execute Call SendDlgItemMessageW(hDlg, &H47C, WM_SETTEXT, 0, StrPtr(szData)) then it works.  So, on my end I’m still back to the same problem.  Any ideas?
Top Expert 2010
Commented:
We can try take it one step further and see what happens on your end if you wait until the window is visible.
 

Option Explicit

Private Const DlgTitle As String = "Save As"
 
Private Const WM_SETTEXT = &HC&
Private Const BM_CLICK = &HF5&
 
Private Declare Function FindWindowW Lib "user32" (ByVal lpClassName As Long, ByVal lpWindowName As Long) As Long
Private Declare Function SendDlgItemMessageW Lib "user32" (ByVal hDlg As Long, ByVal nIDDlgItem As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Function IsWindowVisible Lib "user32" (ByVal hWnd As Long) As Long
Private Declare Sub Sleep Lib "kernel32" (ByVal dwmilliseconds As Long)
 
Dim hDlg As Long
 
Public Sub QuerySaveAsDialog(ByVal szData As String)
 
Do
  hDlg = FindWindowW(0, StrPtr(DlgTitle))
  Sleep 100
  DoEvents
Loop Until IsWindowVisible(hDlg)

Call SendDlgItemMessageW(hDlg, &H47C, WM_SETTEXT, 0, StrPtr(szData))
 
End Sub

Open in new window

Author

Commented:
From the sounds of it, I think IsWindowVisible might work; I haven’t had a chance to try it yet.

What exactly does IsWindowVisible check for?  Does the window have to be active / on top and actually viewable to the user or if it is in a visible state, but hidden by other windows is it still considered visible?
Top Expert 2010

Commented:
It checks to see if the WS_VISIBLE bit flag is set so it might give you that little extra time needed before the loop exits however it does work for me either way meaning when findwindow returns <> 0.
velcrow,
I think that if this doesn't work your only option will be to implement some type of pause even if for a short duration of a couple hundred milliseconds.

Author

Commented:
eql1044:

I just wanted to let you know that I do still intend on testing your last suggestion and awarding points if it works.  The past few weeks have been so extremely busy that I have not had a chance to get back on this project since your last post.

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial