Introduction
In this article, I will talk about how I've created MagicWord
s, a .NET 2.0 SlickRun
clone.
SlickRun
- and so is MagicWord
s - 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 MagicWord
s.
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:
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:
List<MagicWord> m_MagicWords = new List<MagicWord>();
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();
}
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.
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
.
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:
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
:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
uxStartUpModesColumn.DataSource = Enum.GetValues(typeof (System.Diagnostics.ProcessWindowStyle));
uxDataGridView.DataSource = m_MagicWords;
}
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;
}
}
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.
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.
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):
class Program
{
[STAThread]
static void Main()
{
MagicWordsApplicationContext applicationContext = new MagicWordsApplicationContext();
Application.Run(applicationContext);
}
}
public class MagicWordsApplicationContext : ApplicationContext
{
private System.ComponentModel.IContainer components;
private System.Windows.Forms.NotifyIcon m_NotifyIcon;
private SystemHotkey m_SystemHotkey;
public MagicWordsApplicationContext()
{
InitializeContext();
m_NotifyIcon.ContextMenuStrip = Forms.LauncherForm.Current.ContextMenuStrip;
}
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);
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.Shortcut = Properties.Settings.Default.TypeWordHotKey;
m_SystemHotkey.Pressed += new EventHandler(OnSystemHotkeyPressed);
Application.ApplicationExit += new EventHandler(OnApplicationExit);
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
}
void OnApplicationExit(object sender, EventArgs e)
{
Context.Current.SaveMagicWords();
}
private void OnNotifyIconDoubleClick(object sender,System.EventArgs e)
{
ShowForm();
}
private void OnSystemHotkeyPressed(object sender, EventArgs e)
{
ShowForm();
}
private void ShowForm()
{
if (Forms.LauncherForm.Current.Visible)
{
Forms.LauncherForm.Current.Activate();
}
else
{
Forms.LauncherForm.Current.Show();
}
}
protected override void ExitThreadCore()
{
if (Forms.LauncherForm.Current != null)
{
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 MagicWord
s. 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 :
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:
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:
uxUserSettingsGrid.SelectedObject = Properties.Settings.Default;
Saving the settings is also done with one line of code:
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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.