Click here to Skip to main content
15,888,733 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.
// BalloonHelp.cpp : implementation file
// Copyright 2001, Joshua Heyer
//  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.
//

#include "stdafx.h"
#include "BalloonHelp.h"
#include <atlbase.h>

//
// constants that may not be defined if you don't have the latest SDK
// (but i like to use them anyway)
//

#ifndef DFCS_HOT
#define DFCS_HOT 0x1000
#endif

#ifndef WS_EX_LAYERED
#define WS_EX_LAYERED   0x00080000
#define LWA_COLORKEY    0x00000001
#define LWA_ALPHA       0x00000002
#endif

#ifndef CS_DROPSHADOW
#define CS_DROPSHADOW   0x00020000
#endif

#ifndef SPI_GETDROPSHADOW
#define SPI_GETDROPSHADOW  0x1024
#endif

#ifndef SPI_GETTOOLTIPANIMATION
#define SPI_GETTOOLTIPANIMATION 0x1016
#endif

#ifndef SPI_GETTOOLTIPFADE
#define SPI_GETTOOLTIPFADE 0x1018
#endif

/////////////////////////////////////////////////////////////////////////////
// CBalloonHelp

// these could go in resources, if they make you nervous
// if someone knows a better way to deal with timers than to use
// constant IDs, pls let me know?
#define ID_TIMER_SHOW      3333
#define ID_TIMER_HIDE      3334
#define ID_TIMER_CLOSE     3335
#define ID_TIMER_HOTTRACK  3336

// option constants (bits)
const unsigned int   CBalloonHelp::unCLOSE_ON_LBUTTON_UP = 0x001;
const unsigned int   CBalloonHelp::unCLOSE_ON_RBUTTON_UP = 0x002;
const unsigned int   CBalloonHelp::unCLOSE_ON_MOUSE_MOVE = 0x004;
const unsigned int   CBalloonHelp::unCLOSE_ON_KEYPRESS   = 0x008;
const unsigned int   CBalloonHelp::unDELETE_THIS_ON_CLOSE= 0x010;
const unsigned int   CBalloonHelp::unSHOW_CLOSE_BUTTON   = 0x020;
const unsigned int   CBalloonHelp::unSHOW_INNER_SHADOW   = 0x040;
const unsigned int   CBalloonHelp::unSHOW_TOPMOST        = 0x080;
const unsigned int   CBalloonHelp::unDISABLE_FADEIN      = 0x100;
const unsigned int   CBalloonHelp::unDISABLE_FADEOUT     = 0x200;
const unsigned int   CBalloonHelp::unDISABLE_FADE        = CBalloonHelp::unDISABLE_FADEIN|CBalloonHelp::unDISABLE_FADEOUT;

// layout constants (should prolly be configurable)
const int            CBalloonHelp::nTIP_TAIL             = 20;
const int            CBalloonHelp::nTIP_MARGIN           = 8;
// synchronization for keyboard hook
CComCriticalSection     CBalloonHelp::s_KeyboardHookSection;
// windows using keyboard hook
CAtlArray<HWND>      CBalloonHelp::s_apKeyboardCloseWnds;
// handle to the keyboard hook, if set
HHOOK                CBalloonHelp::s_hKeyboardHook       = NULL;

void FillSolidRect(HDC hdc, const RECT *pRect, COLORREF clr)
{
	SetBkColor(hdc, clr);
	ExtTextOut(hdc, 0, 0, ETO_OPAQUE, pRect, NULL, 0, NULL);
}

CBalloonHelp::CBalloonHelp() :
	m_fnSetLayeredWindowAttributes(NULL),
	m_unOptions(0),
	m_unTimeout(0),
	m_strURL(""),
	m_strContent(""),
	m_nAlpha(255),
	m_nMouseMoveTolerance(3),
	m_uCloseState(0),
	m_pTitleFont(NULL),
	m_pContentFont(NULL),
	m_crForeground(::GetSysColor(COLOR_INFOTEXT)),
	m_crBackground(::GetSysColor(COLOR_INFOBK)),
	m_ilIcon(NULL),
	m_rgnComplete(NULL)
{
	s_KeyboardHookSection.Init();
	m_ptAnchor.x = m_ptAnchor.y = 0;
	m_ptMouseOrig.x = m_ptMouseOrig.y = 0;
	// retrieve layered window API if available
	HMODULE hUser32 = GetModuleHandle(_T("USER32.DLL"));
	m_fnSetLayeredWindowAttributes = (FN_SET_LAYERED_WINDOW_ATTRIBUTES)GetProcAddress(hUser32, _T("SetLayeredWindowAttributes"));
}

CBalloonHelp::~CBalloonHelp()
{
	if ( NULL != m_pTitleFont )
		DeleteObject(m_pTitleFont);
	m_pTitleFont = NULL;
	if ( NULL != m_pContentFont )
		DeleteObject(m_pContentFont);
	m_pContentFont = NULL;
	if (m_ilIcon)
		ImageList_Destroy(m_ilIcon);
	if (m_rgnComplete)
		DeleteObject(m_rgnComplete);
}



