Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A WPF-Friendly Shell_NotifyIcon Wrapper Class - Part 1: Notify Icon Wrapper

0.00/5 (No votes)
2 Dec 2020 2  
This series of articles explores a new WPF-friendly wrapper class for Shell_NotifyIcon.
This article describes the API exposed by the wrapper library and provides some simple examples to show off its features.

Introduction

This series of articles introduces a new wrapper class for Shell_NotifyIcon, a notoriously ornery Win32 API. Although WinForms provides a NotifyIcon class, its API is only slightly better than the Win32 API and likewise for its documentation. I wanted it to be independent of WinForms, so I created this wrapper class.

This article (Part 1: Notify Icon Wrapper) describes the API exposed by the wrapper library and provides some simple examples to show off its features.

Background

The Notify Icon

After studying a few relevant projects I found online, I decided that it would be worthwhile to eliminate as many dependencies on WinForms as possible. The biggest stumbling block here was the WinForms NotifyIcon class. This class has an unfriendly API. It's based on a Win32 API (Shell_NotifyIcon), that has an even worse API and the available documentation for both of them is scanty and atrocious. For reference, I sought out an example of the use of the Win32 API. What I found was called "StealthDialog". I can't find my source (for attribution) at the moment, but I have the original code and it has no fingerprints. I didn't use any of the code directly, but being straight old C++ gave me a secure feeling that I was seeing the Win32 API up close and personal. I did some cross-checking between this code and the Reference Source for NotifyIcon, which helped me understand any subtleties and gave me the confidence to create my own code using C# with PInvoke.

From a Windows 10 user experience perspective, there appear to be two different kinds of notification icons: traditional icons that appear in the system tray and migrate to the overflow area and modern notifications that fly out from the right edge of your screen for a period of time, then migrate to the manually-operated notification panel that flies out when you click an icon in the bottom-right corner of the screen. From a programmer's perspective, these two concepts differ only in which parameters have been set and which have not.

I created an API for the NotifyIconWrappper class and placed it in a library (NotifyIconLibrary). I experimented with packaging it as a NuGet package and confirmed that it was doable, but I decided that I didn't want to take on the burden of keeping it up to date. NotifyIconWrapper lives in the NotifyIconLibrary namespace.

In order to be able to deal with an arbitrary number of processes, threads and Notify Icons without an object-oriented architecture, the Shell_NotifyIcon Win32 API relies on its clients to keep track of much of the state information needed to perform its functions. The Notify Icons for all processes in the current Window Session are effectively contained in some collection that the Win32 API can search. Every interaction either creates a new notify icon or acts on one that had been created earlier. NotifyIconWrapper holds this state so you do not have to do it.

Every Notify Icon must be associated with a valid Win32 window handle (hWnd). The Win32 API requires its clients to provide such a handle and the window with which it is associated. This window acts as a proxy for the Notify Icon. Messages that would be received by the Notify Icon if it were an ordinary window are repackaged and routed to the proxy window within a message of a different type. Handling these re-routed messages requires that the client provide a window procedure (WndProc) for this purpose. NotifyIconWrapper does this for you. It creates a hidden window with a WndProc so that you don't have to. In creating the sample applications, I have attempted to explore the types of messages that are relevant. I have surfaced many of these messages as events in NotifyIconWrapper API. If my guesses have missed some mark, I would consider modifying the API. I'll be watching for comments and suggestions.

There are two ways to identify a Notify Icon:

  1. You can use the hWnd and an arbitrary number that uniquely distinguishes each of the Notify Icons associated with that hWnd from the others.
  2. You can use a GUID to protect against notify icon spoofing. You still need to supply the hWnd.

    There is a lot of confusion on the internet about the use of GUIDs for this purpose. The first time you add a notify icon with a particular GUID on a particular computer, the OS records the full path of the application and associates it permanently. I have no idea where they put it. Perhaps they use a protected part of the system registry. The claim is that you can't remove these entries. I don't want to chase this down a rabbit hole, but ProcMon from SysInternals (a division of Microsoft) might reveal something. You cannot successfully add a notify icon using that GUID except from a program of the same name at the same absolute path. I would suggest something along the lines of using an appropriate registry entry or the program's settings file to store the GUID and the Program's full path. The first time you want to add a particular NotifyIcon during a given program run, if the GUID and path haven't yet been stored, or if the path doesn't match, randomly generate a new GUID by using System.Guid.NewGuid() and store it, along with your program's absolute path, for future reference. In the case of a mismatch, make sure you store the info over the old info. If the path matches, use the GUID you stored with it as the GUID for adding the notify icon.

    Unless notify icon spoofing presents a real security risk for you, I would avoid using the GUID method of identification like the plague. You should also be forewarned that, as a consequence of this opinion, I have not done very much testing on this part of the API.

