Ini Ins and Outs
Exploiting your app’s ini file(s) with INI.CC
by David Stone

 
 
   


THIS article will introduce the reader to ini file management by providing and explaining a working example of the use of INI.CC, a neat little custom class created by Keith Chuvala, Bowen Moursund, and Kenneth Chan. INI.CC makes use of the three Windows API (Application Programming Interface) functions commonly used to manage ini files: GetPrivateProfileString(),WritePrivateProfileString(), and GetPrivateProfileSection(), and relieves us of having to know about or manage these API functions. Instead, INI.CC gives us several normal-looking functions that make management of ini files a much simpler matter.

Quick-Start: If you find line-by-line instructions tedious, you may wish to just run the included form to see how it and its two sub-forms behave, then examine the code, which is pretty extensively commented. If there’s anything you don’t understand, come back here to read about it.

Ini Introduction

Ini (initialization) files have been around since Windows 3.1. They are standard ASCII text files that contain data organized into categories called Sections. Their role, like that originally provided by the DOS files config.sys and autoexec.bat, is to “remember” configuration information used by both the system (win.ini, system.ini, etc.) and by individual applications (e.g., CorelDRAW’s coreldrw.ini). The information in an application’s ini file is mainly comprised of user-specifiable program settings, from recent files to onscreen window coordinates to the location of app-related data files.

Win95, a more complex system than Win3.1, introduced the Registry. It was intended to be a single central repository of settings for the operating system, hardware, and user applications as well, thus providing an alternative to the welter of app-related ini files on a hard drive. The Registry differs from all prior configuration files in that it is a binary database file rather than a simple text file, and is thus less directly accessible to users intent on mischief (e.g., finding a password). Furthermore, since it is hidden in the system folder, it is far less likely to be accidentally deleted. Although the Registry is extensively used by the Windows OS and is well-used by many applications, ini files are still with us. In fact, even win.ini is still with us and is regularly consulted and updated by the Windows OS. If you have any doubts about the continuing use of ini files, even in a clean-installed Win2K system, do a search for *.ini and watch the ListView control fill up.

On the other hand, here’s what Microsoft has to say about the use of their GetPrivateProfileString(): “This function is provided for compatibility with 16-bit Windows-based applications. Win32-based applications should store initialization information in the registry.”

dBASE’s use of ini files

In addition to its relatively few entries in the Registry, dBASE relies heavily on ini files. The dBASE executable itself uses DB2K.INI in preference to the Registry to store the wide variety of user preferences, and any application that you compile to an exe file in dBASE expects to have its own ini file, named after the application’s exe file. So if your exe is called MyBigApp.exe, it will, sooner or later, have an ini file called MyBigApp.ini.

dBASE will generate an ini file for your compiled application if there isn’t one: If you do not provide the ini file, the dBASE runtime will create one when the exe file is first run, just after the form opens. It is possible to deduce the Moment of Creation by placing “ini file detection code” to display a message in the command window such as
 
 
? "Now in header section... is ini file present yet? " +file("Myini.ini")
   

in the header section and in a custom Open() function, which always runs before the form is opened. The ini file is thus detectable in the form’s onOpen() event (which follows the opening of the form), but not prior to it.

What is in the ini file of a compiled dBASE app? Below is an auto-generated ini file for a compiled dBASE application:
 
 
[Toolbars]
Standard=0
StandardFloat=0
StandardStyle=0

[Desktop]
Maximized=1
Position=64 102 1216 921
StatusBar=0

[CommandWindow]
Open=1
Maximized=0
Minimized=1

[Directories]
0=V:\MenBase
Current=0

[Database]
Name=

   

Other than the location of the default folder under [Directories], there is nothing specific to this application in the auto-generated ini file. And there need never be, if you do not wish to use the ini file any further than this. However, the point of this article is to show you how to make your application more useful to the user by letting it “remember” user preferences, and restore them each time the program loads.

Dependencies

You will, of course, need a copy of INI.CC and its two support files. These three files are part of the dUFLP (dBASE Users’ Function Library Project) package. The version of INI.CC you have will depend on which version of the dUFLP you have. If you have an older version of the dUFLP, the files required are INI.CC, STRINGEX.CC and dUFLP.H. If you have either of the last couple of versions of the dUFLP, the files are INI.CC, STRINGEX.CC and SETPROC.PRG. If you do not have the dUFLP package, it is available at dbase.com . However, a set of the three ini-related files from the dUFLP is also included with the zipped demo forms via a link near the top of this article. Although the newest version of these files is set up to use Source Aliasing, I prefer to simply place a copy of these three files in the folder of the application I am working on, as this makes it easier to get them into the app’s Project file when it’s time to compile and build the exe. Keep in mind that each of the sets of 3 files is a “matched” set, so do not use the INI.CC from one set and the SETPROC.PRG from another; the INI.CC in each set expects to find the correct versions of its dependency files. Although the newer INI.CC file suggests that you must have dUFLP Source Aliasing set up in order to use the ini-related files, it is not necessary — just have a copy of each of the three files in your working folder and all will be well.

A general strategy for managing ini files

Since an ini file’s role is to store information and recall it, decisions must be made as to when, in the running of an app, these processes should occur, how the information should be stored while the app is running, and at what point new/changed preference data should be saved to the ini file. Programmers are likely to vary their approaches according to the amount and type of data that will be managed by the ini file, but here’s a basic approach that will do the job:

