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

InternetToolTip

0.00/5 (No votes)
2 Dec 2011 1  
A better ToolTip for Windows Forms and more.

InternetToolTip control provides the basic ToolTip functionality and a lot more

Introduction

This control extends the standard ToolTip with several possibilities like retrieving a ToolTip asynchronously from an external (e.g., Internet) source. It is easy to implement and to extend.

Background

For years I’ve been struggling with the idea of using the ToolTip control that is offered by the .NET Framework in Windows Forms applications. On one hand it is a nice control that offers the possibility of easily integrating visible tooltips. On the other hand, it is not possible to extend it in any way. Since I am currently working on a major update of one of my applications, I wanted to include some fancy tooltips that can come from an arbitrary data source.

This standard ToolTip control does not include two major customization features: drawing it at will and using the data at will. InternetToolTip creates the basis for writing our own data providers (what to do with the tooltip string) as well as our own view providers. The ones that I’ve written give InternetToolTip the same abilities that the standard control has – and more.

Requirements

This code requires C# (Version 2.0 and higher) together with Microsoft’s .NET 2.0 or higher. The technology used is Windows Forms. However, Windows Forms is just required for the test application / in combination with the sample controls and sample view. Basically it should be possible to port this to other presentation technologies or even ASP.NET. However, I suppose some changes in the inheritance of the main class, InternetToolTip, would be required as well. So the project mainly targets Windows Forms.

The goal

The InternetToolTip control should offer the possibility to be fully customized, i.e., displaying data in a way the programmer wants to have it. The data source should not depend on the control itself but could be provided by the programmer as well.

The following goals should be achieved:

  • The data view and the data source can be fully customized or rewritten without rewriting the control itself.
  • The control should be similar to the existing tooltip control, i.e., it should be possible to not do any rewriting and have about the same functionality.
  • The control should support asynchronous requests.
  • Due to similar syntax, the efforts of replacing the old tooltip control with the new one should be minimal.

My goal

All those goals are nice but a little bit too general. What are my personal goals for this control?

  • I want to write a (more sophisticated than the provided) webservice data provider that fetches the data from a specific webservice.
  • The tooltips provided in the form are used as data request strings, i.e., they will be one of the arguments of the webservice call.
  • The return value is used in order to draw some information and can be used in order to open a browser with an even more detailed help entry.

The sample application

The provided code builds the basis for the tooltip control and derived controls. I’ve included two basic controls:

  1. A really simple light bulb control. Its usage is really simple – it’s just a custom image (default is the light bulb displayed).
  2. A more complex tooltip textbox control. It extends the standard textbox with a cue and displays a tiny light bulb behind the textbox. This control actually has an InternetToolTip control included – here you’d only need to provide a custom data provider and data view if necessary.

I’ve also included a basic data provider as well as a basic data view. Both aim to give InternetToolTip the same basic functionality as the standard tooltip control. The view control goes a little bit further by containing some animation functions like:

  • Appear (the tooltip will just appear),
  • Fade (the tooltip will fade in),
  • Slide (the tooltip will slide down from the top of the screen) and
  • SlideFade (the tooltip will fade in and slide down from the top of the screen).

Those animations can run for any amount of time (specified in ms) that is set (e.g., 500ms, 2000ms, ...).

The provided solution contains a sample implementation of a custom data provider. Here I simulate the work of fetching some data from an arbitrary data source (internet website / service, database …) with an instance of a Timer class. The Tick event is used for simulating an asynchronous request while the Thread.Sleep() method is used to simulate a synchronous request. This should display the power of asynchronous requests.

Concept

The concept behind the structure of the InternetToolTip control

In order to stay flexible, I used interfaces. Interfaces give us several advantages over using (abstract) classes. First of all, it does not restrict you from inheriting from an existing class, which means you do not have to inherit from my interface directly. Just use the construction plan where you need it. Secondly, I did not have to repeat the abstract keyword everywhere, since interfaces can only contain abstract methods and properties. I also did not have to specify that those methods are virtual since every method in an interface is virtual. So I got a cleaner code (less keywords required), which provides more flexibility.

