Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#
Article

MagicWords: How to build a SlickRun clone in one hour

Rate me:
Please Sign up or sign in to vote.
4.76/5 (15 votes)
11 Feb 20076 min read 47.8K   892   44   9
An improved command line utility inspired by SlickRun.
MagicWords - a SlickRun clone

Introduction

In this article, I will talk about how I've created MagicWords, a .NET 2.0 SlickRun clone.
SlickRun - and so is MagicWords - is an improved command line utility that can launch an application, open files or urls by typing a "magic word". You can see it as a enhanced version of the Windows "Run..." TextBox.
For keyboard lovers, this tool is a very good alternative to desktop shortcuts and menus, and quickly becomes a "must have".

Before going further, I will ask you to be indulgent enough as this is my first article here and English is not my mother tongue.

Background

The application can be split in the following parts:

  • A Tray Icon: Our application is running as a background task most of the time, so the tray icon will indicate to the user that the application is running. It also has a contextual menu to interact with. To add even more usability, we will add a global HotKey (CTRL+F12) to open the "input" box.
  • The input textbox used to type and launch applications: We want it to have AutoCompletion, to appear on a HotKey press and to be displayed at the bottom right of the screen
  • The "MagicWord" edition user interface to insert, edit, delete MagicWords.

As an extra feature, we will also add a HotKey to easily create a MagicWord from the current focused application.

Our first task is to model the object representation of the "magic words". Given below is the class diagram:
MagicWords (SlickRun Clone) - Class diagram

Notes: I've implemented the INotifyPropertyChanged interface so our entity will be compliant with the Windows Forms DataBinding mechanism.

Implementation

This application presents no difficulty, mainly it's just a matter of putting together features of the .NET framework.

MagicWord persistence

We hold the MagicWord library as a generic list List<MagicWord> that we persist and load using simple XML serialization:

C#
List<MagicWord> m_MagicWords = new List<MagicWord>();

/// <summary>
/// Loads the magic words.
/// </summary>
private void LoadMagicWords()
{
    XmlSerializer serializer = new XmlSerializer(typeof(List<MagicWord>));
    StreamReader reader = File.OpenText(m_wordsPath);
    m_MagicWords = (List<MagicWord>)serializer.Deserialize(reader);
    reader.Close();
}

/// <summary>
/// Saves the magic words.
/// </summary>
public void SaveMagicWords()
{
    XmlSerializer ser = new XmlSerializer(typeof(List<MagicWord>));
    StreamWriter sw = new StreamWriter(m_wordsPath);
    ser.Serialize(sw, m_MagicWords);
    sw.Close();
}

The input TextBox

We use a standard TextBox, thanks to .NET 2.0 which supports AutoCompletion natively. We configure it so that the matching suggestion is automatically appended, with no suggestion list displayed. We also choose a custom source that we fill from our MagicWord's List.

C#
TextBox uxInputText = new TextBox();
...

uxInputText.AutoCompleteMode = AutoCompleteMode.Append;
uxInputText.AutoCompleteSource = AutoCompleteSource.CustomSource;

AutoCompleteStringCollection sr = new AutoCompleteStringCollection();
foreach (MagicWord word in m_MagicWords)
{
    sr.Add(word.Alias);
}

uxInputText.AutoCompleteCustomSource = sr;

This TextBox is in a singleton Form, it avoids the need to create and dispose a new form each time we want to type a MagicWord.

C#
public partial class LauncherForm : Form
{
    ...
    private static volatile LauncherForm _singleton;
    private static object syncRoot = new Object();

    public static LauncherForm Current
    {
        get
        {
            if (_singleton == null)
            {
                lock (syncRoot)
                {
                    if (_singleton == null)
                    {
                        _singleton = new LauncherForm();
                    }
                }
            }

            return _singleton;
        }
    }
}

MagicWord invoker

To execute the MagicWord, we use the System.Diagnostics.Process class:

