Dr. VBish

Reading Binary Data Files -- Even with Class -- and Playing Shell Games

Bill Shadish

I recently lost some valuable time on hoax virus warnings (grhhh). Perhaps I can save you some time by simply alerting you to HTTP://CIAC.LLNL.GOV and http://kumite.com/myths/. Also recommended: http://kumite.com/myths/home.htm and HTTP://SASSMAN.NET.

Hey, guess what, sports (or should I say, VB) fans? I'm happy to note that this column marks the beginning of my third year as your doctor. And surprise! So far, there have been no known deaths resulting from my doctoral scribings. However, our legal experts do tell me that I must mention that there have been some side effects on some of our readers. Known cases involve wheezing, gasping, and, occasionally, light chuckling. However, the virtual fine print say that this is known to affect less than one percent of you, so, as my helpful and esteemed (he's also a noted TV personality) colleague Dr. Nick has been known to say, "This is a good day to . . . operate."

And on that note, off we go.

Binary files?
I have a binary file that I need to open. Each record is 80 bytes in length. I need to open this file with VB5 and parse out the data in each record. Can you please suggest some code to get me started on the right track?

Gary Cassel (gcassel@home.com)
Baltimore, MD


I'll assume that your binary file is a collection of ANSI characters kept within a file where the actual record format is known. If you're using an ISAM (indexed sequential access method) file system such as BTRIEVE to create and house your binary data, then you might not be able to use the solution I propose below. That's because ISAM structures can keep a variety of information in their tightly coupled index files and might even store variable-length records. I also assume you know the contents of the file, and if you're working with a binary file whose structure is a mystery, well, you've got a different problem. You might, however, be able to use my approach as a tool to partially automate a brute-force trial-and-error approach to deciphering the contents . . . But, for our purposes, let's assume that this is just a single fixed record length structure file where you know what to expect within each record.

In that case, it's not too hard to get at the data. First, define your record format in a Type'd data structure, and then use the VB Read statement to pull it out. Given the "record layout" as described in the Employee_Record shown, the Read menu item in this month's example will cycle through the records in the binary file, one at a time. The employee's name for each record, in turn, is shown in a Msgbox that's displayed during the loop (see Listing 1).

Listing 1. Code to read 80-byte records in a binary file.
 
 Type Employee_Record
    SSN As String * 9
    FirstName As String * 15
    MiddleName As String * 15
    LastName As String * 15
    Dept As String * 5           ' Dept Key
    Mgr As String * 5            ' Mgr Key
     SecurityLevel As String * 1  ' Numeric Code
     Title As String * 15 
 End Type
 Dim EmpRec As Employee_Record
 Private Sub mnuRead_Click()
    Dim strMsg$
    Dim BinFile As Long    
    strPath$ = getPath()
    BinFile = FreeFile    
    Open strPath$ & "\" & "Temp.Bin" _
       For Binary Access Read As #BinFile Len = 80
    Dim lRecCount As Long   
    lRecCount = 1    
    While Not EOF(BinFile)            
       Get #BinFile, (lRecCount * Len(EmpRec) -_
          Len(EmpRec) + 1), EmpRec    
       MsgBox EmpRec.SSN & ", " & EmpRec.FirstName & _
          ", " & EmpRec.LastName        
        lRecCount = lRecCount + 1   
    Wend    
    Close #BinFile    
 End Sub


While there's plenty of file-specific hard-coding going on in this example -- for example, the record length and the fact that the empRec's variables must be known -- there are a couple of techniques I think you'll find useful. Given our assumptions (80-byte record), the Get method that we use to read the data must be positioned at the beginning of each record manually. That is, Get must be set to 1 the first time through, 81 the second time, and so on. Now, this isn't something you'll want to hard-code, especially if there are a number of different file lengths involved. So, the calculation (lRecCount * Len(EmpRec) - Len(EmpRec) + 1) handles this for us by using the size of the Employee_Record (formerly known as the EmpRec) to advance to the next record each time we travel through the loop. Also, we pull in the entire EmpRec in one read, rather than the more tedious approach of reading in each variable at a time, in order. Notice that it's a simple matter to address the individual variables within the EmpRec once the overall record is retrieved as illustrated in the MsgBox statement.

Read with class
Now, it might be a little more interesting to build a generic Class file to handle your binaries. This is one of those "write it better once up front and make it easier ever after" scenarios. We'll let our class handle Binary or ASCII files, but only the binary portions are built into the example in Listing 2. The properties for the Class (cFile) to handle your files might look something like the Listing.

Listing 2. Properties for a binary file-reading class. 
 
 Public Const EMPLOYEE = 1
 Public Const DEPARTMENT = 2
 Public Const DIVISION = 3
 Public Property Get RecSize() As Variant
    RecSize = mRecSize
 End Property
 Public Property Let RecSize(ByVal vNewValue _
    As Variant)
    If IsNumeric(vNewValue) Then
       mRecSize = vNewValue
    Else
       mRecSize = 1
    End If
 End Property
 Public Property Get FileType() As Variant
    FileType = mFileType
 End Property
 Public Property Let FileType(ByVal vNewValue _
    As Variant)
    If IsNumeric(vNewValue) Then mFileType = vNewValue
 End Property


Notice the FileType property, which allows you to have code handling binary, ASCII, or other types of files. Also, the RecSize property will let us feed in the record size for a file exactly one time (rather than having it hard-coded throughout the application code itself).

Then we'll add the generic routines to handle the normal Open, Get, and Close VB statements for processing a file (see Listing 3). Notice that we use the properties defined in the class to avoid hard-coding them throughout the application. This makes maintenance easier -- you won't go kicking and screaming if someone wants to up the record size to 81, since a change for this only needs to be made in two places now. Also, since the actual calls to manipulate the file are now found in one place, this more easily allows you to move away from a binary file structure in the future, if so desired. The RecPointer property will be used to automate away the runtime record-keeping of determining which record you're on and the calculations that are required to move you the correct number of bytes into the file to read or add new records. You'll find more routines to handle these basic tasks in the accompanying Download file.

Listing 3. The class methods.
 
 Public Sub bClose(Optional bFile)
    Close mFile
 End Sub
 Private Property Get RecPointer() As Variant
    RecPointer = mRecPointer
 End Property
 Private Property Let RecPointer(ByVal vNewValue _
    As Variant)
    mRecPointer = vNewValue
 End Property


Now we'll add functionality to handle things like parsing a record once we've read it. The Parse routine shown in Listing 4 will trim and separate fields within a record, using a definable character, which is # in our example. The mnuReadWithClass_Click routine is called from the menu, to use our cFile class to read and parse a record. Figure 1 shows the resulting parsed record.

Listing 4. The class extensions for the parsing routine.
 
 Private Const PARSECHAR = "#"
 Public Function ParseRecord(iRecType As Integer) As String
    Dim strBuf$
    Select Case iRecType
       Case EMPLOYEE
          strBuf$ = EmpRec.SSN & PARSECHAR & Trim$_
             (EmpRec.FirstName) & PARSECHAR & _
             Trim$(EmpRec.MiddleName) & PARSECHAR & _
             Trim$(EmpRec.LastName) & PARSECHAR & _
             Trim$(EmpRec.Dept) & PARSECHAR & Trim$_
             (EmpRec.Mgr) & PARSECHAR &_
             EmpRec.SecurityLevel & PARSECHAR & _
             Trim$(EmpRec.Title) & PARSECHAR
    End Select
    ParseRecord = strBuf$
 End Function
 Private Sub mnuReadWithClass_Click()
 '
 'This routine displays the employee record, parsed out
 ' with "#" characters between the fields. (The parse 
 'character can as easily be commas or whatever you'd 
 'like. We return the opened file handle into 
 'FileHandle on open. This syntax allows you to (later) 
 'have multiple files open at the same time. For our  
 'example, we just use the currently opened file,
 ' which is the employee info file.
     
 Dim objFile As New cFile
 Dim lFileHandle As Long
 Dim strBuf$
 objFile.FileType = BINARY_FILE
 objFile.RecSize = 80 
 lFileHandle = objFile.bOpen(READMODE, getPath() _
    & "\Temp.Bin")   
 clearRec EMPLOYEE    
 strBuf$ = objFile.bRead(lFileHandle, EMPLOYEE)
 MsgBox strBuf$, vbOKOnly + vbInformation, "Parsed Record"
 clearRec EMPLOYEE    
 MsgBox objFile.bRead(lFileHandle, EMPLOYEE), _
    vbOKOnly + vbInformation, "Parsed Record"    
 objFile.bClose
    
 End Sub


Ahhhh. Problem solved -- happiness is being able to read the binary data file. However, I advise you not to stop here but to write it to a SQL database structure -- for example, in Jet, Access, or SQL Server -- especially if the data is static. A DBMS will provide much more flexibility in terms of reporting, manipulating, and moving the data, as well as maintainability. The only exceptions I can imagine that might keep me from saving the original data out to a SQL database is if the data is frequently changed or if the overall size of your application files is critical.

Shell games
I've made an EXE file with VB5, and I want to use it in Word97. I use the command shell :

 Sub do_it()
            Shell ("c:\myprogram.exe"), vbNormalFocus
            msgbox "done"
 End Sub


When I activate the Word macro do_it , myprogram.exe will start. The problem is that the statements after the shelled program are executed immediately. It does not wait for the shelled program to finish, even though the users should click on an OK button in the program (so the focus is in the shelled program). Is there any way to get around this?

René (jusho@wxs.nl),
Amsterdam, Netherlands


This is a fairly common problem -- and not only in the world of VBA. The simplest trick is to use a hard-coded semaphore to hold the Word macro up until the VB app runs. In this context, my hard-coded semaphore is going to be nothing more than a small ASCII file that the Word macro and the VB application will be aware of. This file will be used as a flag to signal which step in the process has control at any given point in time.

Using something along the logical lines of the following pseudo-code would work to solve your problem (see Figure 2).

1. Start the Word macro.
2. Create a control file (in a known location).
3. Start the VB app.
4. Fall into a While Loop, within the macro, that checks for the control file.
5. The VB app completes its work and deletes the control file as part of the shutdown process.
6. The Word macro then continues happily along.

Our example adds a parameter ("WAIT") to the VB call within the Word macro (see Listing 5). If that parameter is present when starting the VB app, then the Form Load routine will catch that and kick immediately into the suspense mode, awaiting a user OK click to continue on. The Word macro will wait for this file to disappear and then continue on its way.

Listing 5. The VB and Word macro code to have VB wait.
 
 ' VB code
 Private Sub Form_Load()
    If Left$(UCase$(Command$()), 4) = "WAIT" Then
       Call mnuWait_Click
    End If
 End Sub
 Private Sub mnuWait_Click()
    MsgBox "Let's pretend that this  
    If DoesFileExist("c:\test1.tst") Then Kill _
       "c:\test1.tst"
 End Sub
 
 'Word Macro Code
 Sub RunApp()
 '
 ' RunApp Macro
 ' Macro created 11/02/98 by Bill Shadish
 '
    Dim lControlFile As Long
    lControlFile = FreeFile   
    Open "c:\test1.tst" For Output Access Write As _
       #lControlFile
    Print #lControlFile, "XYZ"
    Close #lControlFile
    Shell "c:\drvb9901.exe WAIT", vbNormalFocus   
    While DoesFileExist("c:\test1.tst")
    'you may wish to add a timeout warning message here
    Wend
    MsgBox "There, that's better!", _
       vbInformation + vbOKOnly
 End Sub


This control file angle is the simplest approach to use. The only real drawback is that you have to create a separate physical file to use as the process controlling flag. As long as your application checks first for a previously created file and removes it when you're done, there are only a few ways that the file approach can fail. You might abort in the middle of running for some as yet unknown reason. This might leave an errant control file floating around where it isn't expected to be, and so on. Again, proper startup and cleanup code, which is completely reusable within other applications, prevents many of the possible problems.

Notice that the creation/deletion logic can be reversed. You can create the control file from either the Word macro side or the VB application -- use whichever piece is easier to do. Note that you might want to use the DoesFileExist routine from the preceding Binary File answer.

If you don't like the ASCII file approach, then you can also store similar control information into a database table. Using a database control table in this way also allows you to keep and track additional data, including the starting and stopping times of all involved components of the application. You can also store successful termination codes or error codes that can become part of an overall status log, available for the reading pleasure of your favorite systems administrator.

How to send me your questions
Send in your questions via the Dr. VB-ish site at HTTP://WWW.FO.COM. At the bottom of this page is a growing list of the very best links that have been discussed in this column so far.

Download drvb199.exe

Bill Shadish is a principal of Fundamental Objects, Inc., where he works with ActiveX controls and OLE server technology. (Why, he's even been known to do some Lotus Notes work from time to time!) Bill writes for a number of VB trade journals, including Visual Basic Developer, Inside Visual Basic, VBPJ, and Visual Programming++. bills@fo.com.