Introduction
This article describes a simple system tray application written in C# and WPF which demonstrates features typical of system tray applications.
The sample code controls a simulated device which transitions between running and non-running states in response to menu commands from the user.
The system tray application implements the following features:
- An icon that appears in the system tray
- A pop up menu that is displayed when the user does a right/left mouse click on the icon
- A set of views launched by menu commands
- Balloon text which appears above the system tray when the device status changes
- Tooltips
- Icons which change according to the state of the device
The menu has a basic command set:
- Display information about the application
- Display status information
- Start the simulated device
- Stop the simulated device
- Exit the system tray application
The code provides a basic framework which you can easily modify to suit your own needs, e.g., control a hardware device attached to a USB port.
The architecture is deliberately kept simple with a small number of objects and a clear separation of responsibilities:
- The application context object does little more than initialize the application.
- The device manager object wraps the (simulated) device, and implements an interface allowing client objects to control the device. Separating the interface from the implementation is good design for many reasons including reducing coupling between components, making it easy to swap between implementations, and allowing clients to be tested with a dummy interface, independent of the implementation.
- The view manager object which manages the user interface. It owns a
NotifyIcon
object, and the various menus and views. It controls the device by means of the device manager interface. - About and status views implemented in WPF, using the view and view model pattern, whereby the UI is described in a XAML view, and the data displayed in the view is stored in a view model object. In a real application, you would normally add a model to contain the source data following the MVVM pattern.
Background
To understand this article, you will need an understanding of .NET and WPF.
The .NET NotifyIcon
class makes it easy to create a system tray application, but it is incompatible with WPF. Thus system tray applications based on the NotifyIcon
class usually implement views and dialog using WinForms. An alternative, adopted here, is to place WPF forms into a separate assembly.
You could if you wished replace the WPF forms with WinForms, but I would advise against this: WPF provides a far richer and significantly more productive development environment for user interfaces.
The Code
The Main
function first checks to see if there is already an instance of the app running, and if there is, it terminates, as only one instance may run at any one time. It detects the presence of another instance by creating a named mutex with a fixed name. If that mutex already exists, then another instance must already be running. The mutex name is the GUID of the assembly which should avoid clashes with other named mutexes in the system.
bool createdNew = false;
string mutexName = System.Reflection.Assembly.GetExecutingAssembly().GetType().GUID.ToString();
using (System.Threading.Mutex mutex = new System.Threading.Mutex(false, mutexName, out createdNew))
{
if (!createdNew)
{
return;
}
The next step is to create the application context instance. Normally, an app would create its main window object and pass it to the Application Run
method. However, we do not need a main window, so instead we pass an application context.
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
try
{
STAApplicationContext context = new STAApplicationContext();
Application.Run(context);
}
catch (Exception exc)
{
MessageBox.Show(exc.Message, "Error");
}
The application context is derived from the ApplicationContext
class and it is responsible for initializing the system. It has only two properties:
private ViewManager _viewManager;
private DeviceManager _deviceManager;
The ViewManager
object manages the user interface, and interacts with the device by means of an IDeviceManager
interface.
The DeviceManager
class manages the simulated device. It implements the IDeviceManager
interface.
The application context initializes the system in its constructor:
public STAApplicationContext()
{
_deviceManager = new DeviceManager();
_viewManager = new ViewManager(_deviceManager);
_deviceManager.OnStatusChange += _viewManager.OnStatusChange;
_deviceManager.Initialise();
}
It creates an instance of the DeviceManager
class, then creates an instance of the ViewManager
class passing in the DeviceManager
class and hence an IDeviceManager
interface.
It then hooks the OnStatusChange
method of the ViewManager
to the OnStatusChange
event exposed by the DeviceManager
instance. This event is fired whenever the device status changes.
The IDeviceManager
interface defines a simple set of commands and properties to control the (simulated) device:
public interface IDeviceManager
{
string DeviceName { get; }
DeviceStatus Status { get; }
List<KeyValuePair<string, bool>> StatusFlags { get; }
void Initialize();
void Start();
void Stop();
void Terminate();
}
The above interface is implemented by the DeviceManager
class. For further details of the DeviceManager
class, please see the sample code. Suffice to say that it is little more than a shell simulating a real device.
The ViewManager
class creates and initializes a NotifyIcon
instance in its constructor:
public ViewManager(IDeviceManager deviceManager)
{
System.Diagnostics.Debug.Assert(deviceManager != null);
_deviceManager = deviceManager;
_components = new System.ComponentModel.Container();
_notifyIcon = new System.Windows.Forms.NotifyIcon(_components)
{
ContextMenuStrip = new ContextMenuStrip(),
Icon = SystemTrayApp.Properties.Resources.NotReadyIcon,
Text = "System Tray App: Device Not Present",
Visible = true,
};
_notifyIcon.ContextMenuStrip.Opening += ContextMenuStrip_Opening;
_notifyIcon.DoubleClick += notifyIcon_DoubleClick;
_notifyIcon.MouseUp += notifyIcon_MouseUp;
_aboutViewModel = new WpfFormLibrary.ViewModel.AboutViewModel();
_statusViewModel = new WpfFormLibrary.ViewModel.StatusViewModel();
_statusViewModel.Icon = AppIcon;
_aboutViewModel.Icon = _statusViewModel.Icon;
_hiddenWindow = new System.Windows.Window();
_hiddenWindow.Hide();
}
The .NET NotifyIcon
class implements the system tray icon.
The above code installs system tray event handlers for the context menu opening, double click, and mouse up events. It also creates instances of view models for the two views, i.e., the about view and the status view.
The ContextMenuStrip_Opening
method constructs the context menu if it does not already exist, and then enables/disables menu items as required:
private void ContextMenuStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = false;
if (_notifyIcon.ContextMenuStrip.Items.Count == 0)
{
_startDeviceMenuItem = ToolStripMenuItemWithHandler(
"Start Device",
"Starts the device",
startStopReaderItem_Click);
_notifyIcon.ContextMenuStrip.Items.Add(_startDeviceMenuItem);
_stopDeviceMenuItem = ToolStripMenuItemWithHandler(
"Stop Device",
"Stops the device",
startStopReaderItem_Click);
_notifyIcon.ContextMenuStrip.Items.Add(_stopDeviceMenuItem);
_notifyIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
_notifyIcon.ContextMenuStrip.Items.Add(ToolStripMenuItemWithHandler
("Device S&tatus", "Shows the device status dialog", showStatusItem_Click));
_notifyIcon.ContextMenuStrip.Items.Add(ToolStripMenuItemWithHandler
("&About", "Shows the About dialog", showHelpItem_Click));
_notifyIcon.ContextMenuStrip.Items.Add(ToolStripMenuItemWithHandler
("Code Project &Web Site", "Navigates to the Code Project Web Site", showWebSite_Click));
_notifyIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
_exitMenuItem = ToolStripMenuItemWithHandler
("&Exit", "Exits System Tray App", exitItem_Click);
_notifyIcon.ContextMenuStrip.Items.Add(_exitMenuItem);
}
SetMenuItems();
}
When the user selects the "Device Status" command, the system invokes the showStatusItem_Click
method:
private void showStatusItem_Click(object sender, EventArgs e)
{
ShowStatusView();
}
private void ShowStatusView()
{
if (_statusView == null)
{
_statusView = new WpfFormLibrary.View.StatusView();
_statusView.DataContext = _statusViewModel;
_statusView.Closing += ((arg_1, arg_2) => _statusView = null);
_statusView.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
_statusView.Show();
UpdateStatusView();
}
else
{
_statusView.Activate();
}
_statusView.Icon = AppIcon;
}
If the view exists, the code simply activates it and sets the icon. Otherwise, it creates a status view, and initializes it including adding a handler for the Closing
event and updating the content.
The about view code is very similar and is included in the sample code.
Building the Sample Code
The sample code is a Microsoft Visual Studio 2013 solution.
History
- 17th March, 2017: First release