//
// Show a help balloon on screen
// Parameters:
//    strTitle    |  Title of balloon
//    strContent  |  Content of balloon
//    ptAnchor    |  point tail of balloon will be "anchor"ed to
//    szIcon      |  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)
//    unOptions   |  One or more of:
//                :     unCLOSE_ON_LBUTTON_UP   |  closes window on WM_LBUTTON_UP
//                :     unCLOSE_ON_RBUTTON_UP   |  closes window on WM_RBUTTON_UP
//                :     unCLOSE_ON_MOUSE_MOVE   |  closes window when user moves mouse past threshhold
//                :     unCLOSE_ON_KEYPRESS     |  closes window on the next keypress message sent to this thread.  (!!! probably not thread safe !!!)
//                :     unSHOW_CLOSE_BUTTON     |  shows close button in upper right
//                :     unSHOW_INNER_SHADOW     |  draw inner shadow in balloon
//                :     unSHOW_TOPMOST          |  place balloon above all other windows
//                :     unDISABLE_FADE          |  disable the fade-in/fade-out effects (overrides system and user settings)
//                :     unDISABLE_FADEIN        |  disable the fade-in effect
//                :     unDISABLE_FADEOUT       |  disable the fade-out effect
//    pParentWnd  |  Parent window.  If NULL will be set to AfxGetMainWnd()
//    strURL      |  If not empty, when the balloon is clicked ShellExecute() will
//                |  be called, with strURL passed in.
//    unTimeout   |  If not 0, balloon will automatically close after unTimeout milliseconds.
//
void CBalloonHelp::LaunchBalloon(const CString& strTitle, const CString& strContent, 
               const CPoint& ptAnchor, 
               LPCTSTR szIcon /*= IDI_EXCLAMATION*/,
               unsigned int unOptions /*= unSHOW_CLOSE_BUTTON*/,
               HWND pParentWnd /*= NULL*/,
               const CString strURL /*= ""*/,
               unsigned int unTimeout /*= 10000*/)
{
   CBalloonHelp* pbh = new CBalloonHelp;
   if ( NULL != szIcon )
   {
      HICON hIcon = (HICON)::LoadImage(NULL, szIcon, 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)
         CDC dc;
         CDC dcTmp1;
         CDC dcTmp2;
         CBitmap bmpIcon;
         CBitmap bmpIconSmall;
         dc.Attach(::GetDC(NULL));
         dcTmp1.CreateCompatibleDC(&dc);
         dcTmp2.CreateCompatibleDC(&dc);
         bmpIcon.CreateCompatibleBitmap(&dc, 32,32);
         bmpIconSmall.CreateCompatibleBitmap(&dc, 16,16);
         ::ReleaseDC(NULL, dc.Detach());
         
         // 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.
         CBitmap* pbmpOld1 = dcTmp1.SelectObject(&bmpIcon);
         CBitmap* pbmpOld2 = dcTmp2.SelectObject(&bmpIconSmall);
         dcTmp1.FillSolidRect(0,0,32,32, pbh->m_crBackground);
         ::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.SelectObject(pbmpOld1);
         dcTmp2.SelectObject(pbmpOld2);

         pbh->SetIcon(bmpIconSmall, pbh->m_crBackground);
      }
   }

   pbh->Create(strTitle, strContent, ptAnchor, unOptions|unDELETE_THIS_ON_CLOSE, 
               pParentWnd, strURL, unTimeout, NULL);
}