The InternetToolTip control relies on two interfaces:

  • IToolTipView – this is responsible for showing the display on screen. I’ve provided a sample one which is similar to the view we gets with the standard ToolTip control. This view has some advantages. I will go into the details later on.
  • IToolTipDataProvider – here we fetch the data. To not make things complicated, I just used one data string. This one data string can be represented as a tooltip using the sample implementation I’ve provided. However, the data string is just responsible for differentiating different tooltips – not the tooltips themselves. So in your (free) implementation of a data provider, you might use the data string for fetching the real tooltip from some database or some other resource. The sample control just returns the data string and thus simulates the behavior of the real tooltip control.

A data view has to contain the following methods:

void ShowToolTip(Point pt, Size sz);
void HideToolTip();
void DrawLoading(string text);
void DrawToolTip(object tooltip);
void DrawException(Exception exception);

This ensures that a tooltip can be shown at some coordinate, where the upper left coordinates and the size of the control (which requests this tooltip) are passed as arguments. The coordinates are always passed as screen coordinates. Also, the InternetToolTip control can tell the view provider to stop displaying its information. This is the case when the mouse is leaving the tooltip area. The draw methods inform the view about a change in the state. If an asynchronous request is started, the loading screen has to be rendered. If the request was returned successfully, the tooltip has to be drawn. In case of an exception, a special routine is required.

A data provider has to contain the following methods:

object RequestData(string request);
void RequestDataAsync(string request, Control userState);
void CancelDataAsync(Control userState);
event ToolTipDataEventHandler RequestDataHandled;

This ensures that data can be requested synchronously and asynchronously. The latter one has a callback which is implemented as an event. The event is integrated with a new delegate containing a more detailed EventArgs implementation. The given more specific EventArgs contains:

  • Control
  • Result
  • Success
  • Exception

Because several (asynchronously requested) requests could take place, we need a state to determine which tooltip has been requested. This is done over the control that contains the tooltip. This could be used for caching tooltips as well!

The result is saved as an object since I have all kinds of objects in my mind. One idea I had was to fetch more data and return it as a preview string with a longer one and an internet URL for the most detailed version. So I would need three information pieces (“Preview”, “View”, “URL”), which would require a special dataview. In order to be flexible, I decided to pick object and use the ToString() method in my general implementation of a data view. So if you want to transport custom objects, which should be painted in a custom way, you have to rewrite my basic view provider or implement your own one. In any case, you do not have to touch InternetToolTip or the data provider interface.

Success determines if the request was successfully returned, i.e., if there was an exception. The exception is saved as well and could (should) be displayed. The basic view I’ve provided displays the exception.

Here are the properties of the InternetToolTip control:

IToolTipDataProvider dataProvider;
IToolTipView dataView;
bool async;
string loadText;

Those are the variables that can be changed with the corresponding properties. The first two are just placeholders for specific implementations of those interfaces. With help of the Async property, you can decide if you want to use the asynchronous or the synchronous requests. The LoadText property sets a string that is shown while loading a tooltip.

Implementation issues

The class diagram

Implementation was (as usual) not always straightforward. I will go into the details of the most interesting parts.

The control itself is a Component. This means we are at some basic level with not much overhead. Since we do not want the control to be displayed (directly), there is no need to inherit from Control or other more sophisticated classes.

[ProvideProperty("ToolTip", typeof(Control))]
public class InternetToolTip : Component, IExtenderProvider
{ /* ... */ }

I did also use the interface IExtenderProvider to tell the designer about the possibility of extending controls that offer the following abilities:

public bool CanExtend(object extendee)
{
  return (extendee is Control && !(extendee is Form) && 
         !(extendee is InternetToolTip));
}

This means only controls and not forms or the InternetToolTip control itself can be extended. Since I set that the property ToolTip should be provided with the control, I had to create two methods:

