Back a million years ago, when computers ran on kerosene and the most recent operating system was Microsoft Windows 3.1, the information required by each program on a permanent basis—user options, for example—was stored either in a general file, WIN.INI, or a file unique to the program, also with a .INI extension. Beginning with Windows 3.0, when Microsoft added the functions for accessing "private" Profiles, the company discouraged cluttering up the official WIN.INI file and encouraged us to use so-called "private profiles" always.
Of course, we no sooner got accustomed to "private profiles" than Windows 95 came out, and the preferred depository for that kind of information became the system registry. The main reason for using the registry was that
Visual Basic's GetSetting and SaveSetting functions once accessed Profiles; but when VB moved to 32bits they were redirected to the Register. That doesn't mean they aren't still useful, though, as the dozens of Internet forms devoted to VB programming can attest. Each contains many requests: "How can I access a .INI file?" And so, I present a class to do just that.
Let's being by calling these things by their right name: Profiles. (Called that, I assume, since "profile file" sounds like the speaker has the hiccups.)
Profiles share a common structure, because they are usually managed by the same API functions. In this format, keyed values are grouped into sections. When there were just two Profiles (SYSTEM and WIN), the section was assumed to be the name of an application, and much of the documentation still refers to the section header that way. Now, with most applications owning their own Profiles, the files contain only one or a few sections.
The name of the section is always set off by brackets, and may include embedded blanks. The following list contains valid section names, the way they might appear in a Profile:
[Microsoft Word] [Clock] [Menus]
Section names are not case sensitive, so entering “CLOCK” or even “cloCK” would access the “Clock” section equally well.
Beneath each section name there is usually a group of keyed values (an empty section is permissible, however). A typical section from a WIN.INI file is shown below:
[Desktop] Pattern=(None) GridGranularity=0 wallpaper=camper.bmp IconSpacing=75 TileWallPaper=1
In this case, there are five keyed values, the first of which is “Pattern,” located in the “Desktop” group. Like section names, keys are not case sensitive.
The value of a key begins at the first character past the equal sign, so
wallpaper=camper.bmp
is not the same as
wallpaper = camper.bmp
Unless you hard-code a fully-qualified pathname as the filename parameter, Windows will always look for a Profile in the Windows directory—not in the Windows\System directory, the application’s “current” directory, nor even any of the directories listed in a PATH environment variable. You can, of course, construct a fully qualified pathname at run-time. At one time it was recommended that you let your Profile reside in the Windows directory with all the others. But now, the convention is to place it in the Program Files folder the application resides in.
One other aspect of Profiles, is that you don't "open" or "close" them like you do "normal" files. In fact, you don't even have to explicitly create them. When you request data from one, you supply a default value. If that key isn't present, or, for that matter, the section, or even the file, the default is returned: no harm, no foul. You can't even tell if the value came from a file or the default!
When you write a new value:
I wanted my Profile class to look like a keyed collection of values. And, since it is a class—and you are free to create as many instances of it as you want—that meant I didn't have to worry overmuch about Sections; let each instance stand for a single Section. Most applications will not need more than one instance.
A keyed collection is like a regular Collection object, except that the items it contains are only accessible by key, not numeric index.
However, I didn't want to lose the generally-useful abilities of the Private Profile API functions, like being able to enumerate Sections and Keys within sections. So I put that in, too.
If you wish, you can simply download the finished class.
Or, you can build it with me.
To do so, open Visual Basic 6 and create a new, standard project. Then click
the Project -> Add Class Module menu command. That will instantly open a
code window. Tap your F4 key to bring up the Properties box, and change the Name
property from "Class1" to "ProfileList".
Option Explicit
' This object represents a single section of
' a Windows Ini file by encapsulating the
' Windows Private Profile API.
Public Sections As New Collection
Public Keys As New Collection
Private i_Filename As String
Private i_Section As String
Private Declare Function GetPrivateProfileSectionNames _
Lib "kernel32" _
Alias "GetPrivateProfileSectionNamesA" ( _
ByVal ReturnBuffer As String, _
ByVal ReturnBufferSize As Long, _
ByVal Filename As String) As Long
Private Declare Function GetPrivateProfileSection _
Lib "kernel32.dll" _
Alias "GetPrivateProfileSectionA" ( _
ByVal Section As String, _
ByVal ReturnBuffer As String, _
ByVal ReturnBufferSize As Long, _
ByVal Filename As String) As Long
Private Declare Function GetPrivateProfileString _
Lib "kernel32" _
Alias "GetPrivateProfileStringA" ( _
ByVal SectionName As String, _
ByVal Key As Any, _
ByVal Default As String, _
ByVal Value As String, _
ByVal ValueSize As Long, _
ByVal Filename As String) As Long
Private Declare Function WritePrivateProfileString _
Lib "kernel32" _
Alias "WritePrivateProfileStringA" ( _
ByVal SectionName As String, _
ByVal Key As Any, _
ByVal Value As Any, _
ByVal Filename As String) As Long
Enum SupportedDataTypes
vbString = VbVarType.vbString
vbDate = VbVarType.vbDate
vbByte = VbVarType.vbByte
vbInteger = VbVarType.vbInteger
vbLong = VbVarType.vbLong
vbDecimal = VbVarType.vbDecimal
vbSingle = VbVarType.vbSingle
vbDouble = VbVarType.vbDouble
End EnumThe bulk of the section is taken by the four API declarations. You'll
notice I "cleaned up" Microsoft's silly naming convention. Because Visual Basic
will prompt you for the arguments to a function by name—that is, the
names in the function's declaration—it makes sense to me to dispense with the
obfuscating prefixes and stick with descriptive, meaningful argument names. And
the tool tip will tell you what the data type is!Above those four declarations, I put two Collection objects—Sections and Keys, so you can guess what those will be used for—and two Private variables, i_Filename and i_Section. The "i_" prefix is one I use to distinguish internal variables from their Public Property Get and Let procedures.
And, afterwards, is a bit of a surprise. Let's save the discussion for later.
Obviously, the ProfileList class will have to know the name of the file containing the profile. That value, which will be used by the API calls every time a profile value is read or written, will be stored in that internal i_Filename property. But how does that property get set? And, once assigned, how can it be read? After all, i_Filename is Private—it's invisible outside this class module.
It's done through a pair of property procedures naturally. These are procedures, very similar to Sub and Function procedures, that "appear" to the program outside the class as regular variables. The big difference: property procedures can do things other than assign or retrieve values.
Let me cheat a little and show you how the value is accessed, first:
Public Property Get Filename() As String Filename = i_Filename End PropertyProperty Get procedures are very much like VB Functions. They return values in just the same way, by assigning the value to be returned, to the name of the procedure itself. BY assigning i_Filename as the return value of the Filename property, I, in effect, make the value Public. So, you've got to ask—why bother?
The short answer is, sometimes you want something to happen when the value is read, or assigned, or both. It might be validation , calculation, or something implied by the class itself. For example, in this class, once the Filename is assigned, the class can be expected to provide a list of all Sections in the profile. So, when Filename is assigned—thus invoking the Property Let procedure—we do a little more than assign the incoming value to i_Filename:
Public Property Let Filename(ByVal Value As String)
i_Filename = Value
Refresh
End Property
Starting from the top, the procedure declaration states that this procedure is:
Looking at the code, we find the expected assignment of Value to i_Filename. But, after that, is a call to...Refresh? What's being refreshed?
To find our, we need to look at the Refresh procedure.
In object-oriented programming, public "procedures" that aren't properties, are called methods. Generally, methods act on the object, make it do something other than just read or retrieve values. In the Visual Basic world, Refresh means that the object is refreshing its data. The data in this case is the Sections list, and possibly the Keys list:
Public Sub Refresh()
Dim CharactersReturned As Long
Dim Result As String
Dim ResultList() As String, i As Integer
Result = Space(2048)
CharactersReturned = GetPrivateProfileSectionNames( _
Result, Len(Result), Filename)
Result = Left(Result, CharactersReturned)
ResultList = Split(Result, Chr(0))
Set Sections = New Collection
For i = 0 To UBound(ResultList)
If Trim(ResultList(i)) > "" Then
Sections.Add Trim(ResultList(i))
End If
Next
If i_Section > "" Then
Section = i_Section
End If
End SubThere are four local variables. They don't need to be declared at the top of
the procedure, but I usually do that just to make things easier for me to read.
(Hey, maybe I was a C programmer just a little too long...!) I'll describe them
as I come to their use.The first two executable statements are concerned with the mysteries of interfacing to the Windows API. See, Windows was written in C, for C programmers. C calling sequences and data types are mostly very different from VB's. In fact, in order to make calling these functions possible at all, some odd little kludges got built into the language.
A very basic difference is the way text strings are stored. In C, strings fill a sequence of bytes and are terminated with a NULL character—that is, a byte whose bits are all zeroes. In Visual Basic, the length of strings is managed differently; and they never take up more space than they need.
In order to pass Result to GetPrivateProfileSectionNames, then, we have to pre-allocate more than enough space; otherwise the return value could be truncated. I arbitrarily chose 2K. The call is then made. GetPrivateProfileSectionNames thoughtfully returns the actual number of characters returned, not including the trailing NULL. The Left function trims any excess characters.
Each of the section names returned, however, will be delimited from the others by a NULL of its own. So we Split the Result into the ResultList array. The Sections collection is re-created; and the section names added to it, one by one.
If you are sharp-eyed, you might have noticed that the Sections collection was already allocated when it was declared. Why do it again? Well, this is actually the fastest way to clear the contents of a collection—by replacing it with a new, empty one. But a better question would be, why pre-allocate it at all?
We want to make our objects as easy-to-use and foolproof as possible. If someone tries to "read" the Sections before assigning a Filename, of course there would be none to read. But if the collection hasn't been allocated, they'll get that dreaded "Object variable or With block variable not set" message. This way, Sections.Count will simply equal zero.
Now, there's an odd appendix to this method:
If i_Section > "" Then
Section = i_Section
End If
What's that about? Well, remember that Refresh, being a Public method, can be called at any time. If the Section property has already been set, then the Keys list should also be available. It now will not surprise you to know that assigning a value to Section automatically loads the Keys collection, just as assigning a Filename automatically loaded the Sections collection. By assigning the same value again, we trigger the mechanism.
Again, the Property Get procedure is short and to the point:
Public Property Get Section() As String
Section = i_Section
End Property
The good stuff is in the Property Let procedure, and this time there's no offloading into a "refresh" procedure; the work is done right here:
Public Property Let Section(ByVal Value As String)
Dim CharactersReturned As Long
Dim Result As String
Dim ResultList() As String, i As Integer, p As Integer
i_Section = Value
Result = Space(2048)
CharactersReturned = GetPrivateProfileSection( _
i_Section, _
Result, Len(Result), Filename)
Result = Left(Result, CharactersReturned)
ResultList = Split(Result, Chr(0))
Set Keys = New Collection
For i = 0 To UBound(ResultList)
If Trim(ResultList(i)) > "" Then
p = InStr(ResultList(i), "=")
If p > 0 Then
Keys.Add Trim(Left(ResultList(i), p - 1))
Else
Keys.Add Trim(ResultList(i))
End If
End If
Next
End PropertySimilar to what we've already seen, this time we call
GetPrivateProfileSection to object the list of keys. In this case, however,
what is returned is the keys and their values—which is more information
than we actually want right now. So, after the Result is Split into the
ResultList array, we take each string and truncate it just
before the "=" that separates the key from its value.So far, we've seen Public properties and methods. Now, let's look at a Private function:
Private Function GetItem(ByVal Key As String, _
ByVal DefaultValue As String) As String
Dim CharactersReturned As Long
Dim Result As String
Result = Space(2048)
CharactersReturned = GetPrivateProfileString(Section, _
Key, DefaultValue, Result, Len(Result), Filename)
If CharactersReturned Then
GetItem = Left(Result, CharactersReturned)
End If
End FunctionGetPrivateProfileString is not that different from the other API calls
we've seen. It's obviously the core function of the class; this is what actually
plucks the data from the profile. There are many modules and even classes on the
Web that end here, providing no more of a favor than to "wrap" the API call in a
VB-friendly procedure.But, for us, it's only the beginning.
The converse of GetItem has to call the fourth API function to place a value into the profile:
Private Sub SaveItem(ByVal Key As String, ByVal Value As String) Call WritePrivateProfileString(Section, Key, Value, Filename) End Sub
Since WritePrivateProfileString doesn't return any values, we don't have to perform any tricks before or after calling it.
Suppose we know that a given key's value is stored as Boolean. That could mean Yes/No, True/False, even 1/0. Wouldn't it be nice if that could be resolved when you requested the value, instead of afterward?
Private Property Get Bool(ByVal Key As String) As Boolean
Dim Text As String
Text = GetItem(Key, "False")
Bool = (UCase(Left(Text, 1)) = "Y") Or _
(UCase(Left(Text, 1)) = "T") Or Text = "1"
End PropertyThis procedure, which makes use of the GetItem
function we just saw,
handles that resolution. If the first character of Key's value is Y, T or
1, the property reads as True. Anything else will be False."But," you say, "this procedure is marked Private. How can anyone make use of it?"
"You'll see," I promise mysteriously.
Meanwhile, here's the Let procedure:
Private Property Let Bool(ByVal Key As String, _
ByVal Value As Boolean)
Dim Text As String
Text = Value
SaveItem Key, Text
End Property
The incoming Value will be coerced into a Boolean; when we assign that to Text, VB automagically converts it into "True" or "False". Thus, we achieve symmetry with the Property Get procedure.
Similarly, we might want a date and/or time value kept in our profile. Here's the Get procedure:
Private Property Get DateTime(ByVal Key As String) As Date
Dim Buffer As String
Buffer = GetItem(Key, "")
If Buffer > "" Then
DateTime = Buffer
End If
End PropertyWe again rely on VB to convert an incoming date and/or time from string
format to Date data type. However, only if a value was, in
fact, retrieved. Otherwise, the "null" Date value of January 1, 1899 will
be returned.The Let procedure is almost identical to the one for Bool:
Private Property Let DateTime(ByVal Key As String, _
ByVal Value As Date)
Dim Text As String
Text = Value
SaveItem Key, Text
End Property
The last of these three "formatting" properties handles numbers, or, rather, integers. The Val function makes for a safe conversion, just in case some other program (or some one manually) placed an invalid valid in the file:
Private Property Get Number(ByVal Key As String) As Long Number = Val(GetItem(Key, 0)) End Property |
Private Property Let Number(ByVal Key As String, _
ByVal Value As Long)
Dim Text As String
Text = Value
SaveItem Key, Text
End Property
Okay, now we're ready for the real interface, the Public one users will see.
First, remember the Enum from the (General)(Declarations) section?
Enum SupportedDataTypes
vbString = VbVarType.vbString
vbDate = VbVarType.vbDate
vbByte = VbVarType.vbByte
vbInteger = VbVarType.vbInteger
vbLong = VbVarType.vbLong
End Enum
I've borrowed a subset of the available VB data types. There are others; you could add to them if you wished.
Now, let's look at the Item property. Remember, my original wish for this class was that it would look like a "keyed collection" of values. All collection classes have an Item property; if you haven't noticed it, it's because the Item property is the default property for the class; you don't have to type it's name. When you code something like
x = List(2)
you are really programming
x = List.Item(2)
So, let's examine the Get procedure:
Public Property Get Item(ByVal Key As String, _
Optional ByVal DataType As SupportedDataTypes = vbString) As Variant
Select Case DataType
Case vbBoolean
Item = Bool(Key)
Case vbByte, vbInteger, vbLong, vbDecimal
Item = Number(Key)
Case vbDate
Item = DateTime(Key)
Case Else
Item = GetItem(Key, "")
End Select
End Property
Thanks to the fact that those Private properties are done, this procedure winds up basically being a switch. If the data type is not specified, String is assumed. The value is retrieved, using whatever procedure is appropriate, and passed back. Remember, even if the actual data type is not one specifically supported, VB's built-in data conversion will ensure the value is returned in a meaningful way.
The DataType property is optional; it defaults to vbString so generally it doesn't even need to be supplied. Because of the rules of declaring property procedures, we have to set up the Property Let procedure in exactly the same way:
Public Property Let Item(ByVal Key As String, _
Optional ByVal DataType As SupportedDataTypes = vbString, _
ByVal Value As Variant)
If DataType = vbString Then
DataType = VarType(Value)
End If
Select Case DataType
Case vbBoolean
Bool(Key) = Value
Case vbByte, vbInteger, vbLong
Number(Key) = Value
Case vbDate
DateTime(Key) = Value
Case Else
SaveItem Key, Value
End Select
End PropertyHowever, in this case, we can be a little smarter.
Value is a Variant, so we can use the built-in VarType
function to override
whatever (may have been) specified. While VarType might return a data
type we don't specifically support (such as vbDecimal), such as data type
will be handled by the Case Else and treated as a string, which is
perfectly adequate.There's one more thing we have to do before this property is complete: We have to mark it as the default property. We do that by selecting the Tools -> Procedure Attributes menu command. After clicking the "Advanced" button, the dialog looks like this:

You can (and should!) add a description; but the cursor is pointing to the important part: the "Procedure ID". By default, it is set to "(None)". You are allowed to make it "(Default)" for one, and only one, property per class.
The WritePrivateProfileString possesses an odd, extra, ability. It can be used to remove a key and its value entirely. Normally, we simply overwrite values. But if you really want to remove a key from the profile so that no other program (or person) can read it, call the Delete method:
Public Sub Delete(ByVal Key As String) WritePrivateProfileString Section, Key, vbNullString, i_Filename End Sub
If a Section and Key are specified, but instead of a Value—any value—the special vbNullString constant is passed, the Key will be deleted.
Similarly, an entire section can be erased. Since the ProfileList class represents an entire section, and acts like a collection, it makes sense that its Clear method not only clear the in-memory copies, but the section itself:
Public Sub Clear() WritePrivateProfileString Section, vbNullString, vbNullString, i_Filename End Sub
As before, we pass the vbNullString constant in place of the Value; but we pass it in place of the Key, as well. That deletes all the keys and values in the Section, and the Section name itself, from the profile.