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

Custom ToolTips for MFC Projects

0.00/5 (No votes)
12 Apr 2007 3  
Put anything you want in a ToolTip

Screenshot - demo.gif

Contents

Introduction

A custom tooltip is a seductive but dangerous notion, and if you don't do it right you could have the Interface Police knocking on your door. Nevertheless, if you decide you do want complete control over the contents of your tooltips and you're using MFC, here's an approach that should get you going.

You'll need to supply two virtual functions to size and draw your tips in a class derived from CustomToolTip, and a little "TipData" class to hold the data that your tips need. Adding the resulting custom tipper into a window is no more work than for a regular tooltip.

Two example custom tooltips (CustomToolTipPlain using GDI, and CustomToolTipPlus using GDI+) are included in the source to get you going. And there are separate demo projects for each.

The source code for the custom tip turns out to be relatively simple, which you might agree is nice for a change.

As a little bonus, there's a harness in place for optional "fade-in" animation. CustomToolTip members let you set the number of frames and duration for the animation, and your virtual tip drawing function receives a floating-point argument that varies from 0.0 (first frame) to 1.0 (last frame). CustomToolTipPlain uses it to vary the background and text color in the tip during the first half-second that each tip is displayed. CustomToolTipPlus puts a variable gradient fill in the background of the tip, and the gradient shows through selected colors in the image at left of the tip.

I've tried to write this up in detail, so apologies in advance for stating the obvious now and then.

Overview

Here's a class diagram with just the good bits to give you the Big Picture:

Screenshot - classes.gif

You derive your custom tip class from CustomToolTip, you derive a tip data holder from TipData (since only you know what data your tip needs), and you put three functions and a data member into your window that wants the tips to make them show.

Given your "WindowThatWantsTips", you can give it custom tooltips by:

  • deciding what custom data the tips will display - perhaps just a CString, perhaps something more elaborate - and deriving your tip data holder TipDataYours from TipData with appropriate data members
  • deriving your own CustomToolTipYours from CustomToolTip: there are only two non-trivial functions, SetTipWindowSize() which determines the size of the tip window for the current tip, and CreateTipImage() which is where you roll up your sleeves and put in the actual custom drawing code
  • bolting your custom tooltip into your WindowThatWantsTips, which involves adding a few lines of code to create the tipper, some AddTool() calls to set up tips for specific controls or rectangles, and a couple of lines in PreTranslateMessage() to show tips when appropriate.

Of course, you can call your derived classes anything you like. In the GDI demo, for example, they're called CustomToolTipPlain and TipDataPlain. (OK that was obvious, but at least I apologized in advance.)

The CustomToolTip base class handles all of the tooltip window behavior, including showing, hiding, positioning and sizing the tip window. It also keeps lists of the CWnds or rects that serve as triggers for the tips, together with a list of TipData pointers for the contents of the tips.

You can use entirely arbitrary data when drawing your custom tips, not just CStrings. That's the point of the TipDataYours in the design, a class where you fill in the exact data members that you want. However, CustomToolTip just tracks a list of pointers to the base class TipData, in order to allow you to have several custom tooltips, each with its own derived version of TipData. As a result, when it comes time to do the sizing and drawing you'll need to cast a generic TipData* to the exact type of data they expect. This happens in your implementations of SetWindowSize() and CreateTipImage() - in the GDI demo, for example, you'll find

TipDataPlain   *theTip = 
    dynamic_cast<TipDataPlain*>(m_tips[tipIndex]);

With that small nuisance out of the way, it might cheer you up to know that you can derive any number of different custom tooltips for use in the same application, and have them share a common TipData holder or use different ones. You can also show tips for a window using two or more custom tooltips.

The actual data in your derived data holder for the tips (such as m_otherData in TipDataYours in the above diagram) can be owned by the custom tooltip or it can be a pointer to some object with a longer lifetime. For example, if TipDataYours owns m_otherData, then put "delete m_otherData;" in the destructor for TipDataYours. If you don't want m_otherData deleted - then don't delete it, and it will live on after your tooltips die.

Try it out!

CustomToolTipDemo shows a relatively plain and simple GDI-based custom tooltipper called CustomToolTipPlain. CustomToolTipPlusDemo shows a tipper called CustomToolTipPlus that uses GDI+ for drawing, and the tip for that one is slightly fancier. If you like the potential after taking a look, you might want to give one of them a try in your own project, to see if the concept will work for you: guides to adding them into your project can be found at the bottom of CustomToolTipPlain.cpp, and CustomToolTipPlus.cpp. All I can promise is that it works for me to provide tips in a highly customized dialog. If the tipper works for you, then you're in good shape to invest the time writing your own version.

Here's a copy of the instructions for adding CustomToolTipPlain to a window in one of your projects, to give you a preview. 'ParentWindow' is the class in your project that you'll be giving custom tips. The source files to add are included in both of the downloads above.

