65.9K
CodeProject 正在变化。 阅读更多。
Home

MFC 项目的自定义工具提示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (26投票s)

2007年4月12日

10分钟阅读

viewsIcon

138775

downloadIcon

6055

在工具提示中放置任何您想要的内容

Screenshot - demo.gif

目录

引言

自定义工具提示是一个诱人但危险的概念,如果你做得不对,可能会引来“界面警察”上门。不过,如果你决定完全控制工具提示的内容并且正在使用 MFC,这里有一个方法应该能帮助你入门。

你需要为从 CustomToolTip 派生的类提供两个虚函数来调整和绘制提示,以及一个小的“TipData”类来保存提示所需的数据。将生成的自定义提示器添加到窗口中并不比普通工具提示更麻烦。

源代码中包含了两个示例自定义工具提示(使用 GDI 的 CustomToolTipPlain 和使用 GDI+ 的 CustomToolTipPlus),以帮助你入门。并且每个都有单独的演示项目。

自定义提示的源代码结果相对简单,你可能会觉得这是一个不错的改变。

作为额外奖励,还内置了一个可选的“淡入”动画框架。CustomToolTip 成员允许你设置动画的帧数和持续时间,并且你的虚拟提示绘制函数会收到一个浮点参数,该参数从 0.0(第一帧)到 1.0(最后一帧)变化。CustomToolTipPlain 用它来在每个提示显示的前半秒内改变提示的背景和文本颜色。CustomToolTipPlus 在提示的背景中放置了一个可变渐变填充,并且渐变会通过提示左侧图像中选定的颜色显示出来。

我尝试详细地写出来,所以提前为有时说明显而易见的事情道歉。

概述

这是一个仅包含重要部分的类图,可以让你了解整体情况

Screenshot - classes.gif

你从 CustomToolTip 派生你的自定义提示类,你从 TipData 派生一个提示数据持有者(因为只有你知道你的提示需要什么数据),并且你在你的窗口中放置三个函数和一个数据成员,这些窗口需要提示来显示它们。

给定你的“WindowThatWantsTips”,你可以通过以下方式为其提供自定义工具提示:

  • 决定提示将显示什么自定义数据——也许只是一个 CString,也许更复杂——并从 TipData 派生你的提示数据持有者 TipDataYours,并包含适当的数据成员
  • CustomToolTip 派生你自己的 CustomToolTipYours:只有两个非平凡函数,SetTipWindowSize() 确定当前提示的提示窗口大小,以及 CreateTipImage(),你在此处卷起袖子并放入实际的自定义绘图代码
  • 将你的自定义工具提示安装到你的 WindowThatWantsTips 中,这涉及添加几行代码来创建提示器,一些 AddTool() 调用来为特定控件或矩形设置提示,以及在 PreTranslateMessage() 中添加几行代码以在适当时候显示提示。

当然,你可以随意命名你的派生类。例如,在 GDI 演示中,它们被称为 CustomToolTipPlain 和 TipDataPlain。(好吧,这很明显,但我至少提前道歉了。)

CustomToolTip 基类处理所有工具提示窗口行为,包括显示、隐藏、定位和调整提示窗口大小。它还维护作为提示触发器的 CWnd 或矩形列表,以及提示内容的 TipData 指针列表。

在绘制自定义提示时,你可以使用完全任意的数据,而不仅仅是 CString。这就是设计中 TipDataYours 的目的,一个你可以填写你想要的确切数据成员的类。但是,CustomToolTip 只跟踪指向基类 TipData 的指针列表,以允许你拥有多个自定义工具提示,每个工具提示都有其自己的派生版本的 TipData。因此,当需要进行大小调整和绘图时,你需要将泛型 TipData* 转换为它们期望的确切数据类型。这发生在你的 SetWindowSize()CreateTipImage() 实现中——例如,在 GDI 演示中,你会发现

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