C#
/// <remarks>Simplified version</remarks>
public void Execute(MagicWord word)
{
    ProcessStartInfo info = new ProcessStartInfo(word.FileName, word.Arguments);
    info.WindowStyle = word.StartUpMode;
    info.WorkingDirectory = word.WorkingDirectory;

    Process.Start(info);
}

What is interesting here is that if you give a filename or a url to the ProcessStartInfo, it will use the default shell action that is defined for the concerned file type. So typing a url will open your browser, etc.

MagicWord Editing

The editing of the entities is done with the help of a binded DataGridView. That gives us the insert/edit/delete operations with no code.
The only tricky part here is to fill a DataGridViewComboBoxColumn with an enum (we need that for the startup mode choice). We do that with the Enum.GetValues helper method; then we use two events of the DataGridView to transform the input and output for this column to the type desired either by the entity, and by the DataGridView:

MagicWords - A SlickRun Clone

C#
protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    uxStartUpModesColumn.DataSource = Enum.GetValues(typeof (System.Diagnostics.ProcessWindowStyle));
    uxDataGridView.DataSource = m_MagicWords;
}

// user has select a value in the combo, it is a string but our entity want a
// System.Diagnostics.ProcessWindowStyle so we cast it
private void OnDataGridViewCellParsing(object sender, DataGridViewCellParsingEventArgs e)
{
    if (e.ColumnIndex == uxStartUpModesColumn.Index && e.Value is string)
    {
        e.Value = (Object)Enum.Parse(typeof(System.Diagnostics.ProcessWindowStyle),
                            e.Value.ToString(), true);
        e.ParsingApplied = true;
    }
}

// the grid try to display the System.Diagnostics.ProcessWindowStyle property so
// we need to convert it to a string
private void OnDataGridViewCellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
    if (e.ColumnIndex == uxStartUpModesColumn.Index &&
                e.Value is System.Diagnostics.ProcessWindowStyle)
    {
        e.Value = e.Value.ToString();
        e.FormattingApplied = true;
    }
}

To be complete, the application also offers a single MagicWord editor through a form containing few TextBoxes and a ComboBox. Those controls are bound to a MagicWord entity, it means that when the entity properties change, the UI changes, and it works both ways: if the UI changes, the entity is updated.

MagicWords - a SlickRun clone

C#
private Entities.MagicWord m_MagicWord;

public Entities.MagicWord MagicWord
{
    get { return this.m_MagicWord; }
    set { this.m_MagicWord = value; }
}

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    uxStartupModeComboBox.DataSource = Enum.GetValues(typeof(System.Diagnostics.ProcessWindowStyle));

    uxAliasTextBox.DataBindings.Add("Text", MagicWord, "Alias", false,
                    DataSourceUpdateMode.OnPropertyChanged);
    uxFilenameTextBox.DataBindings.Add("Text", MagicWord, "FileName", false,
                    DataSourceUpdateMode.OnPropertyChanged);
    uxArgumentsTextBox.DataBindings.Add("Text", MagicWord, "Arguments", false,
                    DataSourceUpdateMode.OnPropertyChanged);
    uxNotesTextBox.DataBindings.Add("Text", MagicWord, "Notes", false,
                    DataSourceUpdateMode.OnPropertyChanged);
}

Tray Icon

Our application will reside in the tray bar and will not show any form when it starts. Even if it's quite easy to display an icon in the Tray with the help of the .NET framework NotifyIcon component, the usual behavior is that this component is part of a Form, which is not what we want.

MagicWords - A SlickRun Clone

My way to do this is to make my own ApplicationContext implementation so I have control on form creation and display. I've learnt this pattern in a great article by Jessica Fosler : Creating Applications with NotifyIcon in Windows Forms.

Here is the class (simplified):

C#
class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        MagicWordsApplicationContext applicationContext = new MagicWordsApplicationContext();
        Application.Run(applicationContext);
    }
}

