VBA autonomous objects - vba

I have the code below which works fine, steps through the rows pinging each host and updating the sheet.
Sub Do_ping()
Set output = ActiveWorkbook.Worksheets(1)
With ActiveWorkbook.Worksheets(1)
Set pinger = CreateObject("WScript.Shell")
pings = 1
pingend = "FALSE"
output.Cells(2, 4) = pings
output.Cells(2, 5) = pingend
Do
Row = 2
Do
If .Cells(Row, 1) <> "" Then
result = pinger.Run("%comspec% /c ping.exe -n 1 -w 250 " _
& output.Cells(Row, 1).Value & " | find ""TTL="" > nul 2>&1", 0, True)
If (result = 0) = True Then
result = "TRUE"
Else
result = "FALSE"
End If
' result = IsConnectible(.Cells(Row, 1), 1, 1000)
output.Cells(Row, 2) = result
End If
Row = Row + 1
Loop Until .Cells(Row, 1) = ""
waitTime = 1
Start = Timer
While Timer < Start + waitTime
DoEvents
Wend
output.Cells(2, 4) = pings
output.Cells(2, 5) = pingend
pings = pings + 1
Loop Until pingend = "TRUE"
End With
End Sub
But suppose I have 50 devices and 40 of them are down. Because it is sequential I have to wait for the pings to time out on these devices and so a single pass can take a long time.
Can I in VBA create an object that I can create multiply instances of, each pinging a separate host, and then simple cycle though the objects pulling back a true/false property from them.
I don't know how possible this is or how you deal with classes in VBA.
I want some thing like
set newhostping = newobject(pinger)
pinger.hostname = x.x.x.x
to set up the object then object would have the logic
do
ping host x.x.x.x
if success then outcome = TRUE
if not success then outcome = FALSE
wait 1 second
loop
so back in the main code I could just use
x = pinger.outcome
to give me the current state of the host, with out needing to wait for the current ping operation to complete. It would just return the result of the last completed attempt
Does any one have any code or ideas they could share?
Thank you
DevilWAH

You could use the ShellAndWait function below to run those calls asynchronously (i.e. in parallel). See my example with a simple tracert command which generally takes a few seconds to run. It opens 50 command windows running at the same time.
Option Explicit
Private Declare Function OpenProcess Lib "kernel32" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32" (ByVal hProcess As Long, lpExitCode As Long) As Long
Private Const STATUS_PENDING = &H103&
Private Const PROCESS_QUERY_INFORMATION = &H400
Public Sub test()
Dim i As Long
For i = 1 To 50
ShellandWait "tracert www.google.com", vbNormalFocus, 1
Next i
End Sub
Public Function ShellandWait(parProgramName As String, Optional parWindowStyle As VbAppWinStyle = vbMinimizedNoFocus, _
Optional parTimeOutValue As Long = 0) As Boolean
'source: http://www.freevbcode.com/ShowCode.Asp?ID=99
'Time out value in seconds
'Returns true if the program closes before timeout
Dim lInst As Long
Dim lStart As Long
Dim lTimeToQuit As Long
Dim sExeName As String
Dim lProcessId As Long
Dim lExitCode As Long
Dim bPastMidnight As Boolean
On Error GoTo ErrorHandler
lStart = CLng(Timer)
sExeName = parProgramName
'Deal with timeout being reset at Midnight
If parTimeOutValue > 0 Then
If lStart + parTimeOutValue < 86400 Then
lTimeToQuit = lStart + parTimeOutValue
Else
lTimeToQuit = (lStart - 86400) + parTimeOutValue
bPastMidnight = True
End If
End If
lInst = Shell(sExeName, parWindowStyle)
lProcessId = OpenProcess(PROCESS_QUERY_INFORMATION, False, lInst)
Do
Call GetExitCodeProcess(lProcessId, lExitCode)
DoEvents
If parTimeOutValue And Timer > lTimeToQuit Then
If bPastMidnight Then
If Timer < lStart Then Exit Do
Else
Exit Do
End If
End If
Loop While lExitCode = STATUS_PENDING
If lExitCode = STATUS_PENDING Then
ShellandWait = False
Else
ShellandWait = True
End If
Exit Function
ErrorHandler:
ShellandWait = False
End Function

Related

Webcam capture using avicap32 on Access 365 form works on single camera laptops(64-bit), black screen in picture box on 2-camera tablet(32-bit)

