I have a process that requires an active VPN connection, but the connection is automatically cut every 8 hours. I need to be able to control that the connection is active and the time left up to the 8 hour limit.
In the properties of the windows connections the time appears (attached capture with the data that I need), but I need to know how to read this data.
Try the next approach, please:
Edited, because of the last request:
Please add two new declarations
Copy the next API functions on top of a standard module:
Option Explicit
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 SendMessage Lib "User32" Alias "SendMessageA" (ByVal hwnd As LongPtr, _
ByVal wMsg As Long, ByVal wParam As LongPtr, lParam As Any) As Long
Private Declare PtrSafe Function GetWindowTextLength Lib "User32" Alias "GetWindowTextLengthA" (ByVal hwnd As LongPtr) As Long
Private Declare PtrSafe Function GetWindowText Lib "User32" Alias "GetWindowTextA" (ByVal hwnd As LongPtr, _
ByVal lpString As String, ByVal cch As Long) As Long
Private Declare PtrSafe Function GetWindow Lib "User32" (ByVal hwnd As LongPtr, ByVal wCmd As Long) As Long
And the next Constant:
Private Const GW_HWNDNEXT = 2
'Added after editing:__________________
Private Const WM_LBUTTON_DOWN = &H201
Private Const BM_CLICK = &HF5
'______________________________________
In the same standard module, copy the next Sub. Please, take care to change Duration: from the code, with the Spanish correct variant ('Duración' [with the necessary accent]):
Sub DurationAPI()
Dim hwndEth As LongPtr, hwndGen As LongPtr, hwndDurlbl As LongPtr, hwndDur As LongPtr
Dim sStr As String, strWindowTitle As String, durationLbl As String, durT As Date, limitD As Date
'added after editing:_____________________________
OpenWiFiConnectionWindow 'open connection window
AppActivate Application.ActiveWindow.Caption
'_________________________________________________
limitD = CDate("08:00:00")
strWindowTitle = "Estado de Wi-Fi"
durationLbl = "Duration:" 'Please change here with your exact label title (in Spanish...)
'I cannot write duracion: with the necessary accent...
hwndEth = FindWindow(vbNullString, strWindowTitle): Debug.Print Hex(hwndEth)
hwndGen = FindWindowEx(hwndEth, 0&, vbNullString, "General"): Debug.Print Hex(hwndGen)
hwndDurlbl = FindWindowEx(hwndGen, 0&, vbNullString, durationLbl): Debug.Print Hex(hwndDurlbl)
hwndDur = GetWindow(hwndDurlbl, GW_HWNDNEXT): Debug.Print Hex(hwndDur)
sStr = String(GetWindowTextLength(hwndDur) + 1, Chr$(0))
GetWindowText hwndDur, sStr, Len(sStr)
durT = CDate(sStr)
MsgBox Format(limitD - durT, "hh:mm:ss") & " left until connection will be interrupted!", _
vbInformation, "Time to connection interruption"
'Added after editing: ____________________________________________________
Dim hwndClose As LongPtr
'closing the connection window:
hwndClose = FindWindowEx(hwndEth, 0&, vbNullString, "&Close"): Debug.Print Hex(hwndClose)
SendMessage hwndClose, WM_LBUTTON_DOWN, 0&, 0&
SendMessage hwndClose, BM_CLICK, 0, ByVal 0&
'_________________________________________________________________________
End Sub
bis Copy the Sub able to show the necessary connection window:
Private Sub OpenWiFiConnectionWindow()
Dim objApp As Object: Set objApp = CreateObject("Shell.Application")
Dim objFolder As Object: Set objFolder = objApp.Namespace(&H31&).self.GetFolder
Dim interface As Variant, interfaceTarget As Object, InterfaceName As String
InterfaceName = "Wi-Fi" 'Please, check here what is show your "Network Connections" folder. It maybe can be slightly different...
'I tested the code on my Ethernet connection, which not was simple "Ethernet". It was "Ethernet 2"...
For Each interface In objFolder.Items
If LCase(interface.Name) = LCase(InterfaceName) Then
Set interfaceTarget = interface: Exit For
End If
Next
Dim Verb As Variant
For Each Verb In interfaceTarget.Verbs
If Verb.Name = "Stat&us" Then
Verb.DoIt
Application.Wait Now + TimeValue("0:00:01")
Exit For
End If
Next
End Sub
Please, try this Sub first, in order to be sure that it shows the necessary connection window. If it doesn't, please look in the "Network Connections" folder and change InterfaceName with an appropriate one.
Run the above DurationAPI() Sub.
All the necessary windows handlers are returned in Immediate window. If one of them is 0 (zero), there must be checked to understand what is happening... I used Spy++ to find the windows titles/classes...
For a window with English titles, it returns correctly and almost instant the necessary connection duration time.
I am writing a VBA code that will save a PDF file from SAP. I've reached the place where SAP asks me where I would like to save my pdf file (opens windows explorer "save as" window).At this point, VBA code stops and I need to manually input the name of the file I want to save. Then, vba continues to run...
I need help to find a way to automate this step.
A possible solution that I am thinking of(but don't know how to actually do it) is to tell vba to run a VB script that ends at save as window. Then I would send a "application.sendkeys(" ") to input the save as path.
Please advise if this is feasible. If it is, next step is I will have to dynamically modify specific lines of the vb script file (I need to loop through a list and change some values every time)
Thank you
So, it has been quite a challenge....Here is my solution to Handle a "Save as" window. It can be way simpler if you would only want to click on "Save" Button. My solution is more complicated because I specify where the file needs to be saved. To do that you need to find the right combobox, which takes a lot of iteration.
WinAPI necessary declarations:
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 GetClassName Lib "user32" Alias "GetClassNameA" _
(ByVal hWnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare Function SendMessage Lib "user32" Alias _
"SendMessageW" (ByVal hWnd As Long, ByVal wMsg As Long, _
ByVal wParam As Long, lParam As Any) As Long
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Public Declare Function SendNotifyMessage Lib "user32" Alias "SendMessageA" ( _
ByVal hWnd As Long, _
ByVal Msg As Integer, _
ByVal ByValByValwParam As Integer, _
ByVal lParam As String) As Integer
Actual VBA Code:
Sub SaveAsWindow()
Dim Winhwnd As Long
Dim prev As Long
Dim abc As Long
Dim strText As String
Dim rty As Variant
Dim Parent As Long
Dim child As Long
Winhwnd = FindWindow(vbNullString, "Save As")
For i = 1 To 20
strText = String$(100, Chr$(0))
abc = GetClassName(Winhwnd, strText, 100)
If Left$(strText, 12) = "DirectUIHWND" Then GoTo here1
Winhwnd = FindWindowEx(Winhwnd, 0&, vbNullString, vbNullString)
Next i
here1:
Parent = Winhwnd
child = FindWindowEx(Parent, 0&, vbNullString, vbNullString)
GoTo skip 'avoid this part for the 1st run
here2:
'fix child3 and child2
If child2 = 0 Then
rty = "0&"
Else
rty = 0
End If
If child3 = 555 Then
rty = "0&"
child3 = ""
End If
skip:
For i = 1 To 20
child = FindWindowEx(Parent, child, vbNullString, vbNullString)
For x = 1 To 20
If child3 = "" Then rty = 0
child2 = FindWindowEx(child, rty, vbNullString, vbNullString)
abc = GetClassName(child2, strText, 100)
If Left$(strText, 8) = "ComboBox" Then
child3 = FindWindowEx(child2, 0&, vbNullString, vbNullString)
If child3 = 0 Then
child3 = 555
GoTo here2
Else
GoTo here3
End If
End If
Next x
Next i
here3:
'this is te filepath. will be pasted into combobox. to adapt to your needs.
SendNotifyMessage child3, &HC, 0&, "C:\Users\username\abc.pdf"
'Get again the Save button
Winhwnd = FindWindow(vbNullString, "Save As")
buttn = FindWindowEx(Winhwnd, 0, "Button", "&Save")
'click on the save button
SendMessage buttn, &HF5&, 0, 0
End Sub
2nd VBA Code : For SAP, as it turns out to be simpler due to ComboboxEx32 being used instead of Combobox.
Sub test()
Dim Winhwnd As Long
Dim strText As String
Winhwnd = FindWindow(vbNullString, "Save As")
combo = FindWindowEx(Winhwnd, 0, vbNullString, vbNullString)
For i = 1 To 20
combo = FindWindowEx(Winhwnd, combo, vbNullString, vbNullString)
strText = String$(100, Chr$(0))
abc = GetClassName(combo, strText, 100)
If Left$(strText, 12) = "ComboBoxEx32" Then GoTo here
Next i
here:
SendNotifyMessage combo, &HC, 0&, "C:\Users\username\abc.pdf"
buttn = FindWindowEx(Winhwnd, 0, "Button", "&Open")
SendMessage buttn, &HF5&, 0, 0
End Sub
Bottom line, this is not the most perfect code, but I couldn't find anything else on the web. I hope this will benefit anyone with the same problem.
Goal
Have an Excel file with a "Search" button that opens a custom program. This program is used for researches. If the program is already opened when the user clicks on the button, make it popup and focus on that given program.
Current Situation
Here's the code I'm trying to use to make it work:
Search Button
Private Sub btnSearch_Click()
Dim x As Variant
Dim Path As String
If Not IsAppRunning("Word.Application") Then
Path = "C:\Tmp\MyProgram.exe"
x = Shell(Path, vbNormalFocus)
End If
End Sub
IsAppRunning()
Function IsAppRunning(ByVal sAppName) As Boolean
Dim oApp As Object
On Error Resume Next
Set oApp = GetObject(, sAppName)
If Not oApp Is Nothing Then
Set oApp = Nothing
IsAppRunning = True
End If
End Function
This code will work only when I put "Word.Application" as the executable. If I try to put "MyProgram.Application" the function will never see the program is running. How can I find that "MyProgram.exe" is currently opened?
Further more, I'd need to put the focus on it...
You can check this more directly by getting a list of open processes.
This will search based on the process name, returning true/false as appropriate.
Sub exampleIsProcessRunning()
Debug.Print IsProcessRunning("MyProgram.EXE")
Debug.Print IsProcessRunning("NOT RUNNING.EXE")
End Sub
Function IsProcessRunning(process As String)
Dim objList As Object
Set objList = GetObject("winmgmts:") _
.ExecQuery("select * from win32_process where name='" & process & "'")
IsProcessRunning = objList.Count > 0
End Function
Here's how I brought the search window to front:
Private Const SW_RESTORE = 9
Private Declare Function BringWindowToTop Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As Any, ByVal lpWindowName As Any) As Long
Private Declare Function ShowWindow Lib "user32" (ByVal hwnd As Long, ByVal nCmdShow As Long) As Long
Private Sub btnSearch_Click()
Dim x As Variant
Dim Path As String
If IsProcessRunning("MyProgram.exe") = False Then
Path = "C:\Tmp\MyProgram.exe"
x = Shell(Path, vbNormalFocus)
Else
Dim THandle As Long
THandle = FindWindow(vbEmpty, "Window / Form Text")
Dim iret As Long
iret = BringWindowToTop(THandle)
Call ShowWindow(THandle, SW_RESTORE)
End If
End Sub
Now if the window was minimized and the user clicks the search button again, the window will simply pop up.
Just want to point out that the Window Text may change when documents are open in the application instance.
For example, I was trying to bring CorelDRAW to focus and everything would work fine so long as there wasn't a document open in Corel, if there was, I would need to pass the complete name to FindWindow() including the open document.
So, instead of just:
FindWindow("CorelDRAW 2020 (64-Bit)")
It would have to be:
FindWindow("CorelDRAW 2020 (64-Bit) - C:\CompletePath\FileName.cdr")
As that is what would be returned from GetWindowText()
Obviously this is an issue as you don't know what document a user will have open in the application, so for anyone else who may be coming here, years later, who may be experiencing the same issue, here's what I did.
Option Explicit
Private Module
Private Const EXE_NAME As String = "CorelDRW.exe"
Private Const WINDOW_TEXT As String = "CorelDRAW 2020" ' This is common with all opened documents
Private Const GW_HWNDNEXT = 2
Private Const SW_RESTORE = 9
Private Declare PtrSafe Function ShowWindow Lib "user32" (ByVal hWnd As Long, ByVal nCmdShow As Long) As Long
Private Declare PtrSafe Function GetWindow Lib "user32" (ByVal hWnd As Long, ByVal wCmd As Long) As Long
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As Any, ByVal lpWindowName As Any) As Long
Private Declare PtrSafe Function GetWindowText Lib "user32" Alias "GetWindowTextA" (ByVal hWnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
Public Sub FocusIfRunning(parAppName as String, parWindowText as String)
Dim oProcs As Object
Dim lWindowHandle As Long
Dim sWindowText As String
Dim sBuffer As String
' Create WMI object and execute a WQL query statement to find if your application
' is a running process. The query will return an SWbemObjectSet.
Set oProcs = GetObject("winmgmts:").ExecQuery("SELECT * FROM win32_process WHERE " & _
"name = '" & parAppName & "'")
' The Count property of the SWbemObjectSet will be > 0 if there were
' matches to your query.
If oProcs.Count > 0 Then
' Go through all the handles checking if the start of the GetWindowText()
' result matches your WindowText pre-file name.
' GetWindowText() needs a buffer, that's what the Space(255) is.
lWindowHandle = FindWindow(vbEmpty, vbEmpty)
Do While lWindowHandle
sBuffer = Space(255)
sWindowText = Left(sBuffer, GetWindowText(lWindowHandle, sBuffer, 255))
If Mid(sWindowText, 1, Len(parWindowText)) Like parWindowText Then Exit Do
' Get the next handle. Will return 0 when there are no more.
lWindowHandle = GetWindow(lWindowHandle, GW_HWNDNEXT)
Loop
Call ShowWindow(lWindowHandle , SW_RESTORE)
End If
End Sub
Private Sub btnFocusWindow_Click()
Call FocusIfRunning(EXE_NAME, WINDOW_TEXT)
End Sub
Hopefully somebody gets use from this and doesn't have to spend the time on it I did.
Just wanted to say thank you for this solution. Only just started playing around with code and wanted to automate my job a bit. This code will paste current selection in excel sheet into an already open application with as single click. Will make my life so much easier!!
Thanks for sharing
Public Const SW_RESTORE = 9
Public Declare Function BringWindowToTop Lib "user32" (ByVal hwnd As Long) As Long
Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As Any, ByVal lpWindowName As Any) As Long
Public Declare Function ShowWindow Lib "user32" (ByVal hwnd As Long, ByVal nCmdShow As Long) As Long
Public Sub updatepart()
'
' updatepart Macro
' copies current selection
' finds and focuses on all ready running Notepad application called Test
' pastes value into Notepad document
' Keyboard Shortcut: Ctrl+u
'
Dim data As Range
Set data = Application.Selection
If data.Count <> 1 Then
MsgBox "Selection is too large"
Exit Sub
End If
Selection.Copy
If IsProcessRunning("Notepad.EXE") = False Then
MsgBox "Notepad is down"
Else
Dim THandle As Long
THandle = FindWindow(vbEmpty, "Test - Notepad")
Dim iret As Long
iret = BringWindowToTop(THandle)
Call ShowWindow(THandle, SW_RESTORE)
End If
waittime (500)
'Call SendKeys("{F7}")
Call SendKeys("^v", True) '{F12}
Call SendKeys("{ENTER}")
End Sub
Function waittime(ByVal milliseconds As Double)
Application.Wait (Now() + milliseconds / 24 / 60 / 60 / 1000)
End Function
Function IsProcessRunning(process As String)
Dim objList As Object
Set objList = GetObject("winmgmts:") _
.ExecQuery("select * from win32_process where name='" & process & "'")
If objList.Count > 0 Then
IsProcessRunning = True
Else
IsProcessRunning = False
End If
End Function
I am using the Windows API with Excel VBA to work with a particular window, using the FindWindow() function, but FindWindow() requires the full title/caption of the window to find.
Question 1
P_Win = FindWindow(vbNullString, "PlusApi_Excel Sample_17_39_12 Api Generated Orders")
in my case the window will change the name (dynamic) (some part of the window name will be fixed and some part will be dynamic)
Ex. The window name is first time "PlusApi_Excel Sample_17_39_12 Api Generated Orders"
and second time it will be "PlusApi_Excel Sample_17_45_13 Api Generated Orders"
I think I need to call window with part name but I don’t know how to do with kindly help me
Question 2
Above challenge I have one more problem the PlusApi will be hidden but my code shows still a positive value.
I think I need to call "visible" window only.
I found the following code in this vbforums.com answer and enhanced it to look for visible or invisible windows as well, therefore hopefully answering both your questions:
Option Explicit
Private Declare Function FindWindow Lib "User32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName 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 GetWindow Lib "User32" (ByVal hWnd As Long, ByVal wCmd As Long) As Long
Private Declare Function IsWindowVisible Lib "User32" (ByVal hWnd As Long) As Boolean
Private Const GW_HWNDNEXT = 2
Private Sub Test()
Dim lhWndP As Long
If GetHandleFromPartialCaption(lhWndP, "Excel") = True Then
If IsWindowVisible(lhWndP) = True Then
MsgBox "Found VISIBLE Window Handle: " & lhWndP, vbOKOnly + vbInformation
Else
MsgBox "Found INVISIBLE Window Handle: " & lhWndP, vbOKOnly + vbInformation
End If
Else
MsgBox "Window 'Excel' not found!", vbOKOnly + vbExclamation
End If
End Sub
Private Function GetHandleFromPartialCaption(ByRef lWnd As Long, ByVal sCaption As String) As Boolean
Dim lhWndP As Long
Dim sStr As String
GetHandleFromPartialCaption = False
lhWndP = FindWindow(vbNullString, vbNullString) 'PARENT WINDOW
Do While lhWndP <> 0
sStr = String(GetWindowTextLength(lhWndP) + 1, Chr$(0))
GetWindowText lhWndP, sStr, Len(sStr)
sStr = Left$(sStr, Len(sStr) - 1)
If InStr(1, sStr, sCaption) > 0 Then
GetHandleFromPartialCaption = True
lWnd = lhWndP
Exit Do
End If
lhWndP = GetWindow(lhWndP, GW_HWNDNEXT)
Loop
End Function
The code searches for a window with a partial title of "Excel" and tells you if it found it and if it's a visible window or not. You should be able to adapt it for your own purposes.
The question comes from code like this.
Set scriptshell = CreateObject("wscript.shell")
Const TIMEOUT_IN_SECS = 60
Select Case scriptshell.popup("Yes or No? leaving this window for 1 min is the same as clicking Yes.", TIMEOUT_IN_SECS, "popup window", vbYesNo + vbQuestion)
Case vbYes
Call MethodFoo
Case -1
Call MethodFoo
End Select
This is a simple way to display a message box with a timeout from VBA (or VB6).
In Excel 2007 (apparently also happens in Internet Explorer at times) the popup window will not timeout, and instead wait for user input.
This issue is tough to debug as it only happens occasionally and I do not know the steps to reproduce the issue. I believe it to be an issue with Office modal dialogs and Excel not recognising the timeout has expired.
See http://social.technet.microsoft.com/Forums/en-US/ITCG/thread/251143a6-e4ea-4359-b821-34877ddf91fb/
The workarounds I found are:
A. Use a Win32 API call
Declare Function MessageBoxTimeout Lib "user32.dll" Alias "MessageBoxTimeoutA" ( _
ByVal hwnd As Long, _
ByVal lpText As String, _
ByVal lpCaption As String, _
ByVal uType As Long, _
ByVal wLanguageID As Long, _
ByVal lngMilliseconds As Long) As Long
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _
ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Public Sub MsgBoxDelay()
Const cmsg As String = "Yes or No? leaving this window for 1 min is the same as clicking Yes."
Const cTitle As String = "popup window"
Dim retval As Long
retval = MessageBoxTimeout(FindWindow(vbNullString, Title), cmsg, cTitle, 4, 0, 60000)
If retval <> 7 Then
Call MethodFoo
End If
End Sub
B. Use a manual timer with a VBA userform that is designed to look like a messagebox. Use a global variable or similar to save any state that needs to be passed back to the calling code. Ensure that the Show method of the userform is called with the vbModeless parameter supplied.
C. Wrap the call to wscript.popup method in the MSHTA process which would allow the code to run out of process and avoid the modal nature of Office.
CreateObject("WScript.Shell").Run "mshta.exe vbscript:close(CreateObject(""WScript.Shell"").Popup(""Test"",2,""Real%20Time%20Status%20Message""))"
What is the best way of A, B or C or your own answer to display a message box with a timeout value in VBA?
This is a long answer, but there's a lot of ground to cover: it's also a late reply, but things have changed since some of the replies to this (and similar questions) have been posted on the stack. That sucks like a vacuum cleaner on triple-phase AC, because they were good answers when they were posted and a lot of thought went into them.
The short version is: I noticed that the Script WsShell Popup solution stopped working for me in VBA a year ago, and I coded a working API timer callback for the VBA MsgBox function.
Skip straight to the code under the heading VBA code to call a Message Box with a Timeout if you need an answer in a hurry - and I did, I have literally thousands of instances of a self-dismissing 'MsgPopup' substitute for VBA.MsgBox to redact, and the code below fits into a self-contained module.
However, the VBA coders here - myself included - need some explanation as to why perfectly good code no longer seems to work. And if you understand the reasons, you may be able to use the partial workaround for 'Cancel' dialogs, buried in the text.
I noticed that the Script WsShell Popup solution stopped working for me in VBA a year ago - The 'SecondsToWait' timeout was being ignored, and the dialog just hung around like the familiar VBA.MsgBox:
MsgPopup = objWShell.PopUp(Prompt, SecondsToWait, Title, Buttons)
And I think I know the reason why: you can no longer send a WM_CLOSE or WM_QUIT message to a dialog window from anywhere other than the thread which opened it. Likewise, the User32 DestroyWindow() function will not close a dialog window unless it's called by the thread that opened the dialog.
Someone in Redmond doesn't like the idea of a script running in the background and sending a WM_CLOSE commands to all those essential warnings that halt your work (and, these days, making them go away permanently needs local admin privileges).
I can't imagine who would write a script like that, it's a terrible idea!
There are consequences and collateral damage to that decision: WsScript.Popup() objects in the single-threaded VBA environment implement their 'SecondsToWait' timeout using a Timer callback, and that callback sends a WM_CLOSE message, or something like it... Which is ignored in most cases, because it's a callback thread, not the owner thread for the dialog.
You might get it to work on a popup with a 'CANCEL' button, and it'll become clear why that is in a minute or two.
I've tried writing a timer callback to WM_CLOSE the popup, and that failed for me, too, in most cases.
I've tried some exotic API callbacks to mess with the VBA.MsgBox and WsShell.Popup window, and I can tell you now that that they didn't work. You can't work with what isn't there: those dialog windows are very simple and most of them don't contain any functionality, at all, except for the responses in the button clicks - Yes, No, OK, Cancel, Abort, Retry, Ignore, and Help.
'Cancel' is an interesting one: it appears that you get a freebie from the primitive Windows API for built-in dialogs when you specify vbOKCancel or vbRetryCancel or vbYesNoCancel - the 'Cancel' function is automatically implemented with a 'close' button in the dialog's Menu bar (you don't get that with the other buttons, but feel free to try it with a dialog containing 'Ignore'), which means that....WsShell.Popup() dialogs will sometimes respond to the SecondsToWait timeout if they have a 'Cancel' option.
objWShell.PopUp("Test Me", 10, "Dialog Test", vbQuestion + vbOkCancel)
That might be a good enough workaround for someone reading this, if all you wanted was to get WsShell.Popup() functions to respond to the SecondsToWait parameter again.
This also means that you can send WM_CLOSE messages to the 'Cancel' dialog using the SendMessage() API call on a callback:
SendMessage(hwndDlgBox, WM_CLOSE, ByVal 0&, ByVal 0&)
Strictly speaking, this should only work for the WM_SYSCOMMAND, SC_CLOSE message - the 'close' box in the command bar is a 'system' menu with a special class of commands but, like I said, we're getting freebies from the Windows API.
I got that to work, and I started thinking: If I can only work with what's there, maybe I'd better find out what's actually there...
And the answer turns out to be obvious: Dialog boxes have their own set of WM_COMMAND message parameters -
' Dialog window message parameters, replicating Enum vbMsgBoxResult:
CONST dlgOK As Long = 1
CONST dlgCANCEL As Long = 2
CONST dlgABORT As Long = 3
CONST dlgRETRY As Long = 4
CONST dlgIGNORE As Long = 5
CONST dlgYES As Long = 6
CONST dlgNO As Long = 7
And, as these are the 'user' messages which return the user responses to the caller (that is to say, the calling thread) of the dialog, the dialog box is happy to accept them and close itself.
You can interrogate a dialog window to see if it implements a particular command and, if it does, you can send that command:
If GetDlgItem(hWndMsgBox, vbRetry) <> 0 Then
SendMessage hWndMsgBox, WM_COMMAND, vbRetry, 0&
Exit For
End If
The remaining challenge is to detect a 'Timeout' and intercept the returning Message Box response, and substitute our own value: -1 if we're following the convention established by the WsShell.Popup() function. So our 'msgPopup' wrapper for a Message Box with a timeout needs to do three things:
Call our API Timer for the delayed dismissal of the dialog;
Open the message Box, passing in the usual parameters;
Either: Detect a timeout and substitute the 'timeout' response...
...Or return the user response to the dialog, if they responded in
time
Elsewhere, we need to declare the API calls for all this, and we absolutely must have a Publicly-declared 'TimerProc' function for the Timer API to call. That function has to exist, and it has to run to 'End Function' without errors or breakpoints - any interruption, and the API Timer() will call down the wrath of the operating system.
VBA code to call a Message Box with a Timeout:
Option Explicit
Option Private Module
' Nigel Heffernan January 2016
' Modified from code published by Microsoft on MSDN, and on StackOverflow: this code is in ' the public domain.
' This module implements a message box with a 'timeout'
' It is similar to implementations of the WsShell.Popup() that use a VB.MessageBox interface
' with an additional 'SecondsToWait' or 'Timeout' parameter.
Private m_strCaption As String
Public Function MsgPopup(Optional Prompt As String, _
Optional Buttons As VbMsgBoxStyle = vbOKOnly, _
Optional Title As String, _
Optional SecondsToWait As Long = 0) As VbMsgBoxResult
' Replicates the VBA MsgBox() function, with an added parameter to automatically dismiss the message box after n seconds
' If dismissed automatically, this will return -1: NOT 'cancel', nor the default button choice.
Dim TimerStart As Single
If Title = "" Then
Title = ThisWorkbook.Name
End If
If SecondsToWait > 0 Then
' TimedmessageBox launches a callback to close the MsgBox dialog
TimedMessageBox Title, SecondsToWait
TimerStart = VBA.Timer
End If
MsgPopup = MsgBox(Prompt, Buttons, Title)
If SecondsToWait > 0 Then
' Catch the timeout, substitute -1 as the response
If (VBA.Timer - TimerStart) >= SecondsToWait Then
MsgPopup = -1
End If
End If
End Function
Public Function MsgBoxResultText(ByVal MsgBoxResult As VbMsgBoxResult) As String
' Returns a text value for the integers returned by VBA MsgBox() and WsShell.Popup() dialogs
' Additional value: 'TIMEOUT', returned when the MsgBoxResult = -1 ' All other values return the string 'ERROR'
On Error Resume Next
If (MsgBoxResult >= vbOK) And (MsgBoxResult <= vbNo) Then
MsgBoxResultText = Split("ERROR,OK,CANCEL,ABORT,RETRY,IGNORE,YES,NO,", ",")(MsgBoxResult)
ElseIf MsgBoxResult = dlgTIMEOUT Then
MsgBoxResultText = "TIMEOUT"
Else
MsgBoxResultText = "ERROR"
End If
End Function
'
'
'
'
'
'
'
'
'
'
Private Property Get MessageBox_Caption() As String
MessageBox_Caption = m_strCaption
End Property
Private Property Let MessageBox_Caption(NewCaption As String)
m_strCaption = NewCaption
End Property
Private Sub TimedMessageBox(Caption As String, Seconds As Long)
On Error Resume Next
' REQUIRED for Function msgPopup
' Public Sub TimerProcMessageBox MUST EXIST
MessageBox_Caption = Caption
SetTimer 0&, 0&, Seconds * 1000, AddressOf TimerProcMessageBox
Debug.Print "start Timer " & Now
End Sub
#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows
' Use LongLong and LongPtr
Public Sub TimerProcMessageBox(ByVal hwnd As LongPtr, _
ByVal wMsg As Long, _
ByVal idEvent As LongPtr, _
ByVal dwTime As LongLong)
On Error Resume Next
' REQUIRED for Function msgPopup
' https://msdn.microsoft.com/en-US/library/windows/desktop/ms644907(v=vs.85).aspx
' Closes a dialog box (Shell.Popup or VBA.MsgBox) having a caption stored in MessageBox_Caption
' This TimerProc sends *any* message that can close the dialog: the objective is solely to close
' the dialog and resume the VBA thread. Your caller must detect the expired TimerProc interval
' and insert a custom return value (or default) that signals the 'Timeout' for responses.
' The MsgPopup implementation in this project returns -1 for this 'Timeout'
Dim hWndMsgBox As LongPtr ' Handle to VBA MsgBox
KillTimer hWndMsgBox, idEvent
hWndMsgBox = 0
hWndMsgBox = FindWindow("#32770", MessageBox_Caption)
If hWndMsgBox < > 0 Then
' Enumerate WM_COMMAND values
For iDlgCommand = vbOK To vbNo
If GetDlgItem(hWndMsgBox, iDlgCommand) <> 0 Then
SendMessage hWndMsgBox, WM_COMMAND, iDlgCommand, 0&
Exit For
End If
Next iDlgCommand
End If
End Sub
#ElseIf VBA7 Then ' 64 bit Excel in all environments
' Use LongPtr only
Public Sub TimerProcMessageBox(ByVal hwnd As LongPtr, _
ByVal wMsg As Long, _
ByVal idEvent As LongPtr, _
ByVal dwTime As Long)
On Error Resume Next
' REQUIRED for Function msgPopup
' https://msdn.microsoft.com/en-US/library/windows/desktop/ms644907(v=vs.85).aspx
' Closes a dialog box (Shell.Popup or VBA.MsgBox) having a caption stored in MessageBox_Caption
' This TimerProc sends *any* message that can close the dialog: the objective is solely to close
' the dialog and resume the VBA thread. Your caller must detect the expired TimerProc interval
' and insert a custom return value (or default) that signals the 'Timeout' for responses.
' The MsgPopup implementation in this project returns -1 for this 'Timeout'
Dim hWndMsgBox As LongPtr ' Handle to VBA MsgBox
Dim iDlgCommand As VbMsgBoxResult ' Dialog command values: OK, CANCEL, YES, NO, etc
KillTimer hwnd, idEvent
hWndMsgBox = 0
hWndMsgBox = FindWindow("#32770", MessageBox_Caption)
If hWndMsgBox < > 0 Then
' Enumerate WM_COMMAND values
For iDlgCommand = vbOK To vbNo
If GetDlgItem(hWndMsgBox, iDlgCommand) <> 0 Then
SendMessage hWndMsgBox, WM_COMMAND, iDlgCommand, 0&
Exit For
End If
Next iDlgCommand
End If
End Sub
#Else ' 32 bit Excel
Public Sub TimerProcMessageBox(ByVal hwnd As Long, _
ByVal wMsg As Long, _
ByVal idEvent As Long, _
ByVal dwTime As Long)
On Error Resume Next
' REQUIRED for Function msgPopup
' The MsgPopup implementation in this project returns -1 for this 'Timeout'
Dim hWndMsgBox As Long ' Handle to VBA MsgBox
KillTimer hwnd, idEvent
hWndMsgBox = 0
hWndMsgBox = FindWindow("#32770", MessageBox_Caption)
If hWndMsgBox < > 0 Then
' Enumerate WM_COMMAND values
For iDlgCommand = vbOK To vbNo
If GetDlgItem(hWndMsgBox, iDlgCommand) <> 0 Then
SendMessage hWndMsgBox, WM_COMMAND, iDlgCommand, 0&
Exit For
End If
Next iDlgCommand
End If
End Sub
#End If
And here are the API declarations - note the conditional declarations for VBA7, 64-Bit Windows, and plain-vanilla 32-bit:
' Explanation of compiler constants for 64-Bit VBA and API declarations :
' https://msdn.microsoft.com/en-us/library/office/ee691831(v=office.14).aspx
#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows ' Use LongLong and LongPtr
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As LongPtr
Private Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" _
(ByVal hwnd As LongPtr, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
ByRef lParam As Any _
) As LongPtr
Private Declare PtrSafe Function SetTimer Lib "user32" _
(ByVal hwnd As LongPtr, _
ByVal nIDEvent As LongPtr, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As LongPtr _
) As Long
Public Declare PtrSafe Function KillTimer Lib "user32" _
(ByVal hwnd As LongPtr, _
ByVal nIDEvent As LongPtr _
) As Long
Private Declare PtrSafe Function GetDlgItem Lib "user32" _
(ByVal hWndDlg As LongPtr, _
ByVal nIDDlgItem As Long _
) As LongPtr
#ElseIf VBA7 Then ' VBA7 in all environments, including 32-Bit Office ' Use LongPtr for ptrSafe declarations, LongLong is not available
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As LongPtr
Private Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" _
(ByVal hwnd As LongPtr, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
ByRef lParam As Any _
) As LongPtr
Private Declare PtrSafe Function SetTimer Lib "user32" _
(ByVal hwnd As LongPtr, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As LongPtr) As LongPtr
Private Declare PtrSafe Function KillTimer Lib "user32" _
(ByVal hwnd As LongPtr, _
ByVal nIDEvent As Long) As Long
Private Declare PtrSafe Function GetDlgItem Lib "user32" _
(ByVal hWndDlg As LongPtr, _
ByVal nIDDlgItem As Long _
) As LongPtr
#Else
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
(ByVal hwnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
ByRef lParam As Any _
) As Long
Private Declare Function SetTimer Lib "user32" _
(ByVal hwnd As Long, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As Long) As Long
Public Declare Function KillTimer Lib "user32" _
(ByVal hwnd As Long, _
ByVal nIDEvent As Long) As Long
Private Declare Function GetDlgItem Lib "user32" _
(ByVal hWndDlg, ByVal nIDDlgItem As Long) As Long
#End If
Private Enum WINDOW_MESSAGE
WM_ACTIVATE = 6
WM_SETFOCUS = 7
WM_KILLFOCUS = 8
WM_PAINT = &HF
WM_CLOSE = &H10
WM_QUIT = &H12
WM_COMMAND = &H111
WM_SYSCOMMAND = &H112
End Enum
' Dialog Box Command IDs - replicates vbMsgBoxResult, with the addition of 'dlgTIMEOUT'
Public Enum DIALOGBOX_COMMAND
dlgTIMEOUT = -1
dlgOK = 1
dlgCANCEL = 2
dlgABORT = 3
dlgRETRY = 4
dlgIGNORE = 5
dlgYES = 6
dlgNO = 7
End Enum
A final note: I would welcome suggestions for improvement from experienced MFC C++ developers, as you are going to have a much better grasp of the basic Windows message-passing concepts underlying a 'Dialog' window - I work in an oversimplified language and it is likely that the oversimplifications in my understanding have crossed the line into outright errors in my explanation.
Going with Answer A. the Win32 solution. This meets the requirements, and is robust from testing so far.
Declare Function MessageBoxTimeout Lib "user32.dll" Alias "MessageBoxTimeoutA" ( _
ByVal hwnd As Long, _
ByVal lpText As String, _
ByVal lpCaption As String, _
ByVal uType As Long, _
ByVal wLanguageID As Long, _
ByVal lngMilliseconds As Long) As Long
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _
ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Public Sub MsgBoxDelay()
Const cmsg As String = "Yes or No? leaving this window for 1 min is the same as clicking Yes."
Const cTitle As String = "popup window"
Dim retval As Long
retval = MessageBoxTimeout(FindWindow(vbNullString, Title), cmsg, cTitle, 4, 0, 60000)
If retval <> 7 Then
Call MethodFoo
End If
End Sub
Easy
Call CreateObject("WScript.Shell").Popup("Timed message box", 1, "Title", vbOKOnly)
Starting with the samples in this post my final code is as follows:
' Coded by Clint Smith
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' tMsgBox Function (Timered Message Box)
' By Clint Smith, clintasm#gmail.com
' Created 04-Sep-2014
' Updated for 64-bit 03-Mar-2020
' This provides an publicly accessible procedure named
' tMsgBox that when invoked instantiates a timered
' message box. Many constants predefined for easy use.
' There is also a global result variable tMsgBoxResult.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Public Const mbBTN_Ok = vbOKOnly 'Default
Public Const mbBTN_OkCancel = vbOKCancel
Public Const mbBTN_AbortRetryIgnore = vbAbortRetryIgnore
Public Const mbBTN_YesNoCancel = vbYesNoCancel
Public Const mbBTN_YesNo = vbYesNo
Public Const mbBTN_RetryCancel = vbRetryCancel
Public Const mbBTN_CanceTryagainContinue = &H6
Public Const mbICON_Stop = vbCritical
Public Const mbICON_Question = vbQuestion
Public Const mbICON_Exclaim = vbExclamation
Public Const mbICON_Info = vbInformation
Public Const mbBTN_2ndDefault = vbDefaultButton2
Public Const mbBTN_3rdDefault = vbDefaultButton3
Public Const mbBTN_4rdDefault = vbDefaultButton4
Public Const mbBOX_Modal = vbSystemModal
Public Const mbBTN_AddHelp = vbMsgBoxHelpButton
Public Const mbTXT_RightJustified = vbMsgBoxRight
Public Const mbWIN_Top = &H40000 'Default
Public Const mbcTimeOut = 32000
Public Const mbcOk = vbOK
Public Const mbcCancel = vbCancel
Public Const mbcAbort = vbAbort
Public Const mbcRetry = vbRetry
Public Const mbcIgnore = vbIgnore
Public Const mbcYes = vbYes
Public Const mbcNo = vbNo
Public Const mbcTryagain = 10
Public Const mbcContinue = 11
Public Const wAccessWin = "OMain"
Public Const wExcelWin = "XLMAIN"
Public Const wWordWin = "OpusApp"
Public tMsgBoxResult As Long
#If VBA7 Then
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Public Declare PtrSafe Function tMsgBoxA Lib "user32.dll" Alias "MessageBoxTimeoutA" ( _
ByVal hwnd As Long, _
ByVal lpText As String, _
ByVal lpCaption As String, _
ByVal uType As Long, _
ByVal wLanguageID As Long, _
ByVal lngMilliseconds As Long) As Long
#Else
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Public Declare Function tMsgBoxA Lib "user32.dll" Alias "MessageBoxTimeoutA" ( _
ByVal hwnd As Long, _
ByVal lpText As String, _
ByVal lpCaption As String, _
ByVal uType As Long, _
ByVal wLanguageID As Long, _
ByVal lngMilliseconds As Long) As Long
#End If
Public Sub tMsgBox( _
Optional sMessage As String = "Default: (10 sec timeout)" & vbLf & "Coded by Clint Smith", _
Optional sTitle As String = "Message Box with Timer", _
Optional iTimer As Integer = 10, _
Optional hNtype As Long = mbBTN_Ok + mbWIN_Top, _
Optional hLangID As Long = &H0, _
Optional wParentType As String = vbNullString, _
Optional wParentName As String = vbNullString)
tMsgBoxResult = tMsgBoxA(FindWindow(wParentType, wParentName), sMessage, sTitle, hNtype, hLangID, 1000 * iTimer)
End Sub
Private Declare Function MsgBoxTimeout _
Lib "user32" _
Alias "MessageBoxTimeoutA" ( _
ByVal hwnd As Long, _
ByVal MsgText As String, _
ByVal Title As String, _
ByVal MsgBoxType As VbMsgBoxStyle, _
ByVal wlange As Long, _
ByVal Timeout As Long) _
As Long
Dim btnOK As Boolean
Dim btnCancel As Boolean
Dim MsgTimeOut As Boolean
Option Explicit
Sub Main
AutoMsgbox("Message Text", "Title", vbOkCancel , 5) '5 sec TimeOut
MsgBox("Pressed OK: " & btnOK & vbNewLine & "Pressed Cancel: " & btnCancel & vbNewLine &"MsgBox Timeout: " & MsgTimeOut)
End Sub
Function AutoMsgbox(MsgText , Title , MsgBoxType , Timeout)
Dim ReturnValue
Dim TimeStamp As Date
TimeStamp = DateAdd("s",Timeout,Now)
Dim MsgText1 As String
Dim TimeOutCounter As Integer
For TimeOutCounter = 0 To Timeout
MsgText1 = MsgText & vbNewLine & vbNewLine & " Auto Selction in " & Timeout - TimeOutCounter & " [s]"
ReturnValue = MsgBoxTimeout(0 , MsgText1 , Title, MsgBoxType, 0 ,1000)
Select Case ReturnValue
Case 1
btnOK = True
btnCancel = False
btnAbort = False
btnRetry = False
btnIgnore = False
btnYes = False
btnNo = False
MsgTimeOut = False
Exit Function
Case 2
btnOK = False
btnCancel = True
btnAbort = False
btnRetry = False
btnIgnore = False
btnYes = False
btnNo = False
MsgTimeOut = False
Exit Function
Case 3
btnOK = False
btnCancel = False
btnAbort = True
btnRetry = False
btnIgnore = False
btnYes = False
btnNo = False
MsgTimeOut = False
Exit Function
Case 4
btnOK = False
btnCancel = False
btnAbort = False
btnRetry = True
btnIgnore = False
btnYes = False
btnNo = False
MsgTimeOut = False
Exit Function
Case 5
btnOK = False
btnCancel = False
btnAbort = False
btnRetry = False
btnIgnore = True
btnYes = False
btnNo = False
MsgTimeOut = False
Exit Function
Case 6
btnOK = False
btnCancel = False
btnAbort = False
btnRetry = False
btnIgnore = False
btnYes = True
btnNo = False
MsgTimeOut = False
Exit Function
Case 7
btnOK = False
btnCancel = False
btnAbort = False
btnRetry = False
btnIgnore = False
btnYes = False
btnNo = True
MsgTimeOut = False
Exit Function
Case 32000
btnOK = False
btnCancel = False
btnAbort = False
btnRetry = False
btnIgnore = False
btnYes = False
btnNo = False
MsgTimeOut = True
Next TimeOutCounter
End Function