See also: Part
Two of this series
In the good old days, Windows arranged programs in the
form of groups of icons inside cascading windows. Never
again, we thought, would we have to battle against multi-level
menus of the sort used by applications in the bad old
days of DOS. Then, with the advent of Windows 95, the
Start Menu appeared and with it came the return of those
hideous multi-level menus. This month we begin a series
in which we shall be coding a program manager as a more
civilised alternative to the Start Menu.
My aim this month is to build a list of all the items
which are displayed on the Windows Start Menu and display
the same items in a program of my own. As a bit of
added window dressing I want to show the appropriate
icon for each item. Here you can see the end results.
Note that you can make adjustments to the layout of
the list by setting the ViewStyle property of the ListView
in which the items are shown.
Before we can even begin to create a program manager,
we need to find out which programs are installed in Windows.
In other words, we need to know which applications appear
on the Start Menu. Fortunately for us, Windows keeps
this information on disk, in a special Start Menu folder.
All we have to do is find that folder and extract the
information. As so often in programming, this is easier
said than done.
The first problem we have to solve is how to locate
the Start Menu folder. On my PC, I can see that the folder
is found beneath the user directory which is beneath
the \Documents and Settings directory on my C:\ drive.
The user directory happens to be called \Huw and is qualified
by a network user-group identifier. I think it’s
pretty certain that most other people won’t have
a directory named Huw so it obviously wouldn’t
be sensible to hard-wire this path into my source code.
Instead, I need to find some way of asking Windows to
provide the appropriate path for me.
There are several ways of obtaining system information.
You can make use of functions to obtain the version number
of the operating system, the processor type, environment
variables and special-purpose directories such as the
Windows directory. For more detailed information on these
routines, refer to the section on Obtaining
System Information later on in this article.
All this information is provided by the operating system
itself. For example, Delphi’s GetWindowsDirectory() function is actually just an interface to the API’s
GetWindowsDirectory() function. Similarly, Delphi’s
TOSVersionInfo record matches the Windows OSVERSIONINFO data structure. When using Delphi’s system information
functions, records and constants, it is often useful
to refer to Delphi source code found in the units in
the \rtl directory (this may be \source\rtl or \source\win32\rtl)
beneath your Delphi installation. For an explanation
of the meanings of constants and return values of functions,
refer to the entries (illustrated, alas, in the C language)
in Delphi’s Windows SDK help.
Start Me Up!
Initially, I had hoped that there would be a GetStartMenuDirectory()
function which would locate the Start Menu directory
with one simple function call. Unfortunately, there isn’t.
Not in Win32 anyway, though .NET, I see, provides an
Environment.SpecialFolder enumeration which simplifies
matters somewhat – if you are using .NET, that
is, which, for the time being, I am not.
The lack of a Win32 function to return the Start Menu
directory left me puzzled for a while. But on searching
around in the help system, I eventually discovered a
handy little function called SHGetSpecialFolderLocation().
You can pass to this one of a set of pre-defined constants
as its second parameter, nFolder. It initialises its
third parameter, ppidl, to “A pointer to an item
identifier list (PIDL) specifying the folder's location
relative to the root of the namespace (the desktop).” That’s
what it says in the help entry, at any rate!
Note that ppidl is
not a string. Rather it is a pointer to a pointer to
a string. In order to get a string representing the directory
path we need to use another function, SHGetPathFromIDList().
This takes pidl as its first parameter and returns the
LPTSTR path, pszPath,
as its second parameter. For an explanation of the LPTSTR data
type, see PChars and Char Arrays.
To see how I have implemented this in Delphi code, load
the SysInfo.dpr project and open the SysStatsUnit unit.
My code can be found in the GetSpecialFolder() function.
This takes an integer parameter, nFolder. This parameter
can be supplied by the calling code, as illustrated in
the ShowSpecialFolders() function, and should be one
of the constants, such as CSIDL_STARTMENU, defined in
Delphi’s ShlObj unit.
The SysInfo application starts up by finding and displaying
the identifiers and types of the drives on your system.
The main si.pas unit contains all the code needed to
do this.
If you double-click one of the drives in the list, this
window pops up, providing you with some more detailed
information on the disk.
After passing the value of nFolder to the SHGetSpecialFolderLocation() function, we test the return value. If all is well, this
value will be equal to the pre-defined constant S_OK.
Some versions of the SDK help state that the return value
is NOERROR. In fact, NOERROR and S_OK are both defined
as 0. In my code, I use S_OK as this is the constant
specified in more recent Microsoft documentation.
When you click the System
Info button a new form pops up displaying all kinds of information ranging
from the OS version number to the system path variables.
All being well, a block of code executes to obtain the
path string to the specified special directory and return
this to the calling code, otherwise an error message
is returned. To test what happens when a directory cannot
be found, I have added a ‘Test’ button to
the form. Run the application, click the ‘System
Info’ button, then click ‘Test’.
This initiates two calls to my GetSpecialFolder() function.
The first call passes to the function a valid constant,
CSIDL_FONTS. The returned string should display the path
to your fonts directory. The second call passes an invalid
constant, NONDIRECTORY, which I have assigned the arbitrary
value 500. This time, an error message is returned and
displayed.
The code in the SysInfo project locates several special
purpose folders including the one that contains all
the information required to populate the Start Menu.
When you are using constants to locate special directories,
you need to be aware that the list shown in the Win SDK
help supplied with some older versions of Delphi is incomplete.
For example, the constants CSIDL_INTERNET_CACHE and CSIDL_FAVORITES may not be documented. Browse through the code in the
ShlObj.pas unit for a complete list.
On The Menu
Now that we’ve located the Start Menu directory,
we need to find a way of displaying the files it contains
which will normally be shortcuts with the extension ‘.lnk’.
To see how this can be done, load the Test.dpr project.
This populates a ListView with the Start Menu files and
their associated icons when button1 is clicked. This
is done by first obtaining the path to the Start Menu
directory and appending to it a backslash ‘\’ character.
This path is then passed to my ShowFiles() procedure,
which contains this code:
_Result := FindFirst(dir+'*.*', faAnyFile, SearchRec);
while _Result = 0 do
begin
FileInfo := GetFileInfo(dir + Searchrec.Name );
NewItem := ListView1.Items.Add;
NewItem.Caption := LowerCase(SearchRec.Name);
NewItem.ImageIndex := FileInfo.iIcon;
_Result := FindNext(SearchRec);
end;
FindClose(SearchRec);
The three functions, FindFirst(), FindNext() and FindClose() are
used together to locate all the files, *.*, in the specified
directory, dir. Here, SearchRec is a record of the TSearchRec type, which contains fields initialised with the Time,
Size, Names and Attributes of a file. The TSHFileInfo record, which is declared in the ShellAPI unit, maps
onto the Windows SDK’s
SHFILEINFO structure. It contains more file information,
including the associated icon. I’ve written a GetFileInfo() function to return an initialised TSHFileInfo record
based on a file name. It does this by calling the SHGetFileInfo function and passing to it some appropriate constants
which are documented in the SDK help. Pay attention to
the SHGFI_SYSICONINDEX which obtains the index of an
icon from the system image list which is a list of images
maintained by the operating system.
Adding Icons
To obtain the icon, we just set the ImageIndex property
of each new item added to the ListView to the index specified
by the iIcon field of the FileInfo record. The SDK help
for SHFILEINFO states that iIcon is the “Index
of the icon image within the system image list”.
Now that I know the location of the Start Menu I can
extract the file names or shortcuts and display the
appropriate icon for each item.
You should note that an application does not have automatic
access to the system image list. You have to do some
preliminary coding in order to get at it. I have done
this in the FormCreate() method.
This starts by obtaining the handle to the system image
list containing small icons. This is returned by the SHGetFileInfo() function
to which I pass the constants SHGFI_SYSICONINDEX
or SHGFI_SMALLICON. I now associate this ImageList
with ListView1 by sending this message:
SendMessage(ListView1.Handle, LVM_SETIMAGELIST, LVSIL_SMALL,
ImageListHandle);
Of course, the ListView is not the only component capable
of displaying file names and icons. With a bit of effort,
you could use almost any visual component for the job.
For example, look at the launch bar which I programmed
in a recent column; it
displays file names and icons on buttons, with a new
button created for each file that dragged from the Windows
Explorer onto the launch bar.
|
It would not be too difficult to adapt the Launchpad
project to display files from the Start Menu directory.
If you don’t care for buttons, you could
use similar techniques to create a menu on the
fly and assign an event handler to each item in
the menu. Or you might want to save yourself some
coding effort and use the ShellListView component.
In some versions of Delphi you will find this,
and other related ‘shell’ components,
on the samples tab. In my installation of Delphi
2006, I was surprised to find that they weren’t
installed. If you have this problem, you will need
to build and install the packages located in the
\VCLWin32\ShellControls subdirectory nested beneath
the \Demos directory. You can build these by loading
the projects into Delphi, right-clicking the bpl item in the Project manager and selecting Install.
The ShellListView control has the benefit of being
able to find display files and icons with minimum
coding required. I shall use this when I take the
next step towards developing a Program Manager
next month.
|
How to find out a range of information on your hardware
and software
Many and varied are the ways of obtaining information
on your PC’s hardware, software and operating system.
To see a few examples, load up the SysInfo.dpr project
and open the SysStatsUnit.pas unit. I have coded several
methods to handle specific types of system information.
Scrolling down from the top of the unit, the first procedure
you will come to is ShowMemoryInfo(). This initialises
a TMemoryStatus record (see the entry on MEMORYSTATUS in the SDK help), by calling the GetMemoryStatus() function.
All we then have to do is access the fields of the record
and divide by 1024 (the number of bytes in a kilobyte)
to obtain a kilobyte value.
My next routine, ShowOSInfo(),
does something similar only this time the record is of
the type TOSVersionInfo (OSVERSIONINFO in the SDK) which
is initialised by GetVersionEx(). The value of dwPlatformId tells us whether the OS is in the Windows 95/ME family
or the NT/XP family. The dwMinorVersion and dwMajorVersion fields gives us an integer from which we can determine
the precise OS.
My ShowHardwareInfo() procedure
uses yet another record type, TSystemInfo,
which is initialised by the GetSystemInfo() function.
Once again, all my code has to do is to analyse the fields
of this record in order to extract the information we
are after. We can compare the wProcessorArchitecture
field with the constant, PROCESSOR_ARCHITECTURE_INTEL,
to determine whether or not this is an Intel processor,
while the wProcessorLevel field contains an integer value
indicating the type of processor.
The ShowDirInfo() procedure
finds the path of certain system directories using self-explanatory
functions such as GetWindowsDirectory().
The GetSpecialFolder() function,
which I discussed earlier on, gives us access to
other system directories. Finally, the ShowEnvironment() procedure calls the GetEnvironmentStrings() function
to return a pointer to a null-terminated string (a PChar)
made up of a sequence of null-terminated substrings,
one for each environment variable. We parse out the substrings
by search for a null char (#0) until the final string
is itself a null char, indicating that the end of the
string has been encountered.
All strings are not equal so you need to do extra coding
when using the API
When calling Windows API functions such as SHGetSpecialFolderLocation() it is often necessary to send a null terminated string
(LPTSTR) as an argument. Strictly speaking, this is a
pointer to a sequence of characters terminated by a null
(#0). The pointer references the address of the first
character in the sequence. In Delphi, the PChar data
type is a pointer to a null terminated string. If you
look at my implementation of GetSpecialFolder() in the
Test.dpr project, you will see that I have declared a
PChar variable then allocated to it enough memory to
hold a path string. This is defined by the constant MAX_PATH which happens to have the value 260.
An alternative way to accomplish the same thing would
be to declare an array of chars from 0 to MAX_PATH. A
reference to an array variable is, in effect, a pointer
to its first character and the array declaration allocates
memory without requiring a call to GetMem(). I have provided
this alternative implementation in comments. You may
want to experiment with this to verify that the two implementations
are interchangeable.
Now that we’ve found out how to use the Start
Menu folder, the time has come to begin work on an alternative
to the Windows Start Menu... (see Part
Two)
February 2006 |