Introduction
Some guys asked me if it is possible to add a hyperlink to the NotifyIcon Balloon Tooltip. It’s not too hard to add it with WinAPI, but an idea was born to embed .NET controls to it. There could be a number of purposes for this. Again, you can add a hyperlink to make a user go to a specified web-site. You can add an image to the tooltip and so on. So I've used a standard .NET NotifyIcon
and tried to modify it. There is an example of how it looks:
But there arose some problems that were resolved during the implementation. Let's review them.
Problems
The First Problem
NotifyIcon
is a sealed class so you cannot inherit from and override its ShowBalloonTip
method. The solution is to make a wrapper class for the NotifyIcon
and implement our own methods and properties. Some of them could be implemented just by redirecting to NotifyIcon
object, then exposed by our class. It’s simple as NotifyIcon
doesn't contain a lot. Let’s make two groups of properties:
BalloonTipIcon
BalloonTipTitle
ContextMenu
ContextMenuStrip
Text
Icon
Visible
In this group, we will use the same properties of NotifyIcon
.
BalloonTipText
As we will redraw a content of the tooltip, there will not be such a property and we replace it with InsidePanel
property and pass a Panel
that will contain any of the .NET controls.
Also, in our wrapper class, we should define NotifyIcon
events:
NotifyClicked
NotifyDoubleClicked
NotifyMouseClicked
NotifyMouseUp
NotifyMouseDown
NotifyMouseMove
NotifyMouseDoubleClicked
There is no sense to implement a BalloonTipClicked
as the tooltip will contain a panel with its own events. There will be only BalloonTipClosed
and BallonTipShown
events. And we will implement our own ShowBalloonTip
method.
The Second Problem
We will need HWND
of the NotifyIcon
but it doesn't expose it. To achieve it, we'll use reflection to get private
field “window
” of NotifyIcon
:
private IntPtr GetHandler(Object @object)
{
FieldInfo fieldInfo = @object.GetType().GetField("window",
BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
NativeWindow nativeWindow = (NativeWindow)fieldInfo.GetValue(@object);
if (nativeWindow.Handle == IntPtr.Zero)
return IntPtr.Zero;
return nativeWindow.Handle;
}
The Third Problem
It’s to find a screen rectangle of Notify Icon to show the tooltip in the right position. Here WINAPI
comes into force. At first, with the use of FindWindowEx
, we'll find windows tray by its “Shell_TrayWnd
” class name:
IntPtr hWndTray = WINAPI.FindWindowEx(IntPtr.Zero, IntPtr.Zero, "Shell_TrayWnd", 0);
if (hWndTray == IntPtr.Zero)
return false;
Then with the use of EnumChildWindows
, we'll find the “ToolbarWindow32
” class.
WINAPI.EnumChildWindowsCallback callback =
new WINAPI.EnumChildWindowsCallback(EnumChildWindowsFunc);
...
WINAPI.EnumChildWindows(hWndTray, callback, 0);
if (!mFound)
return false;
...
protected bool EnumChildWindowsFunc(IntPtr hwnd, IntPtr lParam)
{
StringBuilder sb = new StringBuilder(256);
WINAPI.GetClassName(hwnd, sb, sb.Capacity);
if (sb.ToString().StartsWith("ToolbarWindow32"))
{
mHWndNotify = hwnd;
mFound = true;
return false;
}
mFound = false;
return true;
}
And finally, with the use of MapWindowPoint
, we'll find notify icon rectangle.
Last Steps
At this point, we'll create a native window as a tooltip and balloon. The content of the tooltip will be redrawn in its window proc. So the old window proc should be saved to be used in our implementation to draw the original tooltip.
System.Windows.Forms.CreateParams moCreateParams =
new System.Windows.Forms.CreateParams();
moCreateParams.ClassName = WINAPI.TOOLTIPS_CLASS;
moCreateParams.Style = WINAPI.WS_POPUP | WINAPI.TTS_NOPREFIX |
WINAPI.TTS_ALWAYSTIP | WINAPI.TTS_BALLOON;
moCreateParams.Parent = loNotifyIconHandle;
moNativeWindow.CreateHandle(moCreateParams);
IntPtr loNativeProc = WINAPI.SetWindowLong(moNativeWindow.Handle,
WINAPI.GWL_WNDPROC, wpcallback);
if (loNativeProc == IntPtr.Zero)
return;
if (WINAPI.SetProp(moNativeWindow.Handle, "NATIVEPROC", loNativeProc) == 0)
return;
Note: The window proc delegate should be defined as a field of our class, so it couldn't be garbage collected.
Before showing the tooltip, we have to resize it so that our .NET panel fits its client area. I didn't find how to make it directly and the only solution is to get char
size with the use of GetTextExtentPoint32
and make a corresponding string
so that tooltip has the needed size.
Ok, now it’s time for the window proc. First we call the original window proc, so the tooltip will be formed. While processing WM_PAINT
message, we'll get the tooltip size and clear text area of the tooltip. To have a possibility to close tooltip at the upper right corner of the tooltip, we'll add a .NET button:
moCloseButton.Location = new System.Drawing.Point
(width - (rect.left + 16), rect.top - 2);
moCloseButton.AutoSize = false;
moCloseButton.Click += CloseButtonClick;
And it will close the tooltip:
private void CloseButtonClick(object sender, EventArgs e)
{
CloseToolTip();
}
private void CloseToolTip()
{
WINAPI.TOOLINFO ti = new WINAPI.TOOLINFO();
ti.cbSize = Marshal.SizeOf(ti.GetType());
ti.hwnd = GetHandler(moNotifyIcon);
WINAPI.SendMessage(moNativeWindow.Handle, WINAPI.TTM_DELTOOL, 0, ref ti);
...
}
And at last, the panel with any .NET controls in NotifyIcon
tooltip:
...
moPanel.Location = new Point(rect.left, rect.top + 16);
WINAPI.SetParent(moPanel.Handle, hWnd);
...
Known Issues
First I didn't analyze if SysTray
is positioned at the top, left or right of the screen. I just supposed that it’s at the bottom.
Another issue happens if you set mouse pointer to notify icon, then move it along the SysTray
toolbar to another icon. .NET controls are not added to the tooltip and it’s blinking with the string
used to resize it.
History
- 17th September, 2009: Initial post