public class MagicWordsApplicationContext : ApplicationContext
{
    private System.ComponentModel.IContainer components;   // a list of components to dispose
                               // when the context is disposed
    private System.Windows.Forms.NotifyIcon m_NotifyIcon;  // the icon that sits in the system tray
    private SystemHotkey m_SystemHotkey;

    /// <summary>
    /// This class should be created and passed into Application.Run( ... )
    /// </summary>
    public MagicWordsApplicationContext()
    {
        // create the notify icon
        InitializeContext();

        // we take the same contextmenustrip for the notifyicon and for the input textbox
        m_NotifyIcon.ContextMenuStrip = Forms.LauncherForm.Current.ContextMenuStrip;
    }

    /// <summary>
    /// Create the NotifyIcon UI, the ContextMenu for the NotifyIcon and an Exit menu item.
    /// </summary>
    private void InitializeContext()
    {
        this.components = new System.ComponentModel.Container();
        this.m_NotifyIcon = new System.Windows.Forms.NotifyIcon(this.components);
        this.m_SystemHotkey = new SystemHotkey(this.components);

        // m_NotifyIcon
        this.m_NotifyIcon.DoubleClick += new System.EventHandler(this.OnNotifyIconDoubleClick);
        this.m_NotifyIcon.Icon = Properties.Resources.App;
        this.m_NotifyIcon.Text = "MagicWords";
        this.m_NotifyIcon.Visible = true;

        // m_SystemHotkey
        m_SystemHotkey.Shortcut = Properties.Settings.Default.TypeWordHotKey; //Shortcut.CtrlF12;
        m_SystemHotkey.Pressed += new EventHandler(OnSystemHotkeyPressed);

        Application.ApplicationExit += new EventHandler(OnApplicationExit);
    }

    /// <summary>
    /// When the application context is disposed,; dispose things like the notify icon.
    /// </summary>
    protected override void Dispose( bool disposing )
    {
        if( disposing )
        {
            if (components != null)
            {
                components.Dispose();
            }
        }
    }

    void OnApplicationExit(object sender, EventArgs e)
    {
        Context.Current.SaveMagicWords();
    }

    /// <summary>
    /// When the notify icon is double clicked in the system tray,
    /// bring up a form with a calendar on it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnNotifyIconDoubleClick(object sender,System.EventArgs e)
    {
        ShowForm();
    }

    private void OnSystemHotkeyPressed(object sender, EventArgs e)
    {
        ShowForm();
    }

    /// <summary>
    /// This function will either create a new CalendarForm or activate the
    /// existing one, bringing the window to front.
    /// </summary>
    private void ShowForm()
    {
        if (Forms.LauncherForm.Current.Visible)
        {
            Forms.LauncherForm.Current.Activate();
        }
        else
        {
            Forms.LauncherForm.Current.Show();
        }
    }

    /// <summary>
    /// If we are presently showing a mainForm, clean it up.
    /// </summary>
    protected override void ExitThreadCore()
    {
        if (Forms.LauncherForm.Current != null)
        {
            // before we exit, give the main form a chance to clean itself up.
            Forms.LauncherForm.Current.Close();
        }
        base.ExitThreadCore ();
    }
}

System HotKeys

HotKey mean a global key combination that works even if the program does not have focus, which happens in our case. Pressing the hotkey will show our textbox, and give it the focus, so it's blasting fast to be able to type a MagicWord. The .NET framework does not offer such a component natively, but there is a good article here on CodeProject by Alexander Werner about how to do this: System Hotkey Component.

SystemHotkey is a simple component providing a wrapper for RegisterHotkey / UnregisterHotkey Win32-Api functions. The component creates a window which listen for WM_HOTKEY messages. The Shortcut property sets the Hotkey and activates it. When the Hotkey is pressed the Pressed event is fired. The Hotkey is automatically unregistered at Dispose()

You can use this Component with a Tray-Application which activates when a Hotkey is pressed. The files Systemhotkey.cs and win32.cs should be included in a class-library

As it is a component, we use it "as it" in MagicWords. thank you Alexander.

