|
|
Huw Collingbourne
reads programming manuals for fun. ‘The C++ Programming
Language’ by
Bjarne Stroustrup provides him with hours of mirthful
hilarity. |
See also: Part
One and Part Two of this series
In this series it has been my aim to create a program
manager utility which avoids the cascading menu hell
of the Windows Start button. The final project in my
last column was an application which displayed one or
more cascaded or tiled windows containing the same icons
as the Start menu or any of its submenus. The main deficiency
of that program manager was that it failed to reload
its program groups when from one session to the next.
This month I shall fix that.
The final version of our program manager lets you view
the items in the Start Menu, navigate to other directories
and display items in tiled or cascading windows, change
the view style (as we are doing here) and save and
reload all the windows and their contents between sessions.
Load the ProgGroups.dpr project. This is a development
of last month’s project in which we have a main
form defined in the unit pg.pas and a child form defined
in the unit childform.pas. The main form’s FormStyle
property has been set to fsMDIForm in the Object Inspector
while the child form’s FormStyle property has been
set to fsMDIChild. This means that the main form can
act as a container for one or more child forms. We can
create new child forms at runtime using the New command
from the main form’s Windows menu. This simply
calls the Tchildfm class’s constructor with the
Form1 Self parameter to create a child form of Form1.
Having created a few child forms, you can navigate to
different program groups by double-clicking their icons.
So, for example, you might open one window on the top
level StartMenu directory, another on StartMenu\Programs\ and a third on StartMenu\Programs\Accessories\. As explained
last month, the actual location of the StartMenu directory
will vary from user to user and our code locates it in
the TForm1.Init() procedure.
Having opened windows onto several different program
groups we now come to the problem of restoring those
groups when we next run our program manager. To do this,
you need to write information on the program groups to
disk and use this information to recreate the windows
subsequently. You could do this in many ways - for example,
by writing data to a text file, streaming data to disk
or writing information to the Registry. For the sake
of simplicity, I have chosen to use INI files. As these
are plain text files, they can be loaded into Notepad.
This means that INI files have the notable benefit of
giving us an easy way to verify the data written to disk.
Saving The Program Groups
Since we want to save the program groups which are active
when we exit the application, we need to write to the
INI file when the main form closes. You can find the
relevant code in TForm1.FormClose(). As long as you have
the IniFiles unit in your ‘uses’ list, all
you need to do to write entries into an INI file is to
create a TIniFile object and pass the file name to its
constructor, then use various TIniFile methods to write
specific types of data such as integers, strings and
Booleans. These methods generally take three parameters,
the first is the name of the section header in the INI
file, the next is the name of the Key and the last is
the value to be written. This is how a section named ‘Form’ might
appear in the resulting INI file:
[Form]
Maximized=0
Top=117
Left=207
Height=569
Width=763
Note that the text to the left of the equals sign is
a key and is used to access a specific item while the
text on the right is the value associated with the key.
When the application, an attempt is made to read the
data stored in the INI file. You will find the code of
this in TForm1.FormCreate(). This time, variables are
initialised one at a time by reading the stored entries.
For example, here we initialise the Top property of the
form (Self.Top) by reading the value associated with
the ‘Top’ key from the ‘Form’ section
of the INI file. Just in case the entry does not exists,
the third parameter specifies a default value of 100:
Self.Top := Ini.ReadInteger( 'Form', 'Top', 100 );
Reading Multiple INI Values
But the program group windows are not fixed. They can
be created and destroyed at runtime. This makes it impossible
for us to know in advance the number of values we need
to restore at startup.
Writing information on the existing program groups is
simple enough. We just count down from the maximum MDI
child index (obtained by subtracting one from the MDIChildCount property) to zero. In TForm1.FormClose() we iterate through
any child forms and write their captions, which contain
the path to their ‘root’ directory, into
the INI file. Note that, prior to doing this, we delete
any existing entries from the INI file using the EraseSection() method. If we did not do so, old entries which haven’t
been overwritten with new values would continue exist.
For example, if we previously had five entries but we
now only have two program groups, we would overwrite
only the first two entries. The remaining three entries
would remain untouched.
As we shall be writing an unpredictable number of entries
each time we save information to the ‘ProgGroups’ section,
according to the number of child forms, it does not make
sense to attempt to read in a fixed number of items.
Instead, we make use of the TIniFile’s ReadSectionValues() method. This reads in all the entries from a named section
of the INI file and assigns them to items in a TStrings
or TStringList object. Here I’ve assigned the values
to a TStringList named paths which is declared in the
Form1 class definition towards the top of the unit.
When the program starts, a message box displays the program
groups (which will be empty when you first run this
program) and the paths from which their contents will
be created.
Just so you can see what is going on here, the items
are displayed after they are read in. You will note that
they include the key, which is an identifier such as ‘Group0’,
followed by an equals sign and the string value, which
is a path such as ‘C:\Documents
and Settings\Huw\StartMenu\’.
In fact, we only need the path part. This can be extracted
from each string in a string list using the ValueFromIndex[] property. You can view the result of using this by uncommenting
the line indicated in FormCreate(). This displays the
additional information in the message box (for debugging
purposes!) which pops up when the application starts.
Notice that I haven’t actually attempted to recreate
the program groups from within the FormCreate() method.
This is because the main form must already have been
created before it is possible to create child forms within
it. But the main form will not be fully created until
after the FormCreate() method has executed.
Recreating Program Groups
When an application is loaded, the Show event occurs
just after the form is created. I have decided, therefore,
to create the child forms in the FormShow() event-hander.
In fact, the Show event occurs whenever a form is made
visible. This explains why I have used a Boolean variable,
formjustcreated, which is set to true in FormCreate()
and to false in FormShow(). This ensures that the code
in FormShow() is only executed when the application is
loaded.
The code here iterates through the paths string list
which was initialised by FormCreate() from the ‘ProgGroups’ section
of the INI file. This creates a new child form for each
entry, calling the SetRootFolder() method of each form
and passing to it the path. The SetRootFolder() method
can be found in the childform.pas unit.
It sets the Root property
of the ShellListView component on the child form and
also places the path in its caption. Once all the child
forms have been recreated they will display the icons
stored in the specified directories. The FormShow() method
then frees the paths string list which is no longer required.
A
curiosity of the ProgGroups application is that it creates
a new child form on loading. As a result, you will end
up with more and more groups each time it is run. This
error is fixed in the ProgGroupsPlus program.
When I first coded these methods, I was plagued by a
strange problem. Each time I ran the program I would
end up with one more program group than I expected. Finally
I remembered that Delphi creates an instance of all the
form classes in a project when that project is executed.
Normally this is reasonable behaviour but in the present
instance it is undesirable. To get around this problem
it was necessary to edit the source of the project file.
You can do this by selecting ‘View
Source’ from
the Project menu. I commented out the line which creates
an instance of TChildfm.
Finally, we need to update the caption of each program
group form to display the current folder when that has
been changed by double-clicking an icon. We do that in Tchildfm.ShellListViewDblClick().
This simply reads the PathName of the ShellListView’s
RootFolder property.
By now we have performed most of the essential tasks
needed to save and restore program groups between sessions.
There are, of course, a few extra details that need to
be taken care of, such as restoring the view styles and
opening program groups onto a selected folder. The techniques
needed to perform these tasks are explained below.
For a slightly more complete version of this application,
try out ProgGroupsPlus.dpr. This is now rather like the
old Windows 3.1 Program Manager brought up to date. While
it may not be perfect, I have to say I personally think
it’s a good deal more civilised than that blasted
Start button!
How to open a selected folder in its own program group
window
By default my Program Manager displays the contents
of the selected folder in the current window when you
press Enter or double-click a folder icon. It seemed
to me that it might also be useful to have the option
to open a new window onto the selected folder. I have
added this feature in the ProgGroupsPlus.dpr project.
Select
this menu item or press Ctrl+O to open the selected folder
in a new window
You will see that the child form now has an ‘Open
On Folder’ item in its Groups menu. This has been
assigned the Shortcut Ctrl+O so that you can also access
it direct from the keyboard. The code that executes can
be found in the method, OpenOnFolder(). The first thing
this does is to check that an item is actually selected.
If not, then the SelectedFolder value will be nil and
a message is displayed (this and other messages are there
for debugging only and the ShowMessage() code can be
removed at your leisure). Now, unfortunately, it turns
out that the SelectedFolder property does not make a
distinction between a folder icon and an application
icon or shortcut. This means that we cannot simply execute
the following code to change the folder:
newchildfm.ShellListView.Root := ShellListView.SelectedFolder.PathName;
Were we to do so, the code would attempt change the
path to the folder containing an application when an
application icon is selected. This would produce unpredictable
results and could crash the program. What we really want
to do is to change the folder only when a folder icon
is selected and to do nothing when any other type of
icon is selected. This is why I have used the DirectoryExists() function to check that the PathName property of the selected
item is a valid directory before attempting to assign
this value to ShellListView.Root.
Having done this, opening the folder in a new window
is trivial. All we have to do is to create another instance
of the Tchildfm class and make the folder assignment
to its ShellListView.
When a type can’t be saved to file, you may need
to string it alone
Not every type can be easily written into an INI file.
While the TIniFile class has methods for writing integers,
strings, Booleans and a few other types, it does not
have any special methods for handling odd types such
as TViewStyle which is an enumerated type comprising
four constants.
Now, you could get around this problem by casting the
ViewStyle property to an integer and saving this to disk.
While this is an acceptable solution, it would mean that
you would end up with largely meaningless numbers in
the INI file. It would be better, I think, to be able
to write descriptive entries into the INI file. So if
the ViewStyle is vsReport, the entry in the INI file
will read ‘vsReport’. All this takes is two
functions to convert between ViewStyles and strings.
You will find my implementation of these functions, ViewStyleToStr() and StrToViewStyle() in the pg.pas unit of the ProgGroupsPlus.dpr project. There is nothing complicated about these functions
and I think the clarity they give to our INI files makes
them well worth the effort.
String lists gives us the ability to restore unknown
quantities of data
While the ability to restore information on the number
of windows and their root folders is pretty much essential
for a program manager, you may also want to restore various
other details too. In the ProgGroupsPlus.dpr project
I have saved information on the size and position of
the main form and the ViewStyle of each of each child
form.
Here ViewStyle is
the property of the ShellListView on each child form
and it determines which of four alternative icon styles
and layouts is displayed. A different view style can
be applied to each child form using the View menu.
We write the ViewStyles into the INI file in the form
of strings (see ‘Saving and
Restoring ViewStyles’ above).
Each time the application is run, the view styles are
read in by the FormShow() method. This is done in precisely
the same way that we read in the paths as explained in
the main text above.
We have already assigned the values of the stored view
styles to the items of a string list, viewstyles, in
the FormCreate() method. Now, in FormShow(), we iterate
through the string list items and assign their matching
TViewStyle value, returned by our StrToViewStyle() function,
to each successive child window. In principle, the number
of view styles read from the INI file should always be
identical with the number of paths. However, we have
to allow for the possibility that they may not be (after
all, maybe some idiot has edited the INI file by hand).
For that reason, we always check that the index of viewstyles
string list is valid (i.e. the iterator variable, i,
is less than or equal to viewstyles.Count-1) before attempting
to assign a ViewStyle to the current child form. If the
index is invalid, we assign the default view style, vsReport.
As a finishing touch to my Program Manager, I have added
the ability to save and restore the size and positions
of the child windows As with the paths and view styles,
I have done this by reading a whole section into a string
list. It would be quite messy to have each of the four
integer coordinates stored, one entry at a time, for
each child form, so I have decided to store the coordinates
in comma-delimited strings, with a single entry for each
window (e.g. Win0=0,0,160,820). Then when the values
are read in by FormShow(), I use a string routine called
firstRestStr() which I wrote previously in a unit called
huwStrUtils.pas. This takes a string, s, as the first
parameter and returns the first token as it second parameter
and the remainder of the string as its third parameter.
The tokens are split on any of the DELIMS characters
defined in huwStrUtils.pas. Each token parsed is converted
to an integer and assigned to the relevant property of
each child form. Note that the first argument to firstRestStr() is a const and cannot be modified. This is why we assign
the values of rest to the variable wp rather than pass ‘rest’ itself
in the first parameter.
The first thing that needs to be done to improve this
program manager is to add a great deal more error checking
and recovery. For example, FormShow() in the ProgGroupsPlus.dpr project does not verify that each coordinate parsed from
a string in the childpositions string list is valid and
can also be converted to an integer. If an invalid token
is read, this could cause the program to crash. You might
also want to add a Browse menu item so that the user
can create program groups from any directory on any disk.
May 2006 |