Contents
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.
Here's a class diagram with just the good bits to give you the Big Picture:
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.
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 {
...
public:
CListBox m_aListBox;
protected:
virtual BOOL PreTranslateMessage(MSG* pMsg);
private:
void CreateTip();
void ShowTip();
CustomToolTipPlain *m_tipper;
};
---------- in ParentWindow.cpp-----------
#include "stdafx.h"
#include "ParentWindow.h"
...
ParentWindow::ParentWindow(...)
: m_tipper(0),
...
{
}
ParentWindow::~ParentWindow()
{
delete m_tipper;
}
BOOL ParentWindow::PreTranslateMessage(MSG* pMsg)
{
if (pMsg->message == WM_MOUSEMOVE)
{
ShowTip();
}
return CDialog::PreTranslateMessage(pMsg);
}
void ParentWindow::CreateTip()
{
if (m_tipper == 0)
{
try
{
m_tipper = new CustomToolTipPlain(m_hWnd);
m_tipper->SetMaxTipWidth(450);
m_tipper->SetAnimationNumberOfFrames(5);
m_tipper->SetHideDelaySeconds(10);
m_tipper->SetAvoidCoveringItem(true);
m_tipper->AddTool(&m_aListBox, new TipDataPlain(_T("Tip for list
box...."), IDB_INFO) );
m_tipper->AddTool(&m_aListBox, new TipDataPlain(_T("Tip for list
box....")) );
...
}
catch(...)
{
delete m_tipper;
m_tipper = 0;
}
}
}
void ParentWindow::ShowTip()
{
CreateTip();
if (m_tipper)
{
m_tipper->ShowTipForPosition();
}
}
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()
{}
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:
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)
{
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};
COLORREF bkColor = RGB(255, 254, BYTE(255 - t*48.0));
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);
dcMem.SetBkColor(bkColor);
dcMem.SetTextColor(textColor);
dcMem.FillRect(&bitmapRect, &backBrush);
DrawGraphic(dcMem, t, theTip->m_bitmapID);
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:
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;
dcMem.BitBlt(left, kGraphicMargin, bm.bmWidth, bm.bmHeight,
&dcGraphic, 0, 0, SRCCOPY);
dcGraphic.SelectObject(oldBitmap);
}
}
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.
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.