Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

BUILD YOUR OWN START MENU

In this new series, Huw Collingbourne explains how to replace the Start Menu with Program Groups
Requirements:
Borland's Delphi 7 or later

 

Download The Source Code:
prog_grps1.zip

 

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; // add an appropriate icon
  _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.


Obtaining System Information

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.


PChars and Char Arrays

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.


Next Month:

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

 


Home | Archives | Contacts

Copyright © 2006 Dark Neon Ltd. :: not to be reproduced without permission