Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / WPF

A C# System Tray Application using WPF Forms

Rate me:
Please Sign up or sign in to vote.
5.00/5 (16 votes)
17 Mar 2017CPOL4 min read 72K   5.4K   32   11
How to create a basic system tray app in C# and WPF

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.

C#
// Use the assembly GUID as the name of the mutex which we use to detect 
// if an application instance is already running
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)
    {
        // Only allow one instance
        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.

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
C#/WPF/C++ Windows developer

Comments and Discussions

 
QuestionMessage Closed Pin
17-Nov-21 3:10
Member 1543426917-Nov-21 3:10 
GeneralMy vote of 2 Pin
ThatCodingGuy5-Mar-21 8:01
ThatCodingGuy5-Mar-21 8:01 
Not useful or practical for fast implementation and understanding in a basic WPF app. I'm sure it has it's purposes else but for me, I found in convoluted and too time consuming for the project I was working on.
BugIncorrect GUID for the mutex name Pin
DominikPalo26-Feb-20 23:34
DominikPalo26-Feb-20 23:34 
QuestionWhere is WpfFormLibrary? Pin
jgspeeddemon18-Jul-19 16:30
jgspeeddemon18-Jul-19 16:30 
AnswerRe: Where is WpfFormLibrary? Pin
Leif Simon Goodwin12-Nov-19 3:34
Leif Simon Goodwin12-Nov-19 3:34 
QuestionSystem.Threading.Tasks.TaskCanceledException Pin
to_ny13-Sep-18 23:30
to_ny13-Sep-18 23:30 
QuestionAdd background Process? Pin
ajhalls8-Jan-18 13:01
ajhalls8-Jan-18 13:01 
AnswerRe: Add background Process? Pin
jhwu8717-Apr-19 0:07
jhwu8717-Apr-19 0:07 
QuestionWpfFormLibrary? Pin
jjcarrerae29-Nov-17 8:12
jjcarrerae29-Nov-17 8:12 
QuestionBenefitted much Pin
jhwu8714-Nov-17 22:44
jhwu8714-Nov-17 22:44 
QuestionNice Explanation Pin
Member 1243937912-Jul-17 20:30
Member 1243937912-Jul-17 20:30 
PraiseVery nice! Pin
Umar Farooq3-May-17 3:07
Umar Farooq3-May-17 3:07 

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.