Introduction
This article shows one approach to displaying a balloon tool tip for a notify icon created
using the FCL's NotifyIcon
class. This relatively new feature of notification icons is
not supported by the NotifyIcon
class and adding this feature to my own code was not
immediately obvious without some creative coding. This is why I am presenting it here. The other
reason is that I am hoping somebody can tell me that I'm doing it all wrong and that there is a
much more 'correct' solution.
This article also serves as an example of the use of the platform invoke facility. It shows
how to create a data structure required by a Win32 API function and how to pass that structure to
the API function.
The way I have accomplished the task involves the following two steps:
- Using the Win32 API get the handle to the hidden FCL created notify message window.
- 2. Use the Win32 API to send the 'balloon tip' message to the notify icon.
A Little Background
Notification icons communicate with their parent applications by sending windows messages to
a window supplied when the notify icon is created. The FCL NotifyIcon
class does not
supply any information about this window (one of the disadvantages of working at a higher level
of abstraction). The FCL creates a hidden window for a NotifyIcon
and converts the windows
messages sent to it into .NET consumable events.
In order to use the Win32 API to communicate with the notification icon you must obtain the
Win32 window handle of the window receiving the messages, and also the numeric ID of the icon.
The Solution
Using Spy++ I determined the class name of the hidden window created by the FCL. I then used
the Win32 API to find the handle of that window (looking only at the windows owned by my thread).
The ID was determined by trial and error! I found that if you have one notify icon created by
your app the ID will be 1 (not 0). Once I have the window handle I then call Shell_NotifyIcon()
to send it the 'balloon' message. The only trick to this part is defining the data structure
required to be sent to this API function.
The Caveats
This solution is a basically a hack for a few reasons. Here are the assumptions I made that
may not always be true.
- I assumed that calling the Win32 API function
GetCurrentThreadId()
would return me
the correct thread ID.
- I assume the name of the window class created by the FCL is fixed. Of course this may not be
true in the future.
- I assume that the ID of the icon is 1 for the first created icon.
- Part of the reason I am posting this code is to maybe get some feedback on how I can reduce
the number of assumptions that are made. So let me know if you come up with anything.
Here is the code for the NotifyIcon
class that shows the balloon.
public class NotifyIcon
{
[StructLayout(LayoutKind.Sequential)]
public struct NotifyIconData
{
public System.UInt32 cbSize;
public System.IntPtr hWnd;
public System.UInt32 uID;
public NotifyFlags uFlags;
public System.UInt32 uCallbackMessage;
public System.IntPtr hIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
public System.String szTip;
public System.UInt32 dwState;
public System.UInt32 dwStateMask;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)]
public System.String szInfo;
public System.UInt32 uTimeoutOrVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)]
public System.String szInfoTitle;
public System.UInt32 dwInfoFlags;
}
public enum NotifyCommand {Add = 0, Modify = 1, Delete = 2, SetFocus = 3,
SetVersion = 4}
public enum NotifyFlags {Message = 1, Icon = 2, Tip = 4, State = 8, Info = 16,
Guid = 32}
[DllImport("shell32.Dll")]
public static extern System.Int32 Shell_NotifyIcon(NotifyCommand cmd,
ref NotifyIconData data);
[DllImport("Kernel32.Dll")]
public static extern System.UInt32 GetCurrentThreadId();
public delegate System.Int32 EnumThreadWndProc(System.IntPtr hWnd,
System.UInt32 lParam);
[DllImport("user32.Dll")]
public static extern System.Int32 EnumThreadWindows(System.UInt32 threadId,
EnumThreadWndProc callback,
System.UInt32 param);
[DllImport("user32.Dll")]
public static extern System.Int32 GetClassName(System.IntPtr hWnd,
System.Text.StringBuilder className,
System.Int32 maxCount);
private System.IntPtr m_notifyWindow;
private bool m_foundNotifyWindow;
private System.Int32 FindNotifyWindowCallback(System.IntPtr hWnd,
System.UInt32 lParam)
{
System.Text.StringBuilder buffer = new System.Text.StringBuilder(256);
GetClassName(hWnd, buffer, buffer.Capacity);
if(buffer.ToString() == "WindowsForms10.Window.0.app1")
{
m_notifyWindow = hWnd;
m_foundNotifyWindow = true;
return 0;
}
return 1;
}
public void ShowBalloon(uint iconId, string title, string text, uint timeout)
{
uint threadId = GetCurrentThreadId();
EnumThreadWndProc cb = new EnumThreadWndProc(FindNotifyWindowCallback);
m_foundNotifyWindow = false;
EnumThreadWindows(threadId, cb, 0);
if(m_foundNotifyWindow)
{
NotifyIconData data = new NotifyIconData();
data.cbSize = (System.UInt32)
System.Runtime.InteropServices.Marshal.SizeOf(
typeof(NotifyIconData));
data.hWnd = m_notifyWindow;
data.uID = iconId;
data.uFlags = NotifyFlags.Info;
data.uTimeoutOrVersion = 15000;
data.szInfo = text;
data.szInfoTitle = title;
Shell_NotifyIcon(NotifyCommand.Modify, ref data);
}
}
}
Here is the code that shows the usage of the class.
private void OnShowBalloon(object sender, System.EventArgs e)
{
NotifyBalloonDemo.NotifyIcon notifyIcon = new NotifyBalloonDemo.NotifyIcon();
notifyIcon.ShowBalloon(1, "My Title", "My Text", 15000);
}
So come on people, show me how to do this correctly.