Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Embedding .NET Controls to NotifyIcon Balloon Tooltip

4.73/5 (29 votes)
17 Sep 2009CPOL3 min read 68.3K   2.7K  
How .NET controls could be embedded inside NotifyIcon Balloon Tooltip

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:

C#
BalloonTipIcon
BalloonTipTitle
ContextMenu
ContextMenuStrip
Text
Icon
Visible

In this group, we will use the same properties of NotifyIcon.

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

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

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

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

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

C#
//Create parameters for a new tooltip window
System.Windows.Forms.CreateParams moCreateParams = 
			new System.Windows.Forms.CreateParams();

// New window is a tooltip and a balloon
moCreateParams.ClassName = WINAPI.TOOLTIPS_CLASS;
moCreateParams.Style = WINAPI.WS_POPUP | WINAPI.TTS_NOPREFIX | 
			WINAPI.TTS_ALWAYSTIP | WINAPI.TTS_BALLOON;
moCreateParams.Parent = loNotifyIconHandle;

// Create the tooltip window
moNativeWindow.CreateHandle(moCreateParams);

//We save old window proc to be used later and replace it with our own
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:

C#
// 16 pixs for the button control  and 2 pixels from top
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:

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

C#
...
        //Adding panel to the 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

License

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