Click here to Skip to main content
15,880,405 members
Articles / Desktop Programming / MFC

Balloon Help as a non-modal replacement for MessageBox()

Rate me:
Please Sign up or sign in to vote.
4.98/5 (62 votes)
7 Aug 2002CPOL13 min read 1M   12.3K   228  
Although sometimes useful, message boxes used to display information are often just annoying. This article describes a non-modal replacement.
//
//			CBalloonHelp implementation (WTL version)
//

// Copyright 2001, Joshua Heyer
// Copyright 2002, Maximilian H�nel (WTL version)
//	You are free to use this code for whatever you want, provided you
// give credit where credit is due.
//	I'm providing this code in the hope that it is useful to someone, as i have
// gotten much use out of other peoples code over the years.
//	If you see value in it, make some improvements, etc., i would appreciate it 
// if you sent me some feedback.

//
// Ported to WTL by Maximilian H�nel (max_haenel@smjh.de) 2002
//
#include "stdafx.h"
#include "BalloonHelp.h"

// allow multimonitor-aware code on Win95 systems
// comment out the first line if you already define it in another file
// comment out both lines if you don't care about Win95
#define COMPILE_MULTIMON_STUBS
#include "multimon.h"

#ifndef BALLOON_HELP_NO_NAMESPACE
namespace WTL
{
#endif
	
#ifndef DFCS_HOT
	#define DFCS_HOT 0x1000
#endif

#ifndef SPI_GETTOOLTIPANIMATION
	#define SPI_GETTOOLTIPANIMATION 0x1016
#endif

#ifndef SPI_GETTOOLTIPFADE
	#define SPI_GETTOOLTIPFADE 0x1018
#endif

#ifndef AW_BLEND
	#define AW_BLEND 0x00080000
#endif

#ifndef AW_HIDE
	#define AW_HIDE 0x00010000
#endif

//
//		class CBalloonAnchor helper class for "anchoring"
//

// ctor
CBalloonAnchor::CBalloonAnchor():
		m_ptAnchor(0,0),
		m_sizeOffset(0,0),
		m_wndFollow(NULL)
{
}

//
//			GetAnchorPoint
//
CPoint CBalloonAnchor::GetAnchorPoint()
{
	if(IsFollowMeMode())
	{
		CRect rcWnd;
		m_wndFollow.GetWindowRect(rcWnd);

		CPoint ptAnchor(rcWnd.left+m_sizeOffset.cx,rcWnd.top+m_sizeOffset.cy);

		m_ptAnchor=ptAnchor;
		
		return m_ptAnchor;
	}
	else
	{
		return m_ptAnchor;
	}
}// GetAnchorPoint

//
//			SetAnchorPoint
//
void CBalloonAnchor::SetAnchorPoint(const CPoint& pt)
{
	m_ptAnchor=pt;
	m_wndFollow=NULL;
	m_sizeOffset.SetSize(0,0);

}// SetAnchorPoint

//
//			SetAnchorPoint
//
void CBalloonAnchor::SetAnchorPoint(HWND hWndCenter)
{
	if(!::IsWindow(hWndCenter))
	{
		ATLASSERT(FALSE);
		return;
	}

	CRect rcAnchor;
	::GetWindowRect(hWndCenter, rcAnchor);

	SetAnchorPoint(rcAnchor.CenterPoint());

}// SetAnchorPoint


//
//			SetFollowMe
//
void CBalloonAnchor::SetFollowMe(HWND hWndFollow, POINT* pptAnchor)
{
	// enable "follow me"
	if(::IsWindow(hWndFollow))
	{
		m_wndFollow=hWndFollow;
		CRect rcWnd;
		m_wndFollow.GetWindowRect(rcWnd);

		// if pptAnchor!=NULL use this point, otherwise use center point of hWndFollow
		if(NULL!=pptAnchor)
			m_ptAnchor=*pptAnchor;
		else
			m_ptAnchor=rcWnd.CenterPoint();
		
		m_sizeOffset.SetSize(m_ptAnchor.x-rcWnd.left, m_ptAnchor.y-rcWnd.top);
	}
	else
	{
		CPoint ptAnchor(m_ptAnchor);

		if(NULL!=pptAnchor)
			ptAnchor=*pptAnchor;

		SetAnchorPoint(ptAnchor);
	}

}// SetFollowMe

//
//
//
BOOL CBalloonAnchor::AnchorPointChanged()
{
	CPoint ptOld(m_ptAnchor);
	CPoint ptNew(GetAnchorPoint());

	return ptOld!=ptNew;
}


//
//				class CMemDC ( a simple memory dc for double buffering)
//
class CMemDC: public CDC
{
public:
	//
	CMemDC(HDC hDC, const RECT* pRect = NULL)
	{
		ATLASSERT(NULL!=hDC);
		m_dc=hDC;

		if(NULL!=pRect)
			m_rcClip=*pRect;
		else
			m_dc.GetClipBox(&m_rcClip);

		CreateCompatibleDC(m_dc);

		m_bmpOffScreen.CreateCompatibleBitmap(m_dc,m_rcClip.Width(),m_rcClip.Height());
		m_bmpOld=SelectBitmap(m_bmpOffScreen);

		SetWindowOrg(m_rcClip.left, m_rcClip.top);
	}
	//
	~CMemDC()
	{
		m_dc.BitBlt(m_rcClip.left, m_rcClip.top, m_rcClip.Width(),m_rcClip.Height(),
				*this,m_rcClip.left,m_rcClip.top,SRCCOPY);

		SelectBitmap(m_bmpOld);
	}
	
protected:

