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:
toolinfo tf = new toolinfo;
tf.size = Marshal.SizeOf(typeof(toolinfo));
tf.flag = TTF_SUBCLASS | TTF_TRANSPARENT;
tf.parent = targetcontrol.Handle;
tf.text = �Tooltip text to be displayed�;
IntPtr tempptr;
tempptr = Marshal.AllocHGlobal(tf.size);
Marshal.StructureToPtr(tf, tempptr, false);
SendMessage(tooltipptr, TTM_ADDTOOL, 0, tempptr);
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)
{
...
}
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)
{
...
}
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)
{
hashtable.Remove(parent);
...
}
else
{
if(hashtable.Contains(parent))
{
hashtable[parent] = value;
...
}
else
{
hashtable.Add(parent, value);
...
}
}
}
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.