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 runListBox
- list of the applications found in the Start MenuCheckBox
- include/exclude uninstaller shortcutsButton
- 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 onUp
andDown
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.