// Sets the font used for drawing the balloon title.  Deleted by balloon, do not use CFont* after passing to this function.
void CBalloonHelp::SetTitleFont(HFONT pFont)
{
   if ( NULL != m_pTitleFont )
      DeleteObject(m_pTitleFont);
   m_pTitleFont = pFont;
   // if already visible, resize & move
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the font used for drawing the balloon content.  Deleted by balloon, do not use CFont* after passing to this function.
void CBalloonHelp::SetContentFont(HFONT pFont)
{
   if ( NULL != m_pContentFont )
      DeleteObject(m_pContentFont);
   m_pContentFont = pFont;
   // if already visible, resize & move
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the icon displayed in the top left of the balloon (pass NULL to hide icon)
void CBalloonHelp::SetIcon(HICON hIcon)
{
   if ( NULL != m_ilIcon )
      ImageList_Destroy(m_ilIcon);
   ICONINFO iconinfo;
   if ( NULL != hIcon && ::GetIconInfo(hIcon, &iconinfo) )
   {
      SetIcon(iconinfo.hbmColor, iconinfo.hbmMask);
      ::DeleteObject(iconinfo.hbmColor);
      ::DeleteObject(iconinfo.hbmMask);
   }
   // if already visible, resize & move (icon size may have changed)
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the icon displayed in the top left of the balloon (pass NULL hBitmap to hide icon)
void CBalloonHelp::SetIcon(HBITMAP hBitmap, COLORREF crMask)
{
   if ( NULL != m_ilIcon )
      ImageList_Destroy(m_ilIcon);

   if ( NULL != hBitmap )
   {
      BITMAP bm;
      if (::GetObject(hBitmap, sizeof(bm),(LPVOID)&bm))
      {
         m_ilIcon = ImageList_Create(bm.bmWidth, bm.bmHeight, ILC_COLOR24|ILC_MASK, 1, 0);
         ImageList_AddMasked(m_ilIcon, hBitmap, crMask);
      }
   }
   // if already visible, resize & move (icon size may have changed)
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the icon displayed in the top left of the balloon
void CBalloonHelp::SetIcon(HBITMAP hBitmap, HBITMAP hMask)
{
   if ( NULL != m_ilIcon )
      ImageList_Destroy(m_ilIcon);

   _ASSERTE(hBitmap);
   _ASSERTE(hMask);

   BITMAP bm;
   if (::GetObject(hBitmap, sizeof(bm),(LPVOID)&bm))
   {
      m_ilIcon = ImageList_Create(bm.bmWidth, bm.bmHeight, ILC_COLOR24|ILC_MASK, 1, 0);
      ImageList_Add(m_ilIcon, hBitmap, hMask);
   }
   // if already visible, resize & move (icon size may have changed)
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Set icon displayed in the top left of the balloon to image # nIconIndex from pImageList
void CBalloonHelp::SetIcon(HIMAGELIST pImageList, int nIconIndex)
{
   // sanity checks
   _ASSERTE(pImageList);
   _ASSERTE(nIconIndex >= 0 && nIconIndex < ImageList_GetImageCount(pImageList) );

   HICON hIcon = NULL;
   if ( NULL != pImageList && nIconIndex >= 0 && nIconIndex < ImageList_GetImageCount(pImageList) )
      hIcon = ImageList_ExtractIcon(NULL, pImageList, nIconIndex);
   SetIcon(hIcon);
   if ( NULL != hIcon )
      ::DestroyIcon(hIcon);
   // if already visible, resize & move (icon size may have changed)
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the URL to be opened when balloon is clicked.  Pass "" to disable.
void CBalloonHelp::SetURL(const CString& strURL)
{
   m_strURL = strURL;
}

// Sets the number of milliseconds the balloon can remain open.  Set to 0 to disable timeout.
void CBalloonHelp::SetTimeout(unsigned int unTimeout)
{
   m_unTimeout = unTimeout;
   // if timer is already set, reset.
   if ( NULL != m_hWnd )
   {
      if ( m_unTimeout > 0 )
         SetTimer(ID_TIMER_CLOSE, m_unTimeout, NULL);
      else
         KillTimer(ID_TIMER_CLOSE);
   }
}

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

   // if already visible, move
   if ( NULL != m_hWnd )
   {
      PositionWindow();
   }
}

// Sets the title of the balloon
void CBalloonHelp::SetTitle(const CString& strTitle)
{
   SetWindowText(strTitle);
   // if already visible, resize & move
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the content of the balloon (plain text only)
void CBalloonHelp::SetContent(const CString& strContent)
{
   m_strContent = strContent;
   // if already visible, resize & move
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the forground (text and border) color of the balloon
void CBalloonHelp::SetForegroundColor(COLORREF crForeground)
{
   m_crForeground = crForeground;
   // repaint if visible
   if ( NULL != m_hWnd )
      Invalidate(FALSE);
}

// Sets the background color of the balloon
void CBalloonHelp::SetBackgroundColor(COLORREF crBackground)
{
   m_crBackground = crBackground;
   // repaint if visible
   if ( NULL != m_hWnd )
      Invalidate(FALSE);
}

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

//
// creates a new balloon window
// Parameters:
//    strTitle    |  Title of balloon
//    strContent  |  Content of balloon
//    ptAnchor    |  point tail of balloon will be "anchor"ed to
//    unOptions   |  One or more of:
//                :     unCLOSE_ON_LBUTTON_UP   |  closes window on WM_LBUTTON_UP
//                :     unCLOSE_ON_RBUTTON_UP   |  closes window on WM_RBUTTON_UP
//                :     unCLOSE_ON_MOUSE_MOVE   |  closes window when user moves mouse past threshhold
//                :     unCLOSE_ON_KEYPRESS     |  closes window on the next keypress message sent to this thread.    (!!! probably not thread safe !!!)
//                :     unDELETE_THIS_ON_CLOSE  |  deletes object when window is closed.  Used by LaunchBalloon(), use with care
//                :     unSHOW_CLOSE_BUTTON     |  shows close button in upper right
//                :     unSHOW_INNER_SHADOW     |  draw inner shadow in balloon
//                :     unSHOW_TOPMOST          |  place balloon above all other windows
//                :     unDISABLE_FADE          |  disable the fade-in/fade-out effects (overrides system and user settings)
//                :     unDISABLE_FADEIN        |  disable the fade-in effect
//                :     unDISABLE_FADEOUT       |  disable the fade-out effect
//    pParentWnd  |  Parent window.  If NULL will be set to AfxGetMainWnd()
//    strURL      |  If not empty, when the balloon is clicked ShellExecute() will
//                |  be called, with strURL passed in.
//    unTimeout   |  If not 0, balloon will automatically close after unTimeout milliseconds.
//    hIcon       |  If not NULL, the icon indicated by hIcon will be displayed at top-left of the balloon.
//
// Returns:
//    TRUE if successful, else FALSE
//
BOOL CBalloonHelp::Create(const CString& strTitle, const CString& strContent, 
               const CPoint& ptAnchor, unsigned int unOptions,
               HWND pParentWnd /*=NULL*/,
               const CString strURL /*= ""*/,
               unsigned int unTimeout /*= 0*/,
               HICON hIcon /*= NULL*/)
{
	m_strContent   = strContent;
	m_ptAnchor     = ptAnchor;
	m_unOptions    = unOptions;
	m_strURL       = strURL;
	m_unTimeout    = unTimeout;

	if ( NULL != hIcon )
		SetIcon(hIcon);
	   
	if (NULL == pParentWnd)
		return FALSE;

	// if no fonts set, use defaults
	if ( NULL == m_pContentFont )
	{
		m_pContentFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT))
		if (!m_pContentFont)
			return FALSE;
	}

	// title font defaults to bold version of content font
	if ( NULL == m_pTitleFont )
	{
		LOGFONT LogFont;
		GetObject(m_pContentFont, sizeof(LogFont), &LogFont);
		LogFont.lfWeight = FW_BOLD;
		if (!(m_pTitleFont = (HFONT)CreateFontIndirect(&LogFont)))
			return FALSE;
	}

	// check system settings: if fade effects are disabled, disable here too
	BOOL bFade = FALSE;
	::SystemParametersInfo(SPI_GETTOOLTIPANIMATION, 0, &bFade, 0);
	if (bFade)
		::SystemParametersInfo(SPI_GETTOOLTIPFADE, 0, &bFade, 0);
	if (!bFade || NULL == m_fnSetLayeredWindowAttributes)
		m_unOptions |= unDISABLE_FADE;

	// create invisible at arbitrary position; then position, set region, and finally show

	// the idea with WS_EX_TOOLWINDOW is, you can't switch to this using alt+tab
	DWORD dwExStyle = WS_EX_TOOLWINDOW|WS_EX_LAYERED;
	if ( m_unOptions&unSHOW_TOPMOST )      // make topmost, if requested
		dwExStyle |= WS_EX_TOPMOST;
	if (!__super::Create(pParentWnd, CRect(0, 0, 10, 10), strTitle, WS_POPUP, dwExStyle))
		return FALSE;
	PositionWindow();

	// show, possibly fading
	ShowBalloon();

	if ( m_unOptions & unCLOSE_ON_MOUSE_MOVE )
	{
		::GetCursorPos(&m_ptMouseOrig);
		ScreenToClient(&m_ptMouseOrig);
		SetTimer(ID_TIMER_HOTTRACK, 100, NULL);
	}

	if ( (m_unOptions & unCLOSE_ON_LBUTTON_UP) || (m_unOptions & unCLOSE_ON_RBUTTON_UP) )
		SetCapture();

	if ( m_unOptions&unCLOSE_ON_KEYPRESS )
		SetKeyboardHook();

	// show, but don't take focus away from parent (or whatever)
	ShowWindow(SW_SHOWNOACTIVATE);

	return TRUE;
}

// Wrapper for possibly-existing API call
BOOL CBalloonHelp::SetLayeredWindowAttributes(COLORREF crKey, int nAlpha, DWORD dwFlags)
{
   if ( NULL != m_fnSetLayeredWindowAttributes )
      return (*m_fnSetLayeredWindowAttributes)(m_hWnd, crKey, (BYTE)nAlpha, dwFlags);
   return FALSE;
}

// calculates the area of the screen the balloon falls into
// this determins which direction the tail points
CBalloonHelp::BALLOON_QUADRANT CBalloonHelp::GetBalloonQuadrant()
{
   CRect rectDesktop;
   ::GetWindowRect(::GetDesktopWindow(), &rectDesktop);
   
   if ( m_ptAnchor.y < rectDesktop.Height()/2 )
   {
      if ( m_ptAnchor.x < rectDesktop.Width()/2 )
      {
         return BQ_TOPLEFT;
      }
      else
      {
         return BQ_TOPRIGHT;
      }
   }
   else
   {
      if ( m_ptAnchor.x < rectDesktop.Width()/2 )
      {
         return BQ_BOTTOMLEFT;
      }
      else
      {
         return BQ_BOTTOMRIGHT;
      }
   }

   // unreachable
}

// Calculate the dimensions and draw the balloon header
CSize CBalloonHelp::DrawHeader(HDC pDC, bool bDraw)
{
   CSize sizeHdr(0,0);
   CRect rectClient;
   GetClientRect(&rectClient);   // use this for positioning when drawing
                                 // else if content is wider than title, centering wouldn't work

   // calc & draw icon
   if ( NULL != m_ilIcon )
   {
      int x = 0;
      int y = 0;
      ImageList_GetIconSize(m_ilIcon, &x, &y);
      sizeHdr.cx += x;
      sizeHdr.cy = max(sizeHdr.cy, y);
      ImageList_SetBkColor(m_ilIcon, m_crBackground);
      if (bDraw)
         ImageList_Draw(m_ilIcon, 0, pDC, 0, 0, ILD_NORMAL);//ILD_TRANSPARENT);
      rectClient.left += x;
   }

   // calc & draw close button
   if ( m_unOptions & unSHOW_CLOSE_BUTTON )
   {
      // if something is already in the header (icon) leave space
      if ( sizeHdr.cx > 0 )
         sizeHdr.cx += nTIP_MARGIN;
      sizeHdr.cx += 16;
      sizeHdr.cy = max(sizeHdr.cy, 16);
      if (bDraw)
		 DrawFrameControl(pDC, CRect(rectClient.right-16,0,rectClient.right,16), DFC_CAPTION, DFCS_CAPTIONCLOSE|DFCS_FLAT);
      rectClient.right -= 16;
   }

   // calc title size
   CString strTitle;
   GetWindowText(strTitle);
   if ( !strTitle.IsEmpty() )
   {
      HFONT pOldFont = (HFONT)SelectObject(pDC, m_pTitleFont);

      // if something is already in the header (icon or close button) leave space
      if ( sizeHdr.cx > 0 ) 
         sizeHdr.cx += nTIP_MARGIN;
      CRect rectTitle(0,0,0,0);
	  DrawText(pDC, strTitle, strTitle.GetLength(), &rectTitle, DT_CALCRECT | DT_NOPREFIX | DT_EXPANDTABS | DT_SINGLELINE);
      sizeHdr.cx += rectTitle.Width();
      sizeHdr.cy = max(sizeHdr.cy, rectTitle.Height());

      // draw title
      if ( bDraw )
      {
         SetBkMode(pDC, TRANSPARENT);
         SetTextColor(pDC, m_crForeground);
         DrawText(pDC, strTitle, strTitle.GetLength(), &rectClient, DT_CENTER | DT_NOPREFIX  | DT_EXPANDTABS | DT_SINGLELINE);
      }

      // cleanup
      SelectObject(pDC, pOldFont);
   }

   return sizeHdr;
}

// Calculate the dimensions and draw the balloon contents
CSize CBalloonHelp::DrawContent(HDC pDC, int nTop, bool bDraw)
{
   CRect rectContent;
   ::GetClientRect(::GetDesktopWindow(), &rectContent);

   rectContent.top = nTop;

   // limit to half screen width
   rectContent.right -= rectContent.Width()/2;

   // calc size
   HFONT pOldFont = (HFONT)SelectObject(pDC, m_pContentFont);
   if ( !m_strContent.IsEmpty() )
      DrawText(pDC, m_strContent, m_strContent.GetLength(), &rectContent, DT_CALCRECT | DT_LEFT | DT_NOPREFIX | DT_EXPANDTABS | DT_WORDBREAK);
   else
      rectContent.SetRectEmpty();   // don't want to leave half the screen for empty strings ;)
   
   // draw
   if (bDraw)
   {
      SetBkMode(pDC, TRANSPARENT);
      SetTextColor(pDC, m_crForeground);
      DrawText(pDC, m_strContent, m_strContent.GetLength(), &rectContent, DT_LEFT | DT_NOPREFIX | DT_WORDBREAK | DT_EXPANDTABS);
   }

   // cleanup
   SelectObject(pDC, pOldFont);

   return rectContent.Size();
}

// calculates the client size necessary based on title and content
CSize CBalloonHelp::CalcClientSize()
{
   _ASSERTE(m_hWnd);
   HDC hdc = GetDC();
   CSize sizeHeader = CalcHeaderSize(hdc);
   CSize sizeContent = CalcContentSize(hdc);
   ReleaseDC(hdc);

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

// calculates the size for the entire window based on content size
CSize CBalloonHelp::CalcWindowSize()
{
   CSize size = CalcClientSize();
   size.cx += nTIP_MARGIN*2;
   size.cy += nTIP_TAIL+nTIP_MARGIN*2;
   //size.cx = max(size.cx, nTIP_MARGIN*2+nTIP_TAIL*4);
   return size;
}


// 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()
{
	CSize sizeWnd = CalcWindowSize();

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

	switch (GetBalloonQuadrant())
	{
	case BQ_TOPLEFT:
		ptTopLeft.y = nTIP_TAIL;
		ptTail[0].x = (sizeWnd.cx-nTIP_TAIL)/4 + nTIP_TAIL;
		ptTail[0].y = nTIP_TAIL+1;
		ptTail[2].x = (sizeWnd.cx-nTIP_TAIL)/4;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = 1;
		break;
	case BQ_TOPRIGHT:
		ptTopLeft.y = nTIP_TAIL;
		ptTail[0].x = (sizeWnd.cx-nTIP_TAIL)/4*3;
		ptTail[0].y = nTIP_TAIL+1;
		ptTail[2].x = (sizeWnd.cx-nTIP_TAIL)/4*3 + nTIP_TAIL;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = 1;
		break;
	case BQ_BOTTOMLEFT:
		ptBottomRight.y = sizeWnd.cy-nTIP_TAIL;
		ptTail[0].x = (sizeWnd.cx-nTIP_TAIL)/4 + nTIP_TAIL;
		ptTail[0].y = sizeWnd.cy-nTIP_TAIL-2;
		ptTail[2].x = (sizeWnd.cx-nTIP_TAIL)/4;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = sizeWnd.cy-2;
		break;
	case BQ_BOTTOMRIGHT:
		ptBottomRight.y = sizeWnd.cy-nTIP_TAIL;
		ptTail[0].x = (sizeWnd.cx-nTIP_TAIL)/4*3;
		ptTail[0].y = sizeWnd.cy-nTIP_TAIL-2;
		ptTail[2].x = (sizeWnd.cx-nTIP_TAIL)/4*3 + nTIP_TAIL;
		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 < nTIP_MARGIN )
		ptTail[0].x = nTIP_MARGIN;
	if ( ptTail[0].x > sizeWnd.cx - nTIP_MARGIN )
		ptTail[0].x = sizeWnd.cx - nTIP_MARGIN;
	if ( ptTail[1].x < nTIP_MARGIN )
		ptTail[1].x = nTIP_MARGIN;
	if ( ptTail[1].x > sizeWnd.cx - nTIP_MARGIN )
		ptTail[1].x = sizeWnd.cx - nTIP_MARGIN;
	if ( ptTail[2].x < nTIP_MARGIN )
		ptTail[2].x = nTIP_MARGIN;
	if ( ptTail[2].x > sizeWnd.cx - nTIP_MARGIN )
		ptTail[2].x = sizeWnd.cx - nTIP_MARGIN;

	// get window position
	CPoint ptOffs(m_ptAnchor.x - ptTail[1].x, m_ptAnchor.y - ptTail[1].y);

	// adjust position so all is visible
	CRect rectScreen;
	::GetWindowRect(::GetDesktopWindow(), &rectScreen);
	int nAdjustX = 0;
	int nAdjustY = 0;
	if ( ptOffs.x < 0 )
		nAdjustX = -ptOffs.x;
	else if ( ptOffs.x + sizeWnd.cx >= rectScreen.right )
		nAdjustX = rectScreen.right - (ptOffs.x + sizeWnd.cx);
	if ( ptOffs.y < 0 )
		nAdjustY = -ptOffs.y;
	else if ( ptOffs.y + sizeWnd.cy >= rectScreen.bottom )
		nAdjustY = rectScreen.bottom - (ptOffs.y + sizeWnd.cy);

	// 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
	BOOL bRegionChanged = TRUE;
	HRGN region;
	HRGN regionRound;
	HRGN regionComplete;
	region = CreatePolygonRgn(&ptTail[0], 3, ALTERNATE);
	regionRound = CreateRoundRectRgn(ptTopLeft.x,ptTopLeft.y,ptBottomRight.x,ptBottomRight.y,nTIP_MARGIN*3,nTIP_MARGIN*3);
	regionComplete = CreateRectRgn(0,0,1,1);
	CombineRgn(regionComplete, region, regionRound, RGN_OR);

	if ( NULL == m_rgnComplete )
		m_rgnComplete = CreateRectRgn(0, 0, 1, 1);
	else if ( EqualRgn(m_rgnComplete, regionComplete) )
		bRegionChanged = FALSE;
	::CombineRgn(m_rgnComplete, regionComplete, NULL, RGN_COPY);
	   
	SetWindowRgn(regionComplete, TRUE);
	DeleteObject(region);
	DeleteObject(regionRound);

	// There is a bug with layered windows and NC changes in Win2k
	// As a workaround, redraw the entire window if the NC area changed.
	// Changing the anchor point is the ONLY thing that will change the
	// position of the client area relative to the window during normal
	// operation.
	if ( bRegionChanged )
		RedrawWindow(NULL, NULL, RDW_UPDATENOW| RDW_ERASE | RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN);
}



// Displays the balloon on the screen, performing fade-in if enabled.
void CBalloonHelp::ShowBalloon(void)
{
   m_nAlpha = 5;
   SetLayeredWindowAttributes(0, m_nAlpha, LWA_ALPHA);
   UINT_PTR i = SetTimer(ID_TIMER_SHOW, 25, NULL);
}

// Removes the balloon from the screen, performing the fade-out if enabled
void CBalloonHelp::HideBalloon(void)
{
   m_nAlpha = 250;
   SetTimer(ID_TIMER_HIDE, 25, NULL);
}

// Sets up the keyboard hook; adds this to list to close
void CBalloonHelp::SetKeyboardHook()
{
	s_KeyboardHookSection.Lock();
	// if hook is not yet set, set
	if ( NULL == s_hKeyboardHook )
	{
		s_hKeyboardHook = ::SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, NULL, ::GetCurrentThreadId());
	}
	s_apKeyboardCloseWnds.Add(m_hWnd);
	s_KeyboardHookSection.Unlock();
}


// Removes this from the list of windows to close; removes hook if not in use.
void CBalloonHelp::RemoveKeyboardHook()
{
	s_KeyboardHookSection.Lock();
	INT_PTR i;
	for (i=0; i < (int)s_apKeyboardCloseWnds.GetCount(); ++i)
	{
		if ( s_apKeyboardCloseWnds.GetAt(i) == m_hWnd )
			s_apKeyboardCloseWnds.RemoveAt(i--);
	}

	// if no more windows need to be notified, get rid of hook
	if ( s_apKeyboardCloseWnds.GetCount() == 0 && NULL != s_hKeyboardHook )
	{
		::UnhookWindowsHookEx(s_hKeyboardHook);
		s_hKeyboardHook = NULL;
	}
	s_KeyboardHookSection.Unlock();
}

// Erase client area of balloon
LRESULT CBalloonHelp::OnEraseBackground(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	HDC hdc = (HDC)wParam;
	CRect rect;
	GetClientRect(&rect);
	FillSolidRect(hdc, &rect, m_crBackground);
	return 0;
}

// draw balloon client area (title & contents)
LRESULT CBalloonHelp::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	PAINTSTRUCT ps;
	BeginPaint(&ps);
    CSize sizeHeader = DrawHeader(ps.hdc);
    DrawContent(ps.hdc, sizeHeader.cy + nTIP_MARGIN);
	EndPaint(&ps);
    return 0;
}

// draw balloon shape & boarder
LRESULT CBalloonHelp::OnNcPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	HDC hdc = GetWindowDC();

    // Paint into this DC
	CRect rect;
	GetWindowRect(&rect);
	ScreenToClient(&rect);
	CRect rectClient;
	GetClientRect(&rectClient);
	rectClient.OffsetRect(-rect.left, -rect.top);
	rect.OffsetRect(-rect.left, -rect.top);
	ExcludeClipRect(hdc, rectClient.left, rectClient.top, rectClient.right, rectClient.bottom);
	FillSolidRect(hdc, &rect, m_crBackground);

	_ASSERTE(m_rgnComplete);
	CBrush   brushFg;
	brushFg.CreateSolidBrush(m_crForeground);
	if ( m_unOptions & unSHOW_INNER_SHADOW )
	{
		CBrush   brushHL;
		// slightly lighter color
		int red = 170 + GetRValue(m_crBackground)/3;
		int green = 170 + GetGValue(m_crBackground)/3;
		int blue = 170 + GetBValue(m_crBackground)/3;
		brushHL.CreateSolidBrush(RGB(red,green,blue));
		OffsetRgn(m_rgnComplete, 1, 1);
		FrameRgn(hdc, m_rgnComplete, brushHL, 2, 2);
		// slightly darker color
		red = GetRValue(m_crForeground)/3 + GetRValue(m_crBackground)/3*2;
		green = GetGValue(m_crForeground)/3 + GetGValue(m_crBackground)/3*2;
		blue = GetBValue(m_crForeground)/3 + GetBValue(m_crBackground)/3*2;
		brushHL.DeleteObject();
		OffsetRgn(m_rgnComplete, -2, -2);
		brushHL.CreateSolidBrush(RGB(red,green,blue));
		FrameRgn(hdc, m_rgnComplete, brushHL, 2, 2);
		OffsetRgn(m_rgnComplete, 1, 1);
	}
	// outline
	FrameRgn(hdc, m_rgnComplete, brushFg, 1, 1);
	ReleaseDC(hdc);

	return 0;
}

// Close button handler
LRESULT CBalloonHelp::OnLButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	CPoint point(lParam);
	if (m_unOptions & unSHOW_CLOSE_BUTTON)
	{
		CRect rect;
		GetClientRect(&rect);
		rect.left = rect.right-16;
		rect.bottom = rect.top+16;
		if ( rect.PtInRect(point) )
		{
			m_uCloseState |= DFCS_PUSHED;
			SetCapture();
			OnMouseMove(uMsg, wParam, lParam, bHandled);
		}
	}
	return 0;
}

// Close button handler,
// close on LButton up handler
LRESULT CBalloonHelp::OnLButtonUp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	CPoint point(lParam);
	if ( (m_unOptions & unSHOW_CLOSE_BUTTON) && (m_uCloseState & DFCS_PUSHED) )
	{
		ReleaseCapture();
		m_uCloseState &= ~DFCS_PUSHED;
		CRect rect;
		GetClientRect(&rect);
		rect.left = rect.right-16;
		rect.bottom = rect.top+16;
		if ( rect.PtInRect(point) )
			HideBalloon();
	}
	else if ( m_unOptions & unCLOSE_ON_LBUTTON_UP )
	{
		ReleaseCapture();
		HideBalloon();
	}
	else if ( !m_strURL.IsEmpty() )
	{
		HideBalloon();
		::ShellExecute(NULL, NULL, m_strURL, NULL, NULL, SW_SHOWNORMAL);
	}
	return 0;
}

// Close on RButton up checking
LRESULT CBalloonHelp::OnRButtonUp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	CPoint point(lParam);
	if ( m_unOptions & unCLOSE_ON_RBUTTON_UP )
	{
		ReleaseCapture();
		HideBalloon();
	}
	return 0;
}