public void SetToolTip(Control control, string caption)
{
  if (caption.Equals(string.Empty))
  {
    if (collection.ContainsKey(control))
    {
      control.MouseEnter -= new EventHandler(control_MouseEnter);
      control.MouseLeave -= new EventHandler(control_MouseLeave);
      collection.Remove(control);
    }
    return;
  }
  if (collection.ContainsKey(control))
    collection[control] = caption;
  else
  {
    collection.Add(control, caption);
    control.MouseEnter += new EventHandler(control_MouseEnter);
    control.MouseLeave += new EventHandler(control_MouseLeave);
  }
}
public string GetToolTip(Control control)
{
  if (collection.ContainsKey(control))
    return collection[control].ToString();
  return string.Empty;
}

If the designer or somebody wants to set a tooltip with an empty string, it will either remove the control from the collection of tooltip-controls (including all events) or do nothing. Else it will either change the tooltip that has been set for this control or add the control with the provided tooltip to the collection. In this case, we have to set the events as well.

The get method just looks for the specified control in the collection. If the collection does not contain the control, then an empty string is returned (this means there is no tooltip set for this control).

The real interaction comes with the control events (in this case: mouse-enter and mouse-leave). The code reads:

void control_MouseLeave(object sender, EventArgs e)
{
  dataView.HideToolTip();
  if (loading)
  {
    dataProvider.CancelDataAsync(sender as Control);
    if ((sender as Control).Equals(active))
      loading = false;
  }
}
void control_MouseEnter(object sender, EventArgs e)
{
  loading = async;
  Control c = sender as Control;
  active = c;
  Point p = c.Parent.PointToScreen(c.Location);
  dataView.ShowToolTip(p, c.Size);
  if (async)
  {
    dataView.DrawLoading(loadText);
    dataProvider.RequestDataAsync(collection[c].ToString(), c);
  }
  else
  {
    try
    {
      dataView.DrawToolTip(dataProvider
        .RequestData(collection[c].ToString()));
    }
    catch (Exception ex)
    {
      dataView.DrawException(ex);
    }
  }
}
void OnRequest(object sender, ToolTipDataEventArgs e)
{
  if (!e.Control.Equals(active))
    return;
  loading = false;
  if (e.Success)
    dataView.DrawToolTip(e.Result);
  else
    dataView.DrawException(e.Exception);
}

So what is happening here? First of all, if the mouse leaves the control, we want to hide the tooltip. This seems straightforward but we have also to think about the asynchronous case as well. Maybe the data provider has implemented a cancel method. So, if we are still loading (only possible if we have set Async to true), then we should call the cancel method of the asynchronous data request. Even more, if the control is the active control (the one we are looking at right now), then we can set loading to false since we just left the loading mode.

The next thing is corresponding to the event of the mouse pointer entering a control. The status of loading is determined by the general status of async. In non-asynchronous mode, we will call the method directly in order to get the tooltip string – therefore there won’t be any loading. Next, the active control is set by the one that was just being entered. This is determined by the sender. We can do this cast – since the CanExtend() method made sure that just controls which have inherited from Control can be extended and thus enter our collection / our event system.

After reading out the current screen location, we call the show method of our view and request our tooltip string. The async mode is quite straightforward (we set loading and then place the asynchronous request which will fire the event in case of success or failure) while the other one is wrapped with a try-catch-block. This is the only way of getting an exception as a feedback from this non-asynchronous request, since the result will always be of type object. We do not know if an object of type Exception means that an exception occurred. So this should provide some flexibility.

The last part of the code represents the method that is triggered once the request has been finished. Here we just take a look at the state – if the control is not the current one, then the call is obsolete. Otherwise we distinguish between a successful call and an exception.

Let's have a look at the provided implementation of a data provider. I've included the StringDataProvider in order to give my control the same functionality as the ordinary ToolTip control right away:

