|
Copyright (C) 1997, Paul Piko
Download source code: EXTDIR.MEF (8K)
Introduction
The CAVO20 forum on Compuserve is an excellent venue for discussing issues relating to CA-Visual Objects 2.0. Recently some people in the forum reported that the Directory() function was not returning all the names of the files that it should have. In response to that, I started investigating the possibility of using some of the Windows API functions to provide an alternative. This article explains the solution I came up with, and shows how to include additional information not normally provided by Directory().
Windows API Functions
There are two main functions in the Windows API that provide the basis for our replacement directory function: FindFirstFile() and FindNextFile().
The FindFirstFile() function accepts two parameters, a PSZ containing the required file specification (including wildcards, if necessary) and a pointer to a block of memory that will be filled with information about the first file matching the specification. FindFirstFile() returns a "search handle", which is a value that is passed to the companion FindNextFile() function. Again, FindNextFile takes a second parameter that is a pointer to a block of memory to be filled with details about the next file that is found.
In VO2, the structure of the memory containing the file details is called _WINWIN32_FIND_DATA, listed below:
|
STRUCT _WINWIN32_FIND_DATA
MEMBER dwFileAttributes AS DWORD
MEMBER ftCreationTime IS _WINFILETIME
MEMBER ftLastAccessTime IS _WINFILETIME
MEMBER ftLastWriteTime IS _WINFILETIME
MEMBER nFileSizeHigh AS DWORD
MEMBER nFileSizeLow AS DWORD
MEMBER dwReserved0 AS DWORD
MEMBER dwReserved1 AS DWORD
MEMBER DIM cFileName[ MAX_PATH ] AS BYTE
MEMBER DIM cAlternateFileName[ 14 ] AS BYTE
|
On examination of this structure, we find that we not only have the necessary information to derive the values normally returned by Directory(), but there are also additional members that we can make use of such as cFileName, which contains the long filename, ftCreationTime which contains details of when the file was created, and ftLastAccessTime, containing details of when the file was last accessed. And looking deeper into the dwFileAttributes member reveals there are additonal file attribute flags, such as COMPRESSED, OFFLINE and TEMPORARY.
Having found this information in the Windows API, we see that it is possible not only to match the functionality of Directory(), but to extend it as well.
ExtendedDirectory()
I decided to try to make our new function similar to Directory(), but not exactly the same. I have deliberately left out the disk volume information - in fact that might be a project for another day, since the Windows API function GetVolumeInformation() has additional information not normally provided by Directory().
The skeleton of our new function can be as follows:
|
function ExtendedDirectory(cFileSpec,uAttributes)
LOCAL pFindData IS _WINWIN32_FINDDATA
LOCAL hSearch AS PTR
LOCAL aFiles := {} AS ARRAY
hSearch := FindFirstFile( PSZ(cFileSpec), @pFindData )
IF ! hSearch == INVALID_HANDLE_VALUE
aThisFile := ConvertFindData( @pFindData )
aadd( aFiles, aThisFile )
DO WHILE FindNextFile( hSearch, @pFindData )
aThisFile := ConvertFindData( @pFindData )
aadd( aFiles, aThisFile )
ENDDO
ENDIF
FindClose( hSearch )
RETURN aFiles
|
This code simply sets up a block of memory to contain the file details (by declaring pFindData IS _WINWIN32_FIND_DATA), and attempts to find the first matching file by calling FindFirstFile(). If that is successful, it adds the information to an array by calling ConvertFindData() (more about that below), and then keeps looking for further files by looping and calling FindNextFile(). As long as further files are found the structure information is again analysed by ConvertFindData(), and the results added to the array.
Something more familiar…
The ConvertFindData() function takes the _WINWIN32_FIND_DATA structure and converts the information into a format that is more familiar and friendly to VO programmers. Since the Directory() function returns an array for each file found, I thought we would do the same, but extend it to include the additional information we have at hand.
Each piece of information in the structure needs some type of manipulation to get it into our VO style format. Let's start with the filename.
The structure contains both the long version of the filename and the short (8.3) version. The cFilename member contains the long filename, and cAlternateFilename contains the short filename. We can convert both of these into VO strings as shown below:
|
cLongName := PSZ2String( PSZ( _CAST, @pFindData.cFilename ) )
cShortName := PSZ2String( PSZ( _CAST, @pFindData.cAlternateFilename ) )
|
The size of the file can be calculated from the nFileSizeHigh and nFileSizeLow members:
|
nSize := (pFindData.nFileSizeHigh * MAXDWORD ) + pFindData.nFileSizeLow
|
The time related members, ftLastWriteTime, ftCreationTime, and ftLastAccessTime are actually formatted in Coordinated Universal Time (UTC) format. Windows provides the FileTimeToLocalFileTime() function to convert these into local time, and then FileTimeToSystemTime() to break the time into year, month, day, hour, minute, second, etc. To simplify the process of converting these values, I have come up with the UTC2DateTime() function, which will take a UTC value and return an array containing the corresponding date and time. The code for this function is included below:
|
FUNCTION UTC2DateTime(pFileTime AS _WINFILETIME) AS ARRAY
LOCAL pLocalFileTime IS _WINFILETIME
LOCAL pSystemTime IS _WINSYSTEMTIME
LOCAL dSystemDate AS DATE
LOCAL cSystemTime AS STRING
FileTimeToLocalFileTime( pFileTime, @pLocalFileTime )
FileTimeToSystemTime( @pLocalFileTime, @pSystemTime )
dSystemDate := SToD(PSZ( PadL(pSystemTime.wYear,4,"0") + ;
PadL(pSystemTime.wMonth,2,"0") + ;
PadL(pSystemTime.wDay,2,"0") ) )
cSystemTime := TString( pSystemTime.wHour*60*60 + ;
pSystemTime.wMinute*60 + ;
pSystemTime.wSecond + ;
pSystemTime.wMilliseconds / 1000 )
RETURN { dSystemDate, cSystemTime }
|
With this function, we can easily convert each of the time related members by following the technique listed in these few lines of code:
|
aDateTime := UTC2DateTime(@pFindData.ftLastWriteTime)
dWriteDate] := aDateTime[1]
cWriteTime := aDateTime[2]
|
All that remains is to convert the file attributes, which are stored in a DWORD, into a character string like that provided by directory. This can be achieved by using the following function:
|
FUNCTION dwAttribute2String(dwAttrib AS DWORD) AS STRING
LOCAL cAttributes AS STRING
IF _and(dwAttrib,FILE_ATTRIBUTE_COMPRESSED) > 0
cAttributes += "C"
ENDIF
IF _and(dwAttrib,FILE_ATTRIBUTE_OFFLINE) > 0
cAttributes += "O"
ENDIF
IF _and(dwAttrib,FILE_ATTRIBUTE_TEMPORARY) > 0
cAttributes += "T"
ENDIF
IF _and(dwAttrib,FILE_ATTRIBUTE_ARCHIVE) > 0
cAttributes += "A"
ENDIF
IF _and(dwAttrib,FILE_ATTRIBUTE_DIRECTORY) > 0
cAttributes += "D"
ENDIF
IF _and(dwAttrib,FILE_ATTRIBUTE_SYSTEM) > 0
cAttributes += "S"
ENDIF
IF _and(dwAttrib,FILE_ATTRIBUTE_HIDDEN) > 0
cAttributes += "H"
ENDIF
IF _and(dwAttrib,FILE_ATTRIBUTE_READONLY) > 0
cAttributes += "R"
ENDIF
RETURN cAttributes
|
Using all these components, the whole ConvertFindData() function can be written like this:
|
FUNCTION ConvertFindData(pFindData AS _WINWIN32_FIND_DATA) AS ARRAY
LOCAL aFile AS ARRAY
LOCAL aDateTime AS ARRAY
aFile := ArrayCreate(F_MAX)
aFile[F_NAME] := Psz2String(PSZ(_CAST,@pFindData.cFilename))
aFile[F_ATTR] := dwAttribute2String(pFindData.dwFileAttributes)
aFile[F_SIZE] := (pFindData.nFileSizeHigh * MAXDWORD) + ;
pFindData.nFileSizeLow
aFile[F_ATTRVALUE] := pFindData.dwFileAttributes
aDateTime := UTC2DateTime(@pFindData.ftLastWriteTime)
aFile[F_DATE] := aDateTime[1]
aFile[F_TIME] := aDateTime[2]
aDateTime := UTC2DateTime(@pFindData.ftCreationTime)
aFile[F_CREATEDATE] := aDateTime[1]
aFile[F_CREATETIME] := aDateTime[2]
aDateTime := UTC2DateTime(@pFindData.ftLastAccessTime)
aFile[F_ACCESSDATE] := aDateTime[1]
aFile[F_ACCESSTIME] := aDateTime[2]
aFile[F_SHORTNAME] := Psz2String(;
PSZ(_CAST,@pFindData.cAlternateFilename))
IF Empty(aFile[F_SHORTNAME])
aFile[F_SHORTNAME] := aFile[F_NAME]
ENDIF
RETURN aFile
|
We just need to have a few extra DEFINEs to extend the array that Directory normally provides:
|
DEFINE F_ACCESSDATE := 6
DEFINE F_ACCESSTIME := 7
DEFINE F_CREATEDATE := 8
DEFINE F_CREATETIME := 9
DEFINE F_SHORTNAME := 10
DEFINE F_ATTRVALUE := 11
DEFINE F_MAX := 11
|
Selecting which files to show
As written so far, ExtendedDirectory() returns all directory entries. However, Directory() normally does not include System or Hidden files, or Directories. The final thing we need to do is simulate this behaviour.
Directory() allows you to pass a seconds parameter containing the attributes of files to be included in the returned array. This parameter can be a numeric (e.g. FA_DIRECTORY) or a string (e.g. "D")
We can also do this with ExtendedDirectory(). The code below shows how we can convert a string into the corresponding DWORD in ExtendedDirectory()
|
DO CASE
CASE IsString(uAttributes)
FOR i := 1 TO SLen(uAttributes)
DO CASE
CASE Upper( SubStr3(uAttributes,i,1) ) == "D"
dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_DIRECTORY)
CASE Upper( SubStr3(uAttributes,i,1) ) == "S"
dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_SYSTEM)
CASE Upper( SubStr3(uAttributes,i,1) ) == "H"
dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_HIDDEN)
CASE Upper( SubStr3(uAttributes,i,1) ) == "C"
dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_COMPRESSED)
CASE Upper( SubStr3(uAttributes,i,1) ) == "O"
dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_OFFLINE)
CASE Upper( SubStr3(uAttributes,i,1) ) == "T"
dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_TEMPORARY)
ENDCASE
NEXT
CASE IsNumeric(uAttributes)
dwAttributes := uAttributes
ENDCASE
|
We can then use this requested attributes to aid in determining if the retrieved file's details should be added to the array by making a change:
|
IF ! hSearch == INVALID_HANDLE_VALUE
aThisFile := ConvertFindData( @pFindData )
IF FileAttributesOK( dwAttributes, aThisFile[F_ATTRVALUE] )
aadd( aFiles, aThisFile )
ENDIF
DO WHILE FindNextFile( hSearch, @pFindData )
aThisFile := ConvertFindData( @pFindData )
IF FileAttributesOK( dwAttributes, aThisFile[F_ATTRVALUE] )
aadd( aFiles, aThisFile )
ENDIF
ENDDO
ENDIF
|
And the FileAttributesOK() function can decide if the attributes of the file are suitable:
|
FUNCTION FileAttributeOK(dwDesired AS DWORD,dwValue AS DWORD) AS LOGIC
LOCAL lFileOK AS LOGIC
lFileOk := TRUE
DO CASE
CASE ! _and(dwDesired,FILE_ATTRIBUTE_DIRECTORY) > 0 .and. ;
_and(dwValue,FILE_ATTRIBUTE_DIRECTORY) > 0
lFileOK := FALSE
CASE ! _and(dwDesired,FILE_ATTRIBUTE_SYSTEM) > 0 .and. ;
_and(dwValue,FILE_ATTRIBUTE_SYSTEM) > 0
lFileOK := FALSE
CASE ! _and(dwDesired,FILE_ATTRIBUTE_HIDDEN) > 0 .and. ;
_and(dwValue,FILE_ATTRIBUTE_HIDDEN) > 0
lFileOK := FALSE
CASE ! _and(dwDesired,FILE_ATTRIBUTE_COMPRESSED) > 0 .and. ;
_and(dwValue,FILE_ATTRIBUTE_COMPRESSED) > 0
lFileOK := FALSE
CASE ! _and(dwDesired,FILE_ATTRIBUTE_OFFLINE) > 0 .and. ;
_and(dwValue,FILE_ATTRIBUTE_OFFLINE) > 0
lFileOK := FALSE
CASE ! _and(dwDesired,FILE_ATTRIBUTE_TEMPORARY) > 0 .and. ;
_and(dwValue,FILE_ATTRIBUTE_TEMPORARY) > 0
lFileOK := FALSE
ENDCASE
RETURN lFileOK
|
Conclusion
In this article we have been able to make use of the Windows API to give us a more powerful Directory() function. It highlights the fact that there is additional information in the Windows environment, just waiting to be tapped. And accessing that information from Visual Objects is definitely possible.
|