//
// do mouse tracking:
//   Close on mouse move;
//   Tracking for close button;
//
LRESULT CBalloonHelp::OnMouseMove(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	CPoint point(lParam);
	if (m_unOptions & unSHOW_CLOSE_BUTTON)
	{
		CRect rect;
		GetClientRect(&rect);
		rect.left = rect.right-16;
		rect.bottom = rect.top+16;
		UINT uState = DFCS_CAPTIONCLOSE;
		BOOL bPushed = m_uCloseState&DFCS_PUSHED;
		m_uCloseState &= ~DFCS_PUSHED;
		if ( rect.PtInRect(point) )
		{
			uState |= DFCS_HOT;
			if ( bPushed )
				uState |= DFCS_PUSHED;
			SetTimer(ID_TIMER_HOTTRACK, 500, NULL);
		}
		else
		{
			uState |= DFCS_FLAT;
		}
		if ( uState != m_uCloseState )
		{
			CDC dc;
 			dc.Attach(GetDC());
			dc.DrawFrameControl(&rect, DFC_CAPTION, uState);
			m_uCloseState = uState;
		}
		if ( bPushed )
			m_uCloseState |= DFCS_PUSHED;
	}

	if ( m_unOptions & unCLOSE_ON_MOUSE_MOVE )
	{
		if ( m_nAlpha == 255 && (abs(point.x-m_ptMouseOrig.x) > m_nMouseMoveTolerance || abs(point.y-m_ptMouseOrig.y) > m_nMouseMoveTolerance) )
			HideBalloon();
		else
			SetTimer(ID_TIMER_HOTTRACK, 100, NULL);
	}
	return 0;
}