public class StringDataProvider : IToolTipDataProvider
{
  public object RequestData(string request)
  {
    return request;
  }
  public void RequestDataAsync(string request, Control userState)
  {
    if (RequestDataHandled != null)
      RequestDataHandled(this, new ToolTipDataEventArgs(request, userState));
  }
public void CancelDataAsync(Control userState)
{ /*Nothing to do here*/ }
public event ToolTipDataEventHandler RequestDataHandled;
}

This is a really simple implementation, which just gives back the passed argument. Therefore we can omit the CancelDataAsync. However, due to restrictions using the interface we have to implement the method. So we just set a blank method body.

A little bit more interesting is the provided data view implementation. I’ve called this one BasicToolTipView. After some research, I figured out that using a top-most form will be the solution for having an organized way of drawing the tooltip.

public partial class BasicToolTipView : Form, IToolTipView
{
  //Membervariables ...
  const int SW_SHOWNOACTIVATE = 4;
  const int HWND_TOPMOST = -1;
  const uint SWP_NOACTIVATE = 0x0010;
  const int MS_PER_FRAME = 13;

  public event DrawToolTipEventHandler DrawToolTipView;
  public event MeasureToolTipEventHandler MeasureToolTipView;

  //Constructor ...
  //Some methods ...

  [DllImport("user32.dll", EntryPoint = "SetWindowPos")]
  static extern bool SetWindowPos(...);
  [DllImport("user32.dll")]
  static extern bool ShowWindow(...);

  //Some more methods ...

  public void ShowToolTip(Point p, Size sz)
  { 
    Location = new Point(p.X + sz.Width / 2 - Width / 2, p.Y - Height);
    ShowWindow(Handle, SW_SHOWNOACTIVATE);
    SetWindowPos(Handle.ToInt32(), HWND_TOPMOST, Left, 
                 Top, Width, Height, SWP_NOACTIVATE);

    if (ShowEffect == Effect.Appear)
    {
      //...
    }
    else
    {
      //...
      effectTimer.Start();
    }
  }
  public void HideToolTip()
  {
    if (frames > 0)
      effectTimer.Stop();
    Hide();
  }

  //Even more methods ... 
  //Properties ...

  public enum Effect
  {
     Appear, Fade, Slide, SlideFade
  }
}

First of all, this form not only inherits from Form but also from IToolTipView. This is why I picked interfaces – being totally flexible. What was difficult about this implementation and why is it a good implementation? Creating a top-most form is not really a hard task in .NET. Preventing the form from stealing focus is difficult. However, some smart guys have already made good research and found the right API calls to give our form this (missing) ability. I’ve provided the links that helped me on this topic in the References list. The trick is to use the right DLL calls with the right methods and the right constants. All in all, it’s all about the right call! This is why the methods ShowToolTip() and HideToolTip() will usually excel standard Show() and Hide() methods that are implemented in Windows Forms. My show method does the following things:

  1. It sets the location to the horizontal center and the vertical top of the control it is used on.
  2. It uses the Win-API call to show itself – the difference from the usual call is the constant that tells the Operating System not to give focus to the shown form.
  3. It uses the Win-API call to place itself top-most.
  4. It does some things depending on the animation that is set. If the effect is in Appear mode, it just appears. In fade, it will calculate how much opacity is to be gained per Tick event and set the starting opacity to 0. Slide will have a similar calculation.
  5. If an animation (effect different than Appear) has been set, the timer is started.

My hide method checks if an animation is running and cancels the animation. Then of course the Hide() method is used to hide the form. Close() is not possible here, since we do not want to recreate the form for every tooltip – just show at a different location with a different content.

The Effect enumeration is nested in the class to show the relation. It provides the possible animation effects. Right now there is basically "no animation", "slide from top", "fade in", and a combination of those two effects. Additionally, the duration of the animation can be set. All this (effect and duration) can be done by properties or the following method:

public BasicToolTipView Animation(Effect effect, int duration)
{
  ShowEffectTime = duration;
  ShowEffect = effect;
  return this;
}

This method sets both properties in one call and returns the current instance. This is called chaining. Such a concept allows the user to make use of else non-returning methods.

