图片浏览器开发日志-14(ComboBox和ListBox toolips实现)

按:本来是想实现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, 祝大家节日快乐!

猜你喜欢

转载自blog.csdn.net/Uman/article/details/106479890