Add these files to your project:
Base classes: CustomToolTip.cpp/.h, TipData.h
Your derived classes: for this example, those would be
CustomToolTipPlain.cpp/.h, and TipDataPlain.h

'ParentWindow' is the window that wants to show tips for its controls or
rectangular areas.
---------- in ParentWindow.h-----------
#include "CustomToolTipPlain.h"

...
class ParentWindow : public CDialog { // or some other CWnd derivative

...
public:
    CListBox        m_aListBox; // as an example of item wanting a tip

protected:
    virtual BOOL PreTranslateMessage(MSG* pMsg);
private:
    void CreateTip();
    void ShowTip();
    CustomToolTipPlain  *m_tipper;
};

---------- in ParentWindow.cpp-----------
#include "stdafx.h"

#include "ParentWindow.h"

...

// Set m_tipper to 0 in your constructor.

ParentWindow::ParentWindow(...)
    : m_tipper(0),
    ...
    {
    }

// And delete your tipper somewhere, for example...

ParentWindow::~ParentWindow()
    {
    delete m_tipper;
    }

// Call ShowTip() (see below) when the mouse moves.

BOOL ParentWindow::PreTranslateMessage(MSG* pMsg)
    {
    if (pMsg->message == WM_MOUSEMOVE)
        {
        ShowTip();
        }
    return CDialog::PreTranslateMessage(pMsg); // match the Parent's base class!

    }

// Called by ShowTip(), creates tipper on first call.

// Put your "AddTool" calls here.

