Building an App Launcher for Windows XP using C#

Windows XP didn't have a Start Menu search functionality, a feature that was only introduced with Vista and later versions. As a bit of "retro" coding fun, I developed a Windows Form Application for such a feature using C#.

In this article, I discuss how I built it. Please note that it doesn't provide a step-by-step process and assumes that the reader has a background in C# programming.

Creating the Project

I used Visual Studio 2005 because it was my favorite IDE. This version works only with the .NET Framework 2.0. I created a C# Windows Form Application project.

Designing the Form

The UI has the following controls, arranged in a simple layout: textbox, listbox, checkbox, and button.

  • TextBox - where the user will enter the name of the application to run
  • ListBox - list of the applications found in the Start Menu
  • CheckBox - include/exclude uninstaller shortcuts
  • Button - launch the application

Retrieving the Start Menu Items

The start menu is simply a list of shortcut (.lnk) files that are found in the users' directories under C:\Documents and Settings. These directories are:

  • C:\Documents and Settings\All Users\Start Menu – appears for all users (a.k.a "common")
  • C:\Documents and Settings\{Current User}\Start Menu – appears for the current user, path depends on your account's name

Simply copying the path from Explorer and defining them as constants isn't ideal. It's more appropriate to use certain APIs to retrieve directory paths accurately.

For the current user, it's straightforward:

Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);

The above managed code approach didn't support the common start menu until .NET 3.5, so the only option was to use unmanaged code. On the Program.cs file, I added:

using System.Runtime.InteropServices;
using System.Text;
/* static class Program */
[DllImport("shell32.dll")]
public static extern bool SHGetSpecialFolderPath(IntPtr hwnd, [Out] StringBuilder lpszPath, int nFolder, bool fCreate);
const int CSIDL_COMMON_STARTMENU = 0x16;
public static string GetAllUsersStartMenuDirectory()
{
StringBuilder path = new StringBuilder(512);
SHGetSpecialFolderPath(IntPtr.Zero, path, CSIDL_COMMON_STARTMENU, false);
return path.ToString();
}

The next step involves compiling a list of applications located within these directories. These items are just shortcuts (i.e. files with the .lnk extension).

Similarly to any directory, they can also reside within subdirectories, therefore recursion is needed. To achieve this:

String[] ExtractLinks(String path)
{
return ExtractLinksRecursive(path, new String[0]);
}
String[] ExtractLinksRecursive(String path, String[] files)
{
String[] pathFiles = Directory.GetFiles(path, "*.lnk");
pathFiles = Utils.MergeArrays(pathFiles, files);
String[] pathDirs = Directory.GetDirectories(path);
foreach (String dir in pathDirs) {
String[] dirFilePaths = ExtractLinksRecursive(dir, files);
if (dirFilePaths.Length > 0) {
pathFiles = Utils.MergeArrays(pathFiles, dirFilePaths);
}
}
return pathFiles;
}

These processes involve merging resulting arrays into one. To merge arrays in C#, I created a helper function in Util.cs:

/* class Utils */
public static String[] MergeArrays(String[] a, String[] b)
{
String[] mergedArray = new String[a.Length + b.Length];
a.CopyTo(mergedArray, 0);
b.CopyTo(mergedArray, a.Length);
return mergedArray;
}

With the essential functions ready, I put these together in a method that gets called in the constructor. I used the SortedDictionary class so the apps are listed in ascending order.

SortedDictionary<String, String> LoadStartMenuLinks()
{
SortedDictionary<String, String> dictionary = new SortedDictionary<String, String>();
String[] links = Utils.MergeArrays(
ExtractLinks(Program.GetAllUsersStartMenuDirectory()),
ExtractLinks(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu)));
// the filename without extension becomes the key
// the absolute path is the value
// the SortedDictionary will sort by key in ascending order
foreach (String p in links) {
String filename = Path.GetFileNameWithoutExtension(p);
if (dictionary.ContainsKey(filename)) {
continue;
}
dictionary.Add(filename, p);
}
return dictionary;
}

I created a class member to store this list.

SortedDictionary<String, String> linksList;
// constructor...
linksList = LoadStartMenuLinks();

Populating the List Box

The ListBox can use the SortedDictionary data through a BindingSource object:

appsList.DataSource = new BindingSource(linksList, null);
appsList.DisplayMember = "Key";
appsList.ValueMember = "Value";

To show only the applications matching the search input, I looped the full list and moved the items that contain the search string into a different SortedDictionary, then set that as the ListBox's DataSource:

void FilterLinks(String keyword)
{
SortedDictionary<String, String> filteredData = new SortedDictionary<String, String>();
foreach (KeyValuePair<String, String> val in linksList) {
// exclude items containing "uninstall"
Boolean containsUninstallWord = val.Key.ToLowerInvariant().Contains("uninstall");
if (containsUninstallWord && ExcludeUninstallersCheckbox.Checked) {
continue;
}
// contains the search string
if (val.Key.ToLowerInvariant().Contains(keyword.ToLowerInvariant())) {
filteredData.Add(val.Key, val.Value);
}
}
if (filteredData.Count == 0) {
appsList.DataSource = null;
return;
}
appsList.DataSource = new BindingSource(filteredData, null);
appsList.DisplayMember = "Key";
appsList.ValueMember = "Value";
}

The filtering executes on the form load event and the search box text change event:

void OnMainFormLoad(object sender, EventArgs e)
{
FilterLinks(searchbox.Text);
}
void OnSearchBoxTextChanged(object sender, EventArgs e)
{
FilterLinks(((TextBox)sender).Text);
}

Running the Target Application

The System.Threading.Process can help start the chosen application:

Process.Start(path);

It may throw an Exception on error, so I wrapped it in a try-catch statement. Putting that in a method to execute the selected value from the ListBox:

void ExecuteSelectedLink()
{
String path = appsList.SelectedValue.ToString();
try {
Process.Start(path);
Application.Exit();
} catch {
MessageBox.Show("Failed to run application.", "Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

To execute the program when the user presses the Enter key after typing on the search box:

void OnSearchBoxKeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode) {
case Keys.Enter:
if (appsList.SelectedItems.Count == 0) {
return;
}
ExecuteSelectedLink();
e.SuppressKeyPress = true;
break;
}
}

Or when the user presses the Enter key while the focus is on the ListBox:

void OnAppListKeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter) {
ExecuteSelectedLink();
}
}

And of course, when the Launch Button is clicked:

void OnLaunchActionClick(object sender, EventArgs e)
{
ExecuteSelectedLink();
}

Handling the Expected User Behavior

I also made sure that the launcher handles the other common UX behaviors:

  • Move the ListBox selection on Up and Down key down while focused on the search box
  • Close the launcher on Esc key
  • Launch the selected item when double-clicking on the ListBox
  • Retain the last size and position of the window using Properties.Settings
  • Search box select all on CTRL + A

Source Code

You can get the source code of this project from my Github repositiory.