I included a possibility to use my premade control (so my "sample" tooltip view provider) with custom methods for drawing. As with the ListBox and other Windows Forms controls, this is done by setting a property (called DrawMode) to OwnerDrawFixed or OwnerDrawVariable and handling events triggered by my view provider.

The standard drawing will create such a bubble shape:

Drawing the geometrie of the tooltip bubble

The height is usually 32 pixels. This is only important for loading messages. Real tooltips will not be restricted to a content height of 18 pixels. So those 18 pixels can be seen as a kind of minimal height for the content. To display real tooltips, a maximum width has to be specified. The height then grows as the maximum width does not provide enough space to display the tooltip. Measuring the text size is done with the static TextRenderer class that provides string drawing and measuring capabilities, which are really useful in combination with proposed heights and widths. Those methods also work nicely together with the TextFormatFlags enumeration.

Using the code

You can simply use the control with the designer. Since I've built the control to mimic the original ToolTip control, the way you would use my control is in principle the same. However, if you want to go beyond standard abilities, you have to use the code-behind. The following code creates a new instance of my tooltip control (can be done over the designer, too) and customizes the providers:

//Create instance
InternetToolTip toolTips = new InternetToolTip();
//Sets the text displayed while async. requests
toolTips.LoadText = "Please wait!";
//Sets the data provider to a new one (provided in the sample code)
toolTips.DataProvider = new SampleDataProvider();
//Gets the default view provider
BasicToolTipView toolView = toolTips.DataView as BasicToolTipView;
//Sets the animation effect to slide with a duration of 200ms
toolView.Animation(BasicToolTipView.Effect.Slide, 200);
//Changes the first gradient color to light blue
toolView.GradientColorOne = Color.LightBlue;
//Changes the second gradient color to light gray
toolView.GradientColorTwo = Color.LightGray;

The BasicToolTipView can be drawn by using the following properties and events:

//Sets the DrawMode to a custom mode
toolView.DrawMode = DrawMode.OwnerDrawVariable;
//Assigns the events -- setting the callbacks
toolView.DrawToolTipView += 
  new rsi.Controls.iToolTip.DrawToolTipEventHandler(drawToolTipView);
toolView.MeasureToolTipView += new MeasureToolTipEventHandler(measureToolTipView);

Here we have assigned the two important events to callback methods in our form / control. Those functions could be implemented like this:

void measureToolTipView(object sender, MeasureToolTipEventArgs e)
{
  e.Height = TextRenderer.MeasureText(e.ToolTip.ToString(), 
             e.Font, new Size(e.Width, e.Height)).Height;
}

void drawToolTipView(object sender, rsi.Controls.iToolTip.DrawToolTipEventArgs e)
{
  //Draws the Background
  e.DrawBackground();
  //Draws some Text in the middle of the bubble
  TextRenderer.DrawText(e.Graphics, e.ToolTip.ToString(), e.Font, e.Bounds, e.ForeColor, 
                        TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}

Points of interest

Even though it was not the focus of this project, I did also build a very small control (for the sample application – but it could be useful in other applications as well) that is nothing more than an (selectable) image. However, the size of the control is always exactly the size of the image. The default image is a light bulb (thus LightBulb control). Additionally, I built a larger control which is a (cue) textbox with an integrated InternetToolTip. I built this control since it shows how the control could be used in any custom control. Another reason is to show in which direction I want to go with InternetToolTip: having specialized controls that provide their own tooltips in a custom design.

For me integrating the animation was quite spectacular. It was something I wanted to implement in a Windows Forms application for a long time. I was curious about the performance and I did like the result. I suppose that writing an animation framework for a Windows Forms application would be quite handy and useful. One really good approach is already published here on CodeProject.

References

I found the following links useful:

  • The problem of showing a form without stealing focus was solved on StackOverflow (here).
  • An introductory discussion was necessary in order to determine an efficient way to do the screen drawing (here).
  • A short article about handling exceptions (here).

History

  • v1.0.0 | Initial release | 01.12.2011.

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