按:本来是想实现ComboBox的自绘,从而实现界面按钮的统一,网上ComboBox自绘的代码很多,因此很快达到了目的,后来发现有的字段内容超长,就想实现ComboBox 的tooltips功能。上网搜索了好长时间,因为外网上网实在是困难,弄得心力憔悴,不过,经过近10余天的探索,终于于今日搞定一个比较完美的版本。代码主要参考
http://blog.sina.com.cn/s/blog_6430edb10100yubv.html 上的代码,其代码的出处应该是codeproject 上一个老外n久以前的代码,因为那个代码我不满足下载条件,因此只好使用这个没有h文件的,也几乎不能运行的代码。但是非常感谢这段代码,以及原著。
原著地址:
https://www.codeproject.com/Articles/4438/XTipComboBox-Display-tooltips-for-combobox
ComboBox tooltips实现思路
说明:以下核心内容来自上面做题原著。
ComboBox 的中文含义即 box的的一个Combo(组合餐…),而实际上ComboBox 是由一个edit control 和 ListBox 组合而成,因此要想获得每个Item的内容,必须截获Edit和Listbox的消息。Edit的消息非常容易截获, 而listbox就非常难以截获。原文中,例如两次subclass技术,将listbox的消息,截获,还是非常巧妙的。
原文核心内容拷贝如下,本人就不在啰嗦了。
The class implements one virtual function and four message handlers:
PreSubclassWindow() - this virtual function allows us to create tooltip window, add combobox as its tool, and perform other initialization.
OnCtlColor() - This is not what you think. According to MSDN article HOWTO: Subclass CListBox and CEdit Inside of CComboBox (Q174667), this is actually recommended way of subclassing listbox of a combobox. We use this to subclass listbox only - for edit box, it is simpler to handle inside CXTipComboBox.
OnMouseMove() - This message handler catches mouse moves, and when mouse is inside combo client rect, tooltip will be activated.
OnTimer() - A timer is used only when a tooltip is being displayed. When code in OnTimer() detects that mouse is no longer inside client rect, tooltip is removed.
OnDestroy() - Unsubclasses the listbox.
文中黑体部分是获得subclass listBox 的核心部分,有兴趣的可以仔细看看。
文中提到的MSDN方法链接已经不复存在了,笔者找到了网友转发的内容如下
HOWTO: Subclass CListBox and CEdit Inside of CComboBox (Q174667),感谢这位网友、
效果显示
实现代码及注意事项
原文中的生成tooltips 的窗口在presubclass 函数中,笔者发现不能正常运行,将代码改在了PreTranslateMessage中,并对消隐逻辑进行了重构,原代码消隐逻辑混乱,生成的tips摇曳不定,体验很差。使用时,需要更改默认属性。
CListBoxTips.h 文件
#pragma once
#include <afxwin.h>
// SuperComboBox.h : header file
/*typedef struct tagTOOLINFO {
UINT cbSize;
UINT uFlags;
HWND hwnd;
UINT_PTR uId;
RECT rect;
HINSTANCE hinst;
LPTSTR lpszText;
#if (_WIN32_IE >= 0x0300)
LPARAM lParam;
#endif
#if (_WIN32_WINNT >= 0x0501)
void *lpReserved;
#endif
} ;*/
//————————————————
//版权声明:本文为CSDN博主「文大侠」的原创文章,遵循CC 4.0 BY - SA版权协议,转载请附上原文出处链接及本声明。
//原文链接:https ://blog.csdn.net/wenzhou1219/article/details/27697927
class CListBoxTips : public CListBox
{
DECLARE_DYNAMIC(CListBoxTips)
public:
CEdit m_edit;
CListBox m_listbox;
// 获取子控件CEdit
//CEdit* FindChildEdit();
void CreateToolTipForRect(HWND hwndParent);
private:
CToolTipCtrl m_ToolTip;
int m_tipsID;
HWND m_hWndToolTip;
TOOLINFO m_ToolInfo;
CString m_lpszText;
int m_lastItem = -1;//record the last item index
bool m_bCreatedThisTips = false;
bool m_bCreateNewTips=true;
protected:
//afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
virtual void PreSubclassWindow();
virtual BOOL CListBoxTips::PreTranslateMessage(MSG * pMsg);
virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS);
afx_msg void OnDestroy();
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnTimer(UINT nIDEvent);
DECLARE_MESSAGE_MAP();
};
#pragma once
CListBoxTips.cpp 文件
#include "stdafx.h"
#include "CListBoxTips.h"
// SuperComboBox.cpp : implementation file
BEGIN_MESSAGE_MAP(CListBoxTips, CListBox)
ON_WM_MOUSEMOVE()
ON_WM_TIMER()
END_MESSAGE_MAP()
IMPLEMENT_DYNAMIC(CListBoxTips, CListBox)
BOOL CListBoxTips::PreTranslateMessage(MSG * pMsg)
{
if (m_hWndToolTip == NULL && m_bCreateNewTips ) {
//m_bCreatedThisTips = false;
m_hWndToolTip = ::CreateWindowEx(WS_EX_TOPMOST,
TOOLTIPS_CLASS,
NULL,
TTS_NOPREFIX | TTS_ALWAYSTIP,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
m_hWnd,
NULL,
NULL,
NULL);
ASSERT(m_hWndToolTip);
// initialize toolinfo struct
memset(&m_ToolInfo, 0, sizeof(m_ToolInfo));
m_ToolInfo.cbSize = sizeof(m_ToolInfo);
m_ToolInfo.uFlags = TTF_TRACK | TTF_TRANSPARENT;
m_ToolInfo.hwnd = m_hWnd;
// add list box
::SendMessage(m_hWndToolTip, TTM_SETMAXTIPWIDTH, 0, SHRT_MAX);
::SendMessage(m_hWndToolTip, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
::SendMessage(m_hWndToolTip, TTM_SETTIPBKCOLOR, ::GetSysColor(COLOR_HIGHLIGHT), 0);
::SendMessage(m_hWndToolTip, TTM_SETTIPTEXTCOLOR, ::GetSysColor(COLOR_HIGHLIGHTTEXT), 0);
// reduce top & bottom margins
CRect rectMargins(0, -1, 0, -1);
::SendMessage(m_hWndToolTip, TTM_SETMARGIN, 0, (LPARAM)&rectMargins);
// set font
CFont *pFont = GetFont();
::SendMessage(m_hWndToolTip, WM_SETFONT, (WPARAM)(HFONT)*pFont, FALSE);
// remove border (listbox items only)
LONG lStyle = ::GetWindowLong(m_hWndToolTip, GWL_STYLE);
lStyle &= ~WS_BORDER;
::SetWindowLong(m_hWndToolTip, GWL_STYLE, lStyle);
}
return CListBox::PreTranslateMessage(pMsg);
}
void CListBoxTips::PreSubclassWindow()
{
// TODO: 在此添加专用代码和/或调用基类
//SetWindowLong(m_hWnd, GWL_EXSTYLE, GetWindowLong(m_hWnd, GWL_STYLE) | BS_OWNERDRAW);
CListBox::PreSubclassWindow();
}
void CListBoxTips::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
CDC dc;
dc.Attach(lpDIS->hDC);
CRect rcItem = lpDIS->rcItem;
int nItem = lpDIS->itemID;
if (nItem == -1)
return;
dc.SetBkMode(TRANSPARENT);
if (lpDIS->itemState & ODS_SELECTED)
{
dc.SetTextColor(GetSysColor(COLOR_HIGHLIGHTTEXT));
dc.FillSolidRect(&rcItem, GetSysColor(COLOR_HIGHLIGHT));
if (lpDIS->itemAction & ODA_FOCUS)
dc.DrawFocusRect(&rcItem);
}
else
{
dc.SetTextColor(GetSysColor(COLOR_WINDOWTEXT));
dc.FillSolidRect(&rcItem, GetSysColor(COLOR_WINDOW));
}
CRect rcText = rcItem;
CString strText;
GetText(nItem, strText);
dc.DrawText(strText, &rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
dc.Detach();
}
//CListBox does not invoke this function
void CListBoxTips::OnDestroy()
{
if (m_edit.GetSafeHwnd() != NULL)
m_edit.UnsubclassWindow();
if (m_listbox.GetSafeHwnd() != NULL)
m_listbox.UnsubclassWindow();
CListBox::OnDestroy();
}
void CListBoxTips::OnMouseMove(UINT nFlags, CPoint point)
{
CRect rectClient;
GetClientRect(&rectClient);
if (rectClient.PtInRect(point))
{
CPoint pointScreen;
::GetCursorPos(&pointScreen);
BOOL bOutside = FALSE;
int nItem = ItemFromPoint(point, bOutside); // calculate listbox item number (if any)
if (bOutside ) {
m_lastItem = -1;
if (m_hWndToolTip != NULL) {
::PostMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
}
m_hWndToolTip = NULL;
m_bCreatedThisTips = false;
TRACE("Out side the item ....\n");
}else if (nItem != m_lastItem) {
// new item , close the old item first ...
if (m_hWndToolTip != NULL) {
::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
}
m_hWndToolTip = NULL;
m_bCreatedThisTips = false;
TRACE("A new item ....\n");
m_lastItem = nItem;// should creat a new tooltip for the new item
m_bCreateNewTips = true;
}else if (!bOutside && (nItem >= 0) && (nItem==m_lastItem) && !m_bCreatedThisTips)// the same item
{
m_bCreateNewTips = false;
CString strText = _T("");
GetText(nItem, strText);
m_ToolInfo.lpszText = (LPTSTR)(LPCTSTR)strText;
CRect rect;
GetItemRect(nItem, &rect);
ClientToScreen(&rect);
HDC hDC = ::GetDC(m_hWnd);
ASSERT(hDC);
CFont *pFont = GetFont();
HFONT hOldFont = (HFONT) ::SelectObject(hDC, (HFONT)*pFont);
SIZE size;
::GetTextExtentPoint32(hDC, strText, strText.GetLength(), &size);
::SelectObject(hDC, hOldFont);
::ReleaseDC(m_hWnd, hDC);
// show tool tips if the met the following ...
if (size.cx > (rect.Width() - 3) )
{
m_bCreatedThisTips = true;
::SendMessage(m_hWndToolTip, TTM_UPDATETIPTEXT, 0, (LPARAM)&m_ToolInfo);
::SendMessage(m_hWndToolTip, TTM_TRACKPOSITION, 0,(LPARAM)MAKELONG(rect.right, rect.bottom));
//m_ToolInfo.rect = rect;
::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, TRUE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
SetTimer(1, 80, NULL); // set timer for out-of-rect detection
TRACE("Set timer in listBox....\n");
}
//else
//{
// //::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
// ::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
// m_hWndToolTip = NULL;
// TRACE("Not the same item.....\n");
//}
}
}
else
{
/*if (m_hWndToolTip != NULL) {
::PostMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
}*/
//m_hWndToolTip = NULL;
//m_bCreatedThisTips = false;
TRACE("Not in the rect.....\n");
SetTimer(1, 80, NULL); //
}
CListBox::OnMouseMove(nFlags, point);
}
void CListBoxTips::OnTimer(UINT nIDEvent)
{
CPoint point;
::GetCursorPos(&point);
ScreenToClient(&point);
CRect rectClient;
GetClientRect(&rectClient);
DWORD dwStyle = GetStyle();
//TRACE(_T("In ListBox timer ....... =====\n"));
if ((!rectClient.PtInRect(point)) || ((dwStyle & WS_VISIBLE) == 0))
{
TRACE(_T("In ListBox timer Killer timer ....... =====\n"));
KillTimer(nIDEvent);
//::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, NULL);
::PostMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
m_hWndToolTip = NULL;
m_bCreatedThisTips = false;
}
}
CComboboxTips.h 文件
#pragma once
#include <afxwin.h>
#include "CListBoxTips.h"
// SuperComboBox.h : header file
class CCComboBoxTips : public CComboBox
{
DECLARE_DYNAMIC(CCComboBoxTips)
public:
CListBoxTips m_listboxTips;
virtual BOOL PreTranslateMessage(MSG* pMsg);
virtual void PreSubclassWindow();
private:
HWND m_hWndToolTip;
TOOLINFO m_ToolInfo;
bool m_bCreateNewTips = true;
bool m_bCreatedThisTips = false;
protected:
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
afx_msg void OnDestroy();
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnTimer(UINT nIDEvent);
DECLARE_MESSAGE_MAP();
};
CComboboxTips.cpp 文件
#include "stdafx.h"
#include "CComboboxTips.h"
// SuperComboBox.cpp : implementation file
BEGIN_MESSAGE_MAP(CCComboBoxTips, CComboBox)
ON_WM_MOUSEMOVE()
ON_WM_CTLCOLOR()
ON_WM_TIMER()
END_MESSAGE_MAP()
IMPLEMENT_DYNAMIC(CCComboBoxTips, CComboBox)
BOOL CCComboBoxTips::PreTranslateMessage(MSG * pMsg)
{
if (m_hWndToolTip == NULL && m_bCreateNewTips && !m_bCreatedThisTips)
{
m_hWndToolTip = ::CreateWindowEx(WS_EX_TOPMOST,
TOOLTIPS_CLASS,
NULL,
TTS_NOPREFIX | TTS_ALWAYSTIP,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
m_hWnd,
NULL,
NULL,
NULL);
ASSERT(m_hWndToolTip);
// initialize toolinfo struct
memset(&m_ToolInfo, 0, sizeof(m_ToolInfo));
m_ToolInfo.cbSize = sizeof(m_ToolInfo);
m_ToolInfo.uFlags = TTF_TRACK | TTF_TRANSPARENT;
m_ToolInfo.hwnd = m_hWnd;
// add list box
::SendMessage(m_hWndToolTip, TTM_SETMAXTIPWIDTH, 0, SHRT_MAX);
::SendMessage(m_hWndToolTip, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
::SendMessage(m_hWndToolTip, TTM_SETTIPBKCOLOR, ::GetSysColor(COLOR_HIGHLIGHT), 0);
::SendMessage(m_hWndToolTip, TTM_SETTIPTEXTCOLOR, ::GetSysColor(COLOR_HIGHLIGHTTEXT), 0);
// reduce top & bottom margins
CRect rectMargins(0, -1, 0, -1);
::SendMessage(m_hWndToolTip, TTM_SETMARGIN, 0, (LPARAM)&rectMargins);
// set font
CFont *pFont = GetFont();
::SendMessage(m_hWndToolTip, WM_SETFONT, (WPARAM)(HFONT)*pFont, FALSE);
// remove border (listbox items only)
LONG lStyle = ::GetWindowLong(m_hWndToolTip, GWL_STYLE);
lStyle &= ~WS_BORDER;
::SetWindowLong(m_hWndToolTip, GWL_STYLE, lStyle);
}
return CComboBox::PreTranslateMessage(pMsg);
}
void CCComboBoxTips::PreSubclassWindow()
{
// TODO: 在此添加专用代码和/或调用基类
//SetWindowLong(m_hWnd, GWL_EXSTYLE, GetWindowLong(m_hWnd, GWL_STYLE) | BS_OWNERDRAW);
CComboBox::PreSubclassWindow();
}
HBRUSH CCComboBoxTips::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
if (nCtlColor == CTLCOLOR_EDIT)
{
}
else if (nCtlColor == CTLCOLOR_LISTBOX)
{
if (m_listboxTips.GetSafeHwnd() == NULL)
{
TRACE(_T("subclassing listbox\n"));
m_listboxTips.SubclassWindow(pWnd->GetSafeHwnd());
}
}
HBRUSH hbr = CComboBox::OnCtlColor(pDC, pWnd, nCtlColor);
return hbr;
}
void CCComboBoxTips::OnDestroy()
{
if (m_listboxTips.GetSafeHwnd() != NULL)
m_listboxTips.UnsubclassWindow();
CComboBox::OnDestroy();
}
void CCComboBoxTips::OnMouseMove(UINT nFlags, CPoint point)
{
CRect rectClient;
GetClientRect(&rectClient);
int nComboButtonWidth = ::GetSystemMetrics(SM_CXHTHUMB) + 2;
rectClient.right = rectClient.right - nComboButtonWidth;
if (rectClient.PtInRect(point))
{
ClientToScreen(&rectClient);
CString strText = _T("");
GetWindowText(strText);
m_ToolInfo.lpszText = (LPTSTR)(LPCTSTR)strText;
HDC hDC = ::GetDC(m_hWnd);
ASSERT(hDC);
CFont *pFont = GetFont();
HFONT hOldFont = (HFONT) ::SelectObject(hDC, (HFONT)*pFont);
SIZE size;
::GetTextExtentPoint32(hDC, strText, strText.GetLength(), &size);
::SelectObject(hDC, hOldFont);
::ReleaseDC(m_hWnd, hDC);
if (size.cx > (rectClient.Width() - 6) && !m_bCreatedThisTips)
{
m_bCreatedThisTips = true;
rectClient.left += 1;
rectClient.top += 3;
COLORREF rgbText = ::GetSysColor(COLOR_WINDOWTEXT);
COLORREF rgbBackground = ::GetSysColor(COLOR_WINDOW);
CWnd *pWnd = GetFocus();
if (pWnd)
{
if (pWnd->m_hWnd == m_hWnd)
{
rgbText = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
rgbBackground = ::GetSysColor(COLOR_HIGHLIGHT);
}
}
::SendMessage(m_hWndToolTip, TTM_SETTIPBKCOLOR, rgbBackground, 0);
::SendMessage(m_hWndToolTip, TTM_SETTIPTEXTCOLOR, rgbText, 0);
::SendMessage(m_hWndToolTip, TTM_UPDATETIPTEXT, 0, (LPARAM)&m_ToolInfo);
::SendMessage(m_hWndToolTip, TTM_TRACKPOSITION, 0,
(LPARAM)MAKELONG(rectClient.right, rectClient.bottom));
::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, TRUE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
TRACE(_T("setting timer\n"));
SetTimer(1, 80, NULL);
}
else if(size.cx <= (rectClient.Width() - 6))
{
::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
m_hWndToolTip = NULL;
m_bCreatedThisTips = false;
}
}
else
{
::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
m_hWndToolTip = NULL;
m_bCreatedThisTips = false;
}
CComboBox::OnMouseMove(nFlags, point);
}
void CCComboBoxTips::OnTimer(UINT nIDEvent)
{
CPoint point;
::GetCursorPos(&point);
ScreenToClient(&point);
CRect rectClient;
GetClientRect(&rectClient);
DWORD dwStyle = GetStyle();
if ((!rectClient.PtInRect(point)) || ((dwStyle & WS_VISIBLE) == 0))
{
TRACE(_T("not in listbox =====\n"));
KillTimer(nIDEvent);
//::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, NULL);
::SendMessage(m_hWndToolTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)(LPTOOLINFO)&m_ToolInfo);
m_hWndToolTip = NULL;
m_bCreatedThisTips = false;
}
}
使用方法:
加入到工程中,对话框增加ComboBox 或者 listbox 控件,wizard 增加对应变量,
然后改成如下形式
CListBoxTips m_listBox;
CCComboBoxTips m_comboBox;
在对话框的OnInitDialog()函数中增加下列类似代码
m_comboBox.AddString(L"Item 1, this line is a bit longer than the normal one…");
m_comboBox.AddString(L"Item 2, this line is a bit longer than the normal one…");
//m_comboBox.AddString(L"Item 3");
//m_listBox.SubclassDlgItem(IDC_LIST1, this);// the same function as DDX_Control
m_listBox.AddString(L"List Item1…");
m_listBox.AddString(L"List Item2…")
然后就快车使用了,注意如果文字长度没有超界,tips不会显示。
后记
中间妄图使用微软现成的CToolTipCtrl类,但是由于不能找到item对应的窗口句柄,因此颇费了些周折,最后才使用这个方法,没想到竟然成功。这个方法的核心,一个是subclasswidnow 函数的使用,一个是createWindowEx函数的使用,tooltips window以及其中用的数据结构,微软已经定义好了。
2020-06-01 于泛五道口地区
恰逢61, 祝大家节日快乐!