// 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;
}