	CBitmap	m_bmpOld;
	CBitmap	m_bmpOffScreen;
	CRect	m_rcClip;
	CDCHandle m_dc;
	
};// class CMemDC


//
//
//			class CBalloonHelp
//
//

//
//			ctor
//
CBalloonHelp::CBalloonHelp():
	m_dwOptions(3),
	m_nMouseMoveTolerance(3),
	m_clrBackground(COLOR_INFOBK|OleSysColorBits),
	m_clrForeground(COLOR_INFOTEXT|OleSysColorBits),
	m_ptMouseOrg(0,0),
	m_uCloseState(0),
	m_uTimeout(0),
	m_hKeybHook(NULL),
	m_hMouseHook(NULL),
	m_hCallWndRetHook(NULL),
	m_rcScreen(0,0,0,0)
{
	KeybHook::InitThunk((TMFP)KeyboardHookProc, this);
	MouseHook::InitThunk((TMFP)MouseHookProc, this);
	CallWndRetHook::InitThunk((TMFP)CallWndRetProc, this);
}

//
//			dtor
//
CBalloonHelp::~CBalloonHelp()
{
	// bug or feature? WTL's CImageList doesn't destroy the imagelist in it's dtor...
	if(!m_ilIcon.IsNull())
		m_ilIcon.Destroy();
}

//
//
//
BOOL CBalloonHelp::Show(HWND hWndOwner,
								 UINT nIdTitle,
								 UINT nIdContent, 
								 HWND hWndAnchor,
								 LPCTSTR pszIcon, /*=IDI_EXCLAMATION */
								 DWORD dwOptions, /*=BODefault		 */
								 LPCTSTR pszURL,  /*=NULL			 */
								 UINT uTimeout	  /*=0				 */
								 )
{
	CString strTitle;
	strTitle.LoadString(nIdTitle);

	CString strContent;
	strContent.LoadString(nIdContent);

	return Show(hWndOwner,strTitle,strContent,hWndAnchor,pszIcon,dwOptions,pszURL,uTimeout);
}


//
//
//
BOOL CBalloonHelp::Show(HWND hWndOwner,
						LPCTSTR pszTitle,
						LPCTSTR pszContent, 
						HWND hWndAnchor,
						LPCTSTR pszIcon, /*=IDI_EXCLAMATION */
						DWORD dwOptions, /*=BODefault		*/
						LPCTSTR pszURL,  /*=NULL			*/
						UINT uTimeout    /*=0				*/
						)
{
	CWindow wndAnchor(hWndAnchor);
	if(!wndAnchor.IsWindow())
	{
		ATLASSERT(FALSE);
		return FALSE;
	}

	CRect rcAnchor;
	wndAnchor.GetWindowRect(rcAnchor);

	return Show(hWndOwner, pszTitle,pszContent,rcAnchor.CenterPoint(),pszIcon,dwOptions,pszURL, uTimeout);
	
}

//
//
//
BOOL CBalloonHelp::Show(HWND hWndOwner, 
						UINT nIdTitle,
						UINT nIdContent,
						const CPoint& ptAnchor,
						LPCTSTR pszIcon, /*=IDI_EXCLAMATION */
						DWORD dwOptions, /*=BODefault		*/
						LPCTSTR pszURL,  /*=NULL			*/
						UINT uTimeout    /*=0				*/
						)
{

	CString strTitle;
	strTitle.LoadString(nIdTitle);

	CString strContent;
	strContent.LoadString(nIdContent);

	return Show(hWndOwner, strTitle, strContent, ptAnchor, pszIcon, dwOptions,pszURL, uTimeout);
}

//
//
//
BOOL CBalloonHelp::Show(HWND hWndOwner,
						LPCTSTR pszTitle, 
						LPCTSTR pszContent, 
						const CPoint& ptAnchor,
						LPCTSTR pszIcon, /*=IDI_EXCLAMATION */
						DWORD dwOptions, /*=BODefault		*/
						LPCTSTR pszURL,  /*=NULL			*/
						UINT uTimeout    /*=0				*/
						)
{
	CBalloonHelp* pBalloon=new CBalloonHelp;

	pBalloon->SetIcon(pszIcon);

	dwOptions|=BODeleteThisOnClose;
	BOOL bRet=pBalloon->Create(hWndOwner,pszTitle, pszContent,&ptAnchor,dwOptions,pszURL,uTimeout);
	if(!bRet)
	{
		delete pBalloon;
		pBalloon=NULL;
	}

	return pBalloon?TRUE:FALSE;
}

//
//
//
BOOL CBalloonHelp::Create(
				HWND hWndOwner,
				LPCTSTR pszTitle,
				LPCTSTR pszContent,
				const POINT* pptAnchor, /*=NULL	optional. If set to NULL, the existing Anchor point is preserved.*/
				DWORD dwOptions,		/*=BODefault */
				LPCTSTR pszURL,			/*=NULL		 */
				UINT uTimeout			/*=0		 */
				)
{
	m_strContent= pszContent?pszContent:_T("");
	m_dwOptions = dwOptions;
	m_strURL	= pszURL?pszURL:_T("");
	m_uTimeout	= uTimeout;

	if(NULL!=pptAnchor)
		m_Anchor.SetAnchorPoint(*pptAnchor);

	if(m_ContentFont.IsNull())
	{
		m_ContentFont=AtlGetDefaultGuiFont();
	}

	if(m_TitleFont.IsNull())
	{
		LOGFONT lf={0};
		m_ContentFont.GetLogFont(&lf);
		lf.lfWeight=FW_BOLD;

		m_TitleFont.CreateFontIndirect(&lf);
	}

	// create window if neccessary
	if(!IsWindow())
	{
		DWORD dwExStyle=WS_EX_TOOLWINDOW;
		if(m_dwOptions&BOShowTopMost)
			dwExStyle |= WS_EX_TOPMOST;

		CRect rcPos(0,0,0,0);
		if(!WindowBase::Create(hWndOwner,rcPos,pszTitle,WS_POPUP,dwExStyle))
			return FALSE;
	}
	else
	{
		SetWindowText(pszTitle?pszTitle:_T(""));
	}
	
	PositionWindow();

	::GetCursorPos(&m_ptMouseOrg);
	
	if(m_dwOptions & (BOCloseOnButtonDown|BOCloseOnButtonUp|BOCloseOnMouseMove))
		SetMouseHook();

	if(m_dwOptions & BOCloseOnKeyDown)
		SetKeyboardHook();
	
	if(m_uTimeout>0)
		SetTimer(IdTimerClose,m_uTimeout);

	if(!(m_dwOptions&BONoShow))
	{
		ShowWindow();
		UpdateWindow();
	}

	return TRUE;
}

//
//	Sets the font used for drawing the balloon title.  
//	Deleted by balloon, do not use hFont after passing to this function.
//
void CBalloonHelp::SetTitleFont(HFONT hFont)
{
	ATLASSERT(NULL!=hFont);
	
	if(!m_TitleFont.IsNull())
		m_TitleFont.DeleteObject();
	
	m_TitleFont = hFont;
	
	PositionWindow();
}

//
//	Sets the font used for drawing the balloon content.  
//	Deleted by balloon, do not use hFont after passing to this function.
//
void CBalloonHelp::SetContentFont(HFONT hFont)
{
	ATLASSERT(NULL!=hFont);

	if(!m_ContentFont.IsNull())
		m_ContentFont.DeleteObject();

	m_ContentFont = hFont;
	
	PositionWindow();
}

//
//	Sets the icon displayed in the top left of the balloon (pass NULL to hide icon)
//
//	 pszIcon must be one of:
//					 IDI_APPLICATION
//					 IDI_INFORMATION IDI_ASTERISK (same)
//					 IDI_ERROR IDI_HAND (same)
//					 IDI_EXCLAMATION IDI_WARNING (same)
//					 IDI_QUESTION
//					 IDI_WINLOGO
//					 NULL (no icon)
void CBalloonHelp::SetIcon(LPCTSTR pszIcon)
{
	if(!m_ilIcon.IsNull())
		m_ilIcon.Destroy();

	if(NULL!=pszIcon)
	{
		HICON hIcon=(HICON)::LoadImage(NULL, pszIcon, IMAGE_ICON, 16,16, LR_SHARED);
		if(NULL!=hIcon)
		{
			// Use a scaled standard icon (looks very good on Win2k, XP, not so good on Win9x)

			CWindowDC dc(NULL);

			CDC dcTmp1;
			dcTmp1.CreateCompatibleDC(dc);

			CDC dcTmp2;
			dcTmp2.CreateCompatibleDC(dc);

			CBitmap bmpIcon;
			bmpIcon.CreateCompatibleBitmap(dc, 32,32);

			CBitmap bmpIconSmall;
			bmpIconSmall.CreateCompatibleBitmap(dc, 16,16);


			// i now have two device contexts and two bitmaps.
			// i will select a bitmap in each device context,
			// draw the icon into the larger one,
			// scale it into the smaller one,
			// and set the small one as the balloon icon.
			// This is a rather long process to get a small icon,
			// but ensures maximum compatibility between different
			// versions of Windows, while producing the best possible
			// results on each version.
			CBitmapHandle hbmpOld1 = dcTmp1.SelectBitmap(bmpIcon);
			CBitmapHandle hbmpOld2 = dcTmp2.SelectBitmap(bmpIconSmall);

			dcTmp1.FillSolidRect(0,0,32,32, OleTranslateColor(m_clrBackground));
			::DrawIconEx(dcTmp1, 0,0,hIcon,32,32,0,NULL,DI_NORMAL);
			dcTmp2.SetStretchBltMode(HALFTONE);
			dcTmp2.StretchBlt(0,0,16,16,dcTmp1, 0,0,32,32,SRCCOPY);

			dcTmp1.SelectBitmap(hbmpOld1);
			dcTmp2.SelectBitmap(hbmpOld2);

			SetIcon(bmpIconSmall, OleTranslateColor(m_clrBackground));
		}
	}

	PositionWindow();
	
}
//
//	Sets the icon displayed in the top left of the balloon (pass NULL to hide icon)
//
void CBalloonHelp::SetIcon(HICON hIcon)
{
	if(!m_ilIcon.IsNull())
		m_ilIcon.Destroy();
	
	ICONINFO iconinfo={0};
	if(NULL!=hIcon && ::GetIconInfo(hIcon, &iconinfo))
	{
		SetIcon(iconinfo.hbmColor, iconinfo.hbmMask);
		::DeleteObject(iconinfo.hbmColor);
		::DeleteObject(iconinfo.hbmMask);
	}
	
	PositionWindow();
}

//
//	Sets the icon displayed in the top left of the balloon (pass NULL to hide icon)
//
void CBalloonHelp::SetIcon(HBITMAP hBitmap, COLORREF crMask)
{
	if(!m_ilIcon.IsNull())
		m_ilIcon.Destroy();
	
	if(NULL!=hBitmap)
	{
		BITMAP bm;
		if(::GetObject(hBitmap, sizeof(bm),(LPVOID)&bm))
		{
			m_ilIcon.Create(bm.bmWidth, bm.bmHeight, ILC_COLOR24|ILC_MASK,1,0);
			m_ilIcon.Add(hBitmap, crMask);
		}
	}
	
	PositionWindow();
}

//
//	Sets the icon displayed in the top left of the balloon
//
void CBalloonHelp::SetIcon(HBITMAP hBitmap, HBITMAP hMask)
{
	if(!m_ilIcon.IsNull())
		m_ilIcon.Destroy();
	
	ATLASSERT(NULL!=hBitmap);
	ATLASSERT(NULL!=hMask);
	
	BITMAP bm;
	if (::GetObject(hBitmap, sizeof(bm),(LPVOID)&bm))
	{
		m_ilIcon.Create(bm.bmWidth, bm.bmHeight, ILC_COLOR24|ILC_MASK,1,0);
		m_ilIcon.Add(hBitmap, hMask);
	}
	
	PositionWindow();
}

//
//	Set icon displayed in the top left of the balloon to image # nIconIndex from hImgList
//
void CBalloonHelp::SetIcon(HIMAGELIST hImgList, int nIconIndex)
{
	CImageList imgList(hImgList);

	HICON hIcon=NULL;
	if(!imgList.IsNull() && nIconIndex >= 0 && nIconIndex < imgList.GetImageCount() )
	{
		hIcon = imgList.ExtractIcon(nIconIndex);
	}

	SetIcon(hIcon);

	if(NULL!=hIcon)
		::DestroyIcon(hIcon);

	PositionWindow();
	
	imgList.Detach();
}

//
//	Sets the number of milliseconds the balloon can remain open.  Set to 0 to disable timeout.
//
void CBalloonHelp::SetTimeout(UINT nTimeout)
{
	UINT m_unTimeout=nTimeout;
	if(IsWindow())
	{
		if(m_unTimeout>0)
			SetTimer(IdTimerClose, m_unTimeout);
		else
			KillTimer(IdTimerClose);
	}
}

//
// Sets the URL to be opened when balloon is clicked.  Pass NULL to disable.
//
void CBalloonHelp::SetURL(LPCTSTR pszURL)
{
	m_strURL=pszURL?pszURL:_T("");
}

//
//	Sets the point to which the balloon is "anchored"
//
void CBalloonHelp::SetAnchorPoint(CPoint ptAnchor)
{
	m_Anchor.SetAnchorPoint(ptAnchor);

	if(m_Anchor.IsFollowMeMode())
		SetCallWndRetHook();
	else
		RemoveCallWndRetHook();

	PositionWindow();
}

//
//	Sets the center of hWndAnchor as the anchor point
//	
void CBalloonHelp::SetAnchorPoint(HWND hWndAnchor)
{
	m_Anchor.SetAnchorPoint(hWndAnchor);

	if(m_Anchor.IsFollowMeMode())
		SetCallWndRetHook();
	else
		RemoveCallWndRetHook();

	PositionWindow();
}

//
//	hWnd!=NULL: Enable "follow me" feature.
//				if NULL!=pptAnchor then use this point as this initial anchor point,
//				otherwise use the center of hWndFollow
//
//	hWnd==NULL: Disable "follow me" feature.
//				if NULL!=pptAnchor then use this point as the new anchor point,
//				otherwise preserve existing anchor point
//
void CBalloonHelp::SetFollowMe(HWND hWndFollow, POINT* pptAnchor)
{
	m_Anchor.SetFollowMe(hWndFollow, pptAnchor);

	if(m_Anchor.IsFollowMeMode())
		SetCallWndRetHook();
	else
		RemoveCallWndRetHook();

	PositionWindow();
}

//
//	Sets the title of the balloon
//
void CBalloonHelp::SetTitle(LPCTSTR pszTitle)
{
	ATLASSERT(IsWindow() && pszTitle);
	SetWindowText(pszTitle);
	PositionWindow();
}

//
//	Sets the content of the balloon (plain text only)
//
void CBalloonHelp::SetContent(LPCTSTR pszContent)
{
	ATLASSERT(pszContent);
	m_strContent=pszContent;
	PositionWindow();
}

//
//	Sets the foreground (text and border) color of the balloon
//
void CBalloonHelp::SetForeGroundColor(OLE_COLOR clr)
{
	m_clrForeground=clr;
	PositionWindow();
}

//
//	Sets the background color of the balloon
//
void CBalloonHelp::SetBackgroundColor(OLE_COLOR clr)
{
	m_clrBackground=clr;
	PositionWindow();
}

//
//	Sets the distance the mouse must move before the balloon closes when the BOCloseOnMouseMove option is set.
//
void CBalloonHelp::SetMouseMoveTolerance(int nTolerance)
{
	m_nMouseMoveTolerance=nTolerance;
}

//
// determine bounds of screen anchor is on (Multi-Monitor compatibility)
//
void CBalloonHelp::GetAnchorScreenBounds(CRect& rcBounds)
{
	if(m_rcScreen.IsRectEmpty())
	{     
		const CPoint ptAnchor=m_Anchor.GetAnchorPoint();
		// get the nearest monitor to the anchor
		HMONITOR hMonitor = MonitorFromPoint(ptAnchor, MONITOR_DEFAULTTONEAREST);

		// get the monitor bounds
		MONITORINFO mi;
		mi.cbSize = sizeof(mi);
		GetMonitorInfo(hMonitor, &mi);

		// work area (area not obscured by task bar, etc.)
		m_rcScreen = mi.rcWork;
	}
	rcBounds=m_rcScreen;
}

//
// calculates the area of the screen the balloon falls into
// this determins which direction the tail points
//
CBalloonHelp::BalloonQuadrant CBalloonHelp::GetBalloonQuadrant()
{
	CRect rcDesktop;
	GetAnchorScreenBounds(rcDesktop);
	
	const CPoint ptAnchor=m_Anchor.GetAnchorPoint();

	if(ptAnchor.y<rcDesktop.top+rcDesktop.Height()/2)
	{
		if(ptAnchor.x<rcDesktop.left+rcDesktop.Width()/2)
			return BQTopLeft;
		else
			return BQTopRight;
	}
	else
	{
		if(ptAnchor.x<rcDesktop.left+rcDesktop.Width()/2)
			return BQBottomLeft;
		else
			return BQBottomRight;
	}
}

//
//			Calculate the dimensions and draw the balloon header
//
CSize CBalloonHelp::DrawHeader(HDC hdc, bool bDraw)
{
	ATLASSERT(hdc);
	CDCHandle dc(hdc);
	CSize sizeHdr(0,0);
	CRect rcClient;
	GetClientRect(&rcClient);

	// calc & draw icon
	if(!m_ilIcon.IsNull())
	{
		CSize sizeIcon(0,0);
		m_ilIcon.GetIconSize(sizeIcon);

		sizeHdr.cx+=sizeIcon.cx;
		sizeHdr.cy=max(sizeHdr.cy, sizeIcon.cy);

		m_ilIcon.SetBkColor(OleTranslateColor(m_clrBackground));

		if(bDraw)
			m_ilIcon.Draw(dc, 0, CPoint(0,0), ILD_NORMAL);

		rcClient.left+=sizeIcon.cx;
	}

	// calc & draw close button
	if(m_dwOptions& BOShowCloseButton)
	{
		// if something is already in the header (icon) leave space
		if(sizeHdr.cx>0)
			sizeHdr.cx+=nTipMargin;

		sizeHdr.cx+=nCXCloseBtn;
		sizeHdr.cy=max(sizeHdr.cy, nCYCloseBtn);

		if(bDraw)
		{
			dc.DrawFrameControl(CRect(rcClient.right-nCXCloseBtn,0,rcClient.right,nCYCloseBtn), DFC_CAPTION, DFCS_CAPTIONCLOSE|DFCS_FLAT);
		}

		rcClient.right-=nCXCloseBtn;
	}


	// calc title size
	CString strTitle;
	GetWindowText(strTitle.GetBuffer(255),255);
	strTitle.ReleaseBuffer();

	if(!strTitle.IsEmpty() )
	{
		CFontHandle hOldFont=dc.SelectFont(m_TitleFont);

		// if something is already in the header (icon or close button) leave space
		if(sizeHdr.cx>0)
			sizeHdr.cx+=nTipMargin;

		CRect rectTitle(0,0,0,0);
		dc.DrawText(strTitle,-1, rectTitle, DT_CALCRECT | DT_NOPREFIX | DT_EXPANDTABS | DT_SINGLELINE);

		sizeHdr.cx+=rectTitle.Width();
		sizeHdr.cy =max(sizeHdr.cy, rectTitle.Height());

		// draw title
		if(bDraw)
		{
			dc.SetBkMode(TRANSPARENT);
			dc.SetTextColor(OleTranslateColor(m_clrForeground));
			dc.DrawText(strTitle,-1, rcClient, DT_CENTER | DT_NOPREFIX	| DT_EXPANDTABS | DT_SINGLELINE);
		}

		// cleanup
		dc.SelectFont(hOldFont);
	}

	return sizeHdr;
}

//
//	Calculate the dimensions and draw the balloon contents
//
CSize CBalloonHelp::DrawContent(HDC hdc, int nTop, bool bDraw)
{
	CDCHandle dc(hdc);
	CRect rcContent;
	GetAnchorScreenBounds(rcContent);
	rcContent.OffsetRect(-rcContent.left, -rcContent.top);
	rcContent.top = nTop;
	// limit to half screen width
	rcContent.right -= rcContent.Width()/2;

	CFontHandle hOldFont=dc.SelectFont(m_ContentFont);

	if(!m_strContent.IsEmpty())
		dc.DrawText(m_strContent,-1, &rcContent, DT_CALCRECT | DT_LEFT | DT_NOPREFIX | DT_EXPANDTABS | DT_WORDBREAK);
	else
		rcContent.SetRectEmpty();

	// draw
	if(bDraw)
	{
		dc.SetBkMode(TRANSPARENT);
		dc.SetTextColor(OleTranslateColor(m_clrForeground));
		dc.DrawText(m_strContent,-1, &rcContent, DT_LEFT | DT_NOPREFIX | DT_WORDBREAK | DT_EXPANDTABS);
	}

	// cleanup
	dc.SelectFont(hOldFont);

	return rcContent.Size();
}


//
//	calculates the client size necessary based on title and content
//
CSize CBalloonHelp::CalcClientSize()
{
	ATLASSERT(IsWindow());
	CWindowDC dc(m_hWnd);

	CSize sizeHeader  = CalcHeaderSize(dc);
	CSize sizeContent = CalcContentSize(dc);

	return CSize(max(sizeHeader.cx,sizeContent.cx), sizeHeader.cy + nTipMargin + sizeContent.cy);
}

//
//	calculates the size for the entire window based on content size
//
CSize CBalloonHelp::CalcWindowSize()
{
	CSize size = CalcClientSize();
	size.cx += 2*nTipMargin;
	size.cy += nTipTail+2*nTipMargin;

	return size;
}

//
//
//
void CBalloonHelp::OnDestroy()
{
	RemoveMouseHook();
	RemoveKeyboardHook();
	RemoveCallWndRetHook();
}

//
//	draws the background of the client
//
void CBalloonHelp::DrawBkgnd(HDC hdc)
{
	ATLASSERT(NULL!=hdc);
	CDCHandle dc(hdc);
	CRect rcClient;
	GetClientRect(&rcClient);

	dc.FillSolidRect(rcClient, OleTranslateColor(m_clrBackground));
}

//
//	draws the whole client hHeader and content)
//
void CBalloonHelp::Draw(HDC hdc)
{
	ATLASSERT(NULL!=hdc);
	DrawBkgnd(hdc);
	CSize sizeHeader=DrawHeader(hdc);
	DrawContent(hdc, sizeHeader.cy+nTipMargin);
}

//
//	draw the non client space
//
void CBalloonHelp::DrawNc(HDC hdc, BOOL bExcludeClient/*=TRUE*/)
{
	ATLASSERT(NULL!=hdc);
	
	const COLORREF clrBg=OleTranslateColor(m_clrBackground);
	const COLORREF clrFg=OleTranslateColor(m_clrForeground);

	CRect rcWnd;
	GetWindowRect(&rcWnd);
	ScreenToClient(&rcWnd);
	
	if(bExcludeClient)
	{
		CRect rcClient;
		GetClientRect(&rcClient);

		rcClient.OffsetRect(-rcWnd.left, -rcWnd.top);

		CDCHandle dc(hdc);
		dc.ExcludeClipRect(&rcClient);
	}

	CMemDC dc(hdc);
	
	rcWnd.OffsetRect(-rcWnd.left, -rcWnd.top);
	dc.FillSolidRect(&rcWnd, clrBg);
	
	ATLASSERT(!m_rgnComplete.IsNull());
	
	if(m_dwOptions&BOShowInnerShadow)
	{
		// slightly lighter color
		int red   = 170 + GetRValue(clrBg)/3;
		int green = 170 + GetGValue(clrBg)/3;
		int blue  = 170 + GetBValue(clrBg)/3;

		CBrush brushFrm;
		brushFrm.CreateSolidBrush(RGB(red,green,blue));

		m_rgnComplete.OffsetRgn(1,1);
		dc.FrameRgn(m_rgnComplete, brushFrm, 2, 2);

		// slightly darker color
		red   = GetRValue(clrFg)/3 + GetRValue(clrBg)/3*2;
		green = GetGValue(clrFg)/3 + GetGValue(clrBg)/3*2;
		blue  = GetBValue(clrFg)/3 + GetBValue(clrBg)/3*2;
		
		brushFrm.DeleteObject();
		brushFrm.CreateSolidBrush(RGB(red,green,blue));

		m_rgnComplete.OffsetRgn(-2,-2);
		dc.FrameRgn(m_rgnComplete, brushFrm, 2, 2);

		m_rgnComplete.OffsetRgn(1,1);
	}

	// outline
	CBrush brushFg;
	brushFg.CreateSolidBrush(clrFg);
	dc.FrameRgn(m_rgnComplete, brushFg, 1, 1);
}


//	this routine calculates the size and position of the window relative
//	to it's anchor point, and moves the window accordingly.  The region is also
//	created and set here.
void CBalloonHelp::PositionWindow()
{
	if(!IsWindow())
		return;

	// force refresh of screen bounds
	m_rcScreen.SetRectEmpty();

	CSize sizeWnd=CalcWindowSize();

	CPoint ptTail[3];
	CPoint ptTopLeft(0,0);
	CPoint ptBottomRight(sizeWnd.cx, sizeWnd.cy);

	switch(GetBalloonQuadrant())
	{
	case BQTopLeft:
		ptTopLeft.y = nTipTail;
		ptTail[0].x = (sizeWnd.cx-nTipTail)/4 + nTipTail;
		ptTail[0].y = nTipTail+1;
		ptTail[2].x = (sizeWnd.cx-nTipTail)/4;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = 1;
		break;
	case BQTopRight:
		ptTopLeft.y = nTipTail;
		ptTail[0].x = (sizeWnd.cx-nTipTail)/4*3;
		ptTail[0].y = nTipTail+1;
		ptTail[2].x = (sizeWnd.cx-nTipTail)/4*3 + nTipTail;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = 1;
		break;
	case BQBottomLeft:
		ptBottomRight.y = sizeWnd.cy-nTipTail;
		ptTail[0].x = (sizeWnd.cx-nTipTail)/4 + nTipTail;
		ptTail[0].y = sizeWnd.cy-nTipTail-2;
		ptTail[2].x = (sizeWnd.cx-nTipTail)/4;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = sizeWnd.cy-2;
		break;
	case BQBottomRight:
		ptBottomRight.y = sizeWnd.cy-nTipTail;
		ptTail[0].x = (sizeWnd.cx-nTipTail)/4*3;
		ptTail[0].y = sizeWnd.cy-nTipTail-2;
		ptTail[2].x = (sizeWnd.cx-nTipTail)/4*3 + nTipTail;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = sizeWnd.cy-2;
		break;
	}

	//
	// adjust for very narrow balloons
	//
	if(ptTail[0].x < nTipMargin )
		ptTail[0].x = nTipMargin;

	if(ptTail[0].x > sizeWnd.cx - nTipMargin)
		ptTail[0].x = sizeWnd.cx - nTipMargin;

	if(ptTail[1].x < nTipMargin)
		ptTail[1].x = nTipMargin;

	if(ptTail[1].x > sizeWnd.cx - nTipMargin)
		ptTail[1].x = sizeWnd.cx - nTipMargin;

	if(ptTail[2].x < nTipMargin)
		ptTail[2].x = nTipMargin;

	if(ptTail[2].x > sizeWnd.cx - nTipMargin)
		ptTail[2].x = sizeWnd.cx - nTipMargin;

	// get window position
	const CPoint ptAnchor=m_Anchor.GetAnchorPoint();

	CPoint ptOffs(ptAnchor.x - ptTail[1].x, ptAnchor.y - ptTail[1].y);

	// adjust position so all is visible
	CRect rcWorkArea;
   GetAnchorScreenBounds(rcWorkArea);

	int nAdjustX=0;
	int nAdjustY=0;

   if ( ptOffs.x < rcWorkArea.left )
      nAdjustX = rcWorkArea.left-ptOffs.x;
   else if ( ptOffs.x + sizeWnd.cx >= rcWorkArea.right )
      nAdjustX = rcWorkArea.right - (ptOffs.x + sizeWnd.cx);
   if ( ptOffs.y + nTipTail < rcWorkArea.top )
      nAdjustY = rcWorkArea.top - (ptOffs.y + nTipTail);
   else if ( ptOffs.y + sizeWnd.cy - nTipTail >= rcWorkArea.bottom )
      nAdjustY = rcWorkArea.bottom - (ptOffs.y + sizeWnd.cy - nTipTail);

	// reposition tail
	// uncomment two commented lines below to move entire tail
	// instead of just anchor point

	//ptTail[0].x -= nAdjustX;
	ptTail[1].x -= nAdjustX;
	//ptTail[2].x -= nAdjustX;
	ptOffs.x	+= nAdjustX;
	ptOffs.y	+= nAdjustY;

	// place window
	MoveWindow(ptOffs.x, ptOffs.y, sizeWnd.cx, sizeWnd.cy, TRUE);

	// apply region
	CRgn rgnTail;
	rgnTail.CreatePolygonRgn(&ptTail[0], 3, ALTERNATE);

	CRgn rgnRound;
	rgnRound.CreateRoundRectRgn(ptTopLeft.x,ptTopLeft.y,ptBottomRight.x,ptBottomRight.y,nTipMargin*3,nTipMargin*3);

	CRgn rgnComplete;
	rgnComplete.CreateRectRgn(0,0,1,1);
	rgnComplete.CombineRgn(rgnTail, rgnRound, RGN_OR);

	bool bRegionChanged=true;
	if(m_rgnComplete.IsNull())
	{
		m_rgnComplete.CreateRectRgn(0,0,1,1);
	}
	else if(m_rgnComplete.EqualRgn(rgnComplete))
	{
		bRegionChanged=false;
	}
	

	if(bRegionChanged)
	{
		m_rgnComplete.CopyRgn(rgnComplete);

		SetWindowRgn((HRGN)rgnComplete.Detach(), TRUE);
	}	
	UpdateWindow();
}


//
//
//
void CBalloonHelp::ShowWindow()
{
	Animate(TRUE);
}

//
//
//
void CBalloonHelp::CloseWindow()
{
	if(IsWindow())
	{
		Animate(FALSE);
		PostMessage(WM_CLOSE);
	}
}

//
//
//
void CBalloonHelp::Animate(BOOL bShow)
{
	typedef BOOL (WINAPI* FnAnimateWindow)(IN HWND hWnd,IN DWORD dwTime,IN DWORD dwFlags);

	ATLASSERT(IsWindow());
	
	BOOL bAnimate=bShow?(!(m_dwOptions & BODisableFadeIn)):(!(m_dwOptions & BODisableFadeOut));
	if(bAnimate)
	{
		BOOL bFade=FALSE;
		::SystemParametersInfo(SPI_GETTOOLTIPANIMATION, 0, &bFade, 0);
		if(bFade)
			::SystemParametersInfo(SPI_GETTOOLTIPFADE, 0, &bFade, 0);

		if(bFade)
		{
			HMODULE hModule=::LoadLibrary(_T("User32.dll"));
			FnAnimateWindow AnimateWnd=(FnAnimateWindow)::GetProcAddress(hModule,"AnimateWindow");
			
			if(NULL!=AnimateWnd)
			{
				DWORD dwFlags=AW_BLEND;
				if(!bShow)
					dwFlags|=AW_HIDE;

				if(0!=AnimateWnd(m_hWnd,nAnimDuration,dwFlags))
					return;
			}
		}
	}

	WindowBase::ShowWindow(SW_SHOWNA);
}



//
//
//
void CBalloonHelp::OnPaint(HDC hdc)
{
	if(NULL==hdc)
	{
		CPaintDC dc(m_hWnd);
		{
			CMemDC dcMem(dc);
			Draw(dcMem);
		}
	}
	else
	{
		Draw(hdc);
	}
}

//
//	draw balloon shape & boarder
//
void CBalloonHelp::OnNcPaint(HRGN)
{
	CWindowDC dc(m_hWnd);
	DrawNc(dc);
}

//
//
//
LRESULT CBalloonHelp::OnNcCalcSize(BOOL bCalcValidRects, LPARAM lParam)
{
	NCCALCSIZE_PARAMS* pncsp=(NCCALCSIZE_PARAMS*)lParam;

	// nTipMargin pixel margin on all sides
	::InflateRect(&pncsp->rgrc[0], -nTipMargin,-nTipMargin);

	// nTipTail pixel "tail" on side closest to anchor
	switch (GetBalloonQuadrant())
	{
	case BQTopRight:
	case BQTopLeft:
		pncsp->rgrc[0].top += nTipTail;
		break;
	case BQBottomRight:
	case BQBottomLeft:
		pncsp->rgrc[0].bottom -= nTipTail;
		break;
	}

	// sanity: ensure rect does not have negative size
	if( pncsp->rgrc[0].right < pncsp->rgrc[0].left)
		pncsp->rgrc[0].right = pncsp->rgrc[0].left;
	if( pncsp->rgrc[0].bottom < pncsp->rgrc[0].top)
		pncsp->rgrc[0].bottom = pncsp->rgrc[0].top;

	return 0;
}


//
//	OnCaptureChanged
//
void CBalloonHelp::OnCaptureChanged(HWND)
{
	if(m_dwOptions & BOShowCloseButton)
	{
		m_uCloseState=0;

		CClientDC dc(m_hWnd);

		CRect rcCloseBtn;
		GetClientRect(&rcCloseBtn);
		rcCloseBtn.left	  = rcCloseBtn.right-nCXCloseBtn;
		rcCloseBtn.bottom = rcCloseBtn.top  +nCYCloseBtn;
		
		dc.DrawFrameControl(rcCloseBtn, DFC_CAPTION, DFCS_CAPTIONCLOSE|DFCS_FLAT);
	}

}// OnCaptureChanged


//
//	do mouse tracking:
//	Close on mouse move;
//	Tracking for close button;
//
void CBalloonHelp::OnMouseMove(UINT, CPoint pt)
{
	if(m_dwOptions & BOShowCloseButton)
	{
		CRect rcCloseBtn;
		GetClientRect(&rcCloseBtn);
		rcCloseBtn.left	  = rcCloseBtn.right-nCXCloseBtn;
		rcCloseBtn.bottom = rcCloseBtn.top  +nCYCloseBtn;

		UINT uState = DFCS_CAPTIONCLOSE;
		BOOL bPushed= m_uCloseState&DFCS_PUSHED;

		m_uCloseState &= ~DFCS_PUSHED;

		if(rcCloseBtn.PtInRect(pt))
		{
			uState |= DFCS_HOT;

			if(bPushed)
				uState |= DFCS_PUSHED;

			SetTimer(IdTimerHotTrack, TimerHotTrackElapse);
		}
		else
		{
			uState |= DFCS_FLAT;
		}

		if(uState!=m_uCloseState )
		{
			CClientDC dc(m_hWnd);
			dc.DrawFrameControl(rcCloseBtn, DFC_CAPTION, uState);
			m_uCloseState = uState;
		}

		if(bPushed)
			m_uCloseState |= DFCS_PUSHED;
	}
}

//
//	Close button handler
//
void CBalloonHelp::OnLButtonDown(UINT, CPoint pt) 
{
	if(m_dwOptions & BOShowCloseButton)
	{
		CRect rcClient;
		GetClientRect(&rcClient);

		rcClient.left  = rcClient.right-nCXCloseBtn;
		rcClient.bottom= rcClient.top+nCYCloseBtn;

		if(rcClient.PtInRect(pt))
		{
			m_uCloseState|=DFCS_PUSHED;
			SetCapture();
			OnMouseMove(0, pt);
		}
	}
}


//
//	Close button handler
//
void CBalloonHelp::OnLButtonUp(UINT, CPoint pt) 
{
	if( (m_dwOptions & BOShowCloseButton) && (m_uCloseState & DFCS_PUSHED))
	{
		ReleaseCapture();
		m_uCloseState&=~DFCS_PUSHED;

		CRect rcClient;
		GetClientRect(&rcClient);

		rcClient.left  = rcClient.right-nCXCloseBtn;
		rcClient.bottom= rcClient.top  +nCYCloseBtn;

		if(rcClient.PtInRect(pt))
			CloseWindow();
	}
	else if ( !m_strURL.IsEmpty() )
	{
		CloseWindow();
		::ShellExecute(NULL, NULL, m_strURL, NULL, NULL, SW_SHOWNORMAL);
	}
}

//
//	handler for various timers
//
#if (_WTL_VER<0x0700)
	// This is the bogus version of WTL 3.x
	void CBalloonHelp::OnTimer(UINT nIDEvent, TIMERPROC*)
#else
	// This is the correct version. Fixed in WTL 7.0
	void CBalloonHelp::OnTimer(UINT nIDEvent, TIMERPROC)
#endif 
	{
	if(IdTimerHotTrack==nIDEvent)
	{
		KillTimer(IdTimerHotTrack);

		CPoint pt;
		::GetCursorPos(&pt);
		ScreenToClient(&pt);

		OnMouseMove(0, pt);
	}
	else if(IdTimerClose==nIDEvent)
	{
		KillTimer(IdTimerClose);
		CloseWindow();
	}
	else
	{
		SetMsgHandled(FALSE);
	}
}

//
//
//
void CBalloonHelp::OnActivateApp(BOOL bActivate, DWORD dwTask)
{
	if(!bActivate && (m_dwOptions & BOCloseOnAppDeactivate))
		CloseWindow();
}


//
//	AnimateWindow needs this
//
void CBalloonHelp::OnPrint(HDC hdc,UINT Flags)
{
	ATLASSERT(hdc);

	CDCHandle dc(hdc);

	CPoint ptOrg;
	dc.GetViewportOrg(&ptOrg);

	if(PRF_ERASEBKGND & Flags)
	{
		DrawBkgnd(dc);
	}

	if(PRF_NONCLIENT & Flags)
	{
		DrawNc(dc, FALSE);

		CRect rcWnd;
		GetWindowRect(rcWnd);
		rcWnd.OffsetRect(-rcWnd.TopLeft());

		CRect rc(rcWnd);
		OnNcCalcSize(FALSE,(LPARAM)&rc);

		dc.SetViewportOrg(rc.left-rcWnd.left,rc.top-rcWnd.top);
	}

	if(PRF_CLIENT & Flags)
	{
		Draw(dc);
	}

	dc.SetViewportOrg(ptOrg);
}

//
//
//
void CBalloonHelp::OnFinalMessage(HWND)
{
	if(m_dwOptions & BODeleteThisOnClose)
	{
		delete this;
	}
}

//
//
//
void CBalloonHelp::SetKeyboardHook()
{
	if(NULL==m_hKeybHook)
	{
		m_hKeybHook=::SetWindowsHookEx(
			WH_KEYBOARD,
			(HOOKPROC)KeybHook::GetThunk(),
			NULL,
			::GetCurrentThreadId());
	}
}

//
//
//
void CBalloonHelp::RemoveKeyboardHook()
{
	if(NULL!=m_hKeybHook)
	{
		::UnhookWindowsHookEx(m_hKeybHook);
		m_hKeybHook=NULL;
	}
}

//
//
//
LRESULT CBalloonHelp::KeyboardHookProc( int code, WPARAM wParam, LPARAM lParam)
{
	// Skip if the key was released or if it's a repeat
	// Bit 31:	Specifies the transition state. The value is 0 if the key  
	//			is being pressed and 1 if it is being released (see MSDN).
	if(code>=0 && !(lParam&0x80000000))
	{
		CloseWindow();
	}
	return ::CallNextHookEx(GetKeybHookHandle(), code, wParam, lParam);
}


//
//
//
void CBalloonHelp::SetMouseHook()
{
	if(NULL==m_hMouseHook)
	{
		m_hMouseHook=::SetWindowsHookEx(
			WH_MOUSE,
			(HOOKPROC)MouseHook::GetThunk(),
			NULL,
			::GetCurrentThreadId());
	}
}

//
//
//
void CBalloonHelp::RemoveMouseHook()
{
	if(NULL!=m_hMouseHook)
	{
		::UnhookWindowsHookEx(m_hMouseHook);
		m_hMouseHook=NULL;
	}
}
//
//
//
LRESULT CBalloonHelp::MouseHookProc(int code, WPARAM wParam, LPARAM lParam)
{
	if(code>=0)
	{
		const UINT uMsg=(UINT)wParam;

		if( (uMsg==WM_MOUSEMOVE) )
		{
			if((m_dwOptions & BOCloseOnMouseMove))
			{
				CPoint pt;
				::GetCursorPos(&pt);
				if((abs(pt.x-m_ptMouseOrg.x) > m_nMouseMoveTolerance || abs(pt.y-m_ptMouseOrg.y) > m_nMouseMoveTolerance) )
					CloseWindow();
			}
		}
		else if( (uMsg==WM_NCLBUTTONDOWN || uMsg==WM_LBUTTONDOWN) )
		{
			if((m_dwOptions & BOCloseOnLButtonDown))
				CloseWindow();
		}
		else if( (uMsg==WM_NCMBUTTONDOWN || uMsg==WM_MBUTTONDOWN) )
		{
			if((m_dwOptions & BOCloseOnMButtonDown))
				CloseWindow();
		}
		else if( (uMsg==WM_NCRBUTTONDOWN || uMsg==WM_RBUTTONDOWN) )
		{
			if((m_dwOptions& BOCloseOnRButtonDown))
				CloseWindow();
		}
		else if( (uMsg==WM_NCLBUTTONUP || uMsg==WM_LBUTTONUP) )
		{
			if((m_dwOptions & BOCloseOnLButtonUp))
				CloseWindow();
		}
		else if( (uMsg==WM_NCMBUTTONUP || uMsg==WM_MBUTTONUP) )
		{
			if((m_dwOptions & BOCloseOnMButtonUp))
				CloseWindow();
		}
		else if( (uMsg==WM_NCRBUTTONUP || uMsg==WM_RBUTTONUP) )
		{
			if((m_dwOptions & BOCloseOnRButtonUp))
				CloseWindow();
		}
	}

	return ::CallNextHookEx(GetMouseHookHandle(), code, wParam, lParam);
}

//
//
//
void CBalloonHelp::SetCallWndRetHook()
{
	if(NULL==m_hCallWndRetHook)
	{
		m_hCallWndRetHook=::SetWindowsHookEx(
			WH_CALLWNDPROCRET,
			(HOOKPROC)CallWndRetHook::GetThunk(),
			NULL,
			::GetCurrentThreadId());
	}
}

//
//
//
void CBalloonHelp::RemoveCallWndRetHook()
{
	if(NULL!=m_hCallWndRetHook)
	{
		::UnhookWindowsHookEx(m_hCallWndRetHook);
		m_hCallWndRetHook=NULL;
	}
}

//
//
//	
LRESULT CBalloonHelp::CallWndRetProc(int code, WPARAM wParam, LPARAM lParam)
{
	if(code>=0)
	{
		static BOOL bInCall=FALSE;

		CWPRETSTRUCT* pcwpr=(CWPRETSTRUCT*)lParam;
		
		if(WM_MOVE==pcwpr->message && !bInCall)
		{
			bInCall=TRUE;

			if(m_Anchor.AnchorPointChanged())
				PositionWindow();

			bInCall=FALSE;
		}
	}
	
	return ::CallNextHookEx(GetCallWndRetHandle(), code, wParam, lParam);
}

#ifndef BALLOON_HELP_NO_NAMESPACE
}// namespace WTL
#endif

// eof

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
Poke...

Comments and Discussions