Building a simple Launchy clone in WPF (Part 2/3: Visual Studio)
This is the second part of my tutorial on making a minimalistic clone of Launchy. I’ll assume you’ve finished Part 1. If you haven’t you might want to check it out since this part builds on it. 🙂
1. Use Visual Studio to add a class for our Cache
Add another new class to the project called Cache. Make this class public as well. Before copying the text for this file, add another reference to the project: this time for the COM assembly Windows Script Host Object Model.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | using System; using System.Collections.Generic; using System.ComponentModel; using System.Collections.ObjectModel; namespace Launchy_Clone { public class Cache : INotifyPropertyChanged { // the search hint private string hint; // list of cached items that are able be found by the search List<SearchResult> cached_items; // collection of results that match the search hint ObservableCollection<SearchResult> results; public Cache( ) { cached_items = new List<SearchResult>( ); results = new ObservableCollection<SearchResult>( ); // load start menu items LoadStartMenu( System.Environment.GetFolderPath( Environment.SpecialFolder.StartMenu ) ); LoadStartMenu( System.Environment.GetEnvironmentVariable( "ALLUSERSPROFILE" ) + @"\Start Menu" ); // A default hint. Not really necessary but can be handy during testing. hint = "Calc"; } public string Hint { get { return hint; } set { hint = value; results.Clear( ); foreach ( SearchResult sr in cached_items ) { // really simple case-insensitive substring search. anyone can make this better // and it's not really the point of this tutorial, so we'll go ahead and use it. :) if ( sr.Name.ToLower().Contains( hint.ToLower() ) ) results.Add( sr ); } OnPropertyChanged( new PropertyChangedEventArgs( "Hint" ) ); } } public ReadOnlyObservableCollection<SearchResult> Results { get { return new ReadOnlyObservableCollection<SearchResult>( results ); } } private void LoadStartMenu( string path ) { IWshRuntimeLibrary.WshShell shell = new IWshRuntimeLibrary.WshShell( ); foreach ( string file in System.IO.Directory.GetFiles( path ) ) { System.IO.FileInfo fileinfo = new System.IO.FileInfo( file ); if ( fileinfo.Extension.ToLower( ) == ".lnk" ) { IWshRuntimeLibrary.WshShortcut link = shell.CreateShortcut( file ) as IWshRuntimeLibrary.WshShortcut; SearchResult sr = new SearchResult( fileinfo.Name.Substring( 0, fileinfo.Name.Length - 4 ), link.TargetPath, file ); cached_items.Add( sr ); } } // recurse through the subfolders foreach ( string dir in System.IO.Directory.GetDirectories( path ) ) { LoadStartMenu( dir ); } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged( PropertyChangedEventArgs e ) { PropertyChangedEventHandler h = PropertyChanged; if ( h != null ) h( this, e ); } #endregion } } |
2. Set up the Application’s search result Cache object
2.1. Instantiating the Cache object itself
Since the search result cache acts as the core source of information for our program, we’ll add it to the program’s App object. Open up App.xaml.cs and add Cache member and property:
1 2 | private Cache cache; public Cache Cache { get { return cache; } } |
We’ll also override the OnStartup
function to instantiate this object. The resulting [short and sweet] App class in App.xaml.cs is this:
1 2 3 4 5 6 7 8 9 10 11 | public partial class App : System.Windows.Application { private Cache cache; public Cache Cache { get { return cache; } } protected override void OnStartup( StartupEventArgs e ) { cache = new Cache( ); base.OnStartup( e ); } } |
2.2. Accessing the cache from our window
Open up Window1.xaml.cs and add the following property so we have access to our cache from the Window (in particular so we can bind to members of the cache using Blend).
1 2 3 4 5 6 7 | public Cache Cache { get { return ((App)System.Windows.Application.Current).Cache; } } |
3. Hooking up the search box
We have to head back to Blend for a bit since it makes this part easier. Build your solution in Visual Studio (so Blend will be able see the changes), and open up Window1.xaml in Blend.
3.1. Updating the cache hint when the text changes
- (Read this whole step before doing it.) Click
txtSearchBox
and bind itsText
property to the scene element Window, property Cache.Hint. Click the drop down arrow at the bottom of the Create Data Binding window and select TwoWay binding, update source when PropertyChanged. Now you can click finish.
This is a good place to run your program and test out the changes.
3.2. Displaying the search results in the pop-up list
- Select the
[ListBox]
in the pop-up. Click the dot by theItems
property and click Reset. Do the same forSelectedIndex
. Setting these to temporary values helped up set up the ItemTemplate in part 1, but now it’s time to hook them up to real application logic. 🙂 - Bind the
ItemsSource
property to theCache.Results
property of the top level Window scene element (Element Property).
This is a good place to run your program and test out the changes.
3.3. Setting focus to the text box by default
- In Blend, select the
txtSearchBox
- At the very top of the Properties pane click the Events button ()
- Find the box for the
Loaded
event and double click it. Blend will create a function in Visual Studio and hook up the event for you. Use the handler to set focus to the search box and select its text:
1 2 3 4 5 | private void txtSearchBox_Loaded( object sender, RoutedEventArgs e ) { txtSearchBox.Focus( ); txtSearchBox.SelectAll( ); } |
This is a good place to run your program and test out the changes.
3.4. Scrolling through the results list with the arrow keys
Since the text box is likely to have focus when the user presses the up or down key to move through the results list, we need to listen for up/down arrow keystrokes at an early stage (before the text box handles them).
- Use Blend to set up an event handler for the
PreviewKeyDown
event of txtSearchBox - Fill in the function to move up or down the list if the up or down key was pressed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | private void txtSearchBox_PreviewKeyDown( object sender, KeyEventArgs e ) { switch ( e.Key ) { case Key.Down: if ( ListBox.Items.Count > 0 ) { if ( ListBox.SelectedIndex == -1 ) ListBox.SelectedIndex = 0; else if ( ListBox.SelectedIndex < ListBox.Items.Count - 1 ) ListBox.SelectedIndex++; } break; case Key.Up: if ( ListBox.Items.Count > 0 ) { if ( ListBox.SelectedIndex == -1 ) ListBox.SelectedIndex = 0; else if ( ListBox.SelectedIndex > 0 ) ListBox.SelectedIndex--; } break; default: break; } } |
This is a good place to run your program and test out the changes.
3.5. Handling the Enter (run) and Esc (close) keys
To handle the Escape key by closing the window, use Blend to set up an event handler for the PreviewKeyDown
event of the top level window.
1 2 3 4 5 6 7 8 9 | private void Window_PreviewKeyDown( object sender, KeyEventArgs e ) { switch ( e.Key ) { case Key.Escape: Close( ); break; } } |
Handling the Enter key is slightly more complicated, but does fit in another case
in this same switch
statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 | case Key.Enter: SearchResult sr = ListBox.SelectedItem as SearchResult; if ( sr != null ) { if ( sr.Shortcut != null ) System.Diagnostics.Process.Start( sr.Shortcut ); else System.Diagnostics.Process.Start( sr.Command ); } Close( ); break; |
This is a good place to run your program and test out the changes.
4. A bit more usable – limiting the number of results
As you surely noticed, the pop-up has a tendency to get “a bit” long. To keep this from happening, all we have to do is limit the number of items that get placed in the results
collection. Add a property to the Cache
class to hold this number:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // maximum number of allowed search results private int max_results; public int MaxResults { get { return max_results; } set { max_results = value; } } |
In the class constructor, initialize the value of max_results
to 5
.
1 2 3 | hint = "Calc"; // Show up to 5 results by default. max_results = 5; |
Then in the setter for the Hint
property, stop adding items to results
if/when it contains MaxResults
number of items:
1 2 3 4 5 6 | if ( sr.Name.ToLower().Contains( hint.ToLower() ) ) results.Add( sr ); // stop adding items if we reach the maximum allowed count if ( results.Count >= MaxResults ) break; |
This is a good place to run your program and test out the changes.
- Conclusion
Well that’s all for this part. I hope you’re starting to see why I’m enjoying the new tools/APIs for and in .NET 3.0. To wrap up our clone (Cloney?), head on over to Part 3. 🙂
hey
great tutorial.. but. in part one you write… Add a default TextBox to the [Grid]. At the top of the Properties pane for the image, name it txtCommand. but later in part 1 and now in part 2 you refer to the txtCommand as txtSearchBox. You may want to correct this 🙂 Else, awesome tutorial 🙂
May 12th, 2007 at 7:42 pmThanks for pointing that out. 🙂 Glad you found it useful. 🙂
-Sam
May 12th, 2007 at 10:10 pmHi Sam,
I’m working through the tutorial now on a Windows 7 machine, and I just wanted to let you know that section 1, line 24 of Cache.cs has to be replaced with:
LoadStartMenu( System.Environment.GetEnvironmentVariable( "PROGRAMDATA" ) + @"\Microsoft\Windows\Start Menu" );
if you want it to work on Vista or 7The reference documentation can be found here: http://support.microsoft.com/kb/886549 at step 12
Thanks for the writeup!
April 5th, 2010 at 3:45 pm