void ParentWindow::CreateTip()
    {
    if (m_tipper == 0)
        {
        try
            {
            m_tipper = new CustomToolTipPlain(m_hWnd);
            // or animated: m_tipper = new CustomToolTipPlain(m_hWnd, true);


            m_tipper->SetMaxTipWidth(450); // default 300 px


            // other setup, eg:

            m_tipper->SetAnimationNumberOfFrames(5);
            m_tipper->SetHideDelaySeconds(10); // default 0 == don't hide

            m_tipper->SetAvoidCoveringItem(true); // true == position near 

            // item being tipped, without obscuring it


            // Add controls and rectangles that want tips.

            // E.g. with TipDataPlain derived from TipData (see 

            // TipDataPlain.h),

            // which wants a CString and an optional bitmap resource ID:

            // If you have a picture:

            m_tipper->AddTool(&m_aListBox, new TipDataPlain(_T("Tip for list 
                box...."), IDB_INFO) );
            // If you don't have a picture:

            m_tipper->AddTool(&m_aListBox, new TipDataPlain(_T("Tip for list 
                box....")) );
            ...
            }
        catch(...)
            {
            delete m_tipper;
            m_tipper = 0;
            }
        }
    }

// Called on mouse move, show tip if appropriate.

void ParentWindow::ShowTip()
    {
    CreateTip();

    if (m_tipper)
        {
        m_tipper->ShowTipForPosition();
        }
    }

Writing your Custom ToolTip

For a quick start, make a copy of one of the supplied examples (either CustomToolTipPlain, which uses GDI for drawing, or CustomToolTipPlus, which uses GDI+). Rename the .cpp and .h files, replace the class name with your own new name, and you're ready to go.

Your first bit of work will no doubt be a bit of spec and design, during which you'll come up with a list of the data items that your tip will need for drawing. This doesn't have to be elaborate if it's just a data wrapper: for example, here's the entire data holder that goes with CustomToolTipPlain:

class TipDataPlain : public TipData
    {
    public:
        TipDataPlain(const CString & text, UINT bitmapID = 0)
            : m_message(text), m_bitmapID(bitmapID)
            {}
        ~TipDataPlain()
            {/* For your own tip data, delete members here as needed. */}
        CString     m_message;
        UINT        m_bitmapID;
    };

Both of the supplied examples assume that you will draw your tip into an offscreen buffer image. You'll find the "boilerplate" code for doing that in the examples, and once the setup is done there's no difference between drawing to the offscreen buffer and drawing directly to the screen.

If for some reason you can't buffer your tooltip contents then you'll need to put your drawing code in the virtual function DrawTip() instead of CreateTipImage(), and leave CreateTipImage() as an empty function. Buffering is usually better, since it avoids flicker. From here on I'll assume you'll be buffering your drawing.

You can't draw the tip until you make it the right size, and often you can't make it the right size until you've gone through all the steps needed to draw the contents, minus the actual drawing. So the drawing takes place in two steps, SetTipWindowSize() and CreateTipImage().

void CustomToolTipPlain::SetTipWindowSize(size_t tipIndex)

The sole purpose of SetTipWindowSize() is to set the members m_tipImageHeight and m_tipImageWidth. Those members represent the full width and height of your tip window contents, and will also correspond to the size of your offscreen buffer image. The heart of the plain demo version's SetTipWindowSize(), for example, is:

TipDataPlain    *theTip = dynamic_cast<TipDataPlain*>(m_tips[tipIndex]);
if (theTip)
    {
    int                nCount = theTip->m_message.GetLength();
    CFont* def_font = dc.SelectObject(&font);
    dc.DrawText(theTip->m_message, nCount, &textRect,
                                            DT_CALCRECT | DT_WORDBREAK);
    ...
    m_tipImageWidth = int(textRect.right) + kTipMargin;
    m_tipImageHeight = int(textRect.bottom) + kTipMargin;
    ...

(And then a few lines follow to add in the size of the optional bitmap.)

void CustomToolTipPlain::CreateTipImage(size_t tipIndex, double 
    animationFraction)

Two goals here: resize your offscreen bitmap (CBitmap* m_tipImage in the demo), and draw your tip contents into the offscreen bitmap. The offscreen bitmap management can be copied as-is into your version. And from there, drawing the tip is the same as for any window - for example, the plain demo calls DrawText(), this time without the DT_CALCRECT option, to show the text for the tip.

I'll gamble that you like code, so here's the entire function from TipDataPlain:

/*! CreateTipImage [ virtual private  ]
Called by DoShowTip, and OnTimer (if animating). This GDI version draws tip 
text into a CBitmap, with colored background. The color is animated slightly
if you set animateDrawing to true in the constructor. A bitmap is shown at 
left. If you don't animate your tooltip you can ignore animationFraction
in your implementation. The m_tipImage created here is drawn to the window 
by DrawTip() above.
@param  tipIndex int                              : index into m_tips
@param  animationFraction double  [=1.000000]     : for animation
*/
void CustomToolTipPlain::CreateTipImage(size_t tipIndex, double animationFraction)
    {
    double        t = animationFraction;
    double        oneMt = 1.0 - t;

    delete m_tipImage;
    m_tipImage = 0;

    try
        {
        CFont       font;
        BOOL        madeFont = 
            font.CreateFont(kFontSize, 0, 0, 0, FW_NORMAL, 0, 0, 0,
            DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS,
            DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, _T("Arial"));

        if (madeFont)
            {
            // Note a static_cast will work here, as long as you're sure you

            // have the right type. Dynamic casting requires setting

            // Enable Run-Time Type Info (/GR) in your project.

            TipDataPlain    *theTip = 
                dynamic_cast<TipDataPlain*>(m_tips[tipIndex]);

            if (theTip)
                {
                CClientDC   dc(this);
                m_tipImage = new CBitmap();
                m_tipImage->CreateCompatibleBitmap(&dc, m_tipImageWidth, 
                    m_tipImageHeight);
                CDC         dcMem;
                dcMem.CreateCompatibleDC(&dc);

                int         textLeft = m_graphicWidth + 2*kGraphicMargin;
                int         textWidth = m_tipImageWidth - (m_graphicWidth + 
                    2*kGraphicMargin);
                RECT        bitmapRect = {0, 0, m_tipImageWidth, 
                    m_tipImageHeight};
                RECT        textRect = {3 + textLeft, 3, m_tipImageWidth - 6, 
                    m_tipImageHeight - 6};
                // Note background color in the little bitmap graphics is

                // roughly 255,254,207 (a light yellow).

                // bkColor fades down from white to that.

                COLORREF    bkColor = RGB(255, 254, BYTE(255 - t*48.0));
                // textColor moves from dark gray to black.

                COLORREF    textColor = RGB(BYTE(64*oneMt),
                                        BYTE(64*oneMt), BYTE(64*oneMt));
                CBrush      backBrush(bkColor);

                int         nCount = theTip->m_message.GetLength();

                CBitmap *oldBitmap = dcMem.SelectObject(m_tipImage);
                CFont* def_font = dcMem.SelectObject(&font);

                // Fill in background. 

                dcMem.SetBkColor(bkColor);
                dcMem.SetTextColor(textColor);
                dcMem.FillRect(&bitmapRect, &backBrush);

                // Put a little graphic on the left. This could be animated

                // but currently isn't - see DrawGraphic() below.

                DrawGraphic(dcMem, t, theTip->m_bitmapID);

                // Draw the main text message.

                dcMem.DrawText(theTip->m_message, nCount, &textRect, 
                    DT_WORDBREAK);

                dcMem.SelectObject(oldBitmap);
                dcMem.SelectObject(def_font);
                font.DeleteObject();
                }
            }
        }
    catch(...)
        {
        delete m_tipImage;
        m_tipImage = 0;
        }
    }

That's fairly standard stuff. To avoid mysteries, here's the DrawGraphic() function mentioned above:

/*! DrawGraphic [ private  ]
Pulls in a bitmap from resource fork and draws it at left of tip image.
Your graphics will of course be much prettier:)
@param  dcMem CDC &                 : offscreen image
@param  animationFraction double    : for animation
@param  bitmapID UINT               : bitmap resource ID
*/
void CustomToolTipPlain::DrawGraphic(CDC & dcMem, double animationFraction, 
    UINT bitmapID)
    {
    CBitmap    bitmap;
    if (bitmapID && bitmap.LoadBitmap(bitmapID))
        {
        BITMAP    bm;
        bitmap.GetBitmap(&bm);
        
        CDC       dcGraphic;
        dcGraphic.CreateCompatibleDC(&dcMem);
        CBitmap *oldBitmap = dcGraphic.SelectObject(&bitmap);
        int       left = kGraphicMargin;
        // Slide in from left, as an example of animation:

        ////double        oneMt = 1.0 - animationFraction;

        ////int           left = kGraphicMargin - int(oneMt*bm.bmWidth);

        dcMem.BitBlt(left, kGraphicMargin, bm.bmWidth, bm.bmHeight,
                        &dcGraphic, 0, 0, SRCCOPY);
        dcGraphic.SelectObject(oldBitmap);
        }
    }

CustomToolTip public members

Creation, adding and deleting tips, sizing, animation. If that's not enough control - well, I'm hoping the source in CustomToolTip is simple enough that you'll just rip in there and make it your own.

CustomToolTip(HWND parentHwnd, bool animateDrawing = false)

parentHwnd is the window that wants to show the custom tooltips.

Set animateDrawing true if you plan to provide a little bit of animation when the tooltips are displayed. You'll find examples in the demo projects (eg CustomToolTipPlain::CreateTipImage()).

virtual ~CustomToolTip()

The base class dtor does nothing, but your derived class might want to do some cleanup. ~CustomToolTipPlain for example deletes the image used to display tip contents.

bool AddTool(const CWnd *pWnd, TipData *tipData)
bool AddTool(const CRect & rect, TipData *tipData)
void DelTool(const CWnd *pWnd)
void DelTool(const CRect & rect)

Two variants of AddTool and DelTool let you add tips for any sort of CWnd (in which case the window's screen CRect as given by GetWindowRect() is used) or for a CRect (in which case the rect should be in the local coordinates of the parent window that wants tips). Note AddTool first calls DelTool, so AddTool can also be used to update tips.

The tipData argument should point to the specific sort of TipData that you're using for your tip, such as TipDataPlain in the plain demo. Later, the data is used by your derived CreateTipImage() to do the actual tip drawing.

void SetMaxTipWidth(int maxWidth)

maxWidth is the maximum width of the tip in pixels (default 300). However, since all drawing is under your control, you can interpret this differently if you want to.

void ShowTipForPosition()

Call in the parent window that wants tips, in response to every WM_MOUSEMOVE.

void HideTip()

"Hides" the tip window by shrinking it down and moving to x 0, y 10. This avoids activation flicker in the parent window. You can call it if you need to, but you'll probably find that the default hiding is good enough.

void SetHideDelaySeconds(UINT delayInSeconds)

Seconds after which the tip window is hidden. Zero means never hide the tip.

void SetAvoidCoveringItem(bool avoidItem)

avoidItem true means the tip window will not cover any part of the underlying CWnd or CRect corresponding to the tip. False means the tip will appear near the cursor, wherever it is.

void SetAnimationStepDuration(UINT msecs)

The duration of each startup animation step, in milliseconds. The default is about 55 milliseconds.

void SetAnimationNumberOfFrames(UINT numFrames)

The default is about ten, which in combination with the default of 55 msecs between frames gives about half a second of animation when each tip is shown.

Points of Interest

When the tooltip window first comes up, it immediately re-activates the parent window that called it up, and is never activated again. It "hides" by shrinking down to nothing and moving off to a corner of the screen with

::SetWindowPos(m_hWnd, NULL, 0, 10, 1,  1, SWP_NOZORDER | SWP_NOACTIVATE);

This is perhaps a slightly unusual way of avoiding activation flicker, but works fine. And it's the main reason the tip base class is relatively simple.

If you'd like to see the GDI+ version of these custom tips in a "real" application, you're welcome to try out TenClock, a little freeware app that shows Outloook appointments as colored wedges on the face of a rather nice 3D clock, among other things. After you've installed it, right-click on the clock and pick Configure... to see the dialog with the custom tips. Outlook and 2000/XP are required (not tested yet under Vista - if you try it, please let me know if you see any problems). By the way, if you check TenClock's About box you might see your name there!

If you're on the fence about upgrading your tips, allow me to predict that 2007 will be the Year of the Eye Candy: in fact, I'll bet you'll be seeing a double-Beryled Vista of it - ow, sorry about that....

If you use this - leave a little comment? That would cheer me right up after all this typing.

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