In the header section of the form:
1. Initialize app properties, check for the existence of the ini file, generate it if needed and establish default values
2. Extract values from the ini file and store them in app properties

In a custom Open( ) function of the form:
3. Update the form and controls to reflect preferences

In the open form:
4. Provide a means to specify preferences and update app properties and controls
5. Store preferences in the ini file on program exit

A main form, two sub-forms and an ini file accompany this article and are used in the examples below.

Ini file and INI.CC Basics

Before we proceed with examples of the use of INI.CC, let’s look at the relevant ini file terminology, and also take a quick look at INI.CC’s functions to see how they interact with ini file components.

Below is a Section of an ini file, grabbed from dBASE’s “auto-generated” ini file shown above:
 

[Toolbars] is referred to in ini-speak as a Section. A Section must be set in square brackets. Standard or StandardFloat or StandardStyle is referred to as an Entry. Each of the zeroes is referred to as a Value. An Entry can have more than one Value, usually comma-separated.
 

     
     
 

Ini files only store “character” data. You must always convert numeric and logical values to character strings before storing them in an ini file.
 

 

 
     

That’s all there is to ini terminology. I will always capitalize the first letter of these terms in this article since the words are often used in other contexts, and I want it to be clear when I mean the “ini” versions. Now let’s briefly look at the INI class.

Once an INI object is created (instantiated) with a name such as oMyINI, several ini-related functions (also called “methods”) become available. My guess is that after glancing at the above terminology and recalling the nature of the job at hand, you won’t have much trouble figuring out what most of these functions do:

  Functions  Description
   SetValue(cSection,cEntry,cValue)  To create, as needed, the Section and Entry, then to set the specified Value
   GetValue(cSection,cEntry)  To retrieve the Value from the specified Section/Entry
   GetSection(cSection)  To retrieve all the Section’s Entries and their Values into an array, with its object reference contained in oMyIni.SectionArray
   SectionArrayParse2Arrays([cDelimiter])*  To parse the array returned by GetSection(); see INI.CC for instructions
   DeleteSection(cSection)*  To delete the specified Section and all its Entries/Value(s)
   DeleteEntry(cSection,cEntry)*  To delete the specified Entry and its Value(s)
   Flush()  To force the ini file to be written to disk
   SetIniFile(cIniFile)*  To create a new ini file of the specified name; this can also be accomplished when instantiating the INI object
   Release()  To release the INI object
   * = not used in the example in this article
  Properties  Description
   IniFile  The filename of the INI file
   Section  The Section most recently read or written-to
   Entry  The Entry most recently read or written-to
   Default*  A programmer-provided default value for the GetValue() method, normally left blank (i.e., Value="")
   SectionArray*  The last-read one-dimension array of Entries of a Section in this form: Entry=Value
   EntriesArray*  The last-parsed Entries from SectionArray
   ValuesArray*  The last-parsed Values from SectionArray
   * = not used in the example in this article

The form called DemoIni.wfm supplied with this article allows several kinds of user preferences to be stored in the INI file and then retrieved. Below is a screen shot of the main form and its modal sub-form opened by the Set Preferences button:

The main form will save the following information as Values under the [Preferences] Section of its ini file:
  1. The colorNormal property of the Change my colors entryfield.
  2. The position and size of the form on the screen.
  3. Within the modal sub-form (Preferences): the main form’s colorNormal, the location of the DEO folder, and whether to display a “Daily Tip”.
Only #3 will ask the user to “OK” any changed settings; the others will be saved automatically to the ini file on exiting the program if any one has been changed.
 
     
     
  “DEO” refers to a new feature of dBASE called Dynamic External Objects. Briefly, this feature provides the option to maintain an application as a set of small object (.wfo, .mno, .reo, .dmo, etc.) files rather than as a single large EXE file. The advantage to DEO is that a modification to a program component requires that only the single changed component be re-compiled and sent to users. You are thus saved the time needed to regenerate a large EXE file, and the upload time is short — an object file is usually very small.
 

 

 
     

Creating an INI file
[Top|General Strategy]

The form created for this article has the first portion of ini-related code in its header section.
 

     
     
  The header section, as shown in the figure below, is the region above the line that says 
 
 
** END HEADER -- do not remove this line
   

Any code placed in this region will be run before the form object is created (instantiated). Placement of the ini-related code here is a matter of choice. Instead, it could be associated with the form’s onOpen() event (although it might cause blinking or visible movement of the form), so a better choice would be to place it in a custom Open( ) function or in a custom Init( ) function. I am placing it in the header section mainly out of habit.
 

 

 
     

Below is a screen shot of the Source Code Editor showing the header section.


Initialize the _app properties

We will use custom _app properties to store data extracted from the ini file.
 

     
     
   _app represents the object which is the currently running instance of dBASE. So, like any object, you can tack onto it, dot-style, any custom properties you’d like, as long as you steer clear of the built-in properties (see “_app” in OLH for a list of property names to avoid).

I am using _app properties here because they are visible to the entire application, and are therefore suited to holding information which needs to be seen by the entire application.

You may have read scornful comments about the use of _app properties, since they are functionally very similar to the much-maligned and spurned public variables of the DOS dBASE versions. My advice: ignore those comments and use _app properties where they are useful.
 

 

 
     