// Ensures client area is the correct size relative to window size,
// presearves client contents if possible when moving.
LRESULT CBalloonHelp::OnNcCalcSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	BOOL bCalcValidRects = (BOOL)wParam;
	NCCALCSIZE_PARAMS FAR* lpncsp = (LPNCCALCSIZE_PARAMS)lParam;

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

   // nTIP_TAIL pixel "tail" on side closest to anchor
   switch ( GetBalloonQuadrant() )
   {
   case BQ_TOPRIGHT:
   case BQ_TOPLEFT:
      lpncsp->rgrc[0].top += nTIP_TAIL;
      break;
   case BQ_BOTTOMRIGHT:
   case BQ_BOTTOMLEFT:
      lpncsp->rgrc[0].bottom -= nTIP_TAIL;
      break;
   }

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

   if ( bCalcValidRects )
   {
      // determine if client position has changed relative to the window position
      // if so, don't bother presearving anything.
      if ( !::EqualRect(&lpncsp->rgrc[0], &lpncsp->rgrc[2]) )
      {
         ::SetRectEmpty(&lpncsp->rgrc[2]);
      }
   }
   return 0;
}

// handler for various timers
LRESULT CBalloonHelp::OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	UINT nIDEvent = (UINT)wParam;
   switch (nIDEvent)
   {
   case ID_TIMER_SHOW:
      m_nAlpha += 25;
      if ( m_nAlpha > 255 || m_unOptions&unDISABLE_FADEIN)
      {
         m_nAlpha = 255;
         KillTimer(ID_TIMER_SHOW);
         SetTimeout(m_unTimeout);     // start close timer
      }
         
      SetLayeredWindowAttributes(0, m_nAlpha, LWA_ALPHA);
      break;
   case ID_TIMER_HIDE:
      // just in case...
      KillTimer(ID_TIMER_SHOW);

      m_nAlpha -= 25;
      if ( m_nAlpha < 0 || m_unOptions&unDISABLE_FADEOUT )
      {
         m_nAlpha = 0;
         KillTimer(ID_TIMER_HIDE);
         DestroyWindow();
      }
      else
      {
         SetLayeredWindowAttributes(0, m_nAlpha, LWA_ALPHA);
      }
      break;
   case ID_TIMER_CLOSE:
      KillTimer(ID_TIMER_CLOSE);
      HideBalloon();
      break;
   case ID_TIMER_HOTTRACK:
      {
         CPoint point;
         KillTimer(ID_TIMER_HOTTRACK);
         ::GetCursorPos(&point);
         ScreenToClient(&point);
         OnMouseMove(uMsg, wParam, MAKEWORD(point.x, point.y), bHandled);
         break;
      }
   }
   return 0;
}