After consulting
How to use webcam capture on a Microsoft Access form,
I have a program where the user presses a button on an Excel form to open an Access form and take before & after photos using built-in webcam then save them to a predetermined folder. This works fine on several laptops including mine but when I try to run it on a tablet with front and back camera, it prompts me to choose between UNICAM Rear and UNICAM Front, which I presume means the code works fine and is connecting to the driver. However, the chosen camera doesn't connect; WM_CAP_DRIVER_CONNECT returns False and I get a black screen in the picture frame.
The tablet is an Acer One 10 running Win10 Home 32-bit and Access 365 Runtime. The I've tested the program using Access Runtime through Command Prompt on my laptop and it worked fine, I've checked that other apps are allowed to access the camera, nothing else is using the camera, tested 0 to 9 for WM_CAP_CONNECT parameters, changed LongPtr back to Long (which by the way still makes it work on win10 Pro 64-bit) and it still doesn't work.
I suspect it's an issue with the tablet and not the code since it's a company tablet and there are two cameras, perhaps I may be missing some permissions to connect to the camera via Access or the code doesn't work with two cameras, but I have no idea where to begin checking these.
I'm currently trying to find a laptop with two cameras to test the program on and in the meantime I'm totally lost and would appreciate suggestions for anything I could try to fix this problem, whether related to the code or not - though I would like to avoid running executables like CommandCam, seeing as I'm using company computers.
This is the part of my Excel code that affects opening Access:
Private Sub mainBtn_Click()
Dim LCategoryID As Long
Dim ShellCmd, LPath As String
Dim wsMain, wsRec As Worksheet
Set wsMain = Sheets("Main")
Set wsRec = Sheets("Records")
mainBtn.Enabled = False
LPath = ThisWorkbook.Path + "\Database1.accdb"
If mainBtn.Caption <> "Record" Then
If Dir(PathToAccess) <> "" And oApp Is Nothing Then
ShellCmd = """" & PathToAccess & """ """ & LPath & """"
VBA.Shell ShellCmd
If oApp Is Nothing Then Set oApp = GetObject(LPath)
' Set oApp = CreateObject("Access.Application")
End If
Application.Wait (Now + TimeValue("00:00:05"))
On Error Resume Next
oApp.OpenCurrentDatabase LPath
oApp.Visible = False
On Error GoTo 0
'passing a value through a sub on Access
oApp.Run "getName", wsMain.Range("F5").Value
End If
'before photo
If mainBtn.Caption = "Before Photo" Then
oApp.DoCmd.openform "Before Photo"
mainBtn.Caption = "After Photo"
mainBtn.Enabled = True
This is my code in Access:
Option Compare Database
Const WS_CHILD As Long = &H40000000
Const WS_VISIBLE As Long = &H10000000
Const WM_USER As Long = &H400
Const WM_CAP_START As Long = WM_USER
Const WM_CAP_DRIVER_CONNECT As Long = WM_CAP_START + 10
Const WM_CAP_DRIVER_DISCONNECT As Long = WM_CAP_START + 11
Const WM_CAP_SET_PREVIEW As Long = WM_CAP_START + 50
Const WM_CAP_SET_PREVIEWRATE As Long = WM_CAP_START + 52
Const WM_CAP_DLG_VIDEOFORMAT As Long = WM_CAP_START + 41
Const WM_CAP_FILE_SAVEDIB As Long = WM_CAP_START + 25
Private Declare PtrSafe Function capCreateCaptureWindow _
Lib "avicap32.dll" Alias "capCreateCaptureWindowA" _
(ByVal lpszWindowName As String, ByVal dwStyle As Long _
, ByVal X As Long, ByVal Y As Long, ByVal nWidth As Long _
, ByVal nHeight As Long, ByVal hwndParent As LongPtr _
, ByVal nID As Long) As Long
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 Long
Private Declare Sub sapiSleep Lib "kernel32" _
Alias "Sleep" _
(ByVal dwMilliseconds As Long)
Dim hCap As LongPtr
Dim i As Integer
Private Sub cmd4_click()
' take picture
Dim sFileName, sFileNameSub, dateNow, timeNow As String
i = i + 1
Call SendMessage(hCap, WM_CAP_SET_PREVIEW, CLng(False), 0&)
dateNow = DateValue(Now)
timeNow = TimeValue(Now)
sFileName = CurrentProject.Path + "\dbimages\Before Change " + CStr(Year(dateNow)) + "." + CStr(Month(dateNow)) + "." + CStr(Day(dateNow)) + " " + CStr(Hour(timeNow)) + "h" + CStr(Minute(timeNow)) + "m" + CStr(Second(timeNow)) + "s.jpg"
Call SendMessage(hCap, WM_CAP_FILE_SAVEDIB, 0&, ByVal CStr(sFileName))
DoFinally:
Call SendMessage(hCap, WM_CAP_SET_PREVIEW, CLng(True), 0&)
If i = 4 Then
MsgBox "4 pictures taken. Exiting"
DoCmd.Close
Else
MsgBox "Picture " + CStr(i) + " Taken"
End If
End Sub
Private Sub Cmd3_Click()
Dim temp As Long
temp = SendMessage(hCap, WM_CAP_DRIVER_DISCONNECT, 0&, 0&)
End Sub
Private Sub cmd1_click()
' Dim connectAttempts As Integer
Dim i As Integer
hCap = capCreateCaptureWindow("Take a Camera Shot", WS_CHILD Or WS_VISIBLE, 0, 0, PicWebCam.Width, PicWebCam.Height, PicWebCam.Form.hWnd, 0)
If hCap <> 0 Then
'Call SendMessage(hCap, WM_CAP_DRIVER_CONNECT, 0, 0)
' For i = 0 To 9
If CBool(SendMessage(hCap, WM_CAP_DRIVER_CONNECT, 0, 0)) = False Then
' connectAttempts = connectAttempts + 1
MsgBox "Failed to connect Camera"
Else
' Exit For
End If
' Next i
Call SendMessage(hCap, WM_CAP_SET_PREVIEWRATE, 45, 0&)
Call SendMessage(hCap, WM_CAP_SET_PREVIEW, CLng(True), 0&)
End If
End Sub
Private Sub Cmd2_Click()
'back to excel
'Dim temp As Long
'temp = SendMessage(hCap, WM_CAP_DLG_VIDEOFORMAT, 0&, 0&)
DoCmd.Close
End Sub
Private Sub Form_Close()
Dim temp As Long
temp = SendMessage(hCap, WM_CAP_DRIVER_DISCONNECT, 0&, 0&)
'DoCmd.ShowToolbar "Ribbon", acToolbarYes
End Sub
Private Sub Form_Load()
'DoCmd.ShowToolbar "Ribbon", acToolbarNo
i = 0
cmd1.Caption = "Start Cam"
cmd2.Caption = "Done"
cmd3.Caption = "dummy"
cmd4.Caption = "Tak&e Picture"
DoCmd.RunCommand acCmdAppMinimize
DoCmd.Maximize
If stnName = "Head 1" Or stnName = "Head 2" Then
Pic1.Picture = CurrentProject.Path + "\images\head_s.jpeg"
ElseIf stnName = "Marriage Head" Or stnName = "Plus Clip Head" Then
Pic1.Picture = CurrentProject.Path + "\images\marriage_s.jpeg"
Else
Pic1.Picture = CurrentProject.Path + "\images\6pair_s.jpeg"
End If
cmd1_click
On Error Resume Next
CurrentProject.Application.Visible = True
End Sub
Private Sub sSleep(lngMilliSec As Long)
If lngMilliSec > 0 Then
Call sapiSleep(lngMilliSec)
End If
End Sub
EDIT: Camera app works fine on tablet but I get a black screen in picture box when trying to use it through Access.
EDIT2: Code for WM_CAP_GET_STATUS
Added the following line in main module:
Const WM_CAP_GET_STATUS = WM_CAP_START + 54
Added the following in another module:
Type CAPSTATUS
uiImageWidth As Long
uiImageHeight As Long
fLiveWindow As Long
fOverlayWindow As Long
fScale As Long
ptScroll As POINTAPI
fUsingDefaultPalette As Long
fAudioHardware As Long
fCapFileExists As Long
dwCurrentVideoFrame As Long
dwCurrentVideoFramesDropped As Long
dwCurrentWaveSamples As Long
dwCurrentTimeElapsedMS As Long
hPalCurrent As Long
fCapturingNow As Long
dwReturn As Long
wNumVideoAllocated As Long
wNumAudioAllocated As Long
End Type
New code for starting camera:
Private Sub cmd1_click()
Dim bool1, bool2, bool3 As Boolean
Dim o As Integer
Dim u As Integer
Dim s As CAPSTATUS
Open CurrentProject.Path + "\output.txt" For Output As #1
i = 0 'global variable
hCap = capCreateCaptureWindow("Take a Camera Shot", ws_child Or ws_visible, 0, 0, PicWebCam.Width, PicWebCam.Height, PicWebCam.Form.hwnd, 0)
sSleep 5000
If hCap <> 0 Then
For i = 0 To 9
Print #1, hCap
bool1 = CBool(SendMessage(hCap, WM_CAP_DRIVER_CONNECT, i, 0))
Print #1, bool1
bool3 = SendMessage(hCap, WM_CAP_GET_STATUS, LenB(s), s)
Print #1, bool3
For u = 1 To 4
If bool1 = True Then
bool1 = SendMessage(hCap, WM_CAP_DRIVER_DISCONNECT, i, 0&)
End If
o = u * 7
bool1 = CBool(SendMessage(hCap, WM_CAP_DRIVER_CONNECT, i, 0))
Print #1, Tab(o); bool1
bool3 = SendMessage(hCap, WM_CAP_GET_STATUS, LenB(s), s)
Print #1, Tab(o); bool3
Next u
Call SendMessage(hCap, WM_CAP_SET_PREVIEWRATE, 45, 0&)
Call SendMessage(hCap, WM_CAP_SET_PREVIEW, CLng(True), 0&)
Next i
End If
Close #1
End Sub