排除了这个小麻烦之后,你可能会很高兴知道你可以在同一个应用程序中派生任意数量的不同自定义工具提示,并且它们可以共享一个公共的 TipData 持有者或使用不同的持有者。你还可以使用两个或更多自定义工具提示来显示窗口的提示。

提示的派生数据持有者中的实际数据(例如上面图表中的 TipDataYours 中的 m_otherData)可以由自定义工具提示拥有,也可以是指向生命周期更长的某个对象的指针。例如,如果 TipDataYours 拥有 m_otherData,则在 TipDataYours 的析构函数中放入 "delete m_otherData;"。如果你不希望 m_otherData 被删除,那么就不要删除它,它将在你的工具提示消失后继续存在。

试试看!

CustomToolTipDemo 展示了一个相对朴素简单的基于 GDI 的自定义工具提示器,名为 CustomToolTipPlain。CustomToolTipPlusDemo 展示了一个名为 CustomToolTipPlus 的提示器,它使用 GDI+ 进行绘图,并且该提示器的样式稍微花哨一些。如果你看过之后喜欢它的潜力,你可能希望在自己的项目中尝试其中一个,看看这个概念是否适合你:有关如何将它们添加到项目中的指南可以在 CustomToolTipPlain.cppCustomToolTipPlus.cpp 的底部找到。我能保证的是,它对我在高度自定义的对话框中提供提示有效。如果这个提示器对你有效,那么你就可以投入时间编写自己的版本了。

以下是将 CustomToolTipPlain 添加到项目中某个窗口的说明副本,供你预览。'ParentWindow' 是你的项目中将获得自定义提示的类。要添加的源文件包含在上述两个下载中。

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();
        }
    }

编写自定义工具提示

为了快速开始,复制一个提供的示例(使用 GDI 绘图的 CustomToolTipPlain 或使用 GDI+ 的 CustomToolTipPlus)。重命名 .cpp 和 .h 文件,将类名替换为你自己的新名称,然后就可以开始了。

你的第一项工作无疑将是进行一些规格和设计,在此期间,你将列出你的提示所需的绘图数据项。如果它只是一个数据包装器,则无需过于详细:例如,这是与 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;
    };

提供的两个示例都假设你将提示绘制到一个离屏缓冲区图像中。你可以在示例中找到执行此操作的“样板”代码,一旦设置完成,绘制到离屏缓冲区与直接绘制到屏幕没有区别。

如果由于某种原因你无法缓冲你的工具提示内容,那么你需要将你的绘图代码放在虚函数 DrawTip() 中,而不是 CreateTipImage(),并将 CreateTipImage() 留作一个空函数。缓冲通常更好,因为它避免了闪烁。从现在开始,我将假设你将缓冲你的绘图。

在你将提示调整到合适大小之前,你无法绘制它,而通常在你完成绘制内容所需的所有步骤(减去实际绘制)之前,你无法将其调整到合适大小。因此,绘图分两步进行:SetTipWindowSize()CreateTipImage()

void CustomToolTipPlain::SetTipWindowSize(size_t tipIndex)

SetTipWindowSize() 的唯一目的是设置成员 m_tipImageHeight 和 m_tipImageWidth。这些成员代表你的提示窗口内容的完整宽度和高度,也将对应于你的离屏缓冲区图像的大小。例如,普通演示版本的 SetTipWindowSize() 的核心是

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;
    ...

(然后紧接着几行代码用于添加可选位图的大小。)

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

这里有两个目标:调整你的离屏位图(演示中的 CBitmap* m_tipImage)的大小,并将你的提示内容绘制到离屏位图中。离屏位图管理可以按原样复制到你的版本中。从那里开始,绘制提示与任何窗口的绘制相同——例如,普通演示调用 DrawText(),这次没有 DT_CALCRECT 选项,来显示提示的文本。

我敢打赌你喜欢代码,所以这是 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;
        }
    }

这都是相当标准的东西。为了避免神秘,这是上面提到的 DrawGraphic() 函数