// Called as the window is being destroyed.  Completes destruction after removing keyboard hook.
LRESULT CBalloonHelp::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
   // remove from list, if set
   RemoveKeyboardHook();
   return 0;
}

// close the balloon, performing any set transition effect.
LRESULT CBalloonHelp::OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
   HideBalloon();
   return 0;
}

// Keyboard hook; used to implement the unCLOSE_ON_KEYPRESS option
LRESULT CALLBACK CBalloonHelp::KeyboardProc( int code, WPARAM wParam, LPARAM lParam)
{
	// Skip if the key was released or if it's a repeat
   if (code >= 0 && !(lParam & 0x80000000))
	{
		s_KeyboardHookSection.Lock();
		INT_PTR nNumWnds = CBalloonHelp::s_apKeyboardCloseWnds.GetCount();
		INT_PTR i;
		for (i=0; i<nNumWnds; ++i)
			::PostMessage((HWND)CBalloonHelp::s_apKeyboardCloseWnds.GetAt(i), WM_CLOSE, 0, 0);
		CBalloonHelp::s_apKeyboardCloseWnds.RemoveAll();
		s_KeyboardHookSection.Unlock();
	}
	return ::CallNextHookEx(CBalloonHelp::s_hKeyboardHook, code, wParam, lParam);
}

// Called after window has been destroyed.  Destroys the object if option is set.
void CBalloonHelp::OnFinalMessage(HWND)
{
   // free object if requested
   // be careful with this one :D
   if ( m_unOptions & unDELETE_THIS_ON_CLOSE )
      delete this;
}

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