The Shell is really just Explorer.exe in disguise. When the Shell gets out of whack, you fix it by killing and restarting Explorer.exe. The Shell maintains the Taskbar and most, if not all, things associated with it. When the Shell crashes and recovers or is restarted manually, it sends a dynamically registered unique window message to all windows. If your application handles this message, it can call Recover() to restore the previous state of the Notify Icon.

Using the Code

This is the main window of NotifyIconDemo1:

Main window of NotifyIconDemo1

This is the Balloon Notify Icon displayed by NotifyIconDemo1:

BalloonNotifyIcon of NotifyIconDemo1

The main window constructor for NotifyIconDemo1 is trivial, so let's take a look at where the non-trivial initialization of the main window occurs:

/// <summary>
/// This is a good place to initialize things that depend on the window being
/// connected to its source.
/// </summary>
/// <param name="e">This is the standard <c>EventArgs</c>.</param>
protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);
    _notifyIconWrapper = new NotifyIconWrapper();
    _notifyIconWrapper.Update();
    _notifyIconWrapper.BalloonTipClicked += NotifyIconWrapper_BalloonTipClicked;
    _notifyIconWrapper.BalloonTipClosed += NotifyIconWrapper_BalloonTipClosed;
    _notifyIconWrapper.BalloonTipShown += NotifyIconWrapper_BalloonTipShown;
}        

Note that OnSourceInitialized is an obscure override that comes into play when using Win32 Interop. After calling the base implementation, this method creates a new NotifyIconWrapper, then subscribes to three events provided by NotifyIconWrapper. The handling of these events is straightforward and therefore won't be shown here. See the source code for details.

Now let's see what happens when you click the Notify button:

/// <summary>
/// The Notify button has been clicked.
/// </summary>
/// <param name="sender">This is the source of the event (<c>NotifyButton</c>).</param>
/// <param name="e">This is the <c>RoutedEventArgs</c> used with most routed events.</param>
private void NotifyButton_Click(object sender, RoutedEventArgs e)
{
    if (_notifyIconWrapper != null)
    {
        NotifyButton.IsEnabled = false;
        _notifyIconWrapper.Info = Properties.LocalizedResources.DemoInfo;
        _notifyIconWrapper.InfoTitle = Properties.LocalizedResources.DemoInfoTitle;
        
        if (NoIcon.IsChecked.HasValue && NoIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.None;
        }
        
        if (InfoIcon.IsChecked.HasValue && InfoIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.Info;
        }
        
        if (WarningIcon.IsChecked.HasValue && WarningIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.Warning;
        }
        
        if (ErrorIcon.IsChecked.HasValue && ErrorIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.Error;
        }
        
        if (UserIcon.IsChecked.HasValue && UserIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.User;
        }
        
        _notifyIconWrapper.BalloonIcon =
            _notifyIconWrapper.IconType == NotifyIconType.User
          ? Properties.NonLocalizedResources.TwoTone
          : null;
        _notifyIconWrapper.LargeIcon = 
                   LargeIcon.IsChecked.HasValue && LargeIcon.IsChecked.Value;
        _notifyIconWrapper.NoSound = Silent.IsChecked.HasValue && Silent.IsChecked.Value;
        _notifyIconWrapper.Update();
    }
}        

After null-checking the NotifyIconWrapper reference, this method disables the Notify button. Then it sets the Info property to "Put your message here." and the InfoTitle property to "Put your title here." The method then runs through a series of check boxes. For each check box that is checked, the IconType parameter is set to the corresponding value. Only one of these check boxes can be in the checked state at a time. If the IconType property is NotifyIconType.User, the BalloonIcon property is set to an icon that is red if it is small and green if it is large. This helps you confirm the icon size that is displayed. Two more check boxes are then examined to determine the property values for LargeIcon and NoSound. Update() is called to synchronize the state of the Win32 Notify Icon with the state of the NotifyIconWrapper. This also triggers the Balloon Notification.

