Sam's Blog

30 Mar

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

  1. (Read this whole step before doing it.) Click txtSearchBox and bind its Text 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.

Run 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

  1. Select the [ListBox] in the pop-up. Click the dot by the Items property and click Reset. Do the same for SelectedIndex. 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. :)
  2. Bind the ItemsSource property to the Cache.Results property of the top level Window scene element (Element Property).

Run This is a good place to run your program and test out the changes.

3.3. Setting focus to the text box by default

  1. In Blend, select the txtSearchBox
  2. At the very top of the Properties pane click the Events button (Events button)
  3. 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( );
}

Run 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).

  1. Use Blend to set up an event handler for the PreviewKeyDown event of txtSearchBox
  2. 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;
  }
}

Run 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;

Run 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;

Run This is a good place to run your program and test out the changes.

  1. 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. :)

3 Responses to “Building a simple Launchy clone in WPF (Part 2/3: Visual Studio)”

  1. 1
    Christian Says:

    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 :)

  2. 2
    280Z28 Says:

    Thanks for pointing that out. :) Glad you found it useful. :)

    -Sam

  3. 3
    James Ma Says:

    Hi 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 7

    The reference documentation can be found here: http://support.microsoft.com/kb/886549 at step 12

    Thanks for the writeup!

Leave a Reply

© 2017 Sam's Blog | Entries (RSS) and Comments (RSS)

Your Index Web Directorywordpress logo