/*! 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 公共成员

创建、添加和删除提示,调整大小,动画。如果这些还不足以满足你的控制需求——嗯,我希望 CustomToolTip 中的源代码足够简单,你可以直接修改它,使其成为你自己的版本。

CustomToolTip(HWND parentHwnd, bool animateDrawing = false)

parentHwnd 是需要显示自定义工具提示的窗口。

如果你打算在显示工具提示时提供一些动画,请将 animateDrawing 设置为 true。你可以在演示项目中找到示例(例如 CustomToolTipPlain::CreateTipImage())。

virtual ~CustomToolTip()

基类 dtor 不做任何事情,但你的派生类可能需要进行一些清理工作。例如,~CustomToolTipPlain 删除用于显示提示内容的图像。

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

AddToolDelTool 的两种变体允许你为任何类型的 CWnd 添加提示(在这种情况下,使用 GetWindowRect() 给出的窗口屏幕 CRect),或者为 CRect 添加提示(在这种情况下,矩形应该在需要提示的父窗口的本地坐标中)。请注意,AddTool 首先调用 DelTool,所以 AddTool 也可以用于更新提示。

tipData 参数应指向你用于提示的特定类型的 TipData,例如普通演示中的 TipDataPlain。之后,数据将由你的派生 CreateTipImage() 用于执行实际的提示绘制。

void SetMaxTipWidth(int maxWidth)

maxWidth 是提示的最大宽度(以像素为单位,默认为 300)。但是,由于所有绘图都在你的控制之下,如果你愿意,可以对此进行不同的解释。

void ShowTipForPosition()

在需要提示的父窗口中,响应每次 WM_MOUSEMOVE 调用。

void HideTip()

通过缩小并移动到 x 0,y 10 来“隐藏”提示窗口。这避免了父窗口中的激活闪烁。如果需要,你可以调用它,但你可能会发现默认的隐藏功能已经足够好。

void SetHideDelaySeconds(UINT delayInSeconds)

几秒钟后提示窗口将被隐藏。零表示永不隐藏提示。

void SetAvoidCoveringItem(bool avoidItem)

avoidItem 为 true 表示提示窗口不会覆盖与提示对应的底层 CWnd 或 CRect 的任何部分。为 false 表示提示将出现在光标附近,无论光标在哪里。

void SetAnimationStepDuration(UINT msecs)

每个启动动画步骤的持续时间,以毫秒为单位。默认约为 55 毫秒。

void SetAnimationNumberOfFrames(UINT numFrames)

默认值约为十,结合默认的每帧 55 毫秒,每个提示显示时大约有半秒的动画。

关注点

当工具提示窗口首次出现时,它会立即重新激活调用它的父窗口,并且再也不会被激活。它通过缩小到无形并使用以下方式移动到屏幕的一个角落来“隐藏”

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

这也许是一种略显不寻常的避免激活闪烁的方式,但效果很好。这也是提示基类相对简单的主要原因。

如果您想在“真实”应用程序中看到这些自定义提示的 GDI+ 版本,欢迎尝试 TenClock,这是一个免费的小应用程序,除了其他功能外,它还能在相当漂亮的 3D 时钟表面上以彩色楔形显示 Outlook 日程。安装后,右键单击时钟并选择 Configure... 查看带有自定义提示的对话框。需要 Outlook 和 2000/XP(尚未在 Vista 下测试——如果您尝试过,请告诉我是否遇到任何问题)。顺便说一句,如果您查看 TenClock 的“关于”框,您可能会在那里看到您的名字!

如果您还在犹豫是否要升级您的提示,请允许我预测 2007 年将是“视觉盛宴之年”:事实上,我敢打赌您会看到一个双倍“Beryled”的 Vista——哎,抱歉。

如果您使用了它——留个小评论好吗?这会让我在输入了这么多字之后感到非常高兴。

© . All rights reserved.