Visual Basic is a great language because it is simple enough to enable an intermediate Windows user to learn how to write a Windows application. This simplicity is what has attracted so many programmers, and what enables programmers to write applications in weeks that would take months (or even years) in C. However, this simplicity comes at price. That price is that many Visual Basic functions were written for Visual Basic 1.0, when Microsoft envisioned Visual Basic as being only a hobbyist's programming language or a Windows batch language. No one anticipated that Visual Basic would emerge as the most common programming language for Windows, so many of the version 1.0 functions contain limited functionality. One such function is Dir.
Although Dir suffices for your fundamental needs, it falls short when you try to perform such actions as searching your hard drive for all the files with the extension BAK. The function falls short for a simple reason: It doesn't support nested calls. After recognizing this shortcoming, Microsoft wrote the WinSeek sample, which uses a file and directory list box control to overcome this limitation. However, this workaround is unacceptable. The spaghetti code in WinSeek is hard to follow, poorly commented, and too slow for even the most trivial tasks.
The FindFile class, shown in Listing 35.6, overcomes the shortcomings of Dir and WinSeek. This class encapsulates an API function into a reusable object, which makes the API function as easy to use as a built-in Visual Basic function. FindFile also keeps things simple by including only a minimal amount of core functionality. This simplicity enables users of this class to write their own algorithms for such special tasks as searching an entire drive for a specific type of file. In addition to reviewing the source code and comments for this class in Listing 35.6, you should also open FINDFILE.VBP on the companion CD-ROM and view this class in Object Browser. This exercise will help you to examine this class as both an object as well as a sample piece of code that wraps several Win32 API calls.
Listing 35.6 - FINDFILE.CLS - FindFile.cls is an object that encapsulates the File Search API's
'********************************************************************* ' FindFile.cls - Encapsulates the Win32 FindFile functions '********************************************************************* Option Explicit '********************************************************************* ' Attribute constants which differ from VB '********************************************************************* Private Const FILE_ATTRIBUTE_COMPRESSED = &H800 Private Const FILE_ATTRIBUTE_NORMAL = &H80 '********************************************************************* ' Win32 API constants required by FindFile '********************************************************************* Private Const MAX_PATH = 260 Private Const INVALID_HANDLE_VALUE = -1 '********************************************************************* ' Win32 data types (or structs) required by FindFile '********************************************************************* Private Type FILETIME dwLowDateTime As Long dwHighDateTime As Long End Type Private Type WIN32_FIND_DATA dwFileAttributes As Long ftCreationTime As FILETIME ftLastAccessTime As FILETIME ftLastWriteTime As FILETIME nFileSizeHigh As Long nFileSizeLow As Long dwReserved0 As Long dwReserved1 As Long cFileName As String * MAX_PATH cAlternate As String * 14 End Type Private Type SYSTEMTIME wYear As Integer wMonth As Integer wDayOfWeek As Integer wDay As Integer wHour As Integer wMinute As Integer wSecond As Integer wMilliseconds As Integer End Type '********************************************************************* ' Win32 API calls required by this class '********************************************************************* Private Declare Function FileTimeToLocalFileTime Lib "kernel32" _ (lpFileTime As FILETIME, lpLocalFileTime As FILETIME) As Long Private Declare Function FileTimeToSystemTime Lib "kernel32" _ (lpFileTime As FILETIME, lpSystemTime As SYSTEMTIME) As Long Private Declare Function FindFirstFile Lib "kernel32" Alias _ "FindFirstFileA" (ByVal lpFileName As String, _ lpFindFileData As WIN32_FIND_DATA) As Long Private Declare Function FindNextFile Lib "kernel32" Alias _ "FindNextFileA" (ByVal hFindFile As Long, lpFindFileData As _ WIN32_FIND_DATA) As Long Private Declare Function FindClose& Lib "kernel32" (ByVal hFindFile&) '********************************************************************* ' clsFindFiles private member variables '********************************************************************* Private mlngFile As Long Private mstrDateFormat As String Private mstrUnknownDateText As String Private mwfdFindData As WIN32_FIND_DATA '********************************************************************* ' Public interface for setting the format string used for dates '********************************************************************* Public Property Let DateFormat(strDateFormat As String) mstrDateFormat = strDateFormat End Property '********************************************************************* ' Public interface for setting the string used when the date for a ' file is unknown '********************************************************************* Public Property Let UnknownDateText(strUnknownDateText As String) mstrUnknownDateText = strUnknownDateText End Property '********************************************************************* ' Returns the file attributes for the current file '********************************************************************* Public Property Get FileAttributes() As Long If mlngFile Then FileAttributes = mwfdFindData.dwFileAttributes End Property '********************************************************************* ' Returns true if the compress bit is set for the current file '********************************************************************* Public Property Get IsCompressed() As Boolean If mlngFile Then IsCompressed = mwfdFindData.dwFileAttributes _ And FILE_ATTRIBUTE_COMPRESSED End Property '********************************************************************* ' Returns the value of the Normal attribute bit for dwFileAttributes '********************************************************************* Public Property Get NormalAttribute() As Long NormalAttribute = FILE_ATTRIBUTE_NORMAL End Property '********************************************************************* ' Primary method in this class for finding the FIRST matching file in ' a directory that matches the path &|or pattern in strFile '********************************************************************* Public Function Find(strFile As String, Optional blnShowError _ As Boolean) As String '***************************************************************** ' If you already searching, then end the current search '***************************************************************** If mlngFile Then If blnShowError Then If MsgBox("Cancel the current search?", vbYesNo Or _ vbQuestion) = vbNo Then Exit Function End If '************************************************************* ' Call cleanup routines before beginning new search '************************************************************* EndFind End If '***************************************************************** ' Find the first file matching the search pattern in strFile '***************************************************************** mlngFile = FindFirstFile(strFile, mwfdFindData) '***************************************************************** ' Check to see if FindFirstFile failed '***************************************************************** If mlngFile = INVALID_HANDLE_VALUE Then mlngFile = 0 '************************************************************* ' If blnShowError, then display a default error message '************************************************************* If blnShowError Then MsgBox strFile & " could not be found!", vbExclamation '************************************************************* ' Otherwise, raise a user-defined error with a default err msg '************************************************************* Else Err.Raise vbObjectError + 5000, "clsFindFile_Find", _ strFile & " could not be found!" End If Exit Function End If '***************************************************************** ' Return the found file name without any nulls '***************************************************************** Find = Left(mwfdFindData.cFileName, _ InStr(mwfdFindData.cFileName, Chr(0)) - 1) End Function '********************************************************************* ' Call this function until it returns "" to get the remaining files '********************************************************************* Public Function FindNext() As String '***************************************************************** ' Exit if no files have been found '***************************************************************** If mlngFile = 0 Then Exit Function '***************************************************************** ' Be sure to clear the contents of cFileName before each call to ' avoid garbage characters from being returned in your string. '***************************************************************** mwfdFindData.cFileName = Space(MAX_PATH) '***************************************************************** ' If another file is found, then return it. Otherwise, EndFind. '***************************************************************** If FindNextFile(mlngFile, mwfdFindData) Then FindNext = Left(mwfdFindData.cFileName, _ InStr(mwfdFindData.cFileName, Chr(0)) - 1) Else EndFind End If End Function '********************************************************************* ' A private helper method which is called internally to close the ' FindFile handle and clear mlngFile to end a FindFile operation. '********************************************************************* Private Sub EndFind() FindClose mlngFile mlngFile = 0 End Sub '********************************************************************* ' Return the short name of a found file (default = long file name) '********************************************************************* Public Function GetShortName() As String Dim strShortFileName As String '***************************************************************** ' If no current file, then exit '***************************************************************** If mlngFile = 0 Then Exit Function '***************************************************************** ' Get the short file name (without trailing nulls) '***************************************************************** strShortFileName = Left(mwfdFindData.cAlternate, _ InStr(mwfdFindData.cAlternate, Chr(0)) - 1) '***************************************************************** ' If there is no short file name info, then strShortFilename will ' equal null (because of the (- 1) above '***************************************************************** If Len(strShortFileName) = 0 Then '************************************************************* ' If no short file name, then it's already a short file name, so ' set strShortFileName = .cFileNae. '************************************************************* strShortFileName = Left(mwfdFindData.cFileName, _ InStr(mwfdFindData.cFileName, Chr(0)) - 1) End If '***************************************************************** ' Return the short file name '***************************************************************** GetShortName = strShortFileName End Function '********************************************************************* ' Return the date the current file was created. If the optional args ' are provided, then they will be set = to date and time values. '********************************************************************* Public Function GetCreationDate(Optional datDate As Date, _ Optional datTime As Date) As String If mlngFile = 0 Then Exit Function '***************************************************************** ' If dwHighDateTime, then Win32 couldn't determine the date, so ' return the unknown string. "Unknown" is the default. Set this ' value to something else by using the UnknownDateText property. '***************************************************************** If mwfdFindData.ftCreationTime.dwHighDateTime = 0 Then GetCreationDate = mstrUnknownDateText Exit Function End If '***************************************************************** ' Get the time (in the current local/time zone) '***************************************************************** With GetSystemTime(mwfdFindData.ftCreationTime) '************************************************************* ' If datDate was provided, then set it to a date serial '************************************************************* datDate = DateSerial(.wYear, .wMonth, .wDay) '************************************************************* ' If datTime was provided, then set it to a time serial '************************************************************* datTime = TimeSerial(.wHour, .wMinute, .wSecond) '************************************************************* ' Use datDate and datTime as local variables (even if they ' weren't passed ByRef in the optional args) to create a ' a valid date/time value. Return the date/time formatted ' using the default format of "m/d/yy h:nn:ss AM/PM" or ' the user-defined value which was set using the DateFormat ' property. '************************************************************* GetCreationDate = Format(datDate + datTime, mstrDateFormat) End With End Function '********************************************************************* ' Similar to GetCreationDate. See GetCreationDate for comments. '********************************************************************* Public Function GetLastAccessDate(Optional datDate As Date, _ Optional datTime As Date) As String If mlngFile = 0 Then Exit Function If mwfdFindData.ftLastAccessTime.dwHighDateTime = 0 Then GetLastAccessDate = mstrUnknownDateText Exit Function End If With GetSystemTime(mwfdFindData.ftLastAccessTime) datDate = DateSerial(.wYear, .wMonth, .wDay) datTime = TimeSerial(.wHour, .wMinute, .wSecond) GetLastAccessDate = Format(datDate + datTime, mstrDateFormat) End With End Function '********************************************************************* ' Similar to GetCreationDate. See GetCreationDate for comments. '********************************************************************* Public Function GetLastWriteDate(Optional datDate As Date, _ Optional datTime As Date) As String If mlngFile = 0 Then Exit Function If mwfdFindData.ftLastWriteTime.dwHighDateTime = 0 Then GetLastWriteDate = mstrUnknownDateText Exit Function End If With GetSystemTime(mwfdFindData.ftLastWriteTime) datDate = DateSerial(.wYear, .wMonth, .wDay) datTime = TimeSerial(.wHour, .wMinute, .wSecond) GetLastWriteDate = Format(datDate + datTime, mstrDateFormat) End With End Function '********************************************************************* ' Takes a FILETIME and converts it into the local system time '********************************************************************* Private Function GetSystemTime(ftmFileTime As FILETIME) As SYSTEMTIME Dim ftmLocalTime As FILETIME Dim stmSystemTime As SYSTEMTIME FileTimeToLocalFileTime ftmFileTime, ftmLocalTime FileTimeToSystemTime ftmLocalTime, stmSystemTime GetSystemTime = stmSystemTime End Function '********************************************************************* ' Sets the default values for private members when this object is ' created '********************************************************************* Private Sub Class_Initialize() mstrUnknownDateText = "Unknown" mstrDateFormat = "m/d/yy h:nn:ss AM/PM" End Sub '********************************************************************* ' Ends any open finds, if necessary '********************************************************************* Private Sub Class_Terminate() If mlngFile Then EndFind End Sub
The FindFile class contains private declarations for everything that needs to be both an independent and complete object. Also, the class is about 60 percent faster than WinSeek. However, performance is not the only reason to use the FindFile class. It provides a wealth of information about each found file, and supports searching unmapped networked drives using Universal Naming Convention (UNC) paths.
FindFile is similar to Dir in that your first call specifies the search criteria, and subsequent calls retrieve the files that correspond to that search criteria. However, FindFile is different in that your first call is to the Find() method, and subsequent calls are to the FindNext() method. Your application should keep looping as long as FindNext() is returning strings, or until you are ready to begin the next search by calling Find() again.
Listing 35.7 demonstrates a simple use of the FindFile class. This function's purpose is to retrieve all the files in the current directory that satisfy a given search criteria. All the items found are loaded into a collection that the caller provides. Finally, this function returns the number of files that were added to the colFiles collection.
Listing 35.7 - FINDFILE.FRM - Searching for Files in a Single Directory
'********************************************************************* ' A simple routine that finds all of the files in a directory that ' match the given pattern, loads the results in a collection, then ' returns the number of files that are being returned. '********************************************************************* Private Function FindFilesInSingleDir(ByVal strDir As String, _ strPattern$, colFiles As Collection) As Integer '***************************************************************** ' Create a new FindFile object every time this function is called '***************************************************************** Dim clsFind As New clsFindFile Dim strFile As String '***************************************************************** ' Make sure strSearchPath always has a trailing backslash '***************************************************************** If Right(strDir, 1) <> "\" Then _ strDir = strDir & "\" '***************************************************************** ' Get the first file '***************************************************************** strFile = clsFind.Find(strDir & strPattern) '***************************************************************** ' Loop while files are being returned '***************************************************************** Do While Len(strFile) '************************************************************* ' If the current file found is not a directory... '************************************************************* If (clsFind.FileAttributes And vbDirectory) = 0 Then colFiles.Add strFile ' don't include the path End If '************************************************************* ' Find the next file or directory '************************************************************* strFile = clsFind.FindNext() Loop '***************************************************************** ' Return the number of files found '***************************************************************** FindFilesInSingleDir = colFiles.Count End Function
This function begins by creating a new clsFindFile object and building the search string. A call to the Find() method retrieves the first file, and the function retrieves subsequent files by looping until FindNext() no longer returns a value. If no files are found, FindFilesInSingleDir returns zero, and the function makes no changes to the colFiles collection. This function suffices for your basic needs, but isn't much better than Dir because it lacks support for searching subdirectories. However, this limitation is due to the implementation of the FindFile class, not a limitation of the class itself.
Listing 35.8 goes one step further by including support for searching subdirectories. The FindAllFiles() function overcomes the limitations of Dir and FindFilesInSingleDir, but is slightly slower than the previous function. Your application determines whether it really needs to search subdirectories, then calls the appropriate function. This way, you can obtain the results using the fastest method possible.
Listing 35.8 - FINDFILE.FRM - FindAllFiles() Includes Subdirectories in Its Search, But Imposes a Small Performance Price
'********************************************************************* ' A complex routine that finds all of the files in a directory (and its ' subdirectories), loads the results in a collection, and returns the ' number of subdirectories that were searched. '********************************************************************* Private Function FindAllFiles(ByVal strSearchPath$, strPattern As _ String, Optional colFiles As Collection, Optional colDirs As _ Collection, Optional blnDirsOnly As Boolean, Optional blnBoth _ As Boolean) As Integer '***************************************************************** ' Create a new FindFile object every time this function is called '***************************************************************** Dim clsFind As New clsFindFile Dim strFile As String Dim intDirsFound As Integer '***************************************************************** ' Make sure strSearchPath always has a trailing backslash '***************************************************************** If Right(strSearchPath, 1) <> "\" Then _ strSearchPath = strSearchPath & "\" '***************************************************************** ' Get the first file '***************************************************************** strFile = clsFind.Find(strSearchPath & strPattern) '***************************************************************** ' Loop while files are being returned '***************************************************************** Do While Len(strFile) '************************************************************* ' If the current file found is a directory... '************************************************************* If clsFind.FileAttributes And vbDirectory Then '********************************************************* ' Ignore . and .. '********************************************************* If Left(strFile, 1) <> "." Then '***************************************************** ' If either bln optional arg is true, then add this ' directory to the optional colDirs collection '***************************************************** If blnDirsOnly Or blnBoth Then colDirs.Add strSearchPath & strFile & "\" End If '***************************************************** ' Increment the number of directories found by one '***************************************************** intDirsFound = intDirsFound + 1 '***************************************************** ' Recursively call this function to search for matches ' in subdirectories. When the recursed function ' completes, intDirsFound must be incremented. '***************************************************** intDirsFound = intDirsFound + FindAllFiles( _ strSearchPath & strFile & "\", strPattern, _ colFiles, colDirs, blnDirsOnly) End If '********************************************************* ' Find the next file or directory '********************************************************* strFile = clsFind.FindNext() '************************************************************* ' ... otherwise, it must be a file. '************************************************************* Else '********************************************************* ' If the caller wants files, then add them to the colFiles ' collection '********************************************************* If Not blnDirsOnly Or blnBoth Then colFiles.Add strSearchPath & strFile End If '********************************************************* ' Find the next file or directory '********************************************************* strFile = clsFind.FindNext() End If Loop '***************************************************************** ' Return the number of directories found '***************************************************************** FindAllFiles = intDirsFound End Function
FindAllFiles() can search subdirectories mainly because it calls itself recursively. It does so by checking whether the current file is a directory. If so, the function makes another call itself using all the same parameters that the original caller passed in, with one exception: The strSearchPath parameter is modified to point to the next subdirectory to search.
Now that you have written the search routines, examine some of the code in FINDFILE.FRM (shown in Figure 35.1) that uses these search routines based on requests from the user of the search dialog box. Listing 35.9 shows how to perform this search based on the values that the user sets in the search dialog box. The code also plays a FindFile video during the search, to give the user something to look at during long searches.
FINDFILE.FRM is a Visual Basic version of the Windows FindFile dialog box.
![]()
When using this sample, the caption displays the number of files and directories found when your search completes. This value is correct, but might differ from the values that the MS-DOS Dir command and the Windows Find dialog box return. Both Dir and the Find dialog box use different mechanisms for counting the number of "files" returned, neither of which is completely accurate. Listing 35.9 uses a method that correlates to the value returned when you view a directory's properties in the Windows Explorer.
Listing 35.9 - FINDFILE.FRM - Choosing the Right Search Technique
'********************************************************************* ' Find matching files based on the contents of the text boxes '********************************************************************* Private Sub cmdFind_Click() '***************************************************************** ' Prevent the user from clicking the find button twice, and ' hide the browse button so the AVI can be seen '***************************************************************** cmdFind.Enabled = False cmdBrowse.Visible = False '***************************************************************** ' Give the user a video to watch (wasteful, but cool) '***************************************************************** With aniFindFile .Open App.Path & "\findfile.avi" .Visible = True Refresh .Play End With '***************************************************************** ' Tell the user what you are doing and display an hourglass pointer '***************************************************************** Caption = "Searching..." Screen.MousePointer = vbHourglass '***************************************************************** ' Always clear before performing the operation (in case the list ' is already visible to the user) '***************************************************************** lstFound.Clear '***************************************************************** ' Perform the appropriate search '***************************************************************** If chkSearchSubs Then SearchSubDirs Else SearchCurDirOnly End If '***************************************************************** ' End the video, then restore the buttons and pointer '***************************************************************** aniFindFile.Stop: aniFindFile.Visible = False cmdFind.Enabled = True cmdBrowse.Visible = True Screen.MousePointer = vbDefault End Sub
This code simply controls the user interface, but doesn't actually do any searching. Instead, it determines which helper function to call based on the default value property of the chkSearchSubs control. The code uses this technique because the helper search functions are rather complex, so including in the click event would make this code difficult to read.
Listing 35.10 starts with the simple SearchCurDirOnly() helper routine. This routine simply calls FindFilesInSingleDir and loads the results from the colFiles collection into a list box (if necessary). That part of the code is simple enough, but the next routine, SearchSubDirs(), is a little more complicated. If the user wants to search for all the files with the extension TMP, you first must get a list of all the directories by calling FindAllFiles(). After creating this list of directories, you can search each of them for TMP files.
Listing 35.10 - FINDFILE.FRM - Using the Results from the Find Functions
'********************************************************************* ' Performs a simple search in a single directory (like dir *.*) '********************************************************************* Private Sub SearchCurDirOnly() Dim dblStart As Long Dim colFiles As New Collection '***************************************************************** ' Begin timing, then search '***************************************************************** dblStart = Timer FindFilesInSingleDir txtSearchDir, txtSearchPattern, colFiles '***************************************************************** ' Adding items to the list is slow, so do it only if you have to '***************************************************************** If chkDisplayInList Then LoadCollectionInList colFiles '***************************************************************** ' Tell the user how many files were found and how long it took _ ' to find (and load) the files '***************************************************************** Caption = CStr(colFiles.Count) & " files found in" & _ Str(Timer - dblStart) & " seconds" End Sub '********************************************************************* ' Performs a complex search in multiple directories (like dir *.* /s) '********************************************************************* Private Sub SearchSubDirs() Dim dblStart As Long Dim colFiles As New Collection Dim colDirs As New Collection Dim intDirsFound As Integer Dim vntItem As Variant '***************************************************************** ' Don't forget to add the search directory to your collection '***************************************************************** colDirs.Add txtSearchDir.Text '***************************************************************** ' If the user searches for *.*, then the search is simple (and ' much faster) '***************************************************************** If Trim(txtSearchPattern) = "*.*" Then dblStart = Timer intDirsFound = FindAllFiles(txtSearchDir, "*.*", colFiles, _ colDirs, , True) '***************************************************************** ' Otherwise, things get sort of complicated '***************************************************************** Else '************************************************************* ' First search to get a collection of all the directories '************************************************************* intDirsFound = FindAllFiles(txtSearchDir, "*.*", , colDirs, True) '************************************************************* ' Start timing now, since the last search was just prep work '************************************************************* dblStart = Timer '************************************************************* ' Search for the file pattern in each directory in the list '************************************************************* For Each vntItem In colDirs '********************************************************* ' Display the current search directory in the caption '********************************************************* Caption = vntItem FindAllFiles CStr(vntItem), txtSearchPattern, colFiles Next vntItem End If '***************************************************************** ' Adding items to the list is slow, so do it only if you have to '***************************************************************** If chkDisplayInList Then LoadCollectionInList colFiles '***************************************************************** ' Tell the user how many files were found in how many dirs and ' how long it took to find (and load) the files '***************************************************************** Caption = CStr(colFiles.Count) & " files found in" & _ Str(intDirsFound) & " directories in" & Str(Timer - dblStart) _ & " seconds" End Sub
Notice that when each of the two routines listed previously complete, they display some basic results in the caption. This technique enables you to experiment with the FindFile program so that you can see that FindFile itself is quite fast, but loading the items into a list can be quite slow. When using the FINDFILE.VBP demo program, experiment with different types of searches, such as searching a networked drive using a UNC path. Try improving the program to support all the features that the Windows Find dialog box supports.