背景:我用一个CListBox派生类实现宿主(owner-draw)列表框,这个列表框的项目宽度超过了列表框本身的宽度,因此当鼠标指针指向大宽度的列表框项时,我想显示一个类似Toolbar的提示窗口,在窗口中显示完整的列表框项目文本。
起初我想使用CToolTipCtrl::AddTool的第三个参数lpRectTool来实现这个功能,但没有成功。后来,我采用了自立更生的解决方案,创建了一个可重用窗口类:
CPopupText 类定义和实现
// PupText.h
//
#pragma once
// Get NONCLIENTMETRICS info: ctor calls SystemParametersInfo.
//
class CNonClientMetrics : public NONCLIENTMETRICS {
public:
CNonClientMetrics() {
cbSize = sizeof(NONCLIENTMETRICS);
SystemParametersInfo(SPI_GETNONCLIENTMETRICS,0,this,0);
}
};
// Popup text window, like tooltip.
// Can be right or left justified relative to creation point.
//
class CPopupText : public CWnd {
public:
CSize m_szMargins; // extra space around text: change if you like
enum {JUSTIFYLEFT=0, JUSTIFYRIGHT};
CPopupText();
virtual ~CPopupText();
BOOL Create(CPoint pt, CWnd* pParentWnd, UINT nStyle=0, UINT nID=0);
void ShowDelayed(UINT msec);
void Cancel();
protected:
CFont m_font; // font to use (same as tooltips)
UINT m_nStyle; // style (see enum below)
virtual void PostNcDestroy();
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
afx_msg void OnPaint();
afx_msg void OnTimer(UINT nIDEvent);
afx_msg LRESULT OnSetText(WPARAM wp, LPARAM lp);
DECLARE_DYNAMIC(CPopupText);
DECLARE_MESSAGE_MAP();
};
PupText.cpp
// VCKBASE -- September 2000
// Visual C++ 6.0 环境编译, Windows 98 和 NT 环境运行.
//
#include "stdafx.h"
#include "puptext.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
IMPLEMENT_DYNAMIC(CPopupText,CWnd)
BEGIN_MESSAGE_MAP(CPopupText,CWnd)
ON_WM_PAINT()
ON_MESSAGE(WM_SETTEXT, OnSetText)
ON_WM_TIMER()
END_MESSAGE_MAP()
CPopupText::CPopupText()
{
m_szMargins = CSize(4,4);
// create font ?use system tooltip font
CNonClientMetrics ncm;
m_font.CreateFontIndirect(&ncm.lfStatusFont);
}
CPopupText::~CPopupText()
{
}
// Create window. pt is upper-left or upper-right corner depending on
// nStyle.
//
CPopupText::Create(CPoint pt, CWnd* pParentWnd, UINT nStyle, UINT nID)
{
m_nStyle = nStyle;
return CreateEx(0,
NULL,
NULL,
WS_POPUP|WS_VISIBLE,
CRect(pt,CSize(0,0)),
pParentWnd,
nID);
}
// Someone changed the text: resize to fit new text
//
LRESULT CPopupText::OnSetText(WPARAM wp, LPARAM lp)
{
CClientDC dc = this;
CFont* pOldFont = dc.SelectObject(&m_font);
CRect rc;
GetWindowRect(&rc);
int x = (m_nStyle & JUSTIFYRIGHT) ? rc.right : rc.left;
int y = rc.top;
dc.DrawText(CString((LPCTSTR)lp), &rc, DT_CALCRECT);
rc.InflateRect(m_szMargins);
if (m_nStyle & JUSTIFYRIGHT)
x -= rc.Width();
SetWindowPos(NULL,x,y,rc.Width(),rc.Height(), SWP_NOZORDER|SWP_NOACTIVATE);
return Default();
}
// Paint the text. Use system colors
//
void CPopupText::OnPaint()
{
CPaintDC dc(this);
CRect rc;
GetClientRect(&rc);
CString s;
GetWindowText(s);
CBrush b(GetSysColor(COLOR_INFOBK)); // use tooltip bg color
dc.FillRect(&rc, &b);
// draw text
dc.SetBkMode(TRANSPARENT);
CFont* pOldFont = dc.SelectObject(&m_font);
dc.SetTextColor(GetSysColor(COLOR_INFOTEXT)); // tooltip text color
dc.DrawText(s, &rc, DT_SINGLELINE|DT_CENTER|DT_VCENTER);
dc.SelectObject(pOldFont);
}
// Register class if needed
//
BOOL CPopupText::PreCreateWindow(CREATESTRUCT& cs)
{
static CString sClassName;
if (sClassName.IsEmpty())
sClassName = AfxRegisterWndClass(0);
cs.lpszClass = sClassName;
cs.style = WS_POPUP|WS_BORDER;
cs.dwExStyle |= WS_EX_TOOLWINDOW;
return CWnd::PreCreateWindow(cs);
}
// CPopupText is intended to be used on the stack,
// not heap, so don't auto-delete.
//
void CPopupText::PostNcDestroy()
{
// don't delete this
}
// Show window with delay. No delay means show now.
//
void CPopupText::ShowDelayed(UINT msec)
{
if (msec==0)
// no delay: show it now
OnTimer(1);
else
// delay: set time
SetTimer(1, msec, NULL);
}
// Cancel text梜ill timer and hide window
//
void CPopupText::Cancel()
{
KillTimer(1);
ShowWindow(SW_HIDE);
}
// Timer popped: display myself and kill timer
//
void CPopupText::OnTimer(UINT nIDEvent)
{
ShowWindow(SW_SHOWNA);
Invalidate();
UpdateWindow();
KillTimer(1);
} CPopupText的基类是CWnd。用这个派生类不仅可以在列表框中实现类似Toolbar的提示窗口,还可以在其它的通用控件(如组合框、列表视图等)中实现类似Toolbar的提示窗口。
CPopupText实现了一个类似Toolbar的弹出式窗口-浅黄色的背景,黑色的文本。
CPopupText的使用方法是:实例化对象并创建窗口。
--------------------next---------------------
CPopupText类会根据提示文本的长度自己决定弹出窗口的大小。它的字体默认值与状态行显示字体相同(由SystemParametersInfo(SPI_GETNONCLIENTMETRICS)返回的 NONCLIENTMETRICS 结构成员 lfStatusFont 定义),设置 SW_SHOWNA是很重要的,因为你不想使提示窗口为活动窗口,而只是显示它。另外,CPopupText还提供一个专门的函数CPopupText::ShowDelayed,它的功能是在显示提示窗口之前设置一个毫秒级延时,如果延时为零,则立刻显示提示窗口,你可以使用这个特点来替代对ShowWindow的调用。如果要隐藏提示窗口或取消ShowDelayed,调用CPopupText::Cancel。
OK,前面讲了关于CPopupText类的创建以及使用方法。下面要讲一下对列表框所要做的事情。它是用CListBoxTipHandler类来实现的。这个类的功能是当列表框项目文本宽度超过列表框本身的宽度时,在一个提示窗口中显示鼠标指针所指的列表框项目的完整文本内容。图一是本文的例子程序,LCTest。它是一个对话框的例子。
--------------------next---------------------
CListBoxTipHandler类的设计原则是使它尽量易于使用,它的工作原理又是怎样的呢?如果你仔细研究一下它的代码就会明白。CListBoxTipHandler类的基类是CSubclassWnd,这个类在以前的VCKBASE文章中出现过很多次,CSubclassWnd类的作用是不用派生新类便能在MFC中子类化窗口。这一点很重要,如果你从CListBox派生一个新类,假如说是:CListBoxWithTips,那你就不能在自己已经派生的列表框类(如:CMyListBox)中直接使用它。而要作很多修改。这是不可取的。CSubclassWnd完全可以让CListBoxTipHandler通过实例化来子类你的列表框,而不是通过派生。
当你调用Init函数时,CListBoxTipHandler子类化列表框,然后,截获发送到列表框的所有消息。而只有一个消息是我们感兴趣的,那就是 WM_MOUSEMOVE。
当用户移动鼠标到列表框时,CListBoxTipHandler实例(提示处理器)便检查鼠标所指的列表框项目的文字是否比列表框本身的宽度要宽,从而决定是否启动CPopupText显示文本提示窗口。其中的处理有两个技巧:
CListBoxTipHandler
// ListBoxTip.h
//
#pragma once
#include "subclass.h"
#include "puptext.h"
// Generic tip-handler to display tip for wide text in a listbox.
// To use:
// - instantiate one of these for each listbox
// - call Init
//
class CListBoxTipHandler : public CSubclassWnd {
protected:
UINT m_idMyControl; // id of listbox control
UINT m_nCurItem; // index of current item
BOOL m_bCapture; // whether mouse is captured
static CPopupText g_wndTip; // THE tip window
// subclass window proc
virtual LRESULT WindowProc(UINT msg, WPARAM wp, LPARAM lp);
// virtual fns you can override
virtual void OnMouseMove(CPoint p);
virtual BOOL IsRectCompletelyVisible(const CRect& rc);
virtual UINT OnGetItemInfo(CPoint p, CRect& rc, CString& s);
public:
CListBoxTipHandler();
~CListBoxTipHandler();
static UINT g_nTipTimeMsec; // global: msec wait before showing
// tip
void Init(CWnd* pListBox); // initialize
};
// ListBoxTip.cpp
//
#include "stdafx.h"
#include "PupText.h"
#include "ListBoxTip.h"
// THE popup tip window
CPopupText CListBoxTipHandler::g_wndTip;
// Timeout before showing long listbox tip ?you can change
UINT CListBoxTipHandler::g_nTipTimeMsec = 100; // .1 sec
CListBoxTipHandler::CListBoxTipHandler()
{
m_nCurItem=-1;
m_bCapture = FALSE;
}
CListBoxTipHandler::~CListBoxTipHandler()
{
}
// Install hook. Initialize control ID from listbox and create
// (invisible) tip window.
//
void CListBoxTipHandler::Init(CWnd* pListBox)
{
CSubclassWnd::HookWindow(pListBox);
m_idMyControl = pListBox->GetDlgCtrlID();
if (!g_wndTip) {
// create scroll tip window
g_wndTip.Create(CPoint(0,0), NULL, 0);
}
}
// "Hook" function traps messages sent to listbox/control.
//
LRESULT CListBoxTipHandler::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) {
case WM_MOUSEMOVE:
OnMouseMove(CPoint(GET_X_LPARAM(lp),GET_Y_LPARAM(lp)));
break;
}
return CSubclassWnd::WindowProc(msg, wp, lp);
}
// User moved the mouse.
// Get info for listbox item under mouse:
// If text is too wide to fit in listbox, prepare tip window and start
// timer to display it.
//
void CListBoxTipHandler::OnMouseMove(CPoint pt)
{
CListBox* pListBox = (CListBox*)CWnd::FromHandle(m_hWnd);
if (!m_bCapture) {
::SetCapture(m_hWnd);
m_bCapture = TRUE;
}
// Get text and text rectangle for item under mouse
CString sText; // item text
CRect rcText; // item text rect
UINT nItem = OnGetItemInfo(pt, rcText, sText);
if (nItem==-1 || nItem!=m_nCurItem) {
g_wndTip.Cancel(); // new item, or no item: cancel popup text
if (nItem>=0 && !IsRectCompletelyVisible(rcText)) {
// new item, and not wholly visible: prepare popup tip
CRect rc = rcText;
pListBox->ClientToScreen(&rc); // text rect in screen coords
g_wndTip.SetWindowText(sText); // set tip text to that of item
// move tip window over list text
g_wndTip.SetWindowPos(NULL, rc.left-3, rc.top, rc.Width()+6,
rc.Height(), SWP_NOZORDER|SWP_NOACTIVATE);
g_wndTip.ShowDelayed(g_nTipTimeMsec); // show popup text delayed
}
}
m_nCurItem = nItem;
if (nItem==-1) {
::ReleaseCapture();
m_bCapture=FALSE;
}
}
// Determine if given rectangle is completely visible within listbox
//
BOOL CListBoxTipHandler::IsRectCompletelyVisible(const CRect& rc)
{
CListBox* pListBox = (CListBox*)CWnd::FromHandle(m_hWnd);
CRect rcClient;
pListBox->GetClientRect(&rcClient);
return rcClient.Width() > rc.Width();
}
// Get info (rectangle and text) for item under point
//
UINT CListBoxTipHandler::OnGetItemInfo(CPoint p, CRect& rc, CString& s)
{
CListBox* pListBox = (CListBox*)CWnd::FromHandle(m_hWnd);
ASSERT_VALID(pListBox);
BOOL bOutside;
UINT nItem = pListBox->ItemFromPoint(p,bOutside);
s.Empty();
if (!bOutside) {
pListBox->GetText(nItem, s);
pListBox->GetItemRect(nItem, &rc);
CFont *pFont = pListBox->GetFont();
CClientDC dc(pListBox);
CFont* pOldFont = dc.SelectObject(pFont);
dc.DrawText(s,&rc,DT_CALCRECT);
dc.SelectObject(pOldFont);
return nItem;
}
return -1;
} 第一个技巧:如果用户把鼠标从文本提示窗口上移走,CListBoxTipHandler会调用CPopupText::Cancel 隐藏提示窗。当用户的鼠标在列表框项目间移动时不会有什么问题,但如果将鼠标完全移到列表框之外会发生什么呢?显然,你无法知道哪一个是最后一个WM_MOUSEMOVE消息。为了避免这种情况,CListBoxTipHandler代表列表框来捕获鼠标,所以全部的鼠标消息都到了CListBoxTipHandler,当鼠标移到列表框之外的情况发生时,CListBoxTipHandler释放鼠标。
第二个技巧:关于提示窗的激活与隐藏。为了正确定位提示窗口,CListBoxTipHandler要计算窗口矩形的大小,并调用SetWindowPos函数。这里使用 SWP_NOACTIVATE 很关键,否则提示窗口将是活动的,而且对话框不活动-对话框标题条变灰。在调用 CPopupText::ShowWindow 时之所以必须用SW_SHOWNA 也是一样的道理。
例子程序使用了一个最普通的列表框,但 CListBoxTipHandler 应该处理宿主(owner-draw)列表框,另外还要让 CListBoxTipHandler 获得列表框项目文本并决定文本宽度。CListBoxTipHandler::OnGetItemInfo 和 CListBoxTipHandler::IsRectCompletelyVisible 是两个重载的虚函数,如果你想让 CListBoxTipHandler 类也适用于其它类似列表框一样的控件(如组合框、列表视图等),这两个函数是你必须修改的。不要将CListBoxTipHandler 用于树型控件,因为树型控件本身就内建有toolbar功能。