Finally, let's see what happens when the main window is closed:

/// <summary>
/// This window has been closed.
/// </summary>
/// <param name="e">This is the standard <c>EventArgs</c>.</param>
protected override void OnClosed(EventArgs e)
{
    if (_notifyIconWrapper != null)
    {
        _notifyIconWrapper.BalloonTipClicked -= NotifyIconWrapper_BalloonTipClicked;
        _notifyIconWrapper.BalloonTipClosed -= NotifyIconWrapper_BalloonTipClosed;
        _notifyIconWrapper.BalloonTipShown -= NotifyIconWrapper_BalloonTipShown;
        _notifyIconWrapper.Close();
        _notifyIconWrapper = null;
    }

    base.OnClosed(e);
}        

After null-checking the NotifyIconWrapper reference, the three NotifyIconWrapper events that were used are unsubscribed, the NotifyIconWrapper is closed and its reference is set to null. In any case, the last thing done is the calling of the base implementation.

NotifyIconWrapper Class

public sealed class NotifyIconWrapper : IDisposable

Namespace: NotifyIconLibrary

Assembly: NotifyIconLibrary.dll

Remarks

This class uses System.Windows.Interop.HwndSource and System.Windows.Interop.HwndTarget to create a wrapper for Shell_NotifyIcon. NotifyIconLibrary depends on DefinitionLibrary and SystemInformationLibrary.

Bitmapped flags and packed fields in the Win32 API have generally been implemented in the NotifyIconWrapper API as bool and int properties, respectively.

Constructors

NotifyIconWrapper

public NotifyIconWrapper(int callbackMessage = WindowMessages.User)

This is the constructor for the NotifyIconWrapper class.

callbackMessage is the message number to be used for repackaging messages received by the Notify Icon so that they can be re-routed to the Notify Icon Window. The default value should be used unless you have a reason not to do so.

Properties

Icon

public System.Drawing.Icon Icon { get; set; }

Get or set the Icon to be displayed as the classic notify icon.

ShowTip

public bool ShowTip { get; set; }

Get or set the style to be used in displaying the Tool Tip text. Set this to true to show the original tool tip style. Set it to false to show the balloon tool tip style.

Tip

public String Tip { get; set; }

Get or set the Tool Tip text to be displayed when hovering over the notify icon. The string is limited to 63 characters.

Hidden

public bool Hidden { get; set; }

Get or set a value that indicates whether the notify icon is hidden.

SharedIcon

public bool SharedIcon { get; set; }

Get or set a value that indicates whether the notify icon is shared.

Info

public string Info { get; set; }

Get or set the info text for the balloon notification. The string is limited to 255 characters.

InfoTitle

public string InfoTitle { get; set; }

Get or set the title text for the balloon notification. The string is limited to 63 characters.

IconType

public int IconType { get; set; }

Gets or sets the balloon icon type. The icon type must be a static member of the NotifyIconType class. The size of the icon used is determined by the LargeIcon property.

NoSound

public bool NoSound { get; set; }

Get or set a value that indicates whether the balloon notification should be muted or audible.

LargeIcon

public bool LargeIcon { get; set; }

Get or set a value that indicates whether large or small icons should be displayed in the balloon notification.

RespectQuietTime

public bool RespectQuietTime { get; set; }

Get or set a value that indicates whether quiet time should be respected or ignored when displaying the balloon notification.

GuidItem

public Guid GuidItem { get; set; }

Get or set a Guid to be used to uniquely identify this notify icon.

BalloonIcon

public System.Drawing.Icon BalloonIcon { get; set; }

Get or set the icon to be used in a balloon notification when the IconType property is equal to NotifyIconType.User. The size of this icon is determined by the LargeIcon property.

Methods

SetFocusOnNotifyIcon

public void SetFocusOnNotifyIcon()

This is used when the context menu is closed by pressing the escape key. It returns focus to the notify icon to allow the use of keyboard commands, such as pressing the enter key or the escape key.

Delete

public void Delete()

Prepare the notify icon data structure for the Delete method and perform it. This does not generally need to be followed by a call to the Update method.

Recover

public void Recover()

Recover from a restart of the command shell (explorer.exe). This does not generally need to be followed by a call to the Update method.

Update

public void Update()

If the icon is already being shown in the system tray, modify it with any changes that have accumulated. Otherwise, show the icon in the system tray with all of the specified options.

Close

public void Close()

Close the Notify Icon Window.

Dispose

public void(Dispose)

Implement the IDisposible pattern.

Events

Closed

public event EvenHandler Closed;

The NotifyIconWindow has been closed.

BalloonTipClicked

public event EventHandler BalloonTipClicked;

The balloon tip has been clicked.

BalloonTipClosed

public event EventHandler BalloonTipClosed;

The balloon tip has been closed.

BalloonTipShown

public event EventHandler BalloonTipShown;

The balloon tip has been shown.

MouseMove

public event EventHandler<MouseLocationEventArgs> MouseMove;

The mouse has moved.

LeftMouseButtonDown

public event EventHandler<MouseLocationEventArgs> LeftMouseButtonDown;

The left mouse button has been pressed.

LeftMouseButtonClick

public event EventHandler<MouseLocationEventArgs> LeftMouseButtonClick;

The left mouse button has been clicked.

LeftMouseButtonDoubleClick

public event EventHandler<MouseLocationEventArgs> LeftMouseButtonDoubleClick;

The left mouse button has been double clicked.

LeftMouseButtonUp

public event EventHandler<MouseLocationEventArgs> LeftMouseButtonUp;

The left mouse button has been released.

MiddleMouseButtonDown

public event EventHandler<MouseLocationEventArgs> MiddleMouseButtonDown;

The middle mouse button has been pressed.

MiddleMouseButtonClick

public event EventHandler<MouseLocationEventArgs> MiddleMouseButtonClick;

The middle mouse button has been clicked.

MiddleMouseButtonDoubleClick

public event EventHandler<MouseLocationEventArgs> MiddleMouseButtonDoubleClick;

The middle mouse button has been double clicked.

MiddleMouseButtonUp

public event EventHandler<MouseLocationEventArgs> MiddleMouseButtonUp;

The middle mouse button has been released.

RightMouseButtonDown

public event EventHandler<MouseLocationEventArgs> RightMouseButtonDown;

The right mouse button has been pressed.

RightMouseButtonClick

public event EventHandler<MouseLocationEventArgs> RightMouseButtonClick;

The right mouse button has been clicked.

RightMouseButtonDoubleClick

public event EventHandler<MouseLocationEventArgs> RightMouseButtonDoubleClick;

The right mouse button has been double clicked.

RightMouseButtonUp

public event EventHandler<MouseLocationEventArgs> RightMouseButtonUp;

The right mouse button has been released.

ShowContextMenu

public event EventHandler<MouseLocationEventArgs> ShowContextMenu;

Show the context menu for the Notify Icon.

NotifyIconSelectedViaKeyboard

public event EventHandler NotifyIconSelectedViaKeyboard;

The Notify Icon has been selected via the keyboard.

NotifyIconDemo1 Project

I offer this project as a first demonstration of NotifyIconLibrary. It allows you to explore almost all of the options available when using Balloon Icons.

One warning: Depending on other software you might have installed on your computer and your computer settings, Balloon Icon notifications might be disabled or might be immediately routed to the notification panel without being displayed as a fly-out. In my case, I had to make sure that none of my windows were maximized.

Points of Interest

Although I've tried to remove all dependencies on WinForms, one area remains for sure. Almost every localization scheme I have considered depends upon .resx files which, historically, depend on WinForms. I have implemented the groundwork for this type of localization, but I leave the details to anyone who finds my code useful.

Rudimentary exception handling has been provided for each executable project. In each case, you will find it in a file named program.cs. This file also contains the entry point (Main) for the project. I did not use the default startup code because I felt that it did not suit my purposes, in particular, it doesn't provide a single point at which to handle exceptions.

When it comes to personal skills, mine stop short of the graphic arts. If anyone reading this has the skills and the inclination, my code could really use some fancier icons.

I have not done any .NET Core projects yet. I might try my hand at converting the projects covered by this series of articles to .NET Core. If I do, I will try to update the articles as appropriate.

The next article in this series will deal with minimizing a screen to the Notify Icon Tray, also known as the System Tray.

History

  • 1st December, 2020: Initial 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