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:
- You can use the
hWnd
and an arbitrary number that uniquely distinguishes each of the Notify Icons associated with that hWnd
from the others. - 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
:
This is the Balloon Notify Icon displayed by 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:
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:
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:
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