How to get the application that has focus

Here is a feature that is missing in SlickRun: Add a new MagicWord super quickly with a HotKey (CTRL+F11), that takes the current focused application, and creates a new MagicWord for it.
Getting the information about the current application is done with calls to the Win32 API, we first call GetForegroundWindow to get the window handle, then we call GetWindowThreadProcessId to get its process Id. Finally, we can use the Process.GetProcessById method to get the Process instance from this Id.
Here is the code :

C#
public class NativeWIN32
{
    [DllImport("user32.dll")]
    private static extern int GetForegroundWindow();

    [DllImport("user32")]
    private static extern UInt32 GetWindowThreadProcessId(Int32 hWnd, out Int32 lpdwProcessId);

    private static Int32 GetWindowProcessID(Int32 hwnd)
    {
        Int32 pid = 1;
        GetWindowThreadProcessId(hwnd, out pid);
        return pid;
    }

    public static Process GetFocusedProcess()
    {
        Int32 hwnd = GetForegroundWindow();
        return Process.GetProcessById(GetWindowProcessID(hwnd));
    }
}

Run program at Windows startup

To register our application so it runs at Windows startup, we need to write a Registry key, we use the dedicated .NET API for that:

C#
private static void RunOnStart(string appName, string appPath)
{
    Microsoft.Win32.RegistryKey Key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey
            ("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
    Key.SetValue(appName, appPath);
    Key.Close();
    Key = null;
}

private static void RemoveRunOnStart(string appName)
{
    Microsoft.Win32.RegistryKey Key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey
            ("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
    Key.DeleteSubKey(appName);
    Key.Close();
    Key = null;
}

User settings

.NET 2.0 makes user settings management a pleasure: we have a visual designer to create our settings and Visual Studio automatically generates a strongly typed proxy for access and persistence. Our only task here is to offer to the end user an interface to edit those settings, and we can do this by the use of the PropertyGrid component:

C#
uxUserSettingsGrid.SelectedObject = Properties.Settings.Default;

MagicWords (SlickRun clone) User Settings Editor

Saving the settings is also done with one line of code:

C#
Properties.Settings.Default.Save();

Conclusion

In this article, we have seen how to put together some very common .NET 2.0 features to create a simple, yet useful tool.

By taking things from where they are (in the Framework, on MSDN, on CodeProject, etc), we can use them and focus on our main task: fulfill user expectations.

Online Resources

History

  • 02/05/2007: First version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralThis is the best article I have ever read about data binding in .NET Pin
bscaer21-May-09 4:27
bscaer21-May-09 4:27 
GeneralGreat Job! Pin
thebeekeeper14-Jul-08 7:47
thebeekeeper14-Jul-08 7:47 
GeneralExcellent/Educational Code/Article Pin
flipdoubt14-Feb-07 15:38
flipdoubt14-Feb-07 15:38 
GeneralRe: Excellent/Educational Code/Article Pin
flipdoubt14-Feb-07 15:55
flipdoubt14-Feb-07 15:55 
GeneralRe: Excellent/Educational Code/Article Pin
flipdoubt14-Feb-07 16:29
flipdoubt14-Feb-07 16:29 
GeneralRe: Excellent/Educational Code/Article Pin
jroland20-Feb-07 1:14
jroland20-Feb-07 1:14 
GeneralNice but I don't believe you! Pin
msmits14-Feb-07 1:44
msmits14-Feb-07 1:44 
Hi John,

Very nice tool and good explanation... you've got my 5.
I don't buy the "How to build a SlickRun clone in one hour" though... unless you have Matrix/Mr.Smith typing skills Wink | ;-)

Cheers,
Michel
GeneralGreat article Pin
Emile van Gerwen12-Feb-07 21:20
Emile van Gerwen12-Feb-07 21:20 
GeneralShameless Advertisement Pin
Priyank Bolia12-Feb-07 19:30
Priyank Bolia12-Feb-07 19:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.