Below is the beginning of DemoIni.wfm’s header section where it initializes the custom _app properties:
 
 
// Init the _app properties to be used to store ini data
*----------------------
// Entryfield colors
_app.EF1color = ""

*----------------------
// Form color
_app.formColor = ""

*----------------------
// DEO folder location
_app.deoLoc = ""

*----------------------
// Display daily tip
_app.dailytip = ""

*----------------------
// Form size, position on screen
_app.fheight = ""
_app.fleft   = ""
_app.ftop    = ""
_app.fwidth  = ""

   

As you can see, an _app property has been initialized for an entryfield colorNormal property, another for the main form’s ColorNormal property, another for the location of the DEO folder, another for the Daily Tip option, and then four more to hold size/position coordinates for the main form. Although you could initialize these properties at the time they are loaded with ini data, I prefer having a nice orderly list at the top to remind me what I'm working with.

Role of the _app properties

The role of these properties is, first, to hold their corresponding data when it is extracted from the ini file. They will then be used to modify the corresponding form and object properties before the form opens. If the user changes any of the values while using the program, the _app properties will be updated so they can be used to write the new values back to the ini file when the user exits the program.
 

     
     
 

Use of _app properties, or any variables at all, to manage ini data is not a necessity — it’s entirely possible to achieve the same functionality without them. However, to do without them, you would have to place your initial ini code in a custom Open() or Init() function because you would need access to the properties of the instantiated form and its objects, since it is these properties that you will be setting. Furthermore, you would have to do this for any form in the app that has ini-settable properties. More on this below.
 

 

 
     

Test for existence of the ini file

The next chunk of code is where the action starts:
 
 
// The ini file and its full path...
cInihome = set("directory")+"\DemoIni.ini"
*----------------------
// if we can't find ini-file...
If not file(cInihome)
   msgbox("DemoIni.ini file is missing...so, we'll make one.","FYI",64)
   ...
   

We first need to test for the existence of the ini file so we’ll know which direction we have to take: either create it if it doesn’t exist, or extract information from it if it already exists. Even though it’s safe to assume when running an app that the ini file will be there, we need to provide for either scenario.

As you can see in the above code, we are a establishing a variable called cInihome and assembling contents to be placed in it. The dBL function set("Directory") will return the current drive and path, e.g., c:\dBASE\IniFileApp, and we use the “+” sign to add (concatenate) the necessary backslash, followed by the name of our ini file, to the drive:\path string. cIniHome should then contain a string of characters that looks something like this:
 
 
c:\dBASE\IniFileApp\DemoIni.ini
   
 

     
     
  If you don’t include the full path to the ini file, it will not be automatically placed in the current folder. Instead, if the ini creation code is run from a .wfm file in the IDE, the ini file will be placed in the folder where dB2K.exe is located; or if run from an .exe file, it will be placed in the folder where dB2KRun.exe is installed.

However, if you have no ini file and then allow it to be created automatically when your compiled app is run, the newly created ini file will be placed in the same folder as your exe file.
 

 

 
     

We then start an If/Else/Endif to test for the existence of the ini file. The first section of the If structure assumes we cannot find it, and we thus begin the code to create a new ini file. Incidentally, the whole process will be encased within a nice safe Try/EndTry in case anything goes terribly, tragically wrong.

Instantiate an INI object

We start by using set procedure to to tell dBASE to open the INI.CC file, and we then instantiate an object of the INI class:
 
 
TRY
   set proc to ini.cc additive
   // create new INI object
   oMyIni = new Ini(cInihome)  // Instantiation
   ...
   

The last line above instantiates the INI object into a variable called oMyIni. Notice that we are sending the name of the ini file (which is contained in the variable cInihome) as a parameter. The oMyIni object will look for this file, and if it isn’t there, will create it for us. So, after that line of code has been processed, a new empty ini file of the specified name will exist. We now need to add a Section and some Entries and corresponding Values.

Set Entries and Values

In the code below, we use the oMyIni function called SetValue()for the first time. It uses three parameters to specify the Section, Entry, and Value:

This function must be used once for each Entry or Value you wish to establish. Since we’re on the “ini-file-doesn’t-exist” branch of our If/Else/Endif, we know that the function is going to be creating the Section as well each Entry and Value.
 
 
   ...
   // Use oMyIni.SetValue(cSection,cEntry,cValue)
   // to set values of the entryfield color
   oMyIni.SetValue("Preferences","EF1color","0xffb3ff/0xcc9999")
   // update _app property
   _app.EF1color := "0xffb3ff/0xcc9999"
   ...
   

This first time the SetValue() function is used, oMyIni will notice that there is no [Preferences] Section, and will create it. It will then create an Entry called EF1color, add an = sign after it, then concatenate to that the Value, which is a colorNormal value of  0xffb3ff/0xcc9999

The last line in the code above also assigns the same colorNormal value to the corresponding _app property. Recall that this _app property is being used to transmit the corresponding ini Value to the form.

The result will be the following two lines written to the ini file:
 
 
[Preferences]
EF1color=0xffb3ff/0xcc9999
   

Establish the rest of the Entries

We now reiterate the SetValue() function to establish the rest of the Entries/Values that we wish to include in the ini file, and we also set their corresponding _app properties for later use by the form:
 
 
   ...
   // Form color
   MyIni.SetValue("Preferences","formColor","BtnFace")
   // update _app property
   _app.formColor = "BtnFace"

   *********************************

   // DEO folder location
   oMyIni.SetValue("Preferences","deoLoc",set("directory"))
   // update _app property
   _app.deoLoc = set("directory")

   *********************************

   // Daily tip
   oMyIni.SetValue("Preferences","dailytip","true")
   // update _app property
   _app.dailytip := "true"

   *********************************

   // Form position/size coordinates (height,left,top,width)
   oMyIni.SetValue("Preferences","fcoords","10.6,22,6,58.3") // note parentheses around the string of values...
   // update _app property
   _app.fheight = "10.6"
   _app.fleft   = "22"
   _app.ftop    = "2"
   _app.fwidth  = "58.3"
   ...

   

You may have noticed, above, that the last Entry, fcoords, is composed of four comma-separated Values. Also notice that we are storing the individual values in their _app properties as character strings rather than as numeric values, e.g., _app.fheight = "10.6". This is an arbitrary decision. These four values must, at some point, be converted to numeric values since the form’s size/position properties are numeric. I have chosen to keep all the _app properties as “character” to match the all-character ini Values, and to convert to numeric only as needed when setting object properties. You may choose to do it differently, of course.

We then use oMyIni.Flush() to force the ini file to be written to the disk and we also release the oMyIni object. The rest of the Try/EndTry code is shown here as well:
 
 
   ...
   // force a write of the new ini file to disk
   oMyIni.Flush()

   // release INI object
   oMyIni.Release()

CATCH (Exception e)
   msgbox("Unable to create INI file","INI file creation error",16)
ENDTRY
...

   

Extract information from the ini file and store it in _app properties
[Top|General Strategy]

If the ini file already exists

Still in the header section of the form, we now enter the Else branch of our If/Else/Endif structure, which covers the alternative scenario — the ini file is already present. Here, we will use the GetValue(cSection,cEntry) function to retrieve each Value and store it in its respective oMyIni.Value property. If GetValue() cannot find either the Section or the Entry, or if the Value is blank, the oMyIni.Value will be blank, and we will use SetValue() to create what was missing.

A relevant property that we’re not using here — oMyIni.Default

INI.CC makes use of a “default Value” feature of the GetValue() function: if you load the property called oMyIni.Default with the desired default contents of the Entry’s Value, then call the GetValue() function, if GetValue() cannot find either the Section or the Entry, it will create them as needed, and will return the contents of the Default property to the oMyIni.Value property. However, it will not set the ini file Value with your specified default value. Instead, the Value in the ini file will be left blank. Moreover, if the Section and Entry are already present in the ini file but the existing Value is blank (i.e., un-set), the function assumes that the blank value is valid, and so the contents of oMyIni.Default will not be returned to the oMyIni.Value property; instead, a blank is returned. That’s OK if the desired default is a blank, but it’s not OK if you wish the default to be a string or numeric value.

So, you will have to decide whether to use the oMyIni.Default property or not. For this article’s form, I do not use it at all. Instead, I just test for the GetValue()’s return value (in the oMyIni.Value property), and if it is blank (meaning either no Section, no Entry, or a blank Value), I use SetValue() to create what’s missing and to set the proper Value. The result is this: no blank values in any of the ini file Entries. Since you may desire a different outcome in your application, you can tailor your use of the GetValue() function as needed.

The GetValue() code is shown below for all but the last ini Entry:
 
 
   ...
   // ini-file exists and we need to extract Values from it
Else 
   set proc to ini.cc additive
   oMyIni = new Ini(set("directory")+"\DemoIni.ini")  // Instantiation

   // Check Entries — if any is missing, empty or wrong, fix it. Otherwise, get the Value.
   *---------------------------- 
   oMyIni.GetValue("Preferences","EF1color")  // entryfield colorNormal
   if empty(oMyIni.value)  // no Value set, so set it
      oMyIni.SetValue("Preferences","EF1color","0xffb3ff/0xcc9999")
      _app.EF1color = "0xffb3ff/0xcc9999"
   else  // otherwise, get the Value into the _app property
      _app.EF1color = oMyIni.value 
   endif

   *----------------------------
   oMyIni.GetValue("Preferences","formColor")  // Form color
   if empty(oMyIni.value)  // no value set, so set it
      oMyIni.SetValue("Preferences","formColor","BtnFace")
      _app.formColor = "BtnFace"
   else  // otherwise, get the value into the _app property
      _app.formColor = oMyIni.value 
   endif

   *----------------------------
   oMyIni.GetValue("Preferences","deoLoc")  // DEO folder
   if empty(oMyIni.value)  // no value set, so set it
      oMyIni.SetValue("Preferences","deoLoc",set("directory"))
     _app.deoLoc = set("directory")
   else  // otherwise, get the value into the _app property
      _app.deoLoc = oMyIni.value 
   endif

   *----------------------------
   oMyIni.GetValue("Preferences","dailytip")  // Display daily tip
   if empty(oMyIni.value)  // no value set, so set it
      oMyIni.SetValue("Preferences","dailytip","true")
      _app.dailytip = "true"
   else  // otherwise, get the value into the _app property
      _app.dailytip = oMyIni.value 
   endif
   ...

   

Multiple comma-separated Values

For the sake of illustration, I have used multiple Values for the last Entry (called fcoords). These four Values will specify the form’s size and location on the screen (form.height,form.left, form.top, form.width). Although we could have set four separate ini Entries for these four coordinates, it’s more efficient and also pretty easy to stick them all in the same Entry. However, on retrieving them, we will parse them out to four separate _app properties — not absolutely necessary, but easier to deal with in the form, I think.

GetValue() is used in the same way as above. The only difference here is that if a Value is present we assume it is a comma-separated string of four values and then use the Tokenize() function to place the values in an array called aCoords.
 

     
     
  Tokenize() is a very nice little function that I borrowed from Vic McClung’s Printer Class. Its job is to accept a character-separated (usually comma-separated) sequence of character strings and parse the strings into an array. It takes two parameters: the string to be parsed, and the separation character. Here, our list of form coordinates is comma-separated.

The Tokenize() function is included within the DemoIni.wfm file beneath the Form class so that it can be “seen” by code in the header section (remember that while we’re in the header section, the form has not yet been instantiated, so its methods are not yet available). 

Thus Tokenize() is not a method of the form, and so it can be called directly, i.e., Tokenize() rather than form.Tokenize(). This is shown below along with some TRY/CATCH error checking in case the coordinates have been fooled with — we want to avoid seeing an “invalid subscript reference” error.

“Invalid subscript reference” is a common error which means the program has asked for a non-existent array element. For example, if the code asks for aMyArray[4] and there are only 3 elements in the array, this error will occur. Unfortunately, it’s not always easy to figure out which array is causing the problem.
 

 

 
     
 
 
   (...)
   *----------------------------
   oMyIni.GetValue("Preferences","fcoords")  // Form size & position
   // no Value for this Entry
   if empty(oMyIni.value) 
      // fix the Entry with defaults
      oMyIni.SetValue("Preferences","fcoords","10.6,22,6,58.3")
      // set default _app properties
      _app.fheight = "10.6"
      _app.fleft = "22"
      _app.ftop = "6"
      _app.fwidth = "58.3"
     // there is a value...
   else  // use tokenize function to parse the string
      aCoords = tokenize(oMyIni.value,",")
      TRY   // in case someone has fooled with the coord values
         // and we have fewer than 4 values...
         _app.fheight = aCoords[1]
         _app.fleft = aCoords[2]
         _app.ftop = aCoords[3]
         _app.fwidth = aCoords[4]
      CATCH (Exception e)
         // there was at least 1 missing coordinate...so set default values
         oMyIni.SetValue("Preferences","fcoords","10.6,22,6,58.3")
         _app.fheight = "10.6"
         _app.fleft = "22"
         _app.ftop = "6"
         _app.fwidth = "58.3" 
   endif
   *----------------------------
   
 
     
     
 

I’m sure you can see that there are many opportunities for bad ini data to sabotage our process here (such as nutty coordinates that put the form ½ mile off-screen)… so you will have to decide how much error-checking code you wish to write. And, considering that it is mostly, if not all, in defense against users’ meddling with the ini file, you can now see why the Registry (which is relatively inaccessible) seemed like such a good idea to Microsoft. In fact, we could just as easily perform this entire process using the Registry and REGISTRY.PRG instead of using the ini file and INI.CC. Perhaps it will be the subject of a future dBulletin article.
 

 

 
     

To wrap up this “header section” aspect of ini file management, we dump the oMyIni object.
 
 
   // release and null the oMyIni object
   oMyIni.Release()
   oMyIni = null
Endif
   

Here is what a brand-new ini file will now look like

Let’s assume the worst — that we had to re-create the ini file from scratch. It will now look like this:
 
 
[Preferences]
EF1color=0xffb3ff/0xcc9999
formColor=BtnFace
deoLoc=X:\DemoIni <your own current folder will show up here...>
dailytip=true
fcoords=10.59,22.00,2.00,58.29
   

Where’s the rest of the ini innards??

You may be thinking “Hey, where is the rest of the ini file contents?? All the stuff that dBASE puts in automatically?” Not to worry — when your app is compiled and run as an exe, those extra Sections/Entries will be added automatically. They are not added when the form is run in the IDE.

Ready to instantiate the form

Since we now have an ini file and have filled our _app properties with data, we are ready to instantiate the form. We’ll then use a custom Open() function which extracts the contents of the _app properties and applies them to corresponding properties of the form and its controls.

Update the form and controls to reflect preferences
[Top|General Strategy]

Now we move to the main form and our custom function called Form.open().
 

     
     
  If you have not used a custom form.open() function yet in your coding, here’s why it is useful: A custom form.open() function is called before the FORM class’s built-in Open() function. This means that adjustments to the form occur before it is visible, thus eliminating obvious screen flicker as properties are changing.

There’s one crucially important aspect of using an Open() function: you have to end it with this line:
 
 
return SUPER::Open()
   

which calls the Form class’s own built-in Open() method. It is what actually opens the form.

If you wish to do something analogous with a modal and SDI dialog form opened with MyModalForm.readmodal(), create a custom form.readmodal() function in the dialog and use it to modify properties of the dialog form. It will over-ride the built-in Readmodal() function belonging to the Form class. Your custom form.readmodal() function must end with this line:
 
 
return SUPER::Readmodal()
   

which, as in the previous example, calls the Form class’s own built-in Readmodal() method.
 

 

 
     

So, things are now getting simpler — all we need to do here is use our _app properties to change the relevant form and object properties as shown below:
 
 
Function Open
   // Here is where we use the values of our custom
   // _app properties to set form and object properties.
   // We do it here (particularly the form's coordinates)
   // rather than in the form's onOpen() so that the form 
   // doesn't do funny stuff, like jerk around and blink.

   // set the EF1 entryfield's colorNormal
   form.EF1.colorNormal = _app.EF1color

   // set form's colorNormal
   form.colornormal = _app.formColor

   // set form's size/position
   form.height = val(_app.fheight)
   form.left = val(_app.fleft)
   form.top = val(_app.ftop)
   form.width = val(_app.fwidth)

   // Initialize a flag (as a custom property of the form) 
   // to tell us if we need to update the ini file on exiting
   // the form. Better to use an _app property for this?
   // Your choice.
   form.DoIniUpdate = false

   // this next line is how you always end a custom
   // form.open() function:
   return SUPER::Open() 
   // it calls the form Class's built-in Open() 
   // function to actually open the form.

   

We also initialize a custom property of the form (form.DoIniUpdate) that will be used as a flag to remind us if any changes are made to ini-related data.

Remember that numeric values are “character” in an ini file

At whatever point you grab numeric values from an ini file, always recall that if you wish to use them as numeric, you have to convert them by using the VAL() function. Here, since I chose to leave all the _app properties as character to match the ini file Values, we have to now do the conversion to numeric. You can see the four examples in the above code.

If you want to avoid using _app properties

As mentioned above, it is not necessary to use _app properties at all in this process. If we had decided to not use them, we could have left the header region code-free and instead used our form.open() function. That function is where we would instantiate the oMyIni object and directly alter the form and object properties, as shown (hypothetically!) below:
 
 
set proc to ini.cc additive
oMyIni = new Ini(set("directory")+"\DemoIni.ini")  // Instantiation
if empty(oMyIni.Value)  // no Value set, so set it
   oMyIni.SetValue("Preferences","EF1color","0xffb3ff/0xcc9999")
   form.EF1.colorNormal = "0xffb3ff/0xcc9999"
else  // otherwise, use the Value to set the object property contents
   form.EF1.colorNormal = oMyIni.Value
endif
   

Keep in mind that it would still be necessary to test for the existence of the ini file as we did in the header section. Using the “direct” method, we would save a fair bit of code by not having to initialize the  _app properties and by avoiding the 6 lines of _app-to-object.property code in our Open() function. The other advantage to not using _app properties is that, if there are a lot of them, we save some RAM.

Done! The form is now open.

Check for Daily Tip

In the first few milliseconds after the form opens we have to do our first ini-specified task! We are going to use the form’s onOpen() event to check whether the user wants to see the “Daily Tip” form. If so, we’ll open that form as well. Here’s the form’s onOpen() code:
 
 
Function form_onOpen
   // make the code in these files available to use
   set proc to prefs.wfm additive
   set proc to dailytip.wfm additive

   // if we're supposed to display the Daily Tip form
   if _app.dailytip = true
      // declare a local var to hold the obj. reference to
      // the Daily Tip dialog form
      local dt

      // instantiate the form into the local variable
      dt = new dailytipFORM()

      // establish a communication "hot-line" back to the parent form
      dt.oParent = form

      // open the form "modal"...so, the code pauses until user
      // exits the dialog
      dt.readmodal()

      // now back from the form...zap the obj. reference to main form
      dt.oParent = null

      // release dialog form...not really necessary for a local var,
      // but hey...
      dt.release()

      // null it too...now we're *really* beating a dead horse...
      dt = null
   endif
   return

   

The above is pretty standard code for opening a modal dialog box. We instantiate it into a local variable dt, set up a means to communicate with the parent via dt.oParent, and run the form. The user has the choice in this dialog form of either continuing to see a Daily Tip or not. If the user chooses to turn off the feature, the dt form tells the parent in this way:
 
 
// This is the only relevant code from the dialog form,
// and is the onChange() event code for a checkbox that allows
// the user to turn on or off the Daily Tip option.

Function DAILYTIP_CB_onChange
   // set the _app property according to the value (T/F) of the checkbox
   _app.dailytip = iif(this.value = true, "true", "false")

   // set this parental flag to true so we update ini file on exit
   form.oParent.DoIniUpdate = true 
   return

   

Note: Here we must convert from the logical format of the checkbox value to the character format of the _app property… hence the "true" and "false" rather than true and false.

And, if the _app.dailytip property gets set to "false" and is not changed back to "true" before the program is exited, the ini file will be updated accordingly, and the next time the program is run, the Daily Tip will not be displayed.

Provide a means to specify preferences and update _app properties and controls
[Top|General Strategy]

Change the entryfield colors

On the DemoIni form is a pair of pushbuttons which allow the user to specify the text color and background colors of an entryfield.

While you may not wish to provide users with this particular option, the techniques described below can be applied to any control with a foreground/background colorNormal property.

 
Function EFTXTCOL_PB_onClick
   // use the getcolor() function to return an RGB color triplet
   cColor = getcolor("Pick a color for the text")

   // if a color was picked by the user
   if not empty(cColor)
      // shorten the reference to the colorNormal property
      cn = form.EF1.colornormal

      // change the colorNormal property to the new text color
      // and the existing background color
      form.EF1.colornormal := form.rgbToHex(cColor) + substr(cn,at("/",cn),20)

      // grab the entire colorNormal into its _app property
      _app.EF1color = form.EF1.colornormal

      // set the DoIniUpdate flag to true so we update the ini file on exit
      form.DoIniUpdate := true
   endif
   return

Function EFBGCOL_PB_onClick
   // use the getcolor() function to return an RGB color triplet
   cColor = getcolor("Pick a color for the background")

   // a color was picked by the user
   if not empty(cColor)
      // shorten the reference to the colorNormal property
      cn = form.EF1.colornormal 

      // change the colorNormal property to the existing text color
      // and the new background color
      form.EF1.colornormal = substr(cn,1,at("/",cn)) + form.rgbToHex(cColor)

      // grab the entire colorNormal into its _app property
      _app.EF1color := form.EF1.colornormal

      // set the DoIniUpdate flag to true so we update the ini file on exit
      form.DoIniUpdate = true
   endif 
   return

   

     
     
  The function called form.RGBtoHex() is a sweetheart written by Jay Parsons years ago, and is included in the dUFLP collection as part of Convert.cc

It is fed an RGB (RedGreenBlue) triplet such as 175,175,216 and returns the hex equivalent: 0xD8AFAF

While we’re on the subject, the 0x is not part of the hex numbers — it merely specifies that it is followed by hex numbers.
 

 

 
     

Change form coordinates

The current coordinates of the form change, of course, as the form is moved or sized by the user. What we need to do is notice whether these changes occur. Below is the code that checks whether movement and/or sizing has occurred. As you can see, this is accomplished using codeblocks specified in the form’s constructor code. A codeblock runs if the form is moved or sized, and it sets our custom property form.DoIniUpdate to true:
 
 
class demoiniForm of FORM
with (this)
   onMove  = {;form.DoIniUpdate := true}
   onSize  = {;form.DoIniUpdate := true}
   onOpen  = class::FORM_ONOPEN
   onClose = class::FORM_ONCLOSE
   open    = class::OPEN
   height = 10.6364
   left = 22.2857
   top = 5.9091
   width = 58.2857
   text = "Ini Ins and Outs"
endwith
   

If form.DoIniUpdate is true at exit time, the form’s coordinates are extracted and saved to the ini file.

The Preferences Dialog Box

Another means by which our sample form allows the user to specify two preferences is to use the standard approach — a Preferences dialog box. The idea is simple — temporarily save any changed user preferences; then, if the OK pushbutton is clicked, set the _app properties accordingly and change any form or object properties specified by the user.

Specify the main form’s color

Run the Preferences form from the main form to see how it behaves, then examine the code below which gets the chosen form color from the user, translates the RGB to a more usable format (via the RGBtoHex() function located on the parent form), then stores it in an object property:
 
 
Function CHOOSECOL_PB_onClick
   // user can pick a color from the color chart
   cFormCol = getcolor("Pick a color for the form")

   // a color was picked (in the RGB format, e.g., 175,175,216)
   if not empty(cFormCol)

      // switch RGB result to hex format (call parent function
      // using the obj.ref. oParent)
      cFormCol = form.oParent.rgbToHex(cFormCol)  // returns a value like this: 0xD8AFAF 

      // change the shape's color to display the new color
      form.nb1.FORMCOL_SHAPE.colorNormal = "black/" + cFormCol
   endif 
   return

   

The chosen color (e.g., 0xD8AFAF) is “stored” as part of the colorNormal property of a Shape object, which both illustrates the color for the user and saves the color value in a format that we can use if the user accepts the changes. Thus, the Shape’s colorNormal property might contain something like this: "black/0xD8AFAF"

Specify the DEO folder

We allow the user to specify the DEO folder as follows:
 
 
Function GETDEOFOLDER_PB_onClick
   // user chooses a folder
   cFolder = getdirectory()
   if not empty(cFolder)
      // set the folder name to the Entryfield value
      form.nb1.DEOFOLDER_EF.value = cFolder
   endif
   return
   

dBASE’s getDirectory() function returns the drive:\path\folder name, so this is easy… and we “save” it temporarily by displaying it in the entryfield as the entryfield’s value property.

Accept or Cancel

As usual with a Preferences dialog, the user can either accept or cancel the changes. If changes are cancelled, we just close the form and that’s it. But if the user accepts the changes, we have to do two things:

  1. Save the changed specified preferences to their respective _app properties, and
  2. Update the form/object properties as specified by the user.
These actions take place in the Preferences form’s OK_PB_onClick() method:
 
 
Function OK_PB_onClick
   // shorten the reference to the colorNormal property
   cn = form.nb1.FORMCOL_SHAPE.colorNormal

   // get the last section of the Shape's colorNormal
   // for the main form's color _app property
   _app.formColor = substr(cn,at("/",cn)+1,15)

   // now go ahead and change main form's color
   form.oParent.colorNormal = _app.formColor

   // get the deo folder into its _app property
   _app.deoloc = trim(form.nb1.DEOFOLDER_EF.value)

   // update the DoIniUpdate flag
   form.DoIniUpdate = true

   // close the form
   form.close()
   return

   

At this point, the form’s color has been changed to reflect the user’s choice, and the DEO folder has been stored in its _app property, ready to update the ini file when the user exits.

For the non-_app users

Again, if you prefer to not use _app properties, the above code would need to include, in place of the _app updates, instantiation of an oMyIni object from INI.CC and an update of the Values for deoLoc and formColor via the SetValue() function. The code for this is in the section below.

Store preferences in the ini file on program exit
[Top|General Strategy]

When the user exits the program, we can take the opportunity to update the ini file if needed. Below is the form’s onClose() event code where we first check our custom logical property form.DoIniUpdate to see if we need to update the ini file:
 
 
Function form_onClose
   // Write _app properties to the ini file if needed...
   if form.DoIniUpdate = true
      form.UpdateIni() 
   endif
   return
   

If we do need to update the ini file, the function form.updateIni() is called. Its code is below:
 
 
Function UpdateIni
   // Here is where we update the ini file before 
   // exiting the app.

   // create (instantiate) INI object
   oMyIni = new Ini(set("directory")+"\DemoIni.ini")

   // entryfield colorNormal
   oMyIni.SetValue("Preferences","EF1color",_app.EF1color)

   // Form color
   oMyIni.SetValue("Preferences","formColor",_app.formColor)

   // DEO folder location
   oMyIni.SetValue("Preferences","deoLoc",_app.deoLoc)

   // daily tip
   oMyIni.SetValue("Preferences","dailytip",_app.dailytip)

   // form position/size coordinates
   cAllcoords = ;
   form.height + "," + form.left + "," + form.top + "," + form.width

   oMyIni.SetValue("Preferences","fcoords",cAllcoords)

   // force a write of the ini file to disk
   oMyIni.Flush()

   // release INI object
   oMyIni.release()
   return

   

At this point, you’ll probably be able to figure out most of what’s going on in this function. We create a new INI object, then use the object’s SetValue() function to update the Entries in the ini file. We get the Values directly from our _app properties.

The only slightly tricky aspect of this is to concatenate, in the correct sequence, all four of the main form’s coordinates into a comma-separated string: we’re basically just adding a “,” between each coordinate. Notice that they do not have to be expressly converted to character — dBASE’s “Automatic Type Conversion” does that for us when we concatenate them to character elements (the commas). They are all thus saved as a string as the Value of the fcoords Entry.

Also notice in this UpdateIni() function that we are not testing any of our individual ini Values to see if they were changed. The approach here is basically this: if even one Value is changed during program execution, we’ll update all of the Values, changed or not. The overhead is low, so why not?

Done!

Conclusion
[Top|General Strategy]

This article has presented a working approach to managing ini files by use of the custom class INI.CC and custom _app properties, and has also provided some direction in accomplishing the same ends without the use of _app properties. We thus have the capacity to create from scratch a new ini file with an appropriate Section and its component Entries and Values; to repair an ini file that is missing the Section or any of its components; to extract Values from an ini file on loading an app; and to save changed data back to the ini file on exit from the program.

I hope that the categories of ini-storable data used here are sufficient to cover most of the sorts of data a user might wish to retain: object colors, a data location, whether to show a daily tip, and form coordinates. These settings include character, logical, and numeric data.

Goodies in INI.CC that I didn’t cover

Two of the functions, DeleteSection() and DeleteEntry() are very easy to figure out. The function called SetIniFile() can be used to create a new ini file, but we used the instantiation of the INI object to do the same thing. The pair of related functions called GetSection() and SectionArrayParse2Arrays() and their several related array properties are beyond the needs of most ordinary INI file management, but be sure to have a look at the descriptions of these functions in INI.CC so you can understand their potential.

Don’t think only in terms of MyApp.ini

Please don’t consider that the ini file named after an application’s EXE file is the only one that is editable from within the application. Any of your applications can have as many ini files as it needs in order to store/retrieve information. The only one it must have is the one named after the exe file, and that one will be automatically generated. And, Win.ini or any other ini file on your system is now within your reach as well.

Don’t think only in terms of ini files!

Heck, we’re database mavens! We’re sitting on one of the most powerful desktop database packages in the world. Who needs ini files? We certainly don’t — we can easily create an encrypted Preferences table that would be sufficiently difficult to penetrate that it would make hacking the Registry look boringly simple. Still, ini files have their uses, and they are a part of the Windows universe. A programmer should possess as many tools as possible.

Finally, as mentioned previously, the Registry is made quite approachable by Registry.prg, and all of the ini-related data storage/retrieval described in this article can easily be replaced by analogous Registry actions. But that’s another article.


Thanks to Keith Chuvala, Bowen Moursund, and Kenneth Chan for INI.CC; to Vic McClung for Tokenize(); to Jay Parsons for RGBtoHex(); and to Ken Mayer for adding RGBtoHex()to (and maintaining!) the dUFLP.

Many thanks also to Barbara Coultry for her first-rate editing of this article which substantially improved its readability and sniffed out many errors. I would also like to mention that I have decided to not act on her correct advice about the placement of certain periods within quotation marks and instead retain a syntax that is, I think, both more rational and more appealing to a programmer’s mentality. So, when you see the incorrect

“[Toolbars]” is referred to in ini-speak as a “Section”.
instead of the correct

“[Toolbars]” is referred to in ini-speak as a “Section.

blame the error on me.