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

Balloon ToolTip Control

0.00/5 (No votes)
8 Dec 2005 1  
A balloon tooltip control implementing the IExtender interface.

Introduction

Everyone knows the ToolTip control (the programmer ones!!) which are included in the Common Controls version 4.0 since Win95 (that�s what saw first, I haven't ever caught the Win3.11). Since then the control itself have been modified and enhanced in many ways, and after a while, WinXP came to life and the Common Controls version 6.0 saw light with the Balloon ToolTip control included, which is the subject of this article.

Neither creating a Balloon ToolTip nor implementing an IExtender is a new subject, not even a hard one, but getting the functionality of both techniques could have some attention, and that's what this article discusses: how to combine the good of both, in a way that's as simple as using the native .NET ToolTip control.

While searching around, I found a great article about the Balloon ToolTip control (actually about the ToolTip control, in all its shapes and uses). This article (which could be found at CodeProject too) was a great reference to me and a good, live example of using the Balloon ToolTip control, but the control described suffered from its complexity (not a real complexity, but it had to be operated programmatically), something that pushed me to investigate the IExtender and to accomplish this work.

Background

Reading this article would give you a general understanding of the basic ideas discussed here, but to have a complete understanding of every concept, you must have some knowledge of the Win32 API calls and their uses. You don't have to be a professional to get it right and shouldn't be a fresh guy either (you shouldn't have to jump to the nearest programming book you have to figure out what is the Win32 LPTSTR or how to allocate some bytes in the unmanaged heap). I know that was the first .NET promise - the ability to avoid using the Win32 APIs, but if you didn't knew, the APIs are not dead yet, and will not die soon, so you must get yourself comfortable using them in your own code.

Using the code

After this heavy theoretical talk, we can start getting our hands dirty.

The IExtenderProvider Interface

The IExtender interface's idea is to provide a service to another control, to give the functionality you wrote not to your class but to other classes. A problem you solve in your code is presented to other classes to be used and not used directly by your own class as all ordinary classes do. In other words, you solve other components' problems in their own territory.

This interface provides a single method CanExtend, which expects you to have an object passed to it, and it returns a boolean as an answer. The object passed to this method represents the selected control at design time, and the bool result specifies if the class that implements this method should provide its services to this object. That's the whole story simplified.

For almost any control that implements the IExtender interface, the CanExtend method should return true for each control it should extend, except itself, and any other control that may be illogical to support, and in our case, every control is more than welcome to get served except for this control and the Form control too.

public bool CanExtend(object extendee)
{
    if(extendee is control && !(extendee is BalloonToolTip) 
                           && !(extendee is From))
    {
           return true;
    }
    return false;
}

Based on the result of this call, the VS designer, or any other third-party designer used, decides whether to provide the specified service to the control or not, so when you select a control, the designer calls this method at design time, passing the selected control to it as a parameter, and if its get true, the provided functionality would appear in the selected control property page (and in the code too) as a new property, exactly as if it was implemented in the original control code.

The ToolTip Control

The ToolTip control is somehow confusing while reading about it in the MSDN, and to get it right, you should distinguish between two concepts, the ToolTip control itself and the tool it supports. The first one, the ToolTip control, is the parent window which draws the text and behaves as it is ordered to, and the tool is the logical structure that contains the text to be drawn and controls how to display the tip. For this reason, a single ToolTip control can have as many tools as you want, one for each control you choose.

That's it. You create the ToolTip control which is independent from its associated tools, and has its own general properties (its width, color and period of appearance). Then you create as many tools as you need, and you add these tools to the ToolTip control.

As with any other control, by supplying the CreateWindowEX API function with the correct parameters, a ToolTip control could be created, and a handle to it would be returned. For information regarding parameters usage and their meaning, you may refer to MSDN and have a complete description of each of them, but for this article's sake, it's as follows:

IntPtr toolwindow;
Toolwindow = CreateWindowEx(0, �tooltips_class32�, string.Empty, 
             WS_POPUP | TTS_BALLOON | TTS_NOPREFIX | TTS_ALWAYSTIP, 
             CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 
             CW_USEDEFAULT, IntPtr.Zero, 0, 0, 0);

The most important of these constants is the TTS_BALLOON which enforces the function to create a ToolTip control that has a cartoon-style balloon. The second parameter specifies the class from which to create the window (in this case, the ToolTip class), and for the parent parameter, we pass a zero pointer to indicate that this window has no parent (it's a popup window). And finally, the return value is a handle that represents our ToolTip control for the lifetime of the class.

Other Details

For now, we have a ToolTip control ready to be used, but how do we communicate with it? From the .NET perspective, this ToolTip control is the core of our component, but from the Win32 perspective, it's just a window, and to manipulate it, we have to follow the Win32 message based communication rules through the SendMessage API function.

The ToolTip control defines a number of messages to be sent to it as commands that specify its appearance and its behavior. These messages are listed in MSDN, and our control doesn't use all of them, just the ones that are useful for our implementation.

  • TTM_ACTIVATE: Enable and disable the ToolTip control itself.
  • TTM_ADDTOOL: Add a tool to the ToolTip control.
  • TTM_DELTOOL: Delete a tool from the ToolTip control.
  • TTM_SETTITLE: Add a title to the ToolTip balloon window.
  • TTM_SETTIPBKCOLOR: Set a custom background color to the ToolTip balloon window.
  • TTM_SETTIPTEXTCOLOR: Set a custom foreground color to the ToolTip balloon window text.
  • TTM_SETDELAYTIME: Set the time for the ToolTip behavior.
  • TTM_UPDATETIPTEXT: Update the ToolTip balloon window text.
  • TTM_SETTOOLINFO: Associate the specified tool with the specified TOOLINFO structure.
  • TTM_GETTOOLINFO: Gets the TOOLINFO structure associated with the specified tool.

This summarized list is by no means a reference to the messages that the ToolTip control uses. For a detailed description of each of these listed and any not listed messages, refer to the MSDN documentation. Most of the messages have meaningful names and don't need any further explanation.

The TTM_ADDTOOL message is our key to add a tool to the ToolTip control, and as I said before, the ToolTip control is separated from its associated tools. The ToolTip control does the housekeeping work and displays the balloon window with the specified setting, and each tool of its associated tools is a TOOLINFO structure that holds the information needed to accomplish displaying a balloon window.

Take a look at the TOOLINFO structure here:

typedef struct tagTOOLINFO
{
        UNIT cbSize;   
        UNIT uFlag;    
        HWND hwnd;     
        UNIT_PTR uId;
        RECT rect;
        HINSTANCE hinst;
        LPTSTR lpszText;
        #if(_WIN32_IE >= 0x0300)
        LPARAM lparam
        #endif
}TOOLINFO

This structure represents a tool contained in a ToolTip control. The cbSize member must specify the size of this structure in bytes, the uFlag member controls the display of the ToolTip, hwnd is the handle to the control (the control in your form design) that contains this tool, rect is the member which holds the ClientRectangle of the control that contains this tool, and lpszText is the buffer that contains the string to be displayed in the ToolTip balloon window.

Leaving The Managed World

If you have to deal with unmanaged code like these Win32 API calls, let me introduce you to a good friend, the System.Runtime.InteropServices.Marshal class. For any travel outside the managed world, this tough guy is your best friend. There are handy useful static functions in the Marshal class that would solve almost all your problems with unmanaged code, and one of them shows how to convert a managed version of the TOOLINFO structure into a memory pointer to be passed into the SendMessage API function. This function talks the unmanaged language and cannot accept your managed TOOLINFO object, so you have to go a little bit low-level to workaround this situation, and here is how:

// first we have to construct a managed toolinfo structure

        toolinfo tf = new toolinfo;
// now specify the size of this structure in the size member

        tf.size = Marshal.SizeOf(typeof(toolinfo));
// specify how the balloon window should be displayed

        tf.flag = TTF_SUBCLASS | TTF_TRANSPARENT;
// associate the structure with the target control (the one on your form)

        tf.parent = targetcontrol.Handle;
// specify the text you want to be displayed

        tf.text = �Tooltip text to be displayed�;
// we need a temporary pointer to hold our unmanaged copy of TOOLINFO object

        IntPtr tempptr;
// now allocate enough memory to hold this structure in the unmanaged heap

        tempptr = Marshal.AllocHGlobal(tf.size);
// copy the content of our filled managed

// TOOLINFO object to the newly allocated memory space

        Marshal.StructureToPtr(tf, tempptr, false);
// now send a message to our ToolTip control that we have a tool to be added

// and remember that our ToolTip is just a pointer return by CreateWindowEx

        SendMessage(tooltipptr, TTM_ADDTOOL, 0, tempptr);
// now our tool have been added to the ToolTip control,

// and we have to clean out what we did

        Marshal.FreeHGlobal(tempptr);

For those who started scratching their heads (asking what the hell was that?) if any, I'll just say that explaining about how to work with unmanaged code is beyond the scope of this article. Anything that seems to you to appear either in the Marshal class or in the ToolTip constants and messages, is explained shortly in the comment, and if you don't get it yet, you have to refer to the MSDN (if you still cannot get it, then either I'm a bad writer or you are a bad reader).

Are We Done With The IExtender?

As an answer, not yet. There is a little detail that's not been discussed yet. The IExtender interface provides an extended property to other classes, but our class that implements the IExtender must be marked with the ProviderProperty attribute. This attribute along with the IExtender accomplishes this task. This attribute constructor accept two parameters, a string specifying the name of the property your class will provide to other components, and the type of the receiver of this extended property.

[ProvideProperty(�BalloonText�, typeof(Control))]
public class BalloonToolTip : System.ComponentModel.Component, 
                                            IExtenderProvider
{
    ...
}

After adding this attribute, and when we add our control to the designer, every supported control on the form will have a new property added to its properties, named as the text specified in the ProvideProperty attribute (BalloonText).

Now we have done marking our class with its provided property name and receiver type, but this is still not enough from a code perspective. In our code, there must be a Get/Set pair of functions with exactly the same name as our extended property. To put it another way: we specified �BalloonText� as the name of our new property and we have to supply the �GetBalloonText� and the �SetBalloonText� functions as well.

The Get function is a function that returns a string (surprised? !!) which is the string associated with the control passed to it as a parameter. This parameter must be the same type as the receiver type in the ProvideProperty attribute.

public string GetBalloonText(Control parent)
{
    ... // return the string associated with the tool

        // that have its parent equal to the passed control.

}

And the Set function is a void function (surprised too? !!) and expects to have two parameters passed to it, the control that you want to add the property value to it, and the string value to be added.

public void SetBalloonText(Control parent, string value)
{
        ... // Add the passed string to a new tool

            // with its parent equal to the passed control.

}

These functions do not appear as ordinary functions in the code, but as properties in the control they have extended, so don't get confused (after all, that's what the IExtender should do).

The last thing to say is that it's your choice how to implement these functions. You may have a Hashtable to hold the values, getting it right from the tool the control contains, having an array with somehow the correct index to store these values, or you might even write a whole new class to do this job, but it's your choice, and I chose the Hashtable option.

The hash table is a place to store a collection of key/value pairs, and in our case, we already have these key/value pairs. It's our control/property pair. For each control, we have a unique string as its BalloonText value, so I used a hash table to store these facts. The control is the key and its associated �BallonText� is the value.

For the Get function, there is nothing more to say, but for the Set function there is:

public void SetBalloonText(Control parent, string value)
{
    if(value == null)
        value = string.Empty;
    if(value == string.Empty)
    // the user delete our property value,

    // so he don�t want our service

    {
        hashtable.Remove(parent);
        // remove our pair from the collection


        ... // create a toolinfo object, assign its parent

            // member the handle of the passed control

            // and send a message with TTM_DELTOOL

    }
    else
    // the user has assigned our property a value,

    // so he want our service

    {
        if(hashtable.Contains(parent))
        // check whether we have added the control before or not

        {
            // if we did, then the user has just modified

            // the property text, so update

            // the hashtable and the tool

            hashtable[parent] = value;
                   
            ...
            // create a toolinfo object, assign its parent member

            // the handle of the passed control

            // and send a message with TTM_UPDATETIPTEXT

        }
        else
        {
            // the user assign a new value to the property,

            // so add it to the hashtable and  add a new tool

            hashtable.Add(parent, value);

            ... 
            // create a toolinfo object fill its

            // values and send a message with TTM_ADDTOL

        }
    }
}

Congratulations, mission accomplished successfully!

There are a couple of properties I have added to the control but I didn't mention any of it here, they are too simple to be explained. I tried to comment any interesting or important point in the code example.

Points of Interest

If you take a look at the code example, you may wonder why I would add a new EventHandler for the Resize event of the target control?

Simply, because if you omit this, the BalloonToolTip will not recognize any new size of your control, and will not be activated on all the control client rectangles outside its original one that it was added to with the designer. For that reason, any change in the control size must be updated with the Win32 ToolTip tool associated with that resized control, and where is it better to do that than in the Resize event?.

You may also note that I don't add any EventHandler to the MouseHover of the target controls. That's because I used the TTF_SUBCALSS flag when I created the TOOLINFO structure, and if you refer to the MSDN, you'll find that this flag enforces the Win32 ToolTip control itself to add the EventHandler and perform any required housekeeping, for free!!.

I hope that this article was useful to you.

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