Author: Lou Adler
I have an INI file that is approximately 20K with all entries in one section. If I
use TIniFile's ReadSection method, only part of the section gets loaded. Why?
Answer:
A reader asked me this question a few days ago, and I must admit that it stumped me
at first. He was trying to load an INI file's section that had several lines in it
(amounting to over 16K of text) that he needed to load into a combo box. The
section contained listings of several modem makes and models that he was going to
use in his application so users could pick the modems that were on their machines.
To approach his problem, he created a TIniFile object and used the ReadSection
method to read the section containing the list of modems into a TStrings object,
which happened to be the Items property of a TComboBox. His code worked fine with
one exception: ReadSection got about a third of the way through the list, then
mysteriously stopped loading values, and truncated in the middle of a line!
Intrigued, I decided to look into it, and much to my surprise, found a very
interesting quirk in the code for ReadSection in the IniFiles.pas source file.
An Undocumented Limitation
The first stop in my investigation had me testing some code out in loading a huge
section of an INI file into a ComboBox. I used the following procedure adapted from
a snippet my reader sent to me:
1
2 procedure ComboLoadIniSection(IniFileName, SectionName: string; const List
3 TStrings);
4 var
5 ini: TIniFile;
6 begin
7 list.clear;
8 if FileExists(IniFileName) then
9 ini := TIniFile.Create(IniFileName);
10 with ini do
11 try
12 ReadSection(SectionName, list);
13 finally
14 Free;
15 end;
16 end;
The code above looks pretty straightforward. In fact it works incredibly well, with
absolutely no errors. I used it on some fairly generic INI files with just a few
lines of key values first, and the Items property of my ComboBox was loaded just
fine. It was when I used the sample file the reader sent containing the modem
listings that things went awry. The procedure still executed fine with no errors,
but truncated about a third of the way through the list. It looked like I was going
to have to look into the source file.
Here's the listing for the ReadSection code in the IniFiles.Pas VCL Source file:
17
18 procedure TIniFile.ReadSection(const Section: string; Strings: TStrings);
19 const
20 BufSize = 8192;
21 var
22 Buffer, P: PChar;
23 begin
24 GetMem(Buffer, BufSize);
25 try
26 Strings.BeginUpdate;
27 try
28 Strings.Clear;
29 if GetPrivateProfileString(PChar(Section), nil, nil, Buffer, BufSize,
30 PChar(FFileName)) <> 0 then
31 begin
32 P := Buffer;
33 while P^ <> #0 do
34 begin
35 Strings.Add(P);
36 Inc(P, StrLen(P) + 1);
37 end;
38 end;
39 finally
40 Strings.EndUpdate;
41 end;
42 finally
43 FreeMem(Buffer, BufSize);
44 end;
45 end;
Looks like your basic WinAPI wrapper function. But there's one strange thing about
it, and it has to do with the call to GetPrivateProfileString. This is a
WinAPI-level call that is used to read a specific section of an INI file and loads
one or all of its key values into a buffer. The buffer has the following structure:
keyValue#0keyValue#0keyValue#0keyValue#0#0 where keyValue is a specific key value
in a section. The WinAPI help file states that if the size of the strings in the
section exceed the allocated buffer size, the buffer is truncated to the allocated
size and two nulls are appended to the end of the string.
So going back to the code listing above, what do you see? Right! The buffer size is
only 8K! So any section that has more than 8K in it will be truncated. That's why
only part of the list was added into the ComboBox at runtime. I'm sure there was a
good reason for the developer who wrote this wrapper to do this — probably to save
memory space and go on the assumption that no one would ever need to have anything
larger than 8K to read. But for those that do need to load in more than 8K, this is
a serious limitation.
So how do you work around this? Well, at first thought, I figured upon creating a
new descendant class off of TIniFile. But I checked myself because all the methods
of TIniFile are static, so in order to do an override of a method, I'd have to
write it over completely. Not a big deal, but then I'd have to deal with the
overhead of adding the component into the VCL (and if you're like me, you've got a
lot of components installed on your pallette). In the end, I decided to copy the
source code and make a generic utility routine that I put in a library that I use
for all my programs. Here's the code:
46
47 procedure INISectLoadList(IniFileName, SectionName: PChar; const list: TStrings);
48 const
49 BufSize = 32768; //Changed from 8192
50 var
51 Buffer, P: PChar;
52 begin
53 GetMem(Buffer, BufSize);
54 try
55 list.BeginUpdate;
56 try
57 list.Clear;
58 if GetPrivateProfileString(SectionName, nil, nil, Buffer, BufSize,
59 IniFileName) <> 0 then
60 begin
61 P := Buffer;
62 while P^ <> #0 do
63 begin
64 List.Add(P);
65 Inc(P, StrLen(P) + 1);
66 end;
67 end;
68 finally
69 List.EndUpdate;
70 end;
71 finally
72 FreeMem(Buffer, BufSize);
73 end;
74 end;
This is essentially a replica of the code above, with one exception: It now has a
32K buffer size. If you look up the GetPrivateProfileString in the help system,
you'll see that the function is in the API code for backward compatibility with
16-bit applications. And as you may know, there is a 32K resource limit with 16-
bit apps. Thus, your buffer can't be bigger than this. But this should be plenty of
space to work with for 99 percent of the applications out there. However, for those
of you making the move to Win95 and NT, the registry is where you should put
runtime parameters.
Stay tuned for an article on the registry coming up. I'm still doing the research on it.
|