Lesson 6: File Input/Output

Lesson Objectives

Upon completion of this lesson, the participant will be able to:

Some Topics to be introduced in this lesson include:

FILE INPUT/OUTPUT

This section is a light introduction to file input/output, and emphasizes sequential processing of CSV files.

Visual Basic for Applications has a rich set of file input/output commands. You can read/write data from any offset within a file, or read/write data sequentially. You can easily read/write comma separated files or read/write a byte at a time, a line at a time, or fixed length records.

Sequential processing means that you read from a file or write to a file sequentially, starting from the beginning of the file. Random processing means that you can read data from a file or write data to a file, starting at any position within the file.

Here's a couple of possible uses for file input/output in MSProject macros:

Create custom reports that write to CSV files.

Create (write) or process (read) custom mpx files.

Use a temporary file to "dynamically" exchange data between MSProject and another application or a VB3 program.

The following commands can handle most sequential file input/output needs:

Command

Common Usage

FreeFile Get a free file handle (needed by other commands)
Open Open the file
Input # Read in fields and assign them to variables
Write # Write out data - automatically comma separated
Print # Write out data - not automatically comma separated
EOF Used to check if all records have been read
LOF Length of file - file must have been opened by Open
FileLen Length of file - must not be open - you specify the filename
Close Close a file opened by Open

NOTE: If you leave out the Close command at the end of your macros, or if you get a runtime error before the macro executes the Close command, you may get a message telling you the file is already open when you run the macro again. If this happens, use the menu command Run Reset.

If you need random access, then you'll want to look at some of these commands:

Put Write data starting at a specified position within the file
Get Read data starting at a specified position within the file
Seek Specify the position for the next read/write
Loc Returns the position for the next read/write

See Online Help for the detailed syntax and options available with the above commands.

Formatting Concerns

When reading/writing CSV files, you need to deal with the formatting that the CSV file contains. Different applications format different kinds of data in different ways when creating CSV files. About the only thing that is consistent is that the fields are separated by commas. The table below shows how the same MSProject fields would be written out using the Write# command compared to using FileSaveAs with the CSV format. It shows how the Id, Name, Duration, Start, and Flag1 task properties are written out for a single task, with no additional formatting. The task has Id=1. It starts on 1/2/95 8:00 AM, but the current MSProject Date Format shows the date in a task table as 1/2/95. It is assumed that Activeproject.HoursPerDay is 8.

Screen 1

MsgBox 2

Write# 3

FileSaveAs 4

1 1 1 1
task1 task1 "task1" task1
1d 480 480 1d
1/2/95 1/2/95 8:00:00 AM #1995-01-02 8:00:00# 1/2/95
Yes True #TRUE# No

1 The Screen column shows how the data appears in the task table on screen.

2 The MsgBox column was discovered by examining the output from:

Set t = ActiveProject.Tasks(1)

MsgBox t.ID

MsgBox t.Name

MsgBox t.Duration

MsgBox t.Start

MsgBox t.Flag1

3 The Write# column was discovered by writing to a text file and then examining the text file in NotePad. The code looked like:

Dim fnum As Integer, t As Object

fnum = FreeFile

Open "c:\test.csv" For Output As fnum

Set t = ActiveProject.Tasks(1)

Write #fnum, t.ID, t.Name, t.Duration, t.Start, t.Flag1

4 The FileSaveAs column was created manually by choosing File, Save As, and using the CSV format.

Formatting woes: The # signs in the Write# column aren't recognized as date and boolean (true/false, yes/no) qualifiers when the CSV file created by Write# is manually read back in by MSProject or Excel using File Open (error messages result in MSProject). Also, if the pure number 480 is read back into MSProject using File Open, it doesn't interpret it as minutes, and instead would tack on the default duration units, so you could end up with a duration of 480d.

The table below shows some ways to get around these formatting problems. It assumes the same data as in the previous table.

Screen

Command

Writes this

1d Write# fnum, t.Duration 480
1d 1 Write# fnum, t.Duration / 60 / 8 & "d" "1d"
1d 2 Write# fnum, t.GetField(pjTaskDuration) "1d"
1/2/95 Write# fnum, t.Start #1995-01-02 8:00:00#
1/2/95 2 Write# fnum, CStr(t.Start) "1/2/95 8:00:00 AM"
1/2/95 Write# fnum, t.GetField(pjTaskStart) "1/2/95"
Yes Write# fnum, t.Flag1 #TRUE#
Yes Write# fnum, CStr(t.Flag1) "True"
Yes Write# fnum, Format(t.Flag1,"yes/no") "Yes"
Yes 2 Write# fnum, t.GetField(pjTaskFlag1) "Yes"

1 In general, the expression: T.Duration / 60 / 8

should be replaced by: T.Duration / 60 / Activeproject.HoursPerDay

2 Commonly used methods.

Try This

This MSProject macro writes Id, Name, Duration, Start, and Flag1 values for all tasks in the active project to a CSV file.

1. Open/Create a test file with some tasks.

2. Enter and run the following MSProject macro:

Sub write3()

Dim fnum As Integer

Dim oTask As Object

fnum = FreeFile()

Open "c:\test.csv" For Output As fnum

For Each oTask In ActiveProject.Tasks

Write #fnum, _

oTask.ID, _

oTask.Name, _

oTask.GetField(pjTaskDuration), _

CStr(oTask.Start), _

oTask.GetField(pjTaskFlag1)

Next

Close fnum

End Sub

3. After the macro finishes, open up c:\test.csv in NotePad and examine how the data is formatted. Close NotePad.

4. Open c:\test.csv in Excel and examine how the data is formatted. Close Excel.

5. Use the MSProject menu command File Open to open c:\test.csv. Any problems?

Did a task sheet view have to be active for this macro to work? Did there have to exist a table that begins with fields Id, Name, Duration, Start, and Flag1?

 

Try This

This MSProject reads in task Text1, Name, and Duration fields from a text file and searches for a task in the active project whose Text1 and Name fields match those read in - the search is case sensitive for Text1, but not for Name. If it finds a match, it sets the duration of that task to the duration read in. If doesn't find a match then if adds a new task with the Text1, Name, and Durations read in.

1. Enter the text below in NotePad. Save it as c:\match.txt and then exit NotePad.

"012","task x","10d"

"005","task y","20d"

"007","task w","30d"

2. Create a project with the following task data:

Id Name Duration Text1

1 task a 1d 009

2 task x 1d 011

3 task w 1d 007

4 task x 1d 012

3. Enter the following MSProject sub procedure, but don't run it yet.

Sub MergeFile()

Dim fnum As Integer, otask As Object

Dim Text1Input$, NameInput$, DurationInput$

Dim matchingID As Integer

fnum = FreeFile()

Open "c:\match.txt" For Input As fnum

Do Until EOF(fnum)

Input #fnum, Text1Input, NameInput, DurationInput

matchingID = GetIdOfMatch(Text1Input, NameInput)

If matchingID = 0 Then

Set otask = ActiveProject.Tasks.Add

Else

Set otask = ActiveProject.Tasks(matchingID)

End If

otask.Text1 = Text1Input

otask.Name = NameInput

otask.Duration = DurationInput

Loop

Close fnum

End Sub

4. Enter the following MSProject function. It is passed the Text1 and Name read in, and it returns the ID of the first matching task it finds. If there is no match, it returns a 0.

Function GetIdOfMatch(TextInput$, NameInput$) As Integer

Dim otask As Object

For Each otask In ActiveProject.Tasks

If Not otask Is Nothing Then

If otask.Text1 = TextInput _

And UCase(otask.Name) = UCase(NameInput) Then

GetIdOfMatch = otask.ID

Exit Function

End If

End If

Next

GetIdOfMatch = 0

End Function

5. Run the MergeFile macro and then check a task table to see if it updated the tasks correctly. It should look like this:

Id Name Duration Text1

1 task a 1d 009

2 task x 1d 011

3 task w 30d 007

4 task x 10d 012

5 task y 20d 005

Binary Examples

Example

This MSProject macro plugs the word "hello" into an existing file Test1, starting at the 3rd byte, without changing the rest of the file.

Sub Bin1()

Dim fnum As Integer

Dim s As String

fnum = FreeFile()

Open "c:\Test1" For Binary As fnum

s = "hello"

Put fnum, 3, s

Close fnum

End Sub

Example

This MSProject macro reads 5 characters, one character at a time, from the file Test1, starting with the 3rd byte in the file. The Ascii code for each byte read is displayed in a MsgBox, along with the character, if it's printable, or a message if it's not.

Sub Bin2()

Dim fnum As Integer

Dim n as integer

Dim c As String * 1 'String can hold 1 char only

Dim s As String

 

fnum = FreeFile()

Open "c:\Test1" For Binary As fnum

 

For n = 3 To 5

Get fnum, n, c

If Asc(c) >= 32 Then

s = c

Else

s = "Can't display"

End If

MsgBox "n = " & n & Chr(10) & _

"Ascii code = " & Asc(c) & Chr(10) & _

"Character: " & s

Next

End Sub

Lesson 6 Exercises

  1. Write an MSProject macro that lets the user enter a path and filename in an InputBox, and then saves the comma separated Task UniqueId and Start fields to that file, where the UniqueId's are saved without quotes, and the Start dates are saved with quotes. For example if a task with UniqueId 7 has a start of 10/1/94 then the record written to the CSV file looks like:


  2. 7,"10/1/94 8:00:00 AM"

    Don't write out records for blank tasks. Assume there's at least one nonblank task in the active project. Don't bother with a custom error handler to handle bad filenames etc. Test this on a project with a couple of tasks and examine the file it creates (open it in NotePad).

  3. Create and save a text file in NotePad that contains the Unique Id and duration fields shown below:


  4. 3,"5d"

    1,"2d"

    7,"3d"

    Save the file as "C:\M3L6E2.TXT".

    Write a macro that reads in this file and assigns the durations to the appropriate tasks (matching the Unique Ids). You can hard code the filename "C:\M3L6E2.TXT", but make no assumptions as to the number of records it contains (i.e. use the Do Until EOF(fnum) loop).

    Details: Declare a long variable Uid and a string variable Dur. For each record, the Input# command reads the record into the Uid and Dur variables; then the duration of the task with Unique Id = Uid is set equal to the value of Dur. If there is no task with Unique Id = Uid, then display the message "There is no task with Unique Id = ", followed by the value of the variable Uid. Then continue on to the next record.

    This is much easier than it looks - no searching required. For example, if a task has Unique Id = 5, and you wanted to set its duration to 100d, you could use the Unique Id directly:

    ActiveProject.Tasks.UniqueId(5).Duration ="100d"

    If there is no task with Unique Id = 5, then the above statement would cause a trappable error (On Error Resume Next etc.). Of course, you're working with variables Uid and Dur instead of literals 5 and "100d".

    Test the macro: Create a project with at least 7 tasks. Display the Unique Id column. Run the macro. Check that the durations of the tasks with unique ids 1, 3, and 7 have changed correctly. Then try deleting the task with unique id = 3 and run the macro again. It should give the error message about unique id = 3.