
n the DevX
vb.dotnet.technical group, reader "kriptos" asks how to read an INI file with .NET. Rather unhelpfully, someone answered anonymously that .NET applications use XML configuration files instead. It's true that .NET applications use configuration files, but unless you only need simple ungrouped key/value pairs, configuration files quickly become complicated, especially as you add custom
configuration sections. But you
can use the existing Win32 API calls to read and write from standard INI files using the DllImportAttribute with C#, or with the Declare Function statement in VB.NET; however there are a couple of tricks.
Author Note: The API calls to interact with INI files have been obsolete since the release of Windows 95, and are supported in Win32 for backward compatibility only. This featured discussion contains an INIWrapper class that wraps the most important API calls for interacting with INI files.
The API INI Functions
The API functions are usually paired; there's a Get and a Write version for most of the functions. The API contains special functions to read and write the win.ini file in the $Windows folder, those aren't discussed in this article; however, if you have a need to modify win.ini through .NET, you'll see enough here to declare the function prototypes yourself. For INI files associated with individual applications, the most important Win32 API INI-related functions are:
GetPrivateProfileStringretrieve an individual value associated with a named section and key.
WritePrivateProfileStringset an individual value associated with a named section and key.
GetPrivateProfileIntretrieve an integer value associated with a named section and key.
WritePrivateProfileIntset an integer value associated with a named section and key.
GetPrivateProfileSectionretrieve all the keys and values associated with a named section.
WritePrivateProfileSectionset all the keys and values associated with a named section.
GetPrivateProfileSectionNamesretrieve all the section names in an INI file.
For example, the GetPrivateProfileString API function retrieves an individual value from an INI file. You specify the file, the section, the key, a default value, a string buffer for the returned information, and the size of the buffer. In classic VB, you use a Declare Function statement to declare the API function.
' Classic VB declaration
Public Declare Function _
GetPrivateProfileString _
Lib "kernel32" _
Alias "GetPrivateProfileStringA" _
(ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, _
ByVal lpDefault As String, _
ByVal lpReturnedString As String, _
ByVal nSize As Long, _
ByVal lpFileName As String) As Long
In .NET the equivalent declaration is:
' VB.NET declaration
Private Declare Ansi Function _
GetPrivateProfileString _
Lib "KERNEL32.DLL"
Alias "GetPrivateProfileStringA" _
(ByVal lpAppName As String, _
ByVal lpKeyName As String, _
ByVal lpDefault As String, _
ByVal lpReturnedString As StringBuilder, _
ByVal nSize As Integer, _
ByVal lpFileName As String) As Integer
C# handles things a little differently. In C#, you use the DllImport attribute to declare function prototypes, so the equivalent declaration is:
// C# function prototype
[ DllImport("KERNEL32.DLL",
EntryPoint="GetPrivateProfileString")]
protected internal static extern int
GetPrivateProfileString(string lpAppName,
string lpKeyName, string lpDefault,
StringBuilder lpReturnedString, int nSize,
string lpFileName);
The interesting point here is that the lpReturnedString parameter expects a string buffer nSize in length. In .NET, you pass a StringBuilder object for the lpReturnedString parameter rather than a String object (remember to add a using System.Text; line to your class file, in VB.NET use the Imports System.Text statement). That's because strings in .NET are immutable, so while you can pass them into unmanaged code without errors, any changes made to the string buffer in unmanaged code aren't visible in your .NET code when the .NET framework marshals the data back into managed code. StringBuilder objects are both mutable strings
and you can create them with a fixed buffer size. When calling unmanaged code that needs a fixed-size string buffer, try a StringBuilder first.
The EntryPoint parameter in the C# declaration above isn't strictly required. The EntryPoint parameter contains the name (or the index) of the function you want to declare. You only need to include the EntryPoint parameter if the name of your .NET function isn't the same, because .NET looks for a function named identically to the .NET function if you don't include the parameter. However, if you were to rename the .NET function shown above to getAppInitValue, you would have to include the EntryPoint parameter.
[ DllImport("KERNEL32.DLL",
EntryPoint="GetPrivateProfileString")]
protected internal static extern int
getAppInitValue(string lpAppName,
string lpKeyName, string lpDefault,
StringBuilder lpReturnedString, int nSize,
string lpFileName);
Author Note: I declared the prototypes in the C# class using the protected internal static accessibility level, which means they're only visible from this project and any derived classesyou may want to change the accessibility level, depending on how you want to use the functions.
The sample code includes an INI file named "INIinterop.ini" (see
Listing 1) that looks like this:
[textvalues]
1=item1
2=item2
3=item3
[intvalues]
1=101
2=102
3=103
After setting up the imported GetPrivateProfileString function definition, you can call it just like any other function. For example, using the INI file shown above and assuming it was saved as c:\INIinterop.ini, the following code would retrieve the value of the item in the [textvalues] section with the key "1"the string "item1"and write it to the Output window. Note that you don't need to instantiate an instance of the INIWrapper class, because all the methods are class-level methods (static methods in C#, shared methods in VB.NET).
// C# code
StringBuilder buffer = new StringBuilder(256);
int bufLen = INIWrapper.GetPrivateProfileString
("textvalues", "1", "", buffer, buffer.Capacity,
"c:\\INIinterop.ini");
Debug.WriteLine(buffer.ToString());
' VB.NET code
Dim buffer As StringBuilder = New StringBuilder(256)
Dim sDefault As String = ""
Dim bufLen As Integer = _
INIWrapper.GetPrivateProfileString _
("textvalues", "1", "", buffer, buffer.Capacity, _
"c:\INIinterop.ini") <> 0)
Debug.WriteLine(buffer.ToString())
In contrast, you can
write a new value without using a StringBuilder, because you don't need a return value.
// using C#
INIFileInterop.WritePrivateProfileString
("textvalues", "1", "new Item 1",
"c:\\INIinterop.ini")
' using VB.NET
INIFileInterop.WritePrivateProfileString
("textvalues", "1", "new Item 1",
"c:\\INIinterop.ini")
You can retrieve and write integer values with the GetPrivateProfileInt and WritePrivateProfileInt methods. See
Listing 2 (C#) or
Listing 3 (VB.NET) for the full declarations. To call the GetPrivateProfileInt function, you pass the section name, key name, a default integer value (returned if the key doesn't exist), and the name of the INI file. For example, the following code writes "101" to the Output window.
//using C#
int result = INIWrapper.GetPrivateProfileString
("intvalues", "1", 0, "c:\\ INIinterop.ini");
Debug.WriteLine(result.ToString());
' using VB.NET
dim result as Integer = _
INIWrapper.GetPrivateProfileString _
("intvalues", "1", 0, "c:\INIinterop.ini")
Debug.WriteLine(result.ToString())
Unfortunately calling the GetPrivateProfileSection function isn't quite as easy. The function returns a buffer filled with a null-delimited list of all the keys and values (items) in a specified section, with an additional trailing null character after the last item, so the returned buffer looks like this, where the \0 characters denote nulls:
1 = i t e m 1 \0 2 = i t e m 2 \ 0 3 = i t e m 3 \0 \0
You would expect to declare the function using a StringBuilder object with a pre-defined length for the lpReturnedString buffer parameter, just as with the GetPrivateProfileString functionbut that doesn't work. When you call the function, it returns the proper number of characters, but the Stringbuilder contains only the first item, "item1=first". However, the
return value of the function contains 16, which is correctthe length of the text of the two items in the [textvalues] section plus one null character after each item. In other words, the StringBuilder buffer contains the second itembut you can't reach it; the
first null character in the StringBuilder buffer determines the length of the contents available, and the StringBuilder throws an error if you attempt to index a character past that point. Obviously, you need to pass a managed type that isn't quite so sensitive to null-delimited strings.
Using a Char array doesn't work eitherthe function doesn't alter the array, even though it still returns the correct number of characters. Instead, after much fiddling with the problem, it turns out that you can use a Byte array. You can see the full declaration in
Listing 2 (C#) or
Listing 3 (VB.NET).
When the function call returns, the byte array contains the entire set of items, separated with null characters, as expected. I haven't found a truly simple way to convert the byte array to a set of strings; the best method I've found is to iterate through the byte array creating the individual strings using a StringBuilder object. The sample GetINISection method below wraps the call to the GetPrivateProfileSection API, converts the returned items to strings, collects them in a StringCollection and returns that to the calling code.
// C# code
public static StringCollection GetINISection
(String filename, String section) {
StringCollection items = new StringCollection();
byte[] buffer = new byte[32768];
int bufLen=0;
bufLen = GetPrivateProfileSection(section,
buffer, buffer.GetUpperBound(0), filename);
if (bufLen > 0) {
StringBuilder sb = new StringBuilder();
for(int i=0; i < bufLen; i++) {
if (buffer[i] != 0) {
sb.Append((char) buffer[i]);
}
else {
if (sb.Length > 0) {
items.Add(sb.ToString());
sb = new StringBuilder();
}
}
}
}
return items;
}
' VB.NET code
Public Shared Function GetINISection(ByVal filename _
As String, ByVal section As String) _
As StringCollection
Dim items As StringCollection = New _
StringCollection()
Dim buffer(32768) As Byte
Dim bufLen As Integer = 0
Dim sb As StringBuilder
Dim i As Integer
bufLen = GetPrivateProfileSection(section, _
buffer, buffer.GetUpperBound(0), filename)
If bufLen > 0 Then
sb = New StringBuilder()
For i = 0 To bufLen - 1
If buffer(i) <> 0 Then
sb.Append(ChrW(buffer(i)))
Else
If sb.Length > 0 Then
items.Add(sb.ToString())
sb = New StringBuilder()
End If
End If
Next
End If
Return items
End Function
To use the method, add the line using System.Collections.Specialized; (C#) or Imports System.Collections.Specialized (VB.NET) to the top of the calling class, and then you can write code such as this:
// C# code
StringCollection items =
INIFileInterop.GetINISection
("c:\\INIinterop.ini", "textvalues");
foreach(String s in items) {
Debug.WriteLine(s);
}
' VB.NET code
Dim s As String
Dim items as StringCollection = _
INIFileInterop.GetINISection _
("c:\INIinterop.ini", "textvalues")
For Each s In items
Debug.WriteLine(s)
Next
The sample code consists of two projects. The first is a class library project with an INIWrapper class (INIWrapper.cs in C#, INIWrapper.vb in VB.NET) that contains the API function prototypes and some wrapper methods to simplify calling the APIs. The classes work with any INI file. A second Windows Form project INIFileInteropTest (
the C# version) or INIFileInteropTestVB (the
VB.NET version) contains a full set of examples for using the INIWrapper classes, but the sample code depends on the INIInterop.ini file. You can save the INI file anywhere, but you must specify where it is in the INI File text field on the form before clicking the Start button (see
Figure 1). The project prints the results to the large text field at the bottom of the form.
There are a few other Win32 API calls that work with INI files, and over the years, I've found it useful to add wrapper functions that, for example, return just list of keys in a section, or just the list of values. I've also found it useful to write wrapper functions to insert comments at various places. You can probably think of many more extensions to these simple classes. Finally, this code is meant for example use only. You should add error-trapping and checking (see the DllImportAttribute.SetLastError field in the documentation, and the Marshal.GetLastWin32Error method in the documentation for more information.)