Check if mapped network available - vba

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

Related

Save image from clipboard as .jpg and pass relative link to Access form

Using Access 365. I would like help building code to achieve the following please, I’m trying to streamline adding images to a record.
I would like to use VBA that on clicking a button will save an image as a .jpg from the clipboard (put there by User using Snip tool) to a subfolder of the database, then pass a relative link to this file to the form. I’d like to be able to attach multiple images’ links to a given record in this manner.
Using the code below (without the AltPrintScreen element) I’ve gotten as far as saving from the clipboard and generating an absolute link, but only as a .bmp. (https://www.access-programmers.co.uk/forums/threads/print-screen-into-image-file.245198/). Grateful for any help getting the rest of the way, or suggestion of an entirely different way of doing it. Cheers!
Module...
'***********************************************************************************************
' *
' * Please leave any Trademarks or Credits in place.
' *
' * ACKNOWLEDGEMENT TO CONTRIBUTORS :
' * STEPHEN BULLEN, 15 November 1998 - Original PastPicture code
' * G HUDSON, 5 April 2010 - Pause Function
' * LUTZ GENTKOW, 23 July 2011 - Alt + PrtScrn
' * PAUL FRANCIS, 11 April 2013 - Putting all pieces togeather
' *
' * DESCRIPTION: Creates a standard Picture object from whatever is on the clipboard.
' * This object is then saved to a location on the disc. Please note, this
' * can also be assigned to (for example) and Image control on a userform.
' *
' * The code requires a reference to the "OLE Automation" type library.
' *
' * The code in this module has been derived from a number of sources
' * discovered on MSDN, Access World Forum, VBForums.
' *
' * To use it, just copy this module into your project, then you can use:
' * SaveClip2Bit("C:\Pics\Sample.bmp")
' * to save this to a location on the Disc.
' * (Or)
' * Set ImageControl.Image = PastePicture
' * to paste a picture of whatever is on the clipboard into a standard image control.
' *
' * PROCEDURES:
' * PastePicture : The entry point for 'Setting' the Image
' * CreatePicture : Private function to convert a bitmap or metafile handle to an OLE reference
' * fnOLEError : Get the error text for an OLE error code
' * SaveClip2Bit : The entry point for 'Saving' the Image, calls for PastePicture
' * AltPrintScreen: Performs the automation of Alt + PrtScrn, for getting the Active Window.
' * Pause : Makes the program wait, to make sure proper screen capture takes place.
'**************************************************************************************************
Option Explicit
Option Compare Text
'Declare clipboard clear
Public Declare PtrSafe Function OpenClipboard Lib "user32" (ByVal hwnd As Long) As Long
Public Declare PtrSafe Function CloseClipboard Lib "user32" () As Long
Public Declare PtrSafe Function EmptyClipboard Lib "user32" () As Long
'Declare a UDT to store a GUID for the IPicture OLE Interface
Private Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(0 To 7) As Byte
End Type
'Declare a UDT to store the bitmap information
Private Type uPicDesc
Size As Long
Type As Long
hPic As Long
hPal As Long
End Type
'Windows API Function Declarations
#If Win64 = 1 And VBA7 = 1 Then
'Does the clipboard contain a bitmap/metafile?
Private Declare PtrSafe Function IsClipboardFormatAvailable Lib "user32" (ByVal wFormat As Integer) As Long
'Open the clipboard to read
'Private Declare PtrSafe Function OpenClipboard Lib "user32" (ByVal hwnd As Long) As Long
'Get a pointer to the bitmap/metafile
Private Declare PtrSafe Function GetClipboardData Lib "user32" (ByVal wFormat As Integer) As Long
'Close the clipboard
'Private Declare PtrSafe Function CloseClipboard Lib "user32" () As Long
'Convert the handle into an OLE IPicture interface.
Private Declare PtrSafe Function OleCreatePictureIndirect Lib "oleaut32.dll" (PicDesc As uPicDesc, RefIID As GUID, ByVal fPictureOwnsHandle As Long, IPic As IPicture) As Long
'Create our own copy of the metafile, so it doesn't get wiped out by subsequent clipboard updates.
Declare PtrSafe Function CopyEnhMetaFile Lib "gdi32" Alias "CopyEnhMetaFileA" (ByVal hemfSrc As Long, ByVal lpszFile As String) As Long
'Create our own copy of the bitmap, so it doesn't get wiped out by subsequent clipboard updates.
Declare PtrSafe Function CopyImage Lib "user32" (ByVal handle As Long, ByVal un1 As Long, ByVal n1 As Long, ByVal n2 As Long, ByVal un2 As Long) As Long
'Uses the Keyboard simulation
Private Declare PtrSafe Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long)
#Else
'Does the clipboard contain a bitmap/metafile?
Private Declare Function IsClipboardFormatAvailable Lib "user32" (ByVal wFormat As Integer) As Long
'Open the clipboard to read
Private Declare Function OpenClipboard Lib "user32" (ByVal hwnd As Long) As Long
'Get a pointer to the bitmap/metafile
Private Declare Function GetClipboardData Lib "user32" (ByVal wFormat As Integer) As Long
'Close the clipboard
Private Declare Function CloseClipboard Lib "user32" () As Long
'Convert the handle into an OLE IPicture interface.
Private Declare Function OleCreatePictureIndirect Lib "oleaut32.dll" (PicDesc As uPicDesc, RefIID As GUID, ByVal fPictureOwnsHandle As Long, IPic As IPicture) As Long
'Create our own copy of the metafile, so it doesn't get wiped out by subsequent clipboard updates.
Declare Function CopyEnhMetaFile Lib "gdi32" Alias "CopyEnhMetaFileA" (ByVal hemfSrc As Long, ByVal lpszFile As String) As Long
'Create our own copy of the bitmap, so it doesn't get wiped out by subsequent clipboard updates.
Declare Function CopyImage Lib "user32" (ByVal handle As Long, ByVal un1 As Long, ByVal n1 As Long, ByVal n2 As Long, ByVal un2 As Long) As Long
'Uses the Keyboard simulation
Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long)
#End If
'The API format types we're interested in
Const CF_BITMAP = 2
Const CF_PALETTE = 9
Const CF_ENHMETAFILE = 14
Const IMAGE_BITMAP = 0
Const LR_COPYRETURNORG = &H4
Private Const KEYEVENTF_KEYUP = &H2
Private Const VK_SNAPSHOT = &H2C
Private Const VK_MENU = &H12
' Subroutine : AltPrintScreen
' Purpose : Capture the Active window, and places on the Clipboard.
Sub AltPrintScreen()
'keybd_event VK_MENU, 0, 0, 0
'keybd_event VK_SNAPSHOT, 0, 0, 0
'keybd_event VK_SNAPSHOT, 0, KEYEVENTF_KEYUP, 0
'keybd_event VK_MENU, 0, KEYEVENTF_KEYUP, 0
End Sub
' Subroutine : PastePicture
' Purpose : Get a Picture object showing whatever's on the clipboard.
Function PastePicture() As IPicture
'Some pointers
Dim h As Long, hPtr As Long, hPal As Long, lPicType As Long, hCopy As Long
'Check if the clipboard contains the required format
If IsClipboardFormatAvailable(CF_BITMAP) Then
'Get access to the clipboard
h = OpenClipboard(0&)
If h > 0 Then
'Get a handle to the image data
hPtr = GetClipboardData(CF_BITMAP)
hCopy = CopyImage(hPtr, IMAGE_BITMAP, 0, 0, LR_COPYRETURNORG)
'Release the clipboard to other programs
h = CloseClipboard
'If we got a handle to the image, convert it into a Picture object and return it
If hPtr <> 0 Then Set PastePicture = CreatePicture(hCopy, 0, CF_BITMAP)
End If
End If
End Function
' Subroutine : CreatePicture
' Purpose : Converts a image (and palette) handle into a Picture object.
' NOTE : Requires a reference to the "OLE Automation" type library
Private Function CreatePicture(ByVal hPic As Long, ByVal hPal As Long, ByVal lPicType) As IPicture
' IPicture requires a reference to "OLE Automation"
Dim r As Long, uPicInfo As uPicDesc, IID_IDispatch As GUID, IPic As IPicture
'OLE Picture types
Const PICTYPE_BITMAP = 1
Const PICTYPE_ENHMETAFILE = 4
' Create the Interface GUID (for the IPicture interface)
With IID_IDispatch
.Data1 = &H7BF80980
.Data2 = &HBF32
.Data3 = &H101A
.Data4(0) = &H8B
.Data4(1) = &HBB
.Data4(2) = &H0
.Data4(3) = &HAA
.Data4(4) = &H0
.Data4(5) = &H30
.Data4(6) = &HC
.Data4(7) = &HAB
End With
' Fill uPicInfo with necessary parts.
With uPicInfo
.Size = Len(uPicInfo) ' Length of structure.
.Type = PICTYPE_BITMAP ' Type of Picture
.hPic = hPic ' Handle to image.
.hPal = hPal ' Handle to palette (if bitmap).
End With
' Create the Picture object.
r = OleCreatePictureIndirect(uPicInfo, IID_IDispatch, True, IPic)
' If an error occured, show the description
If r <> 0 Then Debug.Print "Create Picture: " & fnOLEError(r)
' Return the new Picture object.
Set CreatePicture = IPic
End Function
' Subroutine : fnOLEError
' Purpose : Gets the message text for standard OLE errors
Private Function fnOLEError(lErrNum As Long) As String
'OLECreatePictureIndirect return values
Const E_ABORT = &H80004004
Const E_ACCESSDENIED = &H80070005
Const E_FAIL = &H80004005
Const E_HANDLE = &H80070006
Const E_INVALIDARG = &H80070057
Const E_NOINTERFACE = &H80004002
Const E_NOTIMPL = &H80004001
Const E_OUTOFMEMORY = &H8007000E
Const E_POINTER = &H80004003
Const E_UNEXPECTED = &H8000FFFF
Const S_OK = &H0
Select Case lErrNum
Case E_ABORT
fnOLEError = " Aborted"
Case E_ACCESSDENIED
fnOLEError = " Access Denied"
Case E_FAIL
fnOLEError = " General Failure"
Case E_HANDLE
fnOLEError = " Bad/Missing Handle"
Case E_INVALIDARG
fnOLEError = " Invalid Argument"
Case E_NOINTERFACE
fnOLEError = " No Interface"
Case E_NOTIMPL
fnOLEError = " Not Implemented"
Case E_OUTOFMEMORY
fnOLEError = " Out of Memory"
Case E_POINTER
fnOLEError = " Invalid Pointer"
Case E_UNEXPECTED
fnOLEError = " Unknown Error"
Case S_OK
fnOLEError = " Success!"
End Select
End Function
' Routine : SaveClip2Bit
' Purpose : Saves Picture object to desired location.
' Arguments : Path to save the file
Public Sub SaveClip2Bit(savePath As String)
On Error GoTo errHandler:
AltPrintScreen
Pause (2)
SavePicture PastePicture, savePath
errExit:
Exit Sub
errHandler:
Debug.Print "Save Picture: (" & Err.Number & ") - " & Err.Description
Resume errExit
End Sub
' Routine : Pause
' Purpose : Gives a short intreval for proper image capture.
' Arguments : Seconds to wait.
Public Function Pause(NumberOfSeconds As Variant)
On Error GoTo Err_Pause
Dim PauseTime As Variant, start As Variant
PauseTime = NumberOfSeconds
start = Timer
Do While Timer < start + PauseTime
DoEvents
Loop
Exit_Pause:
Exit Function
Err_Pause:
MsgBox Err.Number & " - " & Err.Description, vbCritical, "Pause()"
Resume Exit_Pause
End Function
I've created a solution that does what I want. I've used four Modules I found on various sites and then created a bit of VBA to pull the tasks together in the background.
Module 1 is the one I posted in the question above. I bypass the
AltPrintScreen part of it so it doesn't overwrite the image already in the
clipboard.
Module 2 Converts the BMP created by Modules 1 into a JPG.
Module 3 Scans a folder and populates an unbound listbox with a list
of the files found there.
Module 4 is used to make the filepaths in the listbox act as
hyperlinks to open the images on clicking.
I then created a button on a form that calls the first two Module to create a jpg from the image in the clipboard as follows...
Private Sub Command12_Click()
Dim Foldername As String
Dim FileRoot As String
Dim FilePathBMP As String
Dim FilePathJPG As String
Dim FilePathJPG2 As String
' Fist check to see if a unique folder for the open record exists, create if not
Foldername = CurrentProject.Path & "\xrays\" & Format(record_id.Value)
If Len(Dir(Foldername, vbDirectory)) = 0 Then
MkDir CurrentProject.Path & "\xrays\" & Format(record_id.Value)
End If
On Error GoTo reportErr
'The filename will begin with a date\time stamp then pull text from drop down lists the user can pick. This first creates a base filename without filtype extensions.
FileRoot = CurrentProject.Path & "\xrays\" & Format(record_id.Value) & "\" & Format(Now, "yyyymmddhhmmss") & "_" & Format(Combo14.Value) & "_" & Format(Combo21.Value)
'Creates a BMP and JPG version of the filename
FilePathBMP = FileRoot & ".bmp"
FilePathJPG = FileRoot & ".jpg"
'Save a BMP
SaveClip2Bit FilePathBMP
'Convert to JPG
WIA_ConvertImage FilePathBMP, FilePathJPG, JPEG, 85
'Delete the BMP
Kill (FilePathBMP)
Exit Sub
reportErr:
MsgBox "No image in Clipboard"
Resume Next
End Sub
On the form that will display the file list I've placed an unbound listbox "FileList". The following code calls on Module 3 and is in the OnLoad event of the form...
Call ListFiles(CurrentProject.Path & "\xrays\" & Format(record_id.Value), , , Me.FileList)
The following code is used to refresh the listbox "FileList". It can be put on an OnClick event of a button or added to the end of the above code to automatically refresh after adding a new image
Me.FileList.RowSource = ""
Call ListFiles(CurrentProject.Path & "\xrays\" & Format(record_id.Value), , , Me.FileList)
Me.FileList.Requery
Finally I put the following code in the OnDblClick event of the listbox, which calls Module 4...
Dim sPath As String
Dim sFile As String
sPath = FileList.Column(0)
' The line below extracts the filename from the full path (everything to the right of the last /)
' sFile = Right(sPath, Len(sPath) - InStrRev(sPath, "\"))
GoHyperlink (sPath)
Module 2...
Public Enum wiaFormat
BMP = 0
GIF = 1
JPEG = 2
PNG = 3
TIFF = 4
End Enum
'---------------------------------------------------------------------------------------
' Procedure : WIA_ConvertImage
' Author : Daniel Pineault, CARDA Consultants Inc.
' Website : http://www.cardaconsultants.com
' Purpose : Convert an image's format using WIA
' Copyright : The following is release as Attribution-ShareAlike 4.0 International
' (CC BY-SA 4.0) - https://creativecommons.org/licenses/by-sa/4.0/
' Req'd Refs: Uses Late Binding, so none required
'
' Windows Image Acquisition (WIA)
' https://msdn.microsoft.com/en-us/library/windows/desktop/ms630368(v=vs.85).aspx
'
' Input Variables:
' ~~~~~~~~~~~~~~~~
' sInitialImage : Fully qualified path and filename of the original image to resize
' sOutputImage : Fully qualified path and filename of where to save the new image
' lFormat : Format to convert the image into
' lQuality : Quality level to be used for the conversion process (1-100)
'
' Usage:
' ~~~~~~
' Call WIA_ConvertImage("C:\Users\Public\Pictures\Sample Pictures\Chrysanthemum.jpg", _
' "C:\Users\MyUser\Desktop\Chrysanthemum_2.jpg", _
' JPEG)
'
' Revision History:
' Rev Date(yyyy/mm/dd) Description
' **************************************************************************************
' 1 2017-01-18 Initial Release
' 2 2018-09-20 Updated Copyright
'---------------------------------------------------------------------------------------
Public Function WIA_ConvertImage(sInitialImage As String, _
sOutputImage As String, _
lFormat As wiaFormat, _
Optional lQuality As Long = 85) As Boolean
On Error GoTo Error_Handler
Dim oWIA As Object 'WIA.ImageFile
Dim oIP As Object 'ImageProcess
Dim sFormatID As String
Dim sExt As String
'Convert our Enum over to the proper value used by WIA
Select Case lFormat
Case 0
sFormatID = "{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}"
sExt = "BMP"
Case 1
sFormatID = "{B96B3CB0-0728-11D3-9D7B-0000F81EF32E}"
sExt = "GIF"
Case 2
sFormatID = "{B96B3CAE-0728-11D3-9D7B-0000F81EF32E}"
sExt = "JPEG"
Case 3
sFormatID = "{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}"
sExt = "PNG"
Case 4
sFormatID = "{B96B3CB1-0728-11D3-9D7B-0000F81EF32E}"
sExt = "TIFF"
End Select
If lQuality > 100 Then lQuality = 100
'Should check if the output file already exists and if so,
'prompt the user to overwrite it or not
Set oWIA = CreateObject("WIA.ImageFile")
Set oIP = CreateObject("WIA.ImageProcess")
oIP.Filters.Add oIP.FilterInfos("Convert").FilterID
oIP.Filters(1).Properties("FormatID") = sFormatID
oIP.Filters(1).Properties("Quality") = lQuality
oWIA.LoadFile sInitialImage
Set oWIA = oIP.Apply(oWIA)
'Overide the specified ext with the appropriate one for the choosen format
oWIA.SaveFile Left(sOutputImage, InStrRev(sOutputImage, ".")) & LCase(sExt)
WIA_ConvertImage = True
Error_Handler_Exit:
On Error Resume Next
If Not oIP Is Nothing Then Set oIP = Nothing
If Not oWIA Is Nothing Then Set oWIA = Nothing
Exit Function
Error_Handler:
MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
"Error Number: " & Err.Number & vbCrLf & _
"Error Source: WIA_ConvertImage" & vbCrLf & _
"Error Description: " & Err.Description & _
Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
, vbOKOnly + vbCritical, "An Error has Occurred!"
Resume Error_Handler_Exit
End Function
Module 3...
Option Compare Database
Public Function ListFiles(strPath As String, Optional strFileSpec As String, _
Optional bIncludeSubfolders As Boolean, Optional lst As ListBox)
On Error GoTo Err_Handler
'Purpose: List the files in the path.
'Arguments: strPath = the path to search.
' strFileSpec = "*.*" unless you specify differently.
' bIncludeSubfolders: If True, returns results from subdirectories of strPath as well.
' lst: if you pass in a list box, items are added to it. If not, files are listed to immediate window.
' The list box must have its Row Source Type property set to Value List.
'Method: FilDir() adds items to a collection, calling itself recursively for subfolders.
Dim colDirList As New Collection
Dim varItem As Variant
Call FillDir(colDirList, strPath, strFileSpec, bIncludeSubfolders)
'Add the files to a list box if one was passed in. Otherwise list to the Immediate Window.
If lst Is Nothing Then
For Each varItem In colDirList
Debug.Print varItem
Next
Else
For Each varItem In colDirList
lst.AddItem varItem
Next
End If
Exit_Handler:
Exit Function
Err_Handler:
MsgBox "Error " & Err.Number & ": " & Err.Description
Resume Exit_Handler
End Function
Private Function FillDir(colDirList As Collection, ByVal strFolder As String, strFileSpec As String, _
bIncludeSubfolders As Boolean)
'Build up a list of files, and then add add to this list, any additional folders
Dim strTemp As String
Dim colFolders As New Collection
Dim vFolderName As Variant
'Add the files to the folder.
strFolder = TrailingSlash(strFolder)
strTemp = Dir(strFolder & strFileSpec)
Do While strTemp <> vbNullString
colDirList.Add strFolder & strTemp
strTemp = Dir
Loop
If bIncludeSubfolders Then
'Build collection of additional subfolders.
strTemp = Dir(strFolder, vbDirectory)
Do While strTemp <> vbNullString
If (strTemp <> ".") And (strTemp <> "..") Then
If (GetAttr(strFolder & strTemp) And vbDirectory) <> 0& Then
colFolders.Add strTemp
End If
End If
strTemp = Dir
Loop
'Call function recursively for each subfolder.
For Each vFolderName In colFolders
Call FillDir(colDirList, strFolder & TrailingSlash(vFolderName), strFileSpec, True)
Next vFolderName
End If
End Function
Public Function TrailingSlash(varIn As Variant) As String
If Len(varIn) > 0& Then
If Right(varIn, 1&) = "\" Then
TrailingSlash = varIn
Else
TrailingSlash = varIn & "\"
End If
End If
End Function
Module 4...
Option Compare Database
Option Explicit
'Purpose: Avoid warning and error messages when opening files with FollowHyperlink
'Author: Allen Browne (allen#allenbrowne.com)
'Release: 28 January 2008
'Usage: To open MyFile.doc in Word, use:
' GoHyperlink "MyFile.doc"
' instead of:
' FollowHyperlink "MyFile.doc"
'Rationale:
'FollowHyperlink has several problems:
' a) It errors if a file name contains characters such as #, %, or &.
' b) It can give unwanted warnings, e.g. on a fileame with "file:///" prefix.
' c) It yields errors if the link did not open.
'This replacement:
' a) escapes the problem characters
' b) prepends the prefix
' c) returns True if the link opened (with an optional error message if you care.)
'Limitations:
' - If a file name contains two # characters, it is treated as a hyperlink.
' - If a file name contains % followed by 2 hex digits, it assumes it is pre-escaped.
' - File name must include path.
'Documentation: http://allenbrowne.com/func-GoHyperlink.html
Public Function GoHyperlink(FullFilenameOrLink As Variant) As Boolean
On Error GoTo Err_Handler
'Purpose: Replacement for FollowHyperlink.
'Return: True if the hyperlink opened.
'Argument: varIn = the link to open
Dim strLink As String
Dim strErrMsg As String
'Skip error, null, or zero-length string.
If Not IsError(FullFilenameOrLink) Then
If FullFilenameOrLink <> vbNullString Then
strLink = PrepHyperlink(FullFilenameOrLink, strErrMsg)
If strLink <> vbNullString Then
FollowHyperlink strLink
'Return True if we got here without error.
GoHyperlink = True
End If
'Display any error message from preparing the link.
If strErrMsg <> vbNullString Then
MsgBox strErrMsg, vbExclamation, "PrepHyperlink()"
End If
End If
End If
Exit_Handler:
Exit Function
Err_Handler:
MsgBox "Error " & Err.Number & ": " & Err.Description, vbExclamation, "GoHyperlink()"
Resume Exit_Handler
End Function
Public Function PrepHyperlink(varIn As Variant, Optional strErrMsg As String) As Variant
On Error GoTo Err_Handler
'Purpose: Avoid errors and warnings when opening hyperlinks.
'Return: The massaged link/file name.
'Arguments: varIn = the link/file name to massage.
' strErrMsg = string to append error messages to.
'Note: Called by GoHyperlink() above.
' Can also be called directly, to prepare hyperlinks.
Dim strAddress As String 'File name or address
Dim strDisplay As String 'Display part of hyperlink (if provided)
Dim strTail As String 'Any remainding part of hyperlink after address
Dim lngPos1 As Long 'Position of character in string (and next)
Dim lngPos2 As Long
Dim bIsHyperlink As Boolean 'Flag if input is a hyperlink (not just a file name.)
Const strcDelimiter = "#" 'Delimiter character within hyperlinks.
Const strcEscChar = "%" 'Escape character for hyperlinks.
Const strcPrefix As String = "file:///" 'Hyperlink type if not supplied.
If Not IsError(varIn) Then
strAddress = Nz(varIn, vbNullString)
End If
If strAddress <> vbNullString Then
'Treat as a hyperlink if there are two or more # characters (other than together, or at the end.)
lngPos1 = InStr(strAddress, strcDelimiter)
If (lngPos1 > 0&) And (lngPos1 < Len(strAddress) - 2&) Then
lngPos2 = InStr(lngPos1 + 1&, strAddress, strcDelimiter)
End If
If lngPos2 > lngPos1 + 1& Then
bIsHyperlink = True
strTail = Mid$(strAddress, lngPos2 + 1&)
strDisplay = Left$(strAddress, lngPos1 - 1&)
strAddress = Mid$(strAddress, lngPos1 + 1&, lngPos2 - lngPos1)
End If
'Replace any % that is not immediately followed by 2 hex digits (in both display and address.)
strAddress = EscChar(strAddress, strcEscChar)
strDisplay = EscChar(strDisplay, strcEscChar)
'Replace special characters with percent sign and hex value (address only.)
strAddress = EscHex(strAddress, strcEscChar, "&", """", " ", "#", "<", ">", "|", "*", "?")
'Replace backslash with forward slash (address only.)
strAddress = Replace(strAddress, "\", "/")
'Add prefix if address doesn't have one.
If Not ((varIn Like "*://*") Or (varIn Like "mailto:*")) Then
strAddress = strcPrefix & strAddress
End If
End If
'Assign return value.
If strAddress <> vbNullString Then
If bIsHyperlink Then
PrepHyperlink = strDisplay & strcDelimiter & strAddress & strcDelimiter & strTail
Else
PrepHyperlink = strAddress
End If
Else
PrepHyperlink = Null
End If
Exit_Handler:
Exit Function
Err_Handler:
strErrMsg = strErrMsg & "Error " & Err.Number & ": " & Err.Description & vbCrLf
Resume Exit_Handler
End Function
Private Function EscChar(ByVal strIn As String, strEscChar As String) As String
'Purpose: If the escape character is found in the string,
' escape it (unless it is followed by 2 hex digits.)
'Return: Fixed up string.
'Arguments: strIn = the string to fix up
' strEscChar = the single character used for escape sequqnces. (% for hyperlinks.)
Dim strOut As String 'output string.
Dim strChar As String 'character being considered.
Dim strTestHex As String '4-character string of the form &HFF.
Dim lngLen As Long 'Length of input string.
Dim i As Long 'Loop controller
Dim bReplace As Boolean 'Flag to replace character.
lngLen = Len(strIn)
If (lngLen > 0&) And (Len(strEscChar) = 1&) Then
For i = 1& To lngLen
bReplace = False
strChar = Mid(strIn, i, 1&)
If strChar = strEscChar Then
strTestHex = "&H" & Mid(strIn, i + 1&, 2&)
If Len(strTestHex) = 4& Then
If Not IsNumeric(strTestHex) Then
bReplace = True
End If
End If
End If
If bReplace Then
strOut = strOut & strEscChar & Hex(Asc(strEscChar))
Else
strOut = strOut & strChar
End If
Next
End If
If strOut <> vbNullString Then
EscChar = strOut
ElseIf lngLen > 0& Then
EscChar = strIn
End If
End Function
Private Function EscHex(ByVal strIn As String, strEscChar As String, ParamArray varChars()) As String
'Purpose: Replace any characters from the array with the escape character and their hex value.
'Return: Fixed up string.
'Arguments: strIn = string to fix up.
' strEscChar = the single character used for escape sequqnces. (% for hyperlinks.)
' varChars() = an array of single-character strings to replace.
Dim i As Long 'Loop controller
If (strIn <> vbNullString) And IsArray(varChars) Then
For i = LBound(varChars) To UBound(varChars)
strIn = Replace(strIn, varChars(i), strEscChar & Hex(Asc(varChars(i))))
Next
End If
EscHex = strIn
End Function
I hope all that helps others!

Word 2010 VBA to select printer without changing system default printer

In Word 2010, I'm trying to create a macro that sets the current printer to a specific color printer on our network, without making that printer the user's system default printer. I've hacked together some code below from samples I've found on the web. Everything works, except that the SetColorPrinterEast Sub changes the user's system default printer, which I do not want. I suspect the DoNotSetAsSysDefault in that sub is not working as intended, but I don't know what to do about it. See the comments in the code for further explanation. Any thoughts will be greatly appreciated. Thanks in advance!!!
'I found the code block below on the web. I don't understand it, but
'it seems to work properly with the "SetDefaultPrinter"
'Sub below to get the system default printer.
Public Declare Function GetProfileString Lib "kernel32" _
Alias "GetProfileStringA" _
(ByVal lpAppName As String, _
ByVal lpKeyName As String, _
ByVal lpDefault As String, _
ByVal lpReturnedString As String, _
ByVal nSize As Long) As Long
' This code successfully sets the document to print from
' the system default printer.
Public Sub SetDefaultPrinter()
Dim strReturn As String
Dim intReturn As Integer
strReturn = Space(255)
intReturn = GetProfileString("Windows", ByVal "device", "", _
strReturn, Len(strReturn))
If intReturn Then
strReturn = UCase(Left(strReturn, InStr(strReturn, ",") - 1))
End If
With Dialogs(wdDialogFilePrintSetup)
.Printer = strReturn
.DoNotSetAsSysDefault = True
.Execute
End With
End Sub
' This code correctly sets the printer to a specific color printer
' on our network. The problem is that it makes that printer
' the user's system default printer. I would think that the
' .DoNotSetAsSysDefault = True line would solve this problem
' but still this sub changes the user's system default printer.
Public Sub SetColorPrinterEast()
With Dialogs(wdDialogFilePrintSetup)
.Printer = "\\[*NETWORK PATH*]\Color Printer East"
.DoNotSetAsSysDefault = True
.Execute
End With
End Sub
I had this same problem a few years back, got around it by storing the current default print in a variable, changing the default printer to the one I need, printing, then changing the default printer back to users original default.
This was designed and written for Word 2003 but has continued to work in Word 2010.
Here is the specific code I used:
'Define Printer to add and printer to delete
Const PrintPath = "\\prn001l0003\Colour04"
Const PrintDeletePath = "\\prn001l0003\Colour02"
' Used to see what printers are set up on the user, and to set a new network printer
Public Declare Function EnumPrinters Lib "winspool.drv" Alias "EnumPrintersA" (ByVal flags As Long, ByVal name As String, _
ByVal Level As Long, pPrinterEnum As Long, ByVal cdBuf As Long, pcbNeeded As Long, pcReturned As Long) As Long
Public Declare Function PtrToStr Lib "kernel32" Alias "lstrcpyA" (ByVal RetVal As String, ByVal Ptr As Long) As Long
Public Declare Function StrLen Lib "kernel32" Alias "lstrlenA" (ByVal Ptr As Long) As Long
Const PRINTER_ENUM_CONNECTIONS = &H4
Const PRINTER_ENUM_LOCAL = &H2
Public Sub PrintLetter(ByRef LetterBrochures() As String)
'Print the document
Dim STDprinter As String
On Error Resume Next
Call CheckPrinterLoaded ' Get users loaded printers, remove any old printers used here,
' and add printer I want to users printers
STDprinter = Application.ActivePrinter ' store the current default printer
Application.ActivePrinter = PrintPath ' change default printer to want I want
On Error GoTo printLetterError
Application.DisplayAlerts = wdAlertsNone ' prevent Word showing any alert/warnings etc
With ActiveDocument ' first page is letterhead from tray 2, all others from tray 1, print
.PageSetup.FirstPageTray = 3 ' 3 = Tray 2 on MFLaser
.PageSetup.OtherPagesTray = 1 ' 1 = Tray 1 on MFLaser
.PrintOut Background:=False
End With
Application.DisplayAlerts = wdAlertsAll ' enable Word alets/warning etc
Application.ActivePrinter = STDprinter 'change back users default printer
Exit Sub
printLetterError:
MsgBox "Error printing letter" & vbCrLf & Err.Number & vbCrLf & Err.Description, vbCritical, "Error"
ActiveDocument.Close False
End
End Sub
Public Function CheckPrinterLoaded()
'get users printers
'look for and delete defined printer, PrintDeletePath
'add printer I want to users printers, PrintPath
Dim StrPrinters As Variant, x As Long
Dim StrSetPrinter As String
Dim objNetwork
Set objNetwork = CreateObject("WScript.Network")
StrPrinters = ListPrinters
'Fist check whether the array is filled with anything, by calling another function, IsBounded.
If IsBounded(StrPrinters) Then
For x = LBound(StrPrinters) To UBound(StrPrinters)
If StrPrinters(x) = PrintDeletePath Then
objNetwork.RemovePrinterConnection PrintDeletePath
End If
Next x
objNetwork.AddWindowsPrinterConnection PrintPath
Else
MsgBox "No printers found"
End If
End Function
Private Function ListPrinters() As Variant
Dim bSuccess As Boolean
Dim iBufferRequired As Long
Dim iBufferSize As Long
Dim iBuffer() As Long
Dim iEntries As Long
Dim iIndex As Long
Dim strPrinterName As String
Dim iDummy As Long
Dim iDriverBuffer() As Long
Dim StrPrinters() As String
iBufferSize = 3072
ReDim iBuffer((iBufferSize \ 4) - 1) As Long
'EnumPrinters will return a value False if the buffer is not big enough
bSuccess = EnumPrinters(PRINTER_ENUM_CONNECTIONS Or PRINTER_ENUM_LOCAL, vbNullString, 1, iBuffer(0), iBufferSize, iBufferRequired, iEntries)
If Not bSuccess Then
If iBufferRequired > iBufferSize Then
iBufferSize = iBufferRequired
Debug.Print "iBuffer too small. Trying again with "; iBufferSize & " bytes."
ReDim iBuffer(iBufferSize \ 4) As Long
End If
'Try again with new buffer
bSuccess = EnumPrinters(PRINTER_ENUM_CONNECTIONS Or PRINTER_ENUM_LOCAL, vbNullString, 1, iBuffer(0), iBufferSize, iBufferRequired, iEntries)
End If
If Not bSuccess Then
'Enumprinters returned False
MsgBox "Error enumerating printers."
Exit Function
Else
'Enumprinters returned True, use found printers to fill the array
ReDim StrPrinters(iEntries - 1)
For iIndex = 0 To iEntries - 1
'Get the printername
strPrinterName = Space$(StrLen(iBuffer(iIndex * 4 + 2)))
iDummy = PtrToStr(strPrinterName, iBuffer(iIndex * 4 + 2))
StrPrinters(iIndex) = strPrinterName
Next iIndex
End If
ListPrinters = StrPrinters
End Function
Private Function IsBounded(vArray As Variant) As Boolean
'If the variant passed to this function is an array, the function will return True; otherwise it will return False
On Error Resume Next
IsBounded = IsNumeric(UBound(vArray))
End Function

Reliable way to ensure file is finished writing to disk

I regularly rely on external tools to create files from VBA: 7zip, scanning applications, etc. I need a reliable way to know when the file has finished writing and may be safely used for other operations (including passing it on to other external programs; e.g., email, etc.).
Here are the assumptions:
I have no control over the writing of the file
the file may or may not be on a network file server
I don't know the contents of the file before it's been written
Because I have no control over the writing of the file, I can't use CreateFile with FILE_FLAG_WRITE_THROUGH.
Because the files may be on a network file server, I am leery of the performance impact of using FlushFileBuffers. I'm also not sure it would do what I want.
Because I don't know the contents of the file ahead of time, I can't compare hashes to check the integrity of the file.
I've used simple techniques like using Dir() to ensure that a file exists, but that seems unreliable. I've also tried pausing then retrying an operation until it works or exceeds some timeout that I have set. I've also tried opening the file with an exclusive lock and catching the error to test whether the file is still in use.
These solutions have all worked to some degree, but they all seem less than ideal.
This is a generic problem that causes frequent headaches. The result is a race condition that is difficult to reproduce and troubleshoot. Is there a better approach than what I have already tried?
Update:
As #JasonFaulkner points out in the comments, without knowing the contents ahead of time, it's impossible to be 100% sure that the file has been successfully written. Short of that, I'd like the most efficient and reliable way to determine the following conditions have been met:
the file exists
there is no program accessing the file
there is no data in any cache waiting to be written to the physical disk
I've come up with the following procedure to check if a file is ready. I'm open to any suggestions for improvements or problems that I may have missed.
Currently this is implemented as a Sub that raises an error if the file is not ready and the retry attempts have been exhausted. If the file is ready, then the sub simply completes with no fanfare.
The following declarations go at the top of the module:
'----- VerifyFileReady declarations ----------------------------------------------------
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Private Declare Function CreateFile Lib "kernel32" Alias _
"CreateFileA" (ByVal lpFileName As String, ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, ByVal lpSecurityAttributes As Long, _
ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As Long) As Long
Private Const OPEN_EXISTING = 3
Private Const GENERIC_READ = &H80000000
Private Const GENERIC_WRITE = &H40000000
Private Const GENERIC_ALL = &H10000000
Private Declare Function FlushFileBuffers Lib "kernel32" (ByVal hFile As Long) As Long
Private Declare Function GetFileSize Lib "kernel32" _
(ByVal hFile As Long, lpFileSizeHigh As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" _
(ByVal hFile As Long) As Long
'perform 64-bit arithmetic (see: http://support.microsoft.com/kb/189862)
Private Type Curr64Bit
Value As Currency
End Type
Private Type LongsAs64Bit
LoValue As Long
HiValue As Long
End Type
'=======================================================================================
Here is the sub itself, along with a small helper function to perform file size comparisons for files that may be over 2GB in size:
'---------------------------------------------------------------------------------------
' Procedure : VerifyFileReady
' Author : Mike
' Date : 1/22/2015
' Purpose : Confirm that a file is ready for use; commonly used before passing a
' filename to an outside entity for processing (e.g., a PDF printer,
' compression utility, email, etc.)
' Parameters:
' FName The name of the file
' MinSizeInBytes The minimum file size before confirming a file is ready;
' by default, the file must be non-empty
' RetryAttempts The number of times to retry if a file is not ready
' DelayInMs The amount of time to sleep between retries
' FailureMsg Set to the reason the file is not ready; passed By Reference so that
' the most recent msg will be raised if necessary
'
' Notes - Acts as a gate: if the file is ready, the program continues on; otherwise
' an error is thrown after the number of retry ettampts is exhausted
' - To get the maximum program delay this function will cause, multiply the
' RetryAttempts by the DelayInMs; by default the program will delay a
' maximum of 5 seconds (10 attempts * 500 ms delay per retry attempt)
' - By ready for use, we mean the file meets the following criteria:
' o the file exists
' o the file is not locked by another process
' o the file buffers have been flushed
' o the file meets the minimum size in bytes (by default, it's not empty)
' - There's no way to *really* be sure that the file has been written to disk,
' so this function cannot guarantee transactional integrity
'---------------------------------------------------------------------------------------
'
Sub VerifyFileReady(ByVal FName As String, _
Optional ByVal MinSizeInBytes As Long = 1, _
Optional ByVal RetryAttempts As Integer = 10, _
Optional ByVal DelayInMs As Integer = 500, _
Optional ByRef FailureMsg As String = vbNullString)
Dim FileIsReady As Boolean
FileIsReady = True
On Error GoTo Err_VerifyFileReady
'FlushFileBuffers requires GENERIC_WRITE access
Dim DesiredAccess As Long
DesiredAccess = GENERIC_READ Or GENERIC_WRITE
'Open the file (CreateFile is a generic function that replaces the deprecated OpenFile)
Dim hFile As Long 'File Handle
Err.Clear 'explicitly flush the Err.LastDllError property
hFile = CreateFile(FName, DesiredAccess, 0, 0, OPEN_EXISTING, 0, 0)
Dim FileOpenFailed As Boolean
Const INVALID_HANDLE_VALUE = -1
FileOpenFailed = (hFile = INVALID_HANDLE_VALUE)
If FileOpenFailed Then
FileIsReady = False
Select Case Err.LastDLLError
Case 2: FailureMsg = "The system cannot find the file specified." 'ERROR_FILE_NOT_FOUND
Case 3: FailureMsg = "The system cannot find the path specified." 'ERROR_PATH_NOT_FOUND
Case 4: FailureMsg = "The system cannot open the file." 'ERROR_TOO_MANY_OPEN_FILES
Case 5: FailureMsg = "Access is denied." 'ERROR_ACCESS_DENIED
Case 15: FailureMsg = "The system cannot find the drive specified." 'ERROR_INVALID_DRIVE
Case 20: FailureMsg = "The system cannot find the device specified." 'ERROR_BAD_UNIT
Case 21: FailureMsg = "The device is not ready." 'ERROR_NOT_READY
Case 32: FailureMsg = "The process cannot access the file because it is being used by another process." 'ERROR_SHARING_VIOLATION
Case 33: FailureMsg = "The process cannot access the file because another process has locked a portion of the file." 'ERROR_LOCK_VIOLATION
Case Else: FailureMsg = "CreateFile function failed with error number " & Err.LastDLLError & "."
End Select
End If
If FileIsReady Then
'be sure the file has been physically written to disk
Dim FlushResults As Long
FlushResults = FlushFileBuffers(hFile)
Dim FlushFailed As Boolean
FlushFailed = (FlushResults = 0)
If FlushFailed Then
FileIsReady = False
Select Case Err.LastDLLError
Case 5: FailureMsg = "FlushFileBuffers function failed: Access is denied." 'ERROR_ACCESS_DENIED
Case Else: FailureMsg = "FlushFileBuffers function failed with error number " & Err.LastDLLError & "."
End Select
End If
End If
'check that the file meets the minimum size requirement
' (MinSizeInBytes parameter may not exceed 2GB, but actual
' file sizes beyond 2GB are allowed and will be treated correctly)
If FileIsReady And MinSizeInBytes > 0 Then
Dim FSize64 As Curr64Bit
Dim FileSizeLow As Long, FileSizeHigh As Long
FileSizeLow = GetFileSize(hFile, FileSizeHigh)
Const GetFileSizeError As Long = &HFFFFFFFF
If FileSizeLow = GetFileSizeError Then
FileIsReady = False
FailureMsg = "Error getting file size."
ElseIf TwoLongsTo64(FileSizeLow, FileSizeHigh).Value < TwoLongsTo64(MinSizeInBytes, 0).Value Then
FileIsReady = False
FailureMsg = "File smaller than minimum size of " & MinSizeInBytes & " byte(s)."
End If
End If
'close the handle or *we* will be the ones locking the file
If hFile <> INVALID_HANDLE_VALUE Then CloseHandle hFile
If Not FileIsReady Then
FileNotReady:
If RetryAttempts > 0 Then
'we can't just raise an error or the Resume would send us back to the Err.Raise statement;
' instead we make a recursive call and decrement the RetryAttempts to prevent a stack overflow
Sleep DelayInMs
On Error GoTo 0 'prevent infinite recursion
VerifyFileReady FName, MinSizeInBytes, RetryAttempts - 1, DelayInMs, FailureMsg
Exit Sub
Else
On Error GoTo 0
Err.Raise vbObjectError + 44212312, "FileFunctions.VerifyFileReady", FailureMsg
End If
End If
Exit Sub
Err_VerifyFileReady:
FailureMsg = "Error " & Err.Number & ": " & Err.Description
Resume FileNotReady
End Sub
'64-bit arithmetic in VBA: http://support.microsoft.com/kb/189862
Function TwoLongsTo64(LowVal As Long, HighVal As Long) As Curr64Bit
Dim L As LongsAs64Bit
L.HiValue = HighVal
L.LoValue = LowVal
LSet TwoLongsTo64 = L
End Function

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.

How to retrieve this computer's IP address?

What is the least cumbersome (module-inclusion, code lenght, etc) way to retrieve the machine IP address (of the first interface open)?
I know of some solutions using MSINET, but I believe we can do better.
Don't reply
Function HomeIP() as Atring
HomeIP= "127.0.0.1"
End Function
because it's not that funny... or correct.
The scenario is a question wiht a document ID feature I'm trying to build a reply for.
Here's an adapted example from Technet:
Function GetIPAddress()
Const strComputer As String = "." ' Computer name. Dot means local computer
Dim objWMIService, IPConfigSet, IPConfig, IPAddress, i
Dim strIPAddress As String
' Connect to the WMI service
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
' Get all TCP/IP-enabled network adapters
Set IPConfigSet = objWMIService.ExecQuery _
("Select * from Win32_NetworkAdapterConfiguration Where IPEnabled=TRUE")
' Get all IP addresses associated with these adapters
For Each IPConfig In IPConfigSet
IPAddress = IPConfig.IPAddress
If Not IsNull(IPAddress) Then
strIPAddress = strIPAddress & Join(IPAddress, ", ")
End If
Next
GetIPAddress = strIPAddress
End Function
It requires that you have Microsoft WMI Scripting Library in the project's references.
A couple of examples I found:-
http://www.everythingaccess.com/tutorials.asp?ID=Get-all-IP-Addresses-of-your-machine
http://puremis.net/excel/code/079.shtml
EDIT
Here is the code from the first link with slight modification
Option Explicit
' VBA MODULE: Get all IP Addresses of your machine
' (c) 2005 Wayne Phillips (http://www.everythingaccess.com)
' Written 18/05/2005
'
' REQUIREMENTS: Windows 98 or above, Access 97 and above
'
' Please read the full tutorial here:
' http://www.everythingaccess.com/tutorials.asp?ID=Get-all-IP-Addresses-of-your-machine
'
' Please leave the copyright notices in place.
' Thank you.
'
'Option Compare Database
'A couple of API functions we need in order to query the IP addresses in this machine
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Public Declare Function GetIpAddrTable Lib "Iphlpapi" (pIPAdrTable As Byte, pdwSize As Long, ByVal Sort As Long) As Long
'The structures returned by the API call GetIpAddrTable...
Type IPINFO
dwAddr As Long ' IP address
dwIndex As Long ' interface index
dwMask As Long ' subnet mask
dwBCastAddr As Long ' broadcast address
dwReasmSize As Long ' assembly size
Reserved1 As Integer
Reserved2 As Integer
End Type
Public Function ConvertIPAddressToString(longAddr As Long) As String
Dim IPBytes(3) As Byte
Dim lngCount As Long
'Converts a long IP Address to a string formatted 255.255.255.255
'Note: Could use inet_ntoa instead
CopyMemory IPBytes(0), longAddr, 4 ' IP Address is stored in four bytes (255.255.255.255)
'Convert the 4 byte values to a formatted string
While lngCount < 4
ConvertIPAddressToString = ConvertIPAddressToString + _
CStr(IPBytes(lngCount)) + _
IIf(lngCount < 3, ".", "")
lngCount = lngCount + 1
Wend
End Function
Public Function GetFirstNonLocalIPAddress()
Dim Ret As Long, Tel As Long
Dim bytBuffer() As Byte
Dim IPTableRow As IPINFO
Dim lngCount As Long
Dim lngBufferRequired As Long
Dim lngStructSize As Long
Dim lngNumIPAddresses As Long
Dim strIPAddress As String
On Error GoTo ErrorHandler:
Call GetIpAddrTable(ByVal 0&, lngBufferRequired, 1)
If lngBufferRequired > 0 Then
ReDim bytBuffer(0 To lngBufferRequired - 1) As Byte
If GetIpAddrTable(bytBuffer(0), lngBufferRequired, 1) = 0 Then
'We've successfully obtained the IP Address details...
'How big is each structure row?...
lngStructSize = LenB(IPTableRow)
'First 4 bytes is a long indicating the number of entries in the table
CopyMemory lngNumIPAddresses, bytBuffer(0), 4
While lngCount < lngNumIPAddresses
'bytBuffer contains the IPINFO structures (after initial 4 byte long)
CopyMemory IPTableRow, _
bytBuffer(4 + (lngCount * lngStructSize)), _
lngStructSize
strIPAddress = ConvertIPAddressToString(IPTableRow.dwAddr)
If Not ((strIPAddress = "127.0.0.1")) Then
GetFirstNonLocalIPAddress = strIPAddress
Exit Function
End If
lngCount = lngCount + 1
Wend
End If
End If
Exit Function
ErrorHandler:
MsgBox "An error has occured in GetIPAddresses():" & vbCrLf & vbCrLf & _
Err.Description & " (" & CStr(Err.Number) & ")"
End Function
You could execute the shell command ipconfig and parse the returned results?
There is another easy way using ipconfig.
http://www.vbaexpress.com/kb/getarticle.php?kb_id=537
Codeproject has a good article on how to do this with .net:
http://www.codeproject.com/KB/cs/network.aspx
You could always create a console executable out of this, and call it from VBA.
RO
nbtstat -n might do the job on XP anyway. Not sure about other Windows versions or about localisation in other languages. Partial sample output:
C:\Documents and
Settings\colin>nbtstat -n
Local Area Connection: Node IpAddress:
[192.168.1.100] Scope Id: []
NetBIOS Local Name Table
etc.
Option Explicit
Sub Main()
Dim wsh As Object
Dim strIPOutputFile As String, strSingleLine As String, strIP As String, strToFind As String
Dim intSourceFile As Integer, intLocation As Integer
Set wsh = CreateObject("WScript.Shell")
strIPOutputFile = "C:\Users\MeMeMeMe\Desktop\txtfile.txt"
'Save ipconfig info to file
wsh.Run "%comspec% /c ipconfig/all> """ & strIPOutputFile & """
'Close any open text files
Close
'Get the number of the next free text file
intSourceFile = FreeFile
Open strIPOutputFile For Input As intSourceFile
strToFind = "IPv4 Address. . . . . . . . . . . :" 'This will probably depend on your file
Do Until EOF(intSourceFile)
Input #intSourceFile, strSingleLine
If InStr(1, strSingleLine, strToFind) > 0 Then
Exit Do
End If
Loop
intLocation = Len(strToFind)
strIP = Trim(Mid(strSingleLine,1 + intLocation,Len(strSingleLine) - intLocation))
intLocation = Len(strIP)
While Not IsNumeric(Mid(strIP,intLocation,1))
strIP = Left(strIP, Len(strIP) - 1)
intLocation = Len(strIP)
Wend
Close
MsgBox strIP
End Sub