By Ken Getz and Mike Gilbert
This month, we’ll conclude this two-part series with a look at some Windows API functions that will allow our classes to work with INI files. Last month, we began building the classes and objects based on a hierarchical object model. We also discussed how to manage the resulting collections of objects. This month, we’ll also look at techniques for saving and loading data. Finally, we’ll discuss creating a Parent property that will allow us to navigate up the hierarchy of our object model.
Working with the Windows APIIf you couldn’t read and write from INI files, the classes we created last month wouldn’t be of much use. To work with INI files, you use various Windows API functions (all of which are clearly marked as obsolete in the MSDN documentation, but you can disregard those warnings). The functions we’ve used here are:
GetPrivateProfileSectionNames: When you set the Name property of an IniFile object, its code calls this function to retrieve a list of all the section names. This function call returns a list of section names, with a vbNullChar character — Chr$(0) — between each name, and an extra vbNullChar character at the end of the list.
GetPrivateProfileSection: Once the IniFile object has a list of all the section names, it can call this function to retrieve each section’s information. GetPrivateProfileSection returns the name/value pairs, separated by vbNullChar characters, with an extra vbNullChar character at the end.
WritePrivateProfileSection: When you call the Save method of an IniSection object, the code calls this API function to write the entire section back to disk. It takes its input in the same format as was returned by GetPrivateProfileSection. When you call the Save method of an IniFile object, its code loops through all its IniSection objects, and calls the Save method for each.
WritePrivateProfileString: When you call the Save method of an IniItem object, its code calls this API function, which writes a single name/value pair back to disk.
As with any API function, you must declare each of these functions before you use it. We’ve made each of the declarations private to the class in which it’s used. You’ll find the declarations shown in FIGURE 1.
' From the IniFile class.
Private Declare Function GetPrivateProfileSectionNames _
Lib "kernel32" Alias "GetPrivateProfileSectionNamesA" _
(ByVal lpReturnBuffer As String, ByVal nSize As Long, _
ByVal lpName As String) As Long
' From the IniFile class.
Private Declare Function GetPrivateProfileSection _
Lib "kernel32" Alias "GetPrivateProfileSectionA" _
(ByVal lpAppName As String, _
ByVal lpReturnedString As String, ByVal nSize As Long, _
ByVal lpName As String) As Long
Private Declare Function WritePrivateProfileSection _
Lib "kernel32" Alias "WritePrivateProfileSectionA" _
(ByVal lpAppName As String, ByVal lpString As String, _
ByVal lpName As String) As Long
' From the IniSection class.
Private Declare Function WritePrivateProfileString _
Lib "kernel32" Alias "WritePrivateProfileStringA" _
(ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, ByVal lpString As Any, _
ByVal lpName As String) As Long
FIGURE 1: Each declaration is private to the class in which it’s used.
Loading the DataTo load data from the INI file into memory, the IniFile class calls the ReadData procedure (shown in FIGURE 2). This procedure is the most complex in all three classes, because it has to do the most work. It starts by calling GetPrivateProfileSectionNames to retrieve a list of all the sections in the INI file. To call GetPrivateProfileSectionNames, you must pass in a string buffer to contain the returned list of names. Of course, you have no idea how large to make this buffer before you call the function. If it’s too short, the function doesn’t return an error; it returns (you’ll love this) the size of your original buffer, minus 2. If you get that return value, you know that you need to expand the buffer and try again. If the function call succeeds, it returns the number of characters it placed into your string buffer. It’s up to you to trim the excess. Therefore, the first chunk of ReadData looks like the code fragment shown in FIGURE 3.
Private Sub ReadData()
Dim strSections As String
Dim lngSize As Long
Dim astrSections As Variant
Dim i As Integer
On Error GoTo HandleErrors
' In most cases, 1024 characters is enough, but if it's
' not, the code will double that and try again.
lngSize = 1024
Do
strSections = Space$(lngSize)
lngSize = GetPrivateProfileSectionNames( _
strSections, lngSize, Me.Name)
If lngSize = 0 Then
' No sections, so get out of here!
GoTo ExitHere
ElseIf lngSize = Len(strSections) - 2 Then
' That's how the API indicates you didn't allow
' enough space, but returning the size you originally
' specified, less 2. In that case, just double the
' buffer, and try again.
lngSize = lngSize * 2
Else
' Trim the extra stuff. Use lngSize - 1 because
' there's an extra vbNullChar at the end of this
' string.
strSections = Left$(strSections, lngSize - 1)
Exit Do
End If
Loop
' Now strSections contains the section names, separated
' with vbNullChar.
astrSections = Split(strSections, vbNullChar)
For i = LBound(astrSections) To UBound(astrSections)
' Add the section to the collection, indicating that
' it's not a NEW section. That is, it's not being added
' after the file was read. That way, the code there can
' know to not bother looking for items when being added
' by code later.
Call AddSection(astrSections(i), False)
Next i
ExitHere:
Exit Sub
HandleErrors:
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
FIGURE 2: The IniFile class uses the ReadData procedure to read INI file data.
Do
strSections = Space(lngSize)
lngSize = GetPrivateProfileSectionNames( _
strSections, lngSize, Me.Name)
If lngSize = 0 Then
' No sections, so get out of here!
GoTo ExitHere
ElseIf lngSize = Len(strSections) - 2 Then
' That's how the API indicates that you didn't allow
' enough space, but returning the size you originally
' specified, less 2. In that case, just double the
' buffer, and try again.
lngSize = lngSize * 2
Else
' Trim the extra stuff. Use lngSize - 1 because there's
' an extra vbNullChar at the end of this string.
strSections = Left$(strSections, lngSize - 1)
Exit Do
End If
Loop
FIGURE 3: The first chunk of the ReadData procedure.
If you haven’t been ejected from the procedure, strSections now contains a list of all the section names. At this point, the ReadData procedure breaks apart the string containing section names into individual strings, then calls the AddSection procedure to create each IniSection item. To break apart the delimited string, the code calls the Split function (in the basSplit module), which takes in a delimited string and the delimiter to look for, and returns an array containing each of the “chunks” of the string:
astrSections = Split(strSections, vbNullChar)
For i = LBound(astrSections) To UBound(astrSections)
Call AddSection(astrSections(i), False)
Next i
(If you’re using Office 2000 or Visual Basic 6.0, you should toss this module out and use the built-in Split function instead. It works the same as our function.)
As the code creates each IniSection object, the IniFile object calls the Initialize method of each new IniSection object. This method does all the work of retrieving a list of all the Name=Value pairs in the section, parsing them out, and creating an IniItem object for each. This all happens in the Initialize method of the IniSection object, as shown in FIGURE 4.
Public Sub Initialize()
Dim strItems As String
Dim lngSize As Long
Dim i As Integer
Dim astrAllItems As Variant
Dim astrItem As Variant
Dim strName As String
Dim itm As IniItem
' Get the name of the INI file in question.
strName = Me.Parent.Name
' Pick some arbitrary size, as a guess.
lngSize = 1024
Do
strItems = Space$(lngSize)
lngSize = GetPrivateProfileSection( _
Me.Name, strItems, lngSize, strName)
' If this section doesn't exist in the file, you won't
' get anything back from the API call. In that case
' just exit now.
If lngSize = 0 Then
GoTo ExitHere
ElseIf lngSize = Len(strItems) - 2 Then
' Not enough space! If you double the amount of
' available space, that may solve the problem.
lngSize = lngSize * 2
Else
' Trim the extra stuff. Use lngSize - 1 since there's
' an extra vbNullChar at the end of this string.
If lngSize > 1 Then
strItems = Left$(strItems, lngSize - 1)
End If
Exit Do
End If
Loop
' If you get here, you've got a null-separated list of
' items. Use the Split function to pull them into an
' array of strings.
astrAllItems = Split(strItems, vbNullChar)
' Loop through all the item/value pairs,
' and pull them apart.
For i = LBound(astrAllItems) To UBound(astrAllItems)
' The items are in the format: Key = Value
' Split those out now.
If Len(astrAllItems(i)) > 0 Then
astrItem = Split(astrAllItems(i), "=")
Me.Add Trim$(astrItem(LBound(astrItem))), _
Trim$(astrItem(UBound(astrItem)))
End If
Next i
ExitHere:
Exit Sub
HandleErrors:
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
FIGURE 4: The Initialize method of the IniSection object loads all information for the section into memory.
To do its work, the Initialize method calls the GetPrivateProfileSection API function, which works similarly to the GetPrivateProfileSectionName function. The code must again send in a string buffer, which may or may not be large enough, and check the return value of the function. If it’s not large enough, the function returns two less than the size you sent. Otherwise, it returns the number of characters it placed into the buffer. The code loops, increasing the buffer size, until it succeeds (see FIGURE 5).
lngSize = 1024
Do
strItems = Space(lngSize)
lngSize = GetPrivateProfileSection( _
Me.Name, strItems, lngSize, strName)
If lngSize = 0 Then
GoTo ExitHere
ElseIf lngSize = Len(strItems) - 2 Then
lngSize = lngSize * 2
Else
' Trim the extra stuff. Use lngSize - 1 because there's
' an extra vbNullChar at the end of this string.
If lngSize > 1 Then
strItems = Left$(strItems, lngSize - 1)
End If
Exit Do
End If
Loop
FIGURE 5: The code loops, increasing the buffer size, until it succeeds.
If you’re still in the procedure, you now have a list of Name=Value pairs, delimited by vbNullChar characters. The code calls the Split function to retrieve an array of the Name=Value pairs, and then visits each pair, splitting them again at the equals sign (=). The code retrieves the Name and Value portions (using the LBound and UBound functions), and calls the Add method of the IniSection object to add the new IniItem object to its collection of IniItem objects (see FIGURE 6).
astrAllItems = Split(strItems, vbNullChar)
' Loop through all item/value pairs, pulling them apart.
For i = LBound(astrAllItems) To UBound(astrAllItems)
' The items are in the format Key = Value
' Split those out now.
If Len(astrAllItems(i)) > 0 Then
astrItem = Split(astrAllItems(i), "=")
Me.Add Trim$(astrItem(LBound(astrItem))), _
Trim$(astrItem(UBound(astrItem)))
End If
Next I
FIGURE 6: The Add method of the IniSection object is called to add the new IniItem object to its collection of IniItem objects.
Saving the DataWhen it’s time to save changes to data, you’ll call the Save method of any of the three objects. If you call the Save method of an IniItem object, you’ll use the WritePrivateProfileString function to write out that item’s information. This code, shown in FIGURE 7, must retrieve the name of the section (Me.Parent.Name) and the INI file (Me.Parent.Parent.Name) before calling the function.
' Save the current Name/Value pair back to the
' original file.
Public Sub Save()
On Error GoTo HandleErrors
Dim strININame As String
Dim strSectionName As String
strININame = Me.Parent.Parent.Name
strSectionName = Me.Parent.Name
Call WritePrivateProfileString( _
strSectionName, Name, Value, strININame)
ExitHere:
Exit Sub
HandleErrors:
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
FIGURE 7: To save a particular item, the IniItem object uses this Save method.
When you call the Save method of an IniSection object, you’ll end up calling the WritePrivateProfileSection function to write the entire section to disk at once (see FIGURE 8). Before you can do this, however, you must rebuild a string of Name=Value pairs that this function can use. The BuildSection function does this work for you, looping through the entire private collection of IniItem objects (mItems in the code).
Public Sub Save()
' Save this section back to its INI file.
On Error GoTo HandleErrors
' Although you could simply loop through all the Item
' objects and call the Save method for each, this
' technique is a bit faster. It only involves a single
' access to the INI file.
If WritePrivateProfileSection( _
Me.Name, BuildSection(), Me.Parent.Name) = 0 Then
' An error occurred!
Err.Raise Err.LastDllError, Err.Source, _
"Unable to save INI file section '" & Me.Name & "'"
End If
ExitHere:
Exit Sub
HandleErrors:
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
' Loop through all the items in the section and return a
' string that can be sent to WritePrivateProfileSection.
Private Function BuildSection() As String
Dim strOut As String
Dim itm As IniItem
Dim i As Integer
If mItems Is Nothing Then
Exit Function
Else
For Each itm In mItems
strOut = strOut & itm.Name & "=" & _
itm.Value & vbNullChar
Next itm
BuildSection = strOut & vbNullChar
End If
End Function
FIGURE 8: The IniSection object’s Save method requires you to rebuild the list of Name=Value pairs.
The Save method of the IniFile object (FIGURE 9) is the simplest of the three. It simply loops through all the items in the private collection of IniSection objects, calling the Save method of each.
Public Sub Save()
On Error GoTo HandleErrors
' Loop through all the sections, writing all the items
' back to the file.
Dim sec As IniSection
For Each sec In msecs
sec.Save
Next sec
ExitHere:
Exit Sub
HandleErrors:
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
FIGURE 9: The IniFile object’s Save method loops through all the IniSection objects in the msecs collection.
Creating a Parent PropertyIn the object model as we’ve devised it, it’s easy to work your way down through the IniFile-IniSection-IniItem hierarchy to find a particular IniItem object. But what if you had a reference to a particular IniItem object, and you wanted to work your way back up the hierarchy to find the IniFile object that contained the IniItem object?
In that case, you’d need some sort of back-pointer. Many of the Microsoft object models provide a Parent property, which contains a reference to the parent of the current object. The following table lists the Parent property type for each object in our simple object model:
Object | Parent Type |
IniFile | |
IniSection | IniFile |
IniItem | IniSection |
Although you could use a Property Set/Get pair of procedures to maintain the backward links, it isn’t necessary. In the example project, you’ll find these code snippets:
' From the IniSection class module.
Public Parent As IniFile
' From the IniItem class module.
Public Parent As IniSection
Of course, the Parent property doesn’t do you any good unless you set its value somehow. When should you do this? And what should you set it to?
Imagine you’re writing code in the IniFile class, and you want to set the Parent property of an individual IniSection object to be the current IniFile object. How do you indicate that in code? Use the Me keyword, just as you would when working with a form.
For the IniFile class, the appropriate location to do this is in the class’ Add method, i.e. set the new object’s Parent property when you’re adding a new IniSection to the collection. The code (called from the Add method) from the IniFile class module is shown in FIGURE 10.
Private Function AddSection(ByVal Name As String, _
fNewSection As Boolean) As IniSection
' Add a new Section to the collection of sections.
On Error GoTo HandleErrors
Dim sct As IniSection
Set sct = New IniSection
sct.Name = Name
' The Section object's parent is the INI file.
Set sct.Parent = Me
If Not fNewSection Then
Call sct.Initialize
End If
msecs.Add sct, Name
ExitHere:
Set AddSection = sct
Exit Function
HandleErrors:
' Error-handling code removed.
End Function
FIGURE 10: The code from the IniFile class module.
You’ll find a similar chunk of code in the IniSection class, as shown in FIGURE 11.
Public Function Add(Name As String, Value As String)
On Error GoTo HandleErrors
Dim itm As IniItem
Set itm = New IniItem
' Set the new Item's properties.
itm.Name = Name
itm.Value = Value
Set itm.Parent = Me
' Add the item to the collection of Items.
mItems.Add itm, Name
Set Add = itm
ExitHere:
Exit Function
HandleErrors:
' Error-handling code removed.
End Function
FIGURE 11: This code in the IniSection class is similar to that in FIGURE 10.
Given this new forward and backward capability, you can do all sorts of fun things. For example, given an IniSection object inside the IniFile object, you could find out how many siblings the section has, using code like this:
Debug.Print sec.Parent.Count
Given an IniItem object, you could write code to find the Name property of the parent IniFile object:
Debug.Print itm.Parent.Parent.Name
Why two instances of Parent? The first Parent property retrieves a reference to the parent IniSection object. The second Parent property retrieves the parent object of the IniSection object: an IniFile object.
Finishing UpAlthough this group of classes seems complex, it’s really not. Most of the code is involved with reading and writing from the INI files, and once you get past that, the code for managing the hierarchy of objects is quite simple.
To use these classes in your own applications, follow these steps:
Import the three classes, IniFile, IniSection, and IniItem, into your own project.
If you’re using Office 97 or Visual Basic 5.0, copy in the Split function from the basSplit module.
In your code, declare an object of type IniFile, and instantiate it.
Set the Name property of the IniFile object.
Work with the IniItem and IniSection objects.
Call the Save method of any object you want saved.
As a reminder, if you’re using these objects in a Visual Basic 5.0 or 6.0 application, you’ll want to take the extra steps to provide a default method for each class, and an enumeration function for the IniFile and IniSection classes. See the sidebar in last month’s article for more information on taking these steps.
Finally, if you have suggestions on features to add to this object hierarchy, let us know. We intend to employ it in applications, and would like it to be as useful as possible. If we come up with substantial improvements, we’ll make sure they show up on Informant’s Web site.
Note: Portions of this article are modified from Visual Basic 6.0 courseware written by the authors for Application Developers Training Company (http://www.appdev.com).
Ken Getz (KenG@mcwtech.com) is a Senior Consultant with MCW Technologies, a Microsoft Solution Provider focusing on Visual Basic and the Office and BackOffice suites. Mike Gilbert (mike_gilbert@hotmail.com) is Technical Product Manager for Office Developer Edition and VBA Licensing at Microsoft. They are also co-authors of VBA Developer’s Handbook [SYBEX, 1997], and Access 97 Developer’s Handbook — with Paul Litwin [SYBEX, 1997].