Check if mapped network available

I am trying to have my program check is a mapped network drive is actually connected, and change the curDrive variable based on the result. It works okay, but if the drive is still mapped and the drive is not available, there is a long delay while the program tries to connect (4-6 seconds). I tried two methods and both ways have this delay. I tried the following:
On Error GoTo switch
checker= Dir("F:\")
If checker= "" Then GoTo switch
curDrive = "F:\"
GoTo skip
switch:
curDrive = "C:\"
skip:
........
I also tried:
Dim FSO As Object '//FileSystemObject
Dim f As Object '//File Object
Set FSO = CreateObject("Scripting.FileSystemObject")
With FSO
If .FolderExists("F:\Sample") Then
curDrive = "F:\"
Else
curDrive = "C:\"
End If
End With
Both have the same delay.
After much searching and brainstorming, I put together some info from here and from elsewhere and came up with a method that takes half a second. Basically, I'm pinging the server and reading the results from a text file. I'm also checking to make sure that the F: Drive (the server drive) is available (Someone can be on the server but hasn't set the F: Drive to the server).
Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As Long) As Long
Private Declare Function WaitForSingleObject Lib "kernel32.dll" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAccessas As Long, ByVal bInheritHandle As Long, ByVal dwProcId As Long) As Long
Sub CheckAllConnections()
ServerOn = ComputerIsOnline("server.mmc.local")
FDrive = CreateObject("scripting.filesystemobject").driveexists("F")
test = FDrive - 1
ProgramFolder = False
If ServerOn + FDrive = -2 Then
ProgramFolder = Len(Dir("F:\SampleProgram\")) > 0
End If
MsgBox ("Server connection is " & ServerOn & "." & Chr(10) & "F: Drive available is " & FDrive _
& Chr(10) & "The Program Folder availability is " & ProgramFolder)
End Sub
Public Function ComputerIsOnline(ByVal strComputerName As String) As Boolean
On Error Resume Next
Kill "C:\Logger.txt"
On Error GoTo ErrorHandler
ShellX = Shell("cmd.exe /c ping -n 1 " & strComputerName & " > c:\logger.txt", vbHide)
lPid = ShellX
lHnd = OpenProcess(&H100000, 0, lPid)
If lHnd <> 0 Then
lRet = WaitForSingleObject(lHnd, &HFFFF)
CloseHandle (lHnd)
End If
FileNum = FreeFile
Open "c:\logger.txt" For Input As #FileNum
strResult = Input(LOF(1), 1)
Close #FileNum
ComputerIsOnline = (InStr(strResult, "Lost = 0") > 0)
Exit Function
ErrorHandler:
ComputerIsOnline = False
Exit Function
End Function
Both show the same delay because both methods invoke the same underlying OS functionality to check for the presence of the network drive.
The OS is giving the external resource time to be available. I don't think you can do anything except await the timeout, if you want to know for sure.
If you know that, in your environment the OS timeout is just too long (e.g. "If it has not responded after 1 second, it will not respond), you could use a mechanism such as a timer to avoid waiting the full duration (set a 1 second timer when you start checking, if the timer fires and you still have no reply, the drive was not present).
There is no long delay when testing for a drive letter using the FileSystemObject and DriveExists:
Sub Tester()
Dim n As Integer
For n = 1 To 26
Debug.Print Chr(64 + n), HaveDrive(Chr(64 + n))
Next n
End Sub
Function HaveDrive(driveletter)
HaveDrive = CreateObject("scripting.filesystemobject").driveexists(driveletter)
End Function

VBA timer api memory usage

i am developing an app that will be working for days and implements a Timer (From user32 lib) that is running its routine every 500ms. The problem is that every time the routine executes, the memory required by the Excel App is being increased by 8KB.
As i said i will like the app to be running for days so there is a point that it's memory consumptions is too high, and the app starts to be too slow.
I've searched in this and other places for a way to solve but i haven't find a solution. I read about forcing GC but in vba i cant do it. ¿Can anyone give me some advice?
P.d.: Thank you and sorry for my poor english.
Edit:
Hi again, i use the Timer Event to communicate to a PLC and act in consequence. Maybe 0.5 seconds is such a short period of time that the code can't finish the routine and the events are being located in the stack. I think i could extend the time to 1 second without losing performance.
Here is my code:
API DECLARATIONS
Private Declare Function SetTimer Lib "user32" _
(ByVal hWnd As Long, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib "user32" _
(ByVal hWnd As Long, _
ByVal nIDEvent As Long) As Long
TIMER EVENT ROUTINE
Private Sub TimerEvent()
On Error Resume Next
Hoja1.cmdFecha.Caption = Format(Now, "dd/mm/yy hh:mm:ss")
Hoja6.Range("I40") = 0
'Zona Lectura PLC
Call readFromPLC
If Hoja6.Range("I40") = 0 Then
Hoja4.Range("c11") = 1
Else
Hoja4.Range("c11") = 0
End If
'Zona alarmas
If Hoja4.Range("C7") <> AlarmaAnterior Then
' Interrupcionpo calculo
AlarmaAnterior = Hoja4.Range("D10")
If Hoja4.Range("c7") = 0 Then
Hoja1.Label1.Visible = False
Else
'Hoja4.Range("d8") = Hoja4.Range("d8") + 1
'Call Control
Call AlarmasNuevo
Hoja1.Label1.Visible = True
End If
End If
'Zona actuacion
If Hoja6.Range("d61") <> Hoja6.Range("d62") Then
Hoja6.Range("d62") = Hoja6.Range("d61")
Hoja6.Range("d66") = Hoja6.Range("d66") + 1
Call ControlArchivos
End If
If Hoja6.Range("d63") <> Hoja6.Range("c63") Then
Hoja6.Range("d63") = Hoja6.Range("c63")
Call ResetContadores
End If
If Hoja6.Range("I50") = 0 Then
ElseIf Hoja6.Range("I49") <> Hoja6.Range("j49") Then
Hoja6.Range("J49") = Hoja6.Range("i49")
If Hoja6.Range("I49") <> 0 Then
Call Medir
Else
Call StopAcq
Sheets("ESCPLC").Range("J58") = 0
Hoja1.cmdAvisos.Visible = False
End If
End If
'Zona escritura PLC
If Hoja6.Range("J57") <> Hoja6.Range("L57") Or Hoja6.Range("J58") <> Hoja6.Range("L58") Or Hoja6.Range("J59") <> Hoja6.Range("L59") Or Hoja6.Range("J60") <> Hoja6.Range("L60") Or Hoja6.Range("J61") <> Hoja6.Range("L61") Then
Hoja6.Range("L57") = Hoja6.Range("J57")
Hoja6.Range("L58") = Hoja6.Range("J58")
Hoja6.Range("L59") = Hoja6.Range("J59")
Hoja6.Range("L60") = Hoja6.Range("J60")
Hoja6.Range("L61") = Hoja6.Range("J61")
Call writeToPLC
End If
End Sub
Thank You very much

Wait for Shell to finish, then format cells - synchronously execute a command

I have an executable that I call using the shell command:
Shell (ThisWorkbook.Path & "\ProcessData.exe")
The executable does some computations, then exports results back to Excel. I want to be able to change the format of the results AFTER they are exported.
In other words, i need the Shell command first to WAIT until the executable finishes its task, exports the data, and THEN do the next commands to format.
I tried the Shellandwait(), but without much luck.
I had:
Sub Test()
ShellandWait (ThisWorkbook.Path & "\ProcessData.exe")
'Additional lines to format cells as needed
End Sub
Unfortunately, still, formatting takes place first before the executable finishes.
Just for reference, here was my full code using ShellandWait
' Start the indicated program and wait for it
' to finish, hiding while we wait.
Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As Long) As Long
Private Declare Function WaitForSingleObject Lib "kernel32.dll" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAccessas As Long, ByVal bInheritHandle As Long, ByVal dwProcId As Long) As Long
Private Const INFINITE = &HFFFF
Private Sub ShellAndWait(ByVal program_name As String)
Dim process_id As Long
Dim process_handle As Long
' Start the program.
On Error GoTo ShellError
process_id = Shell(program_name)
On Error GoTo 0
' Wait for the program to finish.
' Get the process handle.
process_handle = OpenProcess(SYNCHRONIZE, 0, process_id)
If process_handle <> 0 Then
WaitForSingleObject process_handle, INFINITE
CloseHandle process_handle
End If
Exit Sub
ShellError:
MsgBox "Error starting task " & _
txtProgram.Text & vbCrLf & _
Err.Description, vbOKOnly Or vbExclamation, _
"Error"
End Sub
Sub ProcessData()
ShellAndWait (ThisWorkbook.Path & "\Datacleanup.exe")
Range("A2").Select
Range(Selection, Selection.End(xlToRight)).Select
Range(Selection, Selection.End(xlDown)).Select
With Selection
.HorizontalAlignment = xlLeft
.VerticalAlignment = xlTop
.WrapText = True
.Orientation = 0
.AddIndent = False
.IndentLevel = 0
.ShrinkToFit = False
.ReadingOrder = xlContext
.MergeCells = False
End With
Selection.Borders(xlDiagonalDown).LineStyle = xlNone
Selection.Borders(xlDiagonalUp).LineStyle = xlNone
End Sub
Try the WshShell object instead of the native Shell function.
Dim wsh As Object
Set wsh = VBA.CreateObject("WScript.Shell")
Dim waitOnReturn As Boolean: waitOnReturn = True
Dim windowStyle As Integer: windowStyle = 1
Dim errorCode As Long
errorCode = wsh.Run("notepad.exe", windowStyle, waitOnReturn)
If errorCode = 0 Then
MsgBox "Done! No error to report."
Else
MsgBox "Program exited with error code " & errorCode & "."
End If
Though note that:
If bWaitOnReturn is set to false (the default), the Run method returns immediately after starting the program, automatically returning 0 (not to be interpreted as an error code).
So to detect whether the program executed successfully, you need waitOnReturn to be set to True as in my example above. Otherwise it will just return zero no matter what.
For early binding (gives access to Autocompletion), set a reference to "Windows Script Host Object Model" (Tools > Reference > set checkmark) and declare like this:
Dim wsh As WshShell
Set wsh = New WshShell
Now to run your process instead of Notepad... I expect your system will balk at paths containing space characters (...\My Documents\..., ...\Program Files\..., etc.), so you should enclose the path in "quotes":
Dim pth as String
pth = """" & ThisWorkbook.Path & "\ProcessData.exe" & """"
errorCode = wsh.Run(pth , windowStyle, waitOnReturn)
What you have will work once you add
Private Const SYNCHRONIZE = &H100000
which your missing. (Meaning 0 is being passed as the access right to OpenProcess which is not valid)
Making Option Explicit the top line of all your modules would have raised an error in this case
Shell-and-Wait in VBA (Compact Edition)
Sub ShellAndWait(pathFile As String)
With CreateObject("WScript.Shell")
.Run pathFile, 1, True
End With
End Sub
Example Usage:
Sub demo_Wait()
ShellAndWait ("notepad.exe")
Beep 'this won't run until Notepad window is closed
MsgBox "Done!"
End Sub
Adapted from (and more options at) Chip Pearson's site.
The WScript.Shell object's .Run() method as demonstrated in Jean-François Corbett's helpful answer is the right choice if you know that the command you invoke will finish in the expected time frame.
Below is SyncShell(), an alternative that allows you to specify a timeout, inspired by the great ShellAndWait() implementation. (The latter is a bit heavy-handed and sometimes a leaner alternative is preferable.)
' Windows API function declarations.
Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAccessas As Long, ByVal bInheritHandle As Long, ByVal dwProcId As Long) As Long
Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As Long) As Long
Private Declare Function WaitForSingleObject Lib "kernel32.dll" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32.dll" (ByVal hProcess As Long, ByRef lpExitCodeOut As Long) As Integer
' Synchronously executes the specified command and returns its exit code.
' Waits indefinitely for the command to finish, unless you pass a
' timeout value in seconds for `timeoutInSecs`.
Private Function SyncShell(ByVal cmd As String, _
Optional ByVal windowStyle As VbAppWinStyle = vbMinimizedFocus, _
Optional ByVal timeoutInSecs As Double = -1) As Long
Dim pid As Long ' PID (process ID) as returned by Shell().
Dim h As Long ' Process handle
Dim sts As Long ' WinAPI return value
Dim timeoutMs As Long ' WINAPI timeout value
Dim exitCode As Long
' Invoke the command (invariably asynchronously) and store the PID returned.
' Note that this invocation may raise an error.
pid = Shell(cmd, windowStyle)
' Translate the PIP into a process *handle* with the
' SYNCHRONIZE and PROCESS_QUERY_LIMITED_INFORMATION access rights,
' so we can wait for the process to terminate and query its exit code.
' &H100000 == SYNCHRONIZE, &H1000 == PROCESS_QUERY_LIMITED_INFORMATION
h = OpenProcess(&H100000 Or &H1000, 0, pid)
If h = 0 Then
Err.Raise vbObjectError + 1024, , _
"Failed to obtain process handle for process with ID " & pid & "."
End If
' Now wait for the process to terminate.
If timeoutInSecs = -1 Then
timeoutMs = &HFFFF ' INFINITE
Else
timeoutMs = timeoutInSecs * 1000
End If
sts = WaitForSingleObject(h, timeoutMs)
If sts <> 0 Then
Err.Raise vbObjectError + 1025, , _
"Waiting for process with ID " & pid & _
" to terminate timed out, or an unexpected error occurred."
End If
' Obtain the process's exit code.
sts = GetExitCodeProcess(h, exitCode) ' Return value is a BOOL: 1 for true, 0 for false
If sts <> 1 Then
Err.Raise vbObjectError + 1026, , _
"Failed to obtain exit code for process ID " & pid & "."
End If
CloseHandle h
' Return the exit code.
SyncShell = exitCode
End Function
' Example
Sub Main()
Dim cmd As String
Dim exitCode As Long
cmd = "Notepad"
' Synchronously invoke the command and wait
' at most 5 seconds for it to terminate.
exitCode = SyncShell(cmd, vbNormalFocus, 5)
MsgBox "'" & cmd & "' finished with exit code " & exitCode & ".", vbInformation
End Sub
Simpler and Compressed Code with examples:
first declare your path
Dim path: path = ThisWorkbook.Path & "\ProcessData.exe"
And then use any one line of following code you like
1) Shown + waited + exited
VBA.CreateObject("WScript.Shell").Run path,1, True
2) Hidden + waited + exited
VBA.CreateObject("WScript.Shell").Run path,0, True
3) Shown + No waited
VBA.CreateObject("WScript.Shell").Run path,1, False
4) Hidden + No waited
VBA.CreateObject("WScript.Shell").Run path,0, False
I was looking for a simple solution too and finally ended up to make these two functions, so maybe for future enthusiast readers :)
1.) prog must be running, reads tasklist from dos, output status to
file, read file in vba
2.) start prog and wait till prog is closed with a wscript shell .exec waitonrun
3.) ask for confirmation to delete tmp file
Modify program name and path variables and run in one go.
Sub dosWOR_caller()
Dim pwatch As String, ppath As String, pfull As String
pwatch = "vlc.exe" 'process to watch, or process.exe (do NOT use on cmd.exe itself...)
ppath = "C:\Program Files\VideoLAN\VLC" 'path to the program, or ThisWorkbook.Path
pfull = ppath & "\" & pwatch 'extra quotes in cmd line
Dim fout As String 'tmp file for r/w status in 1)
fout = Environ("userprofile") & "\Desktop\dosWaitOnRun_log.txt"
Dim status As Boolean, t As Double
status = False
'1) wait until done
t = Timer
If Not status Then Debug.Print "run prog first for this one! then close it to stop dosWORrun ": Shell (pfull)
status = dosWORrun(pwatch, fout)
If status Then Debug.Print "elapsed time: "; Format(Timer - t, "#.00s")
'2) wait while running
t = Timer
Debug.Print "now running the prog and waiting you close it..."
status = dosWORexec(pfull)
If status = True Then Debug.Print "elapsed time: "; Format(Timer - t, "#.00s")
'3) or if you need user action
With CreateObject("wScript.Shell")
.Run "cmd.exe /c title=.:The end:. & set /p""=Just press [enter] to delete tmp file"" & del " & fout & " & set/p""=and again to quit ;)""", 1, True
End With
End Sub
Function dosWORrun(pwatch As String, fout As String) As Boolean
'redirect sdtout to file, then read status and loop
Dim i As Long, scatch() As String
dosWORrun = False
If pwatch = "cmd.exe" Then Exit Function
With CreateObject("wScript.Shell")
Do
i = i + 1
.Run "cmd /c >""" & fout & """ (tasklist |find """ & pwatch & """ >nul && echo.""still running""|| echo.""done"")", 0, True
scatch = fReadb(fout)
Debug.Print i; scatch(0)
Loop Until scatch(0) = """done"""
End With
dosWORrun = True
End Function
Function dosWORexec(pwatch As String) As Boolean
'the trick: with .exec method, use .stdout.readall of the WshlExec object to force vba to wait too!
Dim scatch() As String, y As Object
dosWORexec = False
With CreateObject("wScript.Shell")
Set y = .exec("cmd.exe /k """ & pwatch & """ & exit")
scatch = Split(y.stdout.readall, vbNewLine)
Debug.Print y.status
Set y = Nothing
End With
dosWORexec = True
End Function
Function fReadb(txtfile As String) As String()
'fast read
Dim ff As Long, data As String
'~~. Open as txt File and read it in one go into memory
ff = FreeFile
Open txtfile For Binary As #ff
data = Space$(LOF(1))
Get #ff, , data
Close #ff
'~~> Store content in array
fReadb = Split(data, vbCrLf)
'~~ skip last crlf
If UBound(fReadb) <> -1 Then ReDim Preserve fReadb(0 To UBound(fReadb) - 1)
End Function
I incorporated this into a routine, and it has worked fine (but not used very often) for several years - for which, many thanks !
But now I find it throws up an error :-
Run-time error '-2147024894 (80070002)':
Method 'Run' of object 'IWshSheB' failed
on the line -
ErrorCode = wsh.Run(myCommand, windowStyle, WaitOnReturn)
Very strange !
5 hours later !
I THINK the reason it fails is that dear MicroSoft ("dear" meaning expensive) has changed something radical - "Shell" USED to be "Shell to DOS", but has that been changed >=?
The "Command" that I want the Shell to run is simply DIR
In full, it is "DIR C:\Folder\ /S >myFIle.txt"
. . . . . . . . . . . . . . . . . . . . . .
An hour after that-
Yup !
I have "solved" it by using this Code, which works just fine :-
Sub ShellAndWait(PathFile As String, _
Optional Wait As Boolean = True, _
Optional Hidden As Boolean = True)
' Hidden = 0; Shown = 1
Dim Hash As Integer, myBat As String, Shown As Integer
Shown = 0
If Hidden Then Shown = 1
If Hidden <> 0 Then Hidden = 1
Hash = FreeFile
myBat = "C:\Users\Public\myBat.bat"
Open myBat For Output As #Hash
Print #Hash, PathFile
Close #Hash
With CreateObject("WScript.Shell")
.Run myBat, Shown, Wait
End With
End Sub
I would come at this by using the Timer function. Figure out roughly how long you'd like the macro to pause while the .exe does its thing, and then change the '10' in the commented line to whatever time (in seconds) that you'd like.
Strt = Timer
Shell (ThisWorkbook.Path & "\ProcessData.exe")
Do While Timer < Strt + 10 'This line loops the code for 10 seconds
Loop
UserForm2.Hide
'Additional lines to set formatting
This should do the trick, let me know if not.
Cheers, Ben.

Bandwidth calculation (Internet data transfer)

I have used the following code to get bandwidth
It works but i have following doubts
1.I need Total Internet Data transferred in MB, how do i convert?, total data transferred (download+upload) varies with other bandwidth monitoring applications. how do i get exact data transferred?
2.I need to exclude file transfer in local LAN in Data Transfer, The follwoing method includes internet data transfer + Local file transfer
Option Explicit
Public Enum OperationalStates
MIB_IF_OPER_STATUS_NON_OPERATIONAL = 0
MIB_IF_OPER_STATUS_UNREACHABLE = 1
MIB_IF_OPER_STATUS_DISCONNECTED = 2
MIB_IF_OPER_STATUS_CONNECTING = 3
MIB_IF_OPER_STATUS_CONNECTED = 4
MIB_IF_OPER_STATUS_OPERATIONAL = 5
End Enum
Public Enum InterfaceTypes
MIB_IF_TYPE_OTHER = 1
MIB_IF_TYPE_ETHERNET = 6
MIB_IF_TYPE_TOKENRING = 9
MIB_IF_TYPE_FDDI = 15
MIB_IF_TYPE_PPP = 23
MIB_IF_TYPE_LOOPBACK = 24
MIB_IF_TYPE_SLIP = 28
End Enum
Public Enum AdminStatuses
MIB_IF_ADMIN_STATUS_UP = 1
MIB_IF_ADMIN_STATUS_DOWN = 2
MIB_IF_ADMIN_STATUS_TESTING = 3
End Enum
Private Const MAXLEN_IFDESCR As Integer = 256
Private Const MAXLEN_PHYSADDR As Integer = 8
Private Const MAX_INTERFACE_NAME_LEN As Integer = 256
Private Const ERROR_NOT_SUPPORTED As Long = 50
Private Const ERROR_SUCCESS As Long = 0
Private Type MIB_IFROW
wszName(0 To 511) As Byte
dwIndex As Long '// index of the interface
dwType As Long '// type of interface
dwMtu As Long '// max transmission unit
dwSpeed As Long '// speed of the interface
dwPhysAddrLen As Long '// length of physical address
bPhysAddr(0 To 7) As Byte '// physical address of adapter
dwAdminStatus As Long '// administrative status
dwOperStatus As Long '// operational status
dwLastChange As Long
dwInOctets As Long '// octets received
dwInUcastPkts As Long '// unicast packets received
dwInNUcastPkts As Long '// non-unicast packets received
dwInDiscards As Long '// received packets discarded
dwInErrors As Long '// erroneous packets received
dwInUnknownProtos As Long
dwOutOctets As Long '// octets sent
dwOutUcastPkts As Long '// unicast packets sent
dwOutNUcastPkts As Long '// non-unicast packets sent
dwOutDiscards As Long '// outgoing packets discarded
dwOutErrors As Long '// erroneous packets sent
dwOutQLen As Long '// output queue length
dwDescrLen As Long '// length of bDescr member
bDescr(0 To 255) As Byte '// interface description
End Type
Private m_lngBytesReceived As Long
Private m_lngBytesSent As Long
Private Declare Function GetIfTable _
Lib "IPhlpAPI" (ByRef pIfRowTable As Any, _
ByRef pdwSize As Long, _
ByVal bOrder As Long) As Long
Private Declare Sub CopyMemory _
Lib "kernel32" _
Alias "RtlMoveMemory" (ByRef pDest As Any, _
ByRef pSource As Any, _
ByVal Length As Long)
Private Declare Function FreeResource Lib "kernel32" (ByVal hResData As Long) As Long
Public Property Get BytesReceived() As Long
BytesReceived = m_lngBytesReceived
End Property
Public Property Get BytesSent() As Long
BytesSent = m_lngBytesSent
End Property
Public Function InitInterfaces() As Boolean
Dim arrBuffer() As Byte
Dim lngSize As Long
Dim lngRetVal As Long
Dim Name As String
Dim lngRows As Long
Dim lngRow As Long
Dim i As Integer
Dim j As Integer
Dim IfRowTable As MIB_IFROW
On Error GoTo err
lngSize = 0
m_lngBytesReceived = 0
m_lngBytesSent = 0
lngRetVal = GetIfTable(ByVal 0&, lngSize, 0)
If lngRetVal = ERROR_NOT_SUPPORTED Then
Exit Function
End If
ReDim arrBuffer(0 To lngSize - 1) As Byte
lngRetVal = GetIfTable(arrBuffer(0), lngSize, 0)
If lngRetVal = ERROR_SUCCESS Then
CopyMemory lngRows, arrBuffer(0), 4
If lngRows >= 1 Then
For lngRow = 1 To lngRows
CopyMemory IfRowTable, arrBuffer(4 + (lngRow - 1) * Len(IfRowTable)), Len(IfRowTable)
For i = 0 To 25
Name = Name & Chr(IfRowTable.bDescr(i))
If IfRowTable.bDescr(i) = Chr(0) Then GoTo ok
Next
ok:
If Not InStr(1, Name, "loop", vbTextCompare) > 0 Then
With IfRowTable
m_lngBytesReceived = m_lngBytesReceived + .dwInOctets
m_lngBytesSent = m_lngBytesSent + .dwOutOctets
End With 'IFROWTABLE
'Set IfRowTable = Nothing
InitInterfaces = True
End If
Name = vbNullString
Next
Erase arrBuffer
End If
End If
On Error GoTo 0
Exit Function
err:
Call GErrorHandler(err.Number, err.Description, "CIPHelper:InitInterfaces:" & err.Source, True)
End Function
Private Sub GetBandwidth()
Dim c As New CIpHelper, R As Double, s As Double
Dim r1 As Double, c1 As Double, SendBytes1 As Double, ReceivedBytes1 As Double
On Error GoTo errh:
c.InitInterfaces
If FirstTime Then
FirstTime = False
SendBytes = Format(c.BytesSent / 1024, ".0")
ReceivedBytes = Format(c.BytesReceived / 1024, ".0")
SendBytes1 = c.BytesSent
ReceivedBytes1 = c.BytesReceived
Else 'FIRSTTIME = FALSE/0
R = ((c.BytesReceived / 1024) - ReceivedBytes)
s = ((c.BytesSent / 1024) - SendBytes)
End If
lblBandwidthUsed = R+s
OldR = R
OldS = s
On Error GoTo 0
Exit Sub
errh:
Call GErrorHandler(err.Number, err.Description, "ScreenBlock:GetBandwidth:" & err.Source, True)
End Sub
1) 1024 bytes = 1 kB and 1024 kB = 1 MB. In other words, divide the number of kilobytes by 1024.
2) I assume if you don't want to monitor LAN traffic, you want to monitor Wireless traffic. That might be a little tricky to do as a generic solution, but if you know the MAC address of your lan network card, you can exclude it in the calculations which looks like the "bPhysAddr" variable to me.
You can get the MAC address of a PC executing